본문 바로가기

카테고리 없음

[아이템 8] finalizer와 cleaner 사용을 피하라

 

자바에서 객체소멸은 가비지컬렉터가 담당하고, 비메모리자원회수는 try-with-resources, try-finally로 해결한다.

따라서 프로그래머에게 객체소멸을 위한 어떤 작업을 요구하지 않지만, 다음의 소멸자를 가지고있기는 하다.

 

  • finalizer: 예측할 수 없고 위험할 수 있다. 보통은 불필요하다.

자바 9부터 finalizer는 deprecated되어있다.

  • cleaner: finalizer보다는 덜 위험하지만 여전히 예측할 수 없고 느리다. 역시나 보통은 불필요하다.(자바 9)

 

 

예측할 수 없다?

호출된 후 언제 실행될지 알 수 없다. 즉, 제때 실행되어야 하는 작업은 절대 할 수 없다.

ex) 파일닫기

 

finalizer 스레드는 다른 애플리케이션 스레드보다 우선순위가 낮기 때문에 이에 의존하면 시스템 전체에 문제를 일으킬 수 있다. cleaner는 자신을 수행할 스레드를 제어할 수는 있지만, 역시나 가비지컬렉터에 의존하기 때문에 여전히 사용하지 않는편이 좋다.

 

수행여부?

접근할 수 없는 객체에 대한 종료 작업을 수행하지 못하고 프로그램이 중단될 수 있다. 즉, 상태를 영구적으로 수정하는 작업에 대해서는 finalizer나 cleaner에 의존하면안된다.

System.gc, System.runFinalization

finalizer, cleaner가 실행될 가능성은 높여줄 수 있지만 보장되지는 않는다.

 

System.runFinalizersOnExit, Runtime.runFinalizersOnExit 는 보장해주기는 하지만 다른 스레드가 소멸대상의 객체에 접근하고 있어도 실행해버린다는 매우 치명적인 결함이있다.

 

 

예외

finalizer 동작 중 예외가 발생해면 무시되고 처리할 작업이 남아있어도 종료된다. 경고도 출력되지 않는다

즉, 객체가 훼손될 수 있고 다른 스레드가 이 훼손된 객체에 접근하게 될 수도 있다.

cleaner는 스레드를 통제하기 때문에 위의 문제는 발생하지 않는다.

 

 

성능문제

finalizer와 cleaner는 가비지컬렉터의 효율을 떨어트리기 때문에 심각한 성능문제도 있다.

 

 

finalizer 공격에 노출되어 심각한 보안문제

생성자나 직렬화 과정에서 예외가 발생하면 finalizer가 수행되는데, 이 finalizer를 악의적으로 오버라이딩한 하위클래스의 finalizer가 수행될 수 있다. 심지어 이 finalizer를 정적필드에 할당하면 가비지컬렉터에의해 수거되지도 않는다.

해결책: finalizer를 final로 선언하여 오버라이딩하지 못하도록 한다.

 

관련 자바봄 이슈: https://github.com/Java-Bom/ReadingRecord/issues/7

 

[아이템8] finalizer를 사용한 클래는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수 있다.

이부분 뭔소린지 모르겠다. readObject랑 readResolve포함해서 설명 적어줘

github.com

 

 

대안 - AutoCloseable

파일이나 스레드 등 종료해야 할 자원에 AutoCloseable을 구현한다.

그리고 다 쓴 뒤 close 메서드를 호출한다.

  • 이때 예외가 발생하면 제대로 종료되도록 try-with-resources를 사용한다.
  • close 메서드 호출여부를 필드로 저장하자. 그리고 객체 사용 시 필드를 검사해서 이미 닫혔으면 Exception을 던지도록 구현하자.

 

 

그럼 finalizer와 cleaner는 언제쓸까?

  1. AutoCloseable를 구현하지 않았을 경우를 대비한 "안전망"역할

FileInputStream, FileOutputStream, ThreadPoolExecutor 는 안전망 역할의 finalizer를 제공한다

 

  1. 네이티브 피어와 연결된 객체
  • 네이티브피어란 네이티브 메서드를 통해 위임한 객체
  • 가비지컬렉터는 자바 객체가 아니기 때문에 GC 대상이 되지 못한다.

 

ex) 책예제: Autocloseable + Cleaner

Cleaner는 public API가 아니라는 점에서 finalizer과 차이가있다.

 

package Chap2_GenerateObjectAndDestroy.item8;

import java.lang.ref.Cleaner;

public class Room implements AutoCloseable{
    private static final Cleaner cleaner = Cleaner.create();

    // Room 을 참조하면 Room > State, State > Room 순환참조
    // 서로를 계속 참조하기 때문에 gc에의해 수거되지 않는다.
    // 정적클래스가 아니면 자동으로 바깥객체의 참조를 가짐. 그래서 정적중접클래
    private static class State implements Runnable{
        int numJunkPiles; // 수거대

        public State(final int numJunkPiles) {
            this.numJunkPiles = numJunkPiles;
        }

        @Override
        public void run() { // 1. close를 호출할 때, 2. cleaner(안전망)
            System.out.println("방청소");
            numJunkPiles = 0;
        }
    }

    private final State state;

    private final Cleaner.Cleanable cleanable;

    public Room(final int numJunkFiles) {
        this.state = new State(numJunkFiles);
        cleanable = cleaner.register(this, state); // Runnable 객체를 등록
    }

    @Override
    public void close() throws Exception {
        cleanable.clean();
    }
}
    @DisplayName("잘 짜인 자동청소")
    @Test
    void auto() throws Exception {
        try(Room myRoom = new Room(7)){
            System.out.println("Hello");
        }
        // 방청소 출력
    }