아이템 8. finalizer와 cleaner 사용을 피하라.
사실 2장에서 이 부분이 이해하기에 가장 까다로웠다.
자바에서는 두 가지 객체 소멸자를 제공한다. 그것이 바로 finalizer와 cleaner이다. 이 두 가지 객체 소멸자를 사용하면 명시적으로 바로 메모리를 해제할 수 있을까?
그건 아니다. 언젠가 해제될 순 있지만 곧바로 해제하는 것은 불가능하다.
finalizer는 예측할 수 없고 상황에 따라 위험할 수 있다. 예를 들어, 불완전한 객체가 생성되고 해당 객체의 하위 객체에서 finalizer를 구현하며, finalizer의 정적 필드에 자신의 참조를 할당하여 가비지 컬렉터가 수집하지 못하게 막을 수 있다.
cleaner는 Java9부터 finalizer의 대체제로 제시된다. cleaner는 finalizer보다는 덜 위험하지만 여전히 예측할 수 없으며 느리고 불필요하다.
간단히 알아보았는데, finalizer와 cleaner를 피해야하는 이유에 대해 몇 가지 더 알아보자.
1. 즉시 수행되지 않는다.
객체에 접근할 수 없게 된 다음, finalizer나 cleaner를 활용해서 파괴시키게 된다면 이는 언제 파괴될지 모른다. 예를 들어 File I/O 작업 실행한 상황에서 언제 자원이 회수될 지 모른다면, 열린 채로 대기하는 불필요한 자원이 쌓여 언젠간 문제를 발생시킬 것이다.
그럼 언제 실행될지는 아예 모르는 것인가?
이는 GC의 알고리즘에 달려있으며, 구현마다 다르다. 결과적으로는 finalizer와 cleaner의 수행 시점에 객체의 파괴 시점이 의존된다는 것은 문제이다.
2. 성능 문제를 일으킬 수 있다.
Autocloseable 객체를 생성하고 GC가 수거하기까지 12ns가 걸렸다면, finalizer의 경우에는 550ns가 걸린다. 직접 테스트한 것은 아니고 책에서 나온 결과를 인용하였다.
3. finalizer는 악의적인 공격에 노출될 수 있다.
finalizer로 어떻게 공격이 가능할까? finalizer는 객체를 생성할 때 취약점이 존재한다. 위에서도 잠깐 언급했지만, finalizer 내부에서 자신의 참조를 할당하게 된다면 GC에서 벗어나게 된다. 더불어 부분적으로 구성된 오브젝트 조차도 finalizer 내부에서 부활시킬 수 있다. 객체의 소멸에서 참조를 할당해버린다면 생성될 때 불완전한 조건으로 인해 완전히 생성되지 않은 오브젝트도 부활시킬 수 있다.
자세한 내용은 이 글을 참조하면 좋다.
그럼, finalizer와 cleaner는 언제 사용해야할까?
우선, 까먹고 자원의 회수를 잊은 경우에 대한 안전망으로써 활용할 수 있으며, 네이티브 피어 자원 회수용으로 사용하는 것이 좋다. 이와 같은 경우에도 불확실한 실행 시점과 성능 저하에 주의해야한다.
여기서 네이티브 피어란 일반적인 자바 코드가 아닌, C, C++과 같은 네이티브 언어로 작성된 메서드로 위임되므로 가비지 컬렉터에서 자원이 반납되는지 알 수 없는 코드를 말한다.