ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 8: Avoid finalizers and cleaners
    독서/Effective Java 2021. 12. 5. 06:10

    finalizer와 cleaner를 피하라

     

    finalizer와 cleaner

    Java에서는 GC 대상이 될 때 소멸 메소드를 호출하는 finalizer, cleaner를 제공한다.

    Finalizer: 예측할 수 없고 종종 위험하며 일반적으로 불필요하다. 오동작, 성능 저하 및 이식성 문제의 원인이 될 수 있다. 몇 가지 유효한 용도가 존재하나, 일반적으로는 사용하지 말아야 한다. Java 9부터 deprecated되어 더 이상 사용되지 않는다.

    cleaner: Java 9 부터 finalizer를 대체. Finalizer보다는 덜 위험하지만 여전히 예측할 수 없고 느리고 일반적으로 불필요

     

    finalizer와 cleaner의 부작용

    • finalizer와 cleaner는 즉시 수행된다는 보장이 없다.
      • 객체에 접근할 수 없게 된 후 finalizer나 cleaner가 실행되기까지 얼마나 걸릴지 알 수 없다.
      • 즉, finalizer와 cleaner로는 제때 실행되어야 하는 작업은 절대 할 수 없다.
    • finalizer나 cleaner를 얼마나 신속히 수행할지는 전적으로 GC 알고리즘에 달렸으며, 이는 가비지 컬렉터 구현마다 천차만별이다.
    • 자바 언어 명세에서는 두 소멸자의 수행 시점뿐만 아니라 수행 여부조차 보장하지 않는다.
      • 따라서 상태를 영구적으로 수정하는 작업에서는 finalizer나 cleaner에 의존해서는 안된다.
    • finalizer 동작 중 발생한 예외는 무시되며, 처리할 작업이 남았더라도 그 순간 종료된다.
      • 잡지 못한 예외 때문에 해당 객체는 마무리가 덜 된 상태로 남을 수 있습니다.
    • finalizer와 cleaner는 가비지 컬렉터의 효율을 떨어뜨리기 때문에 심각한 성능 문제도 야기한다.
    • finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수도 있다.
      • 생성자나 직렬화 과정에서 예외가 발생하면, 이 생성되다 만 객체에서 악의적인 하위 클래스의 finalizer가 수행될 수 있게 됩니다. 이 finalizer는 정적 필드에 자신의 참조를 할당하여 가비지 컬렉터가 수집하지 못하게 막을 수 있습니다.final 클래스들은 하위 클래스를 만들 수 없으니 이 공격으로부터 안전합니다.
      • final이 아닌 클래스를 finalizer 공격으로부터 방어하려면 아무 일도 하지 않는 finalize 메서드를 만들고 final로 선언해야 합니다.
      • 이 객체는 메서드 호출을 통해 애초에는 허용되지 않았을 작업을 수행하게 할 수 있게 됩니다.

     

    finalizer, cleaner를 대신하는 방법

    AutoCloseable을 구현하고, 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출해 사용하자.

    예외가 발생해도 제대로 종료될 수 있도록 try-catch-resources(Item 9)를 사용해야 한다.

     

    finalizer와 cleaner의 쓰임새

    • 자원의 소유자가 close() 메서드를 호출하지 않는 것에 대비한 안전망 역할을 한다.
      • finalizer나 cleaner가 언제 호출될지 보장할 수 없지만, 클라이언트가 하지 않은 자원 회수를 늦게라도 하는 것이 낫다.
    • 네이티브 피어(native peer)와 연결된 객체에서 사용한다.
      • 네이티브 객체는 자바 객체가 아니므로 가비지 컬렉터가 그 존재를 알지 못하고,
        이 때문에 가비지 컬렉터가 네이티브 객체 자원을 회수하지 못한다.
      • 이러한 상황을 방지하기 위해 finalizer나 cleaner가 사용된다.
      • 단, 성능 저하를 감당할 수 있거나 네이티브 피어가 심각한 자원을 가지고 있지 않을 때에만 해당한다.
        만약 그렇지 않다면 AutoCloseable의 close() 메서드를 사용하자.
    더보기

    네이티브 피어란 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체를 말한다. 

    자바 클래스 -> 네이티브 메소드 호출 -> 네이티브 객체 (네이티브 Peer)

     

    cleaner 예시


    아래 코드는 Room 자원을 수거하기 전에 쓰레기들을 청소(clean)해야 한다는 로직을 품고 있는 코드다.

    // An autocloseable class using a cleaner as a safety net
    public class Room implements AutoCloseable {
        private static final Cleaner cleaner = Cleaner.create();
        // Resource that requires cleaning. Must not refer to Room!
        private static class State implements Runnable {
            int numJunkPiles; // Number of junk piles in this room
            State(int numJunkPiles) {
                this.numJunkPiles = numJunkPiles;
            }
            // Invoked by close method or cleaner
            @Override public void run() {
                System.out.println("Cleaning room");
                numJunkPiles = 0;
            }
        }
        // The state of this room, shared with our cleanable
        private final State state;
        // Our cleanable. Cleans the room when it’s eligible for gc
        private final Cleaner.Cleanable cleanable;
        public Room(int numJunkPiles) {
            state = new State(numJunkPiles);
            cleanable = cleaner.register(this, state);
        }
        @Override public void close() {
            cleanable.clean();
        }
    }

    static으로 선언된 중첩 클래스인 State는 cleaner가 방을 청소할 때 수거할 자원(numJunkPiles)들을 담고 있다. State는 Runnable을 구현하고 있고, run() 메서드가 cleanable에 의해 한 번만 호출된다.
    run()은 Room의 close() 메서드를 호출하거나 가비지 컬렉터가 Room을 회수할 때까지 클라이언트가 close()를 호출하지 않는다면, cleaner가 State의 run()을 호출(보장되지 않음)할 것이다.
    주의: 여기서 State 인스턴스는 절대로 Room 인스턴스를 참조하면 안된다. Room 인스턴스를 참조할 경우 순환참조가 생기기 때문에 가비지 컬렉터가 Room 인스턴스를 회수할 수 없다. 그래서 State 클래스가 정적 중첩 클래스인 것이고, 정적이 아닌 중첩 클래스는 자동으로 바깥 객체의 참조를 가질 수 있게 된다.

    public class Adult {
      public static void main(String[] args) {
        try (Room myRoom = new Room(7)) {
         System.out.println("Goodbye");
        }
      }
    }

    안전망 내에서 사용한 경우 예상대로 Adult 프로그램을 실행하면 "Goodbye" -> "Cleaning room" 이 출력된 후 종료된다.
    해당 상황이라면?

    public class Teenager {
      public static void main(String[] args) {
        new Room(99);
        System.out.println("Peace out");
      }
    }

    안전망이 없다면 "Peace out"을 출력한 다음 청소실을 출력할 것으로 예상했지만 실행 중 "Cleaning room"을 출력하지 않았다.
    그냥 종료되고, 이것은 우리가 앞에서 말한 예측 불가능성이다.
     

    정리

    안전망이나 중요하지 않은 기본 리소스를 종료하기 위한 경우 제외 cleaner와 finalizer 사용 X
    사용할 때에는 불확정성과 성능 결과에 주의해야 한다.

     

    댓글

Designed by Tistory.