자바에는 두 가지 객체 소멸자인 finalizer, cleaner가 존재한다. finalizer는 나름의 쓰임새가 있긴하지만, 쓰지 않는 것이 좋다. 그래서 자바9에서는 finalizer를 deprecated API로 지정하고 cleaner를 그 대안으로 지정했지만, 이 역시 쓰지 않는 것을 추천한다.
finalizer와 cleaner는 즉시 수행된다는 보장이 없다. 접근할 수 없는 객체가 된 후 finalizer와 cleaner가 언제 실행될지 알 수 없기 때문이다. 즉, finalizer와 cleaner로는 제때 실행되어야 하는 작업은 절대할 수 없다. 예를 들면, 파일 닫기를 이들한테 맡기면 시스템이 동시에 열 수 있는 파일 개수에 한계가 있기 때문에 finalizer나 cleaner 실행이 늦게 되면 파일을 계속 열어둔 상태가 돼 새로운 파일을 열지 못해 오류를 일으킬 수 있다.
또한, 이들은 실행 여부 또한 보장되지 않는다. 이는 일부 객체에 딸린 종료 작업을 전혀 수행하지도 못한채 프로그램이 중단될 수도 있다는 걸 뜻한다. 때문에 상태를 영구적으로 수정하는 작업에서는 절대 finalizer나 cleaner에 의존해서는 안된다.
그렇다면 파일이나 스레드 등 종료해야 할 자원을 담고 있는 객체의 클래스에서 어떻게 처리하는 것이 좋을까? -> AutoCloseable
클라이언트에서 인스턴스를 다 쓰고나면 close 메서드를 호출할 수 있도록 try-with-resources를 사용하면 된다.
close 메서드에서 이 객체는 더 이상 유효하지 않음을 필드에 기록하고, 다른 메서드는 이 필드를 검사해 객체가 닫힌 후에 불렀다면 IllegalStatementException을 던지도록 만들자.
그럼 도대체 finalizer와 cleaner의 역할은 무엇일까? 사실 둘 다 안전망
의 역할을 한다. 즉시 작동하리란 보장은 없지만 없는 것보단 나으니 있는 것.
또, 이 두 개를 적절히 활용하는 예는 네이티브 피어
와 연결된 객체에서다. 네이티브 피어
란 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체를 말한다. 얘는 자바 객체가 아니니 가비지 컬렉터가 존재를 알지 못한다. 그래서 finalizer와 cleaner가 대신 처리하는 것이다. 하지만, 성능저하를 감당할 수 있고 네이티브 피어가 심각한 자원을 가지고 있지 않을때만 해당한다. 그렇지 않은 경우는 앞에서 설명한 close 메서드를 사용해야한다.
public class Room implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
//청소가 필요한 자원, 절대 Room을 참조해서는 안된다.
private static class State implements Runnable {
int numJunkPiles; // 방 안의 쓰레기 수
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(int numJunkPiles) {
state = new State(numJunkPiles);
cleanable = cleaner.register(this, state);
}
@Override
public void close() {
cleanable.clean();
}
}
위 코드는 방(room) 자원을 수거하기 전에 반드시 청소(clean)해야한다고 가정하고 만든 코드이다.
State 인스턴스는 절대 Room 인스턴스를 참조해서는 안된다. Room 인스턴스를 참조할 경우 순환 참조가 생겨 가비지 컬렉터가 Room 인스턴스를 회수해갈 기회가 오지 않는다. State 클래스가 정적 중첩 클래스인 이유가 여기에 있다. 정적이 아닌 중첩 클래스는 자동으로 바깥 객체의 참조를 갖게되기 때문이다. 이와 비슷하게 람다 역시 바깥 객체의 참조를 갖기 쉬우니 사용하지 않는 것이 좋다.
앞서 얘기한대로 Room의 cleaner는 단지 안전망으로만 쓰였다. 클라이언트가 모든 Room 생성을 try-with-resources 블록으로 감쌌다면 자동청소는 전혀 필요하지 않다.
public class Adult {
public static void main(String[] args) {
try(Room myRoom = new Room(7)) {
System.out.println("안녕~");
}
}
}
기대한대로 Adult 프로그램은 "안녕~"을 출력한 후, 이어서 "방 청소"를 출력한다.
public class Teenager {
public static void main(String[] args) {
new Room(99);
System.out.println("아무렴");
}
}
위가 바로 앞서 말한 "예측할 수 없다"고 한 상황이다. "아무렴"에 이어 "방청소"가 출력되리라 기대했겠지만 "방 청소"는 출력되지 않는다.
코드에 System.gc()를 추가해 종료 전 "방 청소"를 출력할 수는 있겠지만, 다른 컴퓨터에서도 그러리라는 보장이 없다.