자바의 특징 중 하나는 Garbage Collector를 이용한 메모리 공간 관리의 자동화다. 객체의 사용 여부를 JVM에서 추적하여 더 이상 사용되지 않는 객체라면 백그라운드에서 공간을 회수한다.
흥미로운 점은 GC 스레드가 객체의 공간을 회수 할 때, 객체의 finalize() 메서드를 호출해준다는 점이다. 다음 코드를 실행해보자.
public class FinalizeTest {
@Override
protected void finalize() throws Throwable {
System.out.println("finalize method is invoked");
super.finalize();
}
public static void main(String[] args) throws InterruptedException {
FinalizeTest test = new FinalizeTest();
// FinalizeTest 객체를 Unreference 상태로 만듦
test = null;
// GC 요청
System.gc();
Thread.sleep(1000L);
}
}
FinalizeTest 클래스에서 Object.finalize() 메서드를 재정의(Override) 했다. finalize() 메서드가 호출되면 "finalize method is invoked" 문자열이 찍히게 된다.
이 후 메인 메서드에서 gc를 유발하는 코드를 작성하고 실행했다.
finalize method is invoked
Process finished with exit code 0
GC 스레드에 의해서 finalize 메서드가 호출되고 예상했던 문자열이 찍혔다.
객체가 소멸할 때 호출된다는 특성 때문에 C++의 소멸자처럼 finalize() 메서드를 사용하는 경우도 더러 있다. 객체에서 사용하는 리소스들을 생성자(Constructor)에서 생성 및 설정하고 finalize() 메서드에서 해제하도록 짜는게 보기 좋을 수는 있다.
하지만 일반적으로 finalize() 메서드는 쓰지 말라고 권한다.
finalize() 메서드 단점
Effective Java 등 다양한 글에서 finalizer의 사용을 피하라고 권한다.
일단 finalize() 메서드가 신속하게 실행된다는 보장이 없다. 객체에 대한 참조가 사라지는 순간부터 finalize() 메서드가 호출되는 시점까지는 긴 시간이 소요될 수 있다. 얼마나 걸리는지는 JVM 종류와 GC 알고리즘에 따라 다르다. 어떤 스레드가 finalize() 메서드를 실행할지도 정확하게 정해져있지 않다.
심지어 finalize() 메서드의 실행이 보장되지도 않는다. 때문에 반드시 실행해야하는 코드는 finalize() 메서드에 포함하면 안된다.
finalize()에서 발생하는 예외는 무시가 된다. 따라서 로직이 불완전한 상태로 종료되어 문제를 발생시킬 수 있다.
마지막으로 GC 과정에서 수행하기 때문에 성능 저하가 발생할 수 있다. JVM 튜닝의 많은 부분이 GC로 인한 stop-the-world를 줄이기 위함일텐데, GC과정 중에 finalize() 메서드의 할일이 늘어나면 전체적인 성능 저하가 발생할 수 있다.
@Deprecated(since="9")
protected void finalize() throws Throwable { }
여러가지 단점으로 인해 Java 9부터는 finalizer() 메서드가 Deprecated 되었다.
객체와 엮여 있는 리소스의 정리는 Closable 인터페이스의 close() 메서드에 구현해두고, 객체의 사용이 끝났을 때 호출해주는 식으로 정리하는게 맞다. finalize() 메서드가 리소스의 정리를 담당하는게 아니라 반드시 정리되어야 하는 리소스의 해제를 한번 더 확인하는 용도까지만 써야한다.
댓글