자바는 finalizer와 cleaner로 객체 소멸자를 제공한다.
finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다. 기본적으로 쓰지 말아야한다. cleaner는 finalizer보다는 덜 위험하지만, 여전히 예측할 수 없고 느리고, 일반적으로 불필요하다.
finalizer와 cleaner는 즉시 수행된다는 보장이 없으므로 제때 실행되어야하는 작업은 절대 할 수 없다. 예를 들어 파일 닫기를 finalizer와 cleaner에 맡기면 중대한 오류를 일으킬 수 있다. 시스템이 동시에 열 수 있는 파일 개수에 한계가 있기 때문이다. finalizer와 cleaner이 실행을 게을리 해 파일을 계속 열어 둔다면 새로운 파일을 열지 못할 것이다.
finalizer와 cleaner가 얼마나 신속히 수행할지는 전적으로 가비리 컬렉터 알고리즘에 달렸으며, 이는 구현마다 천차만별이다.
클래스에 finalizer를 달아두면 그 인스턴스의 자원회수가 제멋대로 지연될 수 있다. finalizer는 우선순위가 낮아 대기열에서 계속 밀려나 회수가 늦어져 OutOfMemoryError
를 낼 수 있다. 이는 자신이 수행할 스레드를 제어할 수 없기 때문이다. cleaner는 스레드를 제어할 수는 있지만, 백그라운드에서 수행되며 GC의 통제하에 있으니 즉각 수행되리란 보장이 없다.
특히 상태를 영구적으로 수정하는 작업에서는 절대 finalizer와 cleaner에 의존해서는 안된다.
finalizer와 cleaner가 실행될 가능성을 높여줄 수는 있으나, 보장해주진 않는다.
finalizer동작중 발생한 예외는 무시되며 처리할 작업이 남아도 그 순간 종료된다. 따라서 해당 객체는 마무리가 덜 된 상태로 남을 수 있고 이 훼손된 객체를 사용하려 하면 어떤 동작이 일어날 지 예측할 수 없다.
try-with-resources
에 비해 finalizer를 사용했을 때 50배나 느린 성능을 보였다. finalizer가 GC의 효율을 떨어뜨리기 때문이다.
finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수도 있다.
객체 생성을 막으려면 생성자에서 예외를 던지는 것만으로 충분하지만, finalizer가 있다면 그렇지도 않다. final 클래스는 이 공격에서 안전하니, final 이 아닌 클래스를 finalizer 공격으로부터 방어하려면 아무 일도 하지 않는 finalize 메서드를 만들고 final로 선언하자.
AutoCloseable을 구현해주고 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출하면 된다.
close메서드에서 이 객체는 더 이상 유효하지 않음을 필드에 기록하고, 다른 메서드는 이 필드를 검사해 객체가 닫힌 후에 불렸다면 IllegalStateException
을 던지게 하면된다.
그렇다면 finalizer와 cleaner는 어디에서 사용하는 것일까?
자원의 소유자가 close 메서드를 호출하지 않는 것에 대비한 안전망 역할이다. finalizer와 cleaner가 즉시 호출되리라는 보장은 없으나, 클라이언트가 하지 않는 회수를 아예안하는 것보단 낫다.
하지만 안전망을 위해 finalizer를 사용할 땐 그럴만한 값어치가 있는지 심사 숙고 하자.
네이티브 피어란 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체를 말한다. 네이티브 피어는 GC가 존재를 알지 못하므로 자바 피어를 회수할 때 회수하지 못한다. 따라서 finalizer와 cleaner가 회수하기 적당하다.
하지만, 성능 저하를 감당할 수 있고 네이티브 피어가 심각한 자원을 가지고 있지 않을 때에만 해당되며 이게 아니라면, close 메서드를 사용해야한다.
방자원을 수거하기 전 반드시 청소를 해야한다고 가정하자. Room 클래스는 AutoCloseable을 구현한다.
import java.lang.ref.Cleaner;
public class Room implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
// Resource that requires cleaning. Must not refer to Room!
private static class State implements Runnable {
int numJunkPiles; // Number of junk piles in this room
State(int numJunkPiles) {
this.numJunkPiles = numJunkPiles;
}
// Invoked by close method or cleaner
@Override public void run() {
System.out.println("Cleaning room");
numJunkPiles = 0;
}
}
// The state of this room, shared with our cleanable
private final State state;
// Our cleanable. Cleans the room when it’s eligible for gc
private final Cleaner.Cleanable cleanable;
public Room(int numJunkPiles) {
state = new State(numJunkPiles);
cleanable = cleaner.register(this, state);
}
@Override public void close() {
cleanable.clean();
}
}
State 인스턴스는 절대 Room 인스턴스를 참조해서는 안된다. Room 인스턴스를 참조할 경우 순환참조가 생겨 GC가 Room 인스턴스를 회수해갈 기회가 오지 않는다. State가 static인 이유도 바깥 객체를 참조하지 않기 위해서이다. 위의 cleaner는 단지 안전망으로만 쓰였다. 클라이언트가 모든 Room 생성을 try-with-resources 블록으로 감쌌다면 자동 청소는 전혀 필요하지 않다.
public class Adult {
public static void main(String[] args) {
try (Room myRoom = new Room(7)) {
System.out.println("Goodbye");
}
}
}
위 코드는 Goodbye
를 출력한 후 Cleaning room
을 출력한다.
import java.util.concurrent.TimeUnit;
public class Teenager {
public static void main(String[] args) {
new Room(99);
System.out.println("Peace out");
// Uncomment next line and retest behavior, but note that you MUST NOT depend on this behavior!
// System.gc();
}
}
그러나 위의 코드에서는 Cleaning room
이 출력되지 않는다.
cleaner의 명세를 살펴보자.
System.exit을 호출할때의 cleaner 동작은 구현하기 나름이다. 청소가 이뤄질지는 보장하지 않는다.
cleaner는 안전망이나 중요하지 않은 네이티브 자원회수용으로만 사용하자. 물론 이런 경우라도 불확실성과 성능 저하에 주의해야 한다.
이펙티브 자바 3/E