ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Finalizer와 Cleaner는 피하라
    JAVA/Effective java 2021. 1. 3. 23:16

     

    Finalizer는 예측 불가능하고, 위험하며, 대부분 불필요하다.


    • 그걸 쓰면 이상하게 동작하기도 하고, 성능도 안 좋아지고, 이식성에도 문제가 생길 수 있다.
    • Finalizer를 유용하게 쓸 수 있는 경우는 극히 드물다.
    • finalizer는 GC가 돌때 호출이 되는데 언제 호출되는지 예측할 수 없다. GC에 대상이 된다고 해서 바로 GC가 되는 것이 아니기 때문이다.

     

    • 다음의 코드는 run() 실행 후 1초 있다가 종료되게 된다.
    • run() 호출후의 finalizerExample의 레퍼런스는 유효하지 않게 되지만 실행해보면 Clean Up을 실행하지는 않는다.
    • GC에 대상은 되지만 바로 되지는 않는다.

     

     

     

    그럼 언제 써야 될까?


    딱 두가지 경우

    • 안전망 역할로 자원을 반납하고자 하는 경우.
    • 네이티브 리소스를 정리해야 하는 경우.

     

    • 일단 자바 9에서는 Finalizer가 deprecated 됐으면 Cleaner라는게 새로 생겨서 별도의 쓰레드를 사용하기 때문에 Finalizer 보다 덜 위험하지만, 여전히 예측 불가능하며, 느리고, 일반적으로 불필요하다.
    • C++에서의 destructor랑은 다르다고 한다. C++의 destructor는 어떤 객체와 연관이는 자원을 반납할 때도 사용하지만, 자바에서는 try-with-resources또는 try-finally 블록이 그 역할을 한다.

     

     

    단점 1


    • 언제 실행될지 알 수 없다.
    • 어떤 객체가 더 이상 필요 없어진 시점에 그 즉시 finalizer 또는 cleaner가 바로 실행되지 않을 수도 있다.
    • 그 사이에 시간이 얼마나 걸릴지는 아무도 모른다.

     

    • 따라서 타이밍이 중요한 작업을 절대로 finalizer나 cleaner에서 하면 안 된다. 
    • 예를 들어, 파일 리소스를 반납하는 작업을 그 안에서 처리한다면, 실제로 그 파일 리소스 처리가 언제 될지 알 수 없고, 자원 반납이 안돼서 더 이상 새로운 파일을 열 수 없는 상황이 발생할 수도 있다.

    단점 2


    • 특히 Finalizer는 인스턴스 반납을 지연시킬 수도 있다.
    • Finalizer 스레드는 우선순위가 낮아서 언제 실행될지 모른다.
    • 따라서, Finalizer 안에 어떤 작업이 있고, 그 작업을 스레드가 처리 못해서 대기하고 있다면, 해당 인스턴스는 GC가 되지 않고 계속 쌓이다가 결국엔 OutOfMomoryException이 발생할 수도 있다.

     

    • Clenaer는 별도의 스레드로 동작하니까 이 부분에 있어서 해당 스레드의 우선순위를 높게 주는 등 조금은 나을 수도 있지만 여전히 해당 스레드는 백그라운드에서 동작하고 언제 처리될지는 알 수 없다.

     

    단점 3


    • 즉시 실행되지 않는다는 건 차치하고 Finalizer나 Cleaner를 아예 실행하지 않을 수도 있다.
    • 따라서, Finalizer나 Cleaner로 저장소 상태를 변경하는 일을 하지 말라. 
    • 데이터베이스 같은 자원의 락을 그것들로 반환하는 작업을 한다면 전체 분산 시스템이 멈춰 버릴 수도 있다.

     

    • System.gc나 System.runFinalization에 속지 말라.
    • 그걸 실행해도 Finalizer나 Cleaner를 바로 실행한다고 보장하진 못한다. 그걸 보장해주겠다고 만든 System.runFinalizersOnExit와 그 쌍둥이 Runtime.runFinalizersOnExit은 오랫동안 deprecated 상태라고 한다.

     

     

    단점 4


    • 심각한 성능 문제도 있다. 
    • AutoCloseable 객체를 만들고, try-with-resource로 자원 반납을 하는데 걸리는 시간은 12ns인데 반해, Finalizer를 사용한 경우에 550ns가 걸렸다.
    • 약 50배가 걸렸다. Cleaner를 사용한 경우에는 66ns가 걸렸다 약 5배.

     

     

    단점 5


    • Finalizer 공격이라는 심각한 보안 이슈에도 이용될 수 있다.
    • 어떤 클래스가 있고 그 클래스를 공격하려는 클래스가 해당 클래스를 상속받는다. 그리고 그 나쁜 클래스의 인스턴스를 생성하는 도중에 예외가 발생하거나, 직렬화 할 때 예외가 발생하면, 원래는 죽었어야 할 객체의 finalizer가 실행될 수 있다.
    • 그럼 그 안에서 해당 인스턴스의 레퍼런스를 기록할 수도 있고, GC가 되지 못하게 할 수도 있다.

     

    • 또한 그 안에서 인스턴스가 가진 메서드를 호출할 수도 있다.
    • 원래는 생성자에서 예외가 발생해서 존재하질 않았어야 하는 인스턴스인데, Finalizer 때문에 살아남아 있는 것이다.

    • Final 클래스는 상속이 안되니까 근본적으로 이런 공격이 막혀 있으며, 다른 클래스는 finalize() 메서드에 final 키워드를 사용해서 상속해서 오버 라이딩하는 것을 만을 수 있다.

     

     

    자원 반납하는 방법


    자원 반납이 필요한 클래스 AutoCloseable 인터페이스를 구현하고 try-with-resource를 사용하거나, 클라이언트가 close 메서드를 명시적으로 호출하는 게 정석이다.

     

     

    Try-with-resources 사용


    try-with-resources 사용

     

     

     

    • 여기서 추가로 언급하자면, close 메서드는 현재 인스턴스의 상태가 이미 종료된 상태인지 확인하고, 이미 반납이 끝난 상태에서 close가 다시 호출됐다면 IllegalStateException을 던져야 한다.

     

     

     

    Finalizer와 Cleaner를 안전망으로 쓰기


    • 자원 반납에 쓸 close 메서드를 클라이언트가 호출하지 않았다는 가정하에, 물론 실제로 Finalizer나 Cleaner가 호출될지 안될지 언제 호출될지도 모르긴 하지만, 안 하는 것보다는 낫기 때문이다.
    • 실제로 자바에서 제공하는 FileInputStream, FileOutputStream, ThreadPoolExecutor 그리고 java.sql.Connection에는 안전망으로 동작하는 finalizer가 있다.

     

     

    네이티브 피어 정리할 때 쓰기


    • 자바 클래스 -> 네이티브 메서드 호출 -> 네이티브 객체 (네티 이브 Peer)
    • 네이티브 객체는 일반적인 객체가 아니라서 GC가 그 존재를 모른다.
    • 네이티브 피어가 들고 있는 리소스가 중요하지 않은 자원이며, 성능상 영향이 크지 않다면 Cleaner나 Finalizer를 사용해서 해당 자원을 반납할 수도 있을 것이다.
    • 하지만, 중요한 리소스인 경우에는 위에서 언급한 대로 close 메서드를 사용하는 게 좋다.

     

     

    Cleaner 예제 코드


    0

     

     

     

    주의할 점


    • Cleaner 스레드(CleanerRunner)는 정리할 대상인 인스턴스 (CleanerSample)을 참조하면 안 된다. 순환 참조가 생겨서 GC 대상이 되질 못한다.
    • Cleaner 스레드를 만들 클래스는 반드시 static 클래스여야 한다. non-static 클래스(익명 클래스도 마찬가지)의 인스턴스는 그걸 감싸고 있는 클래스의 인스턴스를 잠조하지 않는다.

     

    참고 자료 


    이펙티브 자바

Designed by Tistory.