자바는 두 가지 객체 소멸자를 제공한다.
그 중 finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다.
오동작, 낮은 성능, 이식성 문제의 원인이 되기도 한다.
finalizer는 나름의 쓰임새가 몇 가지 있지만 기본적으로 사용하지 말아야 한다.
cleaner는 finalizer보다 덜 위험하지만 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.
이 두가지는 즉시 수행된다는 보장이 없다.
객체에 접근 할 수 없게 된 후 실행되기까지 얼마나 걸릴지 알 수 없다.
즉, finalizer와 cleaner로는 저때 실행되어야 하는 작업은 절대 할 수 없다.
예를 들어 시스템이 동시에 열 수 있는 파일 개수에 한계가 있을 때, 파일 닫기를 맡기면 중대한 오류를 일으킬 수 있다.
얼마나 신속히 수행할지는 전적으로 GC에 달렸으며, GC 구현마다 천차만별이다.
동일하게, System.gc와 System.runFinalization 메서드에 현혹되지 말자.
finalizer와 cleaner가 실행될 가능성을 높여줄 수 는 있으나, 보장해주진 않는다.
내 경우에도 자바와 네이티브를 수없이 오가는 JNI를 사용한 모듈을 만든적이 있었다.
JNI 특성상 메모리 누수에 노출되기 쉬운데, 구현 후 특정 부분에서 지속적으로 메모리 누수가 발생하는 것을 확인했다.
눈을 씻고 찾아봐도 누수 부분을 찾을 수 없어서 System.gc, finalizer 등 온갖 방법을 사용해봤지만 전혀 소용이 없었다.
결국 문제는 자바와 네이티브 두 군데에 동시에 있었으며, 참조를 직접 해제해줘서 해결했었다.
finalizer는 동작 중 예외가 발생해도 예외가 무시되고 처리할 작업이 남았더라도 그 순간 종료된다.
무시 된 예외 때문에 객체가 마무리가 덜 된 상태로 남아있을 수 있고 프로그램에 어떤 영향을 끼칠 지 알 수 없다.
또한 심각한 성능 , 보안 문제를 동반한다.
이쯤이면 이 두가지는 대체 어디에 쓰이는 물건인지 궁금해진다.
적절한 두가지 쓰임새가 있는데,
하나는 자원의 소유자가 close 메서드를 호출하지 않는 것에 대비한 안전망이다.
cleaner와 finlizer가 즉시 호출되리라는 보장은 없지만, 클라이언트가 하지 않은 자원 회사를 늦게라도 해주는 것이
아예 안 하는 것보다는 낫기 때문이다.
FileInputStream, FileOutputStream, ThreadPoolExceutor 등이 안정망 역할의 finalizer를 제공한다.
두번째는 네이티브 피어와 연결된 객체에서다.
네이티브 피어란 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체를 말한다.
네이티브 피어는 자바 객체가 아니니 가비지 컬렉터는 그 존재를 알지 못한다.
자바 피어를 회수 할 때 네이티브 객체를 회수하지 못한다면 cleaner나 finalizer가 나서서 처리하기 적당한 작업이겠다.
// 코드 8-1 cleaner를 안전망으로 활용하는 AutoCloseable 클래스 (44쪽)
public class Room implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
// 청소가 필요한 자원. 절대 Room을 참조해서는 안 된다!
private static class State implements Runnable {
int numJunkPiles; // Number of junk piles in this room
State(int numJunkPiles) {
this.numJunkPiles = numJunkPiles;
}
// close 메서드나 cleaner가 호출한다.
@Override public void run() {
System.out.println("Cleaning room");
numJunkPiles = 0;
}
}
// 방의 상태. cleanable과 공유한다.
private final State state;
// cleanable 객체. 수거 대상이 되면 방을 청소한다.
private final Cleaner.Cleanable cleanable;
public Room(int numJunkPiles) {
state = new State(numJunkPiles);
cleanable = cleaner.register(this, state);
}
@Override public void close() {
cleanable.clean();
}
}
cleaner의 예시이다.
Room은 자원을 수거하기 전에 반드시 clean 을 해야한다.
Room 클래스는 AutoCloseable을 구현한다.
static으로 선언된 중첩 클래스인 Statesms cleaner가 방을 청소할 때 수거할 자원들을 담고 있다.
이 예에서는 단순히 방 안의 쓰레기 수를 뜻하는 numJunkPiles 필드가 수거할 자원에 해당한다.
더 현실적으로 만들려면 이 필드는 네이티브 피어를 가리키는 포인터를 담은 final long 변수여야 한다.
State는 Runnable을 구현하고, 그 안의 run 메서드는 cleanable에 의해 딱 한번만 호출될 것 이다.
run 메서드가 호출되는 상황은 둘 중 하나다.
보통은 Room의 close 메서드를 호출할 때다. close 메서드에서 Cleanable의 clean을 회수할 때 까지
클라이언트가 close를 호출하지 않는다면, cleaner가 state의 run 메서드를 호출해 줄 것이다.
State 인스턴스는 절대로 Room 인스턴스를 참조해서는 안된다.
순환참조가 발생해 GC가 Room 인스턴스를 회수하지 않는다.
앞서 이야기한 대로 Room의 cleaner는 단지 안전망으로만 쓰였다.
클라이언트가 모든 Room 생성을 try-with-resources 블록으로 감쌋다면 자동 청소는 전혀 필요하지 않다.
// cleaner 안전망을 갖춘 자원을 제대로 활용하는 클라이언트 (45쪽)
public class Adult {
public static void main(String[] args) {
try (Room myRoom = new Room(7)) {
System.out.println("안녕~");
}
}
}
// cleaner 안전망을 갖춘 자원을 제대로 활용하지 못하는 클라이언트 (45쪽)
public class Teenager {
public static void main(String[] args) {
new Room(99);
System.out.println("Peace out");
// 다음 줄의 주석을 해제한 후 동작을 다시 확인해보자.
// 단, 가비지 컬렉러를 강제로 호출하는 이런 방식에 의존해서는 절대 안 된다!
System.gc();
}
}
프로젝트 중 Closeable을 사용한 적이 있었다.
AMQP를 통해 이미지 데이터를 동영상으로 녹화하는 프로젝트 였었는데 다수의 세션에서 동시에 이미지가 전송됐다.
속도를 위해 멀티스레드를 이용했는데, 동영상의 서순을 맞추기 위해선 이미지 데이터를 순차적으로 처리해야 했다.
때문에 Set과 Map을 사용해 요청을 순차적으로 지연처리하는 부분을 만들었고
이를 excutor와 future를 이용해 다중스레드로 관리하며 사용했다.
AMQP에 메세지가 들어오면 리스너에서 옵저버 패턴으로 구현한 핸들러를 호출하고, 핸들러에서 작업을 특정 스레드에 할당한다.
그 핸들러의 부모클래스에 Closeable을 상속해서
close()메서드를 오버라이드 했다.
@Override
public void close() throws IOException {
// 신규 작업 중단.
this.executor.shutdown();
try {
// 작업 종료.
if ( !this.executor.awaitTermination( 1, TimeUnit.SECONDS ) ) {
this.executor.shutdownNow();
}
} catch ( InterruptedException e ) {
this.executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
try-with-resources 는 AMQP 리스너에서 감싸주었고, 사용이 끝나면 close()가 호출되어 스레드가 종료되도록 구현했다.
이에 관한 코드리뷰를 후에 포스팅으로 적어보려고 한다.
'JAVA > 이펙티브 자바' 카테고리의 다른 글
10. equals는 일반 규약을 지켜 재정의하라 (0) | 2023.06.26 |
---|---|
9.try-finally보다는 try-with-resources를 사용하라 (0) | 2023.06.20 |
7. 다 쓴 객체 참조를 해제하라 (0) | 2023.06.19 |
6. 불필요한 객체 생성을 피하라 (0) | 2023.06.14 |
5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2023.06.11 |