자바가 제공하는 두 가지 객체 소멸자인 finalizer와 cleaner의 사용 자제를 크게 6가지 이유로 설명한다.
finalizer나 cleaner를 얼마나 신속히 수행할지는 전적으로 GC 알고리즘에 달려 있으며, GC마다 다르다.
public class FinalizerDemo {
@Override
protected void finalize() throws Throwable {
System.out.println("Clean Up");
}
public void hello() {
System.out.println("hello");
}
}
class Main {
public static void main(String[] args) throws InterruptedException {
new Main().run();
Thread.sleep(1000);
}
private void run() {
FinalizerDemo finalizerDemo = new FinalizerDemo();
finalizerDemo.hello();
}
}
위 코드에서 run() 메서드가 종료되면 더 이상 FinalizerDemo 인스턴스에 대한 참조가 존재하지 않기 때문에 1초 사이에 "Clean Up"이 출력될 것을 기대할 수 있다. 그러나 기대와는 다르게 finalize() 메서드는 호출되지 않는다.
System.gc()나 System.runFinalization() 메서드를 호출해도 실행될 가능성이 높아질 뿐, 실행이 보장되지는 않는다.
finalizer 스레드는 다른 애플리케이션의 스레드보다 우선순위가 낮아 finalization queue에 객체가 쌓여 OutOfMemory가 발생할 수 있다.
cleaner는 자신을 수행할 스레드를 제어할 수 있어 조금 낫긴 하지만, 여전히 GC의 통제하에 있으며 즉시 수행된다는 보장이 없다.
데이터베이스 같은 공유 자원의 영구 락(lock) 해제를 finalizer나 cleaner에 맡겨 놓으면 분산 시스템 전체가 서서히 멈출 것이다.
잡지 못한 예외 때문에 해당 객체는 마무리가 덜 된 상태로 남을 수 있다.
잡지 못한 예외가 발생하더라도 finalizer는 경고조차 출력하지 않는다. cleaner를 사용하는 라이브러리는 자신의 스레드를 통제하기 때문에 이러한 문제가 발생하지 않지만 사용하지 않는 것을 권장한다.
finalizer는 GC의 효율을 떨어뜨리기 때문에 성능 저하를 초래한다.
생성자나 직렬화 과정에서 예외가 발생하면, 생성되다 만 객체에서 악의적인 하위 클래스의 finlizer가 수행될 수 있게 된다.
따라서, final이 아닌 클래스를 finalizer 공격으로부터 방어하려면 아무 일도 하지 않는 finalize 메서드를 만들고 fianl로 선언해야 한다.
책에서는 finalizer나 cleaner의 사용을 지양하고 있으며, AutoCloseable을 구현하고, 인스턴스를 다 쓰고 나면 close 메서드를 호출하는 방법으로 대안을 제공하고 있다.
그렇다면, 언제 finalizer와 cleaner를 사용하면 좋을까?
클라이언트가 자원을 반환하지 않았을 경우에 대비한 안전망으로써 finalizer나 cleaner를 사용한다.
자바에서 제공하는 FileInputStream, FileOutputStream, ThreadPoolExecutor에는 안전망으로 동작하는 finalizer가 있다.
public class Room implements AutoCloseable{
private static final Cleaner cleaner = Cleaner.create();
// 청소가 필요한 자원으로 절대 Room을 참조해서는 안된다.
private static class State implements Runnable {
int numJunkPiles; // 방(Room) 안의 쓰레기 수
public State(int numJunkPiles) {
this.numJunkPiles = numJunkPiles;
}
// close 메소드나 cleaner가 호출한다.
@Override
public void run() {
System.out.println("방 청소");
numJunkPiles = 0;
}
}
// 방의 상태. cleanable와 공유한다.
private final State state;
// cleanable 객체. 수거 대상이 되면 방을 청소한다.
private final Cleaner.Cleanable cleanable;
public Room(State state, Cleaner.Cleanable cleanable) {
this.state = new State(state.numJunkPiles);
this.cleanable = cleaner.register(this,state);
}
@Override
public void close() throws Exception {
cleanable.clean();
}
}
자바 클래스 -> 네이티브 메서드 호출 -> 네이티브 객체 (네이티브 피어)
네이티브 피어란 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체를 말한다. 네이티브 피어는 자바 객체가 아니라서 GC가 그 존재를 모르고 자바 피어를 회수할 때 네이티브 객체까지 회수하지 못한다.
이 때, finalizer나 cleaner를 사용하면 되는데 성능 저하를 감당할 수 있고 네이티브 피어가 심각한 자원을 가지고 있지 않을 때에만 사용하는 것이 좋다.