
자바는 C++와 달리 파괴자(destructor)가 없다. 대신 가비지 컬렉터(GC)가 자동으로 메모리를 회수한다. 그러나 자바도 예전에 "finalizer"라는 메커니즘, 그리고 자바 9부터 "cleaner"라는 메커니즘을 제공한다. 자바에서는 finalizer나 cleaner 대신에, try-with-resources나 try-finally 같은 방법으로 비메모리 자원을 회수하도록 한다.
finalizer
finalize() 메서드가 자동으로 호출되는 구조이다.cleaner
finalizer나cleaner를 얼마나 신속히 수행할지는 전적으로 가비지 컬렉터 알고리즘에 달려있다고 한다. 그래서 처음 읽을 때finalize()가 그렇다면 가비지 컬렉터의 알고리즘인가?라고 오해를 했다.
finalize는 가비지 컬렉터의 알고리즘이 아니고 실제로 가비지 컬렉터가 객체를 수거해가기 직전에 호출되는 메서드일 뿐이다. 그렇기 때문에 실제로 가비지 컬렉터가 수행되지 않으면finalzie도 수행되지 않는 것이다.
finalizer와 cleaner에게 파일 닫기를 맡기면 파일이 계속 열려 있을 수 있게되어서 새 파일 열기를 실패할 수 있다.finalizer와 cleaner에 맡기게 되면 가비지 컬렉터가 돌 때까지 영원히 락이 유지되어서 공유 자원의 접근이 막혀서 전체 시스템이 멈출 수도 있다. finalizer 내부에서 예외가 발생해도 발생한 예외는 무시된다. 처리할 작업이 있어도 종료되어 버린다.cleaner는 사용자가 스레드를 어느정도 통제할 수 있기 때문에(Runnable.run() 메서드의 내부에서 구현 가능) 예외처리나 로그 등을 남길 수 있어서 이런 문제들이 발생하지 않는다.finalizer또는 cleaner가 달린 객체는 가비지 컬렉터가 처리할 때 수십 배 느려질 수 있다.finalize()를 오버라이드해서 정상적으로 생성되지 않은 객체를 이상하게 이용할 수 있다.final 클래스로 막거나, finalize()를 빈 메서드만들고 fianl로 선언해서 공격을 방어할 수 있다.자바에서 자원 해제를 안정적으로 하기 위해 AutoCloseable 인터페이스를 구현하는 방법이 있다.
private boolean closed = false; // 닫혔는지 추적
@Override
public void close() {
if (!closed) {
// 여기서 자원 해제 로직(파일 닫기, 소켓 닫기 등)
closed = true;
}
}
public void doSomething() {
if (closed) {
throw new IllegalStateException("이미 닫힌 자원입니다.");
}
// 실제 작업 로직...
}
}
try-with-resources 구문을 사용할 수 있다.closed 필드를 통해서 이미 닫힌 상태인지를 추적하면, 닫힌 후에 잘못 사용하려고하더라도 IllegalStateException 같은 예외를 던져서 버그를 빨리 발견할 수 있게 된다.try (MyResource r = new MyResource()) {
// r 사용
} // 스코프가 끝나고 r.close() 자동 호출
close() 메서드를 자동으로 호출하므로 가비지 컬렉션 시점에 의존하지 않아도 즉시 자원을 해제할 수 있다.close() 호출을 깜빡했을 때 뒤늦게라도 자원을 회수하려는 목적으로 사용하는 것이다.FileInputStream, ThreadPoolExecutor 등)도 이런 안전망 용도로 finalizer를 제공하고 있다.finalizer와 cleaner로 뒤늦게라도 처리하는 것이다.Room 코드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("Cleaning room");
numJunkPiles = 0;
}
}
// 방의 상태. cleanable과 공유한다.
private final State state;
// cleanable 객체. 수거 대상이 되면 방을 청소한다.
private final Cleaner.Cleanable cleanable;
public Room(int numJunkPiles) {
state = new State(numJunkPiles);
// this 객체가 가비지 컬렉터에 수거될 때, state.run()을 호출
cleanable = cleaner.register(this, state);
}
@Override public void close() {
cleanable.clean(); // 여기서 clean()을 호출하면 => State.run()이 바로 실행
}
}
}
Room 객체에 쓰레기 더미(State)가 있고, cleaner.register로 등록해둔다.close()를 직접 호출하면 즉시 cleanable.clean()을 실행하고 run() 메서드가 호출된다.close()를 깜빡하면 나중에 Room이 회수될 때(시점 불명) state.run()이 실행된다.State가 Room을 참조하면 서로 물고 있는 순환 참조가 되어 가비지 컬렉터가 수거할 수 없으므로, 정적 중첩 클래스로 만들어 Room 참조를 막아야 한다.public class Adult {
public static void main(String[] args) {
try (Room myRoom = new Room(7)) {
System.out.println("안녕!");
} // 스코프 끝나고 close() 호출 -> 방 청소 즉시 수행
}
}
public class Teenager {
public static void main(String[] args) {
new Room(99); // GC에게 맡김
System.out.println("아무렴");
}
}
close() 호출을 하지 않기 때문에 "아무렴"만 출력될 수도 있고 "방 청소"도 출력될 수도있다.System.gc()를 호출해도 반드시 "방 청소"를 출력할 수 있는 것이 아니다.try-with-resources 또는 AutoCloseable 방식을 사용하자.close()를 깜빡했을 때 뒷수습) 용도, 또는