제리 devlog

[이펙티브자바] finalizer와 cleaner 사용을 피하라 본문

Java

[이펙티브자바] finalizer와 cleaner 사용을 피하라

제리 . 2020. 11. 17. 15:55

자바에서 제공하는 두 가지 객체 소멸자를 제공한다.

1. finalizer

2. cleaner

 

두 가지 객체 소멸자를 사용하면 c언어처럼 명시적으로 메모리를 해제할 수 있을까?

-> 곧바로 메모리를 해제하는 것은 불가능하다.

 

finalizer예측할 수 없고 상황에 따라 위험할 수 있어 일반적으로 불필요함. 나름에 쓰임새가 있지만 기본적으로는 쓰지 말아야 한다(자바9부터 deprecated)

 

cleaner는 자바9부터 finalizer를 대신 제시됨. cleaner는 finalizer보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요함

 

C++에서는 파괴자(destructor)가 존재한다. 특정 객체를 생성하는 생성자와 대척점으로 자원을 회수하는 보편적인 방법.

JAVA에서는 가비지 컬렉터에게 자원회수를 맡긴다. c++에서 파괴자를 사용한 자원 회수와 JAVA에서 자원 회수는 다름. 자바에서는 try-with-resource나 try-finally를 사용해 해결함

 

finalizer와 cleaner의 문제점

 

1. 즉시 수행된다는 보장이 없다. 

 

객체에 접근 할 수 없게 된 후 finalizer나 cleaner는 언제 실행될지 모름. 예를 들어 파일 닫기를 맡긴상황에서 언제 자원이 회수될지 모른다면 중대한 오류가 발생할 수 있다. 시스템이 동시에 열 수 있는 파일 개수에 한계가 있기 때문.

언제 실행될지는 전적으로 가비지 컬렉터 알고리즘에 달렸지만 가비지 컬렉터 구현 마다 천차만별. finalizer나 cleaner의 수행 시점에 의존되는 프로그램의 동작은 시한 폭탄이 된다.

 

저자의 경험에서 나온 예시로, GUI 애플리케이션이 OOM상황이 발생한 적이있었다. 그래픽스 객체 수천 개가 finalizer 대기열에서 회수만 기다리다가 발생한 것이다. finalizer 스레드는 다른 애플리케이션 스레드보다 우선 순위가 낮아서 실행될 기회를 제대로 얻지 못한 것.

 

자바 언어 명세에서 finalizer를 어떤 스레드가 수행할 것인지 명시하지 않았음. 결국, 해결방법은 사용하지않는 것. 그나마 cleaner는 자신을 수행할 스레드를 제어할 수 있음. 그러나 GC통제하에 언제 실행될지는 모름. 자바 언어 명세는 finalizer나 cleaner수행 시점 뿐만아니라 수행 여부조차 보장하지않음. 그래서 상태를 영구적으로 수정하는 작업에서는 절대 사용해서는 안됨. (DB에 lock해제를 finalizer나 clean에 맡기면 분산 시스템 전체가 서서히 멈출 것)

 

System.gc 나 System.runFinalization 메서드가 있긴하다. 이것은 실행 가능성을 높여줄 수는 있으나 보장해주지는 않음.

보장해주겠다는 메서드가 있긴했는데 심각한 결함으로(다른 스레드에서 동시에 조작할 때 호출되면 비정상적인 행동이나 데드락이 걸릴 수 있다함) deprecate됨

 

finalizer동작 중 예외상황 발생시 예외는 무시되고 처리할 작업이 남았어도 그 순간 종료됨. 잡지 못한 예외로 해당 객체는 마무리가 덜 된 상태로 남을 수 있고 다른 스레드가 훼손된 객체를 사용하다가 발생할 수 있는 문제가 추가로 생김

보통 예외라면 예외가 스레드를 중단하고 스택 trace로 내용을 출력하겠지만 finalizer는 경고조차 출력하지않음 . cleaner는 자신의 스레드를 통제하기 때문에 이러한 문제가 생기지는 않음

 

2. 심각한 성능문제

autoclosable객체를 생성하고 가비지 컬렉터가 수거하기까지 12ns가 걸렸는데 finalizer는 550ns가 걸림. finalizer를 사용한 객체를 생성하고 파괴하니 50배나 느렸음. cleaner도 클래스의 모든 인스턴스를 수거하는 형태로 사용하면 성능은 finalizer와 비슷하다고함 

 

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

생성자나 직렬화과정(readObject, readResolve)에서 예외가 발생하면 생성되다만 객체가 악의적인 하위 클래스의 finalizer가 수행될 수 있게한다. finalizer를 오버라이딩한다음 static 필드에 자신의 참조를 할당하면 GC가 수거하지 않음.

객체 생성을 막으려면 생성자에서 예외를 던지는 것으로 가능하지만 finalizer가 있다면 그렇지도 않다.

yangbongsoo.gitbook.io/study/finalizer-attack

 

Finalizer attack

 

yangbongsoo.gitbook.io

 

final클래스는 하위 클래스를 만들지않아 안전하다. final 클래스가 아니라면 아무일도 하지않는 finalizer메서드를 만들고 final로 선언하면된다.  

 

finalizer, cleaner의 역할은 누가?

결국 파일 자원이나 스레드등 종료할 자원을 담고있는 객체의 클래스에서 finalizer와 cleaner를 대신할 방법이 필요하다. 그저 autocloseable을 구현하면 된다. 클라이언트에서 사용이 종료되면 close()를 호출하면됨. 좀 더 확실히 하는 방법은 close메서드에 이 객체가 유효하지 않음을 필드에 기록하는 로직을 만들고 객체가 닫힌뒤 불렸다면 예외를 던지게 하면됨.  

 

 

그렇다면 finalizer, cleaner는 필요가 없나?

첫번째로 close메서드를 클라이언트가 호출하지 않을때 사용. 자원을 회수하지 않는것보다는 낫기때문. 하지만 그럴만한 가치가 있는지는 고민이 필요함. 이런 역할을 하는 자바 클래스는 FileInputStream, FileOutputStream, ThreadPoolExecutor가 대표적

 

두번째는 네이티브 피어(native peer)와 연결된 객체 네이티브 피어는 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체. 네이티브 피어는 자바 객체가 아니니 GC는 그존재를 모르고 자바 피어를 회수할 때 네이티브 객체까지 회수하지 못함. 이때 자원회수에 사용하면된다. 단, 성능 저하를 감당할 수 있고 네이티브 피어가 심각한 자원을 가지고 있지 않을 때 해당. 자원을 즉시회수하고 성능저하를 감당할 수 없다면 close메서드를 사용해야함 

 

 

public class Room implements AutoCloseable {
	private static final Cleaner cleaner = cleaner.create();
    
    //청소가 필요한 자원, 절대 Room을 참조해서는 안 된다!
    private static class State implements Runnable {
    	int numJunkPiles; //방 안의 쓰레기 수
        
        State(int numJunkPiles) {
        	this.numJunkPiles = numJunkPiles;
        }
        
        //close 메서드나 cleaner가 호출된다.
        @Override public void run() {
        	System.out.println("방 청소");
            numJunkPiles = 0;
        }
        
    }
    
    //방의 상태, cleanable과 공유한다.
 	private final State state;
    
    //cleanable 객체. 수거 대상이 되면 방을 청소한다.
    private final Cleaner.Cleanable cleanable;
    
    public Room(int numJunkPiles) {
    	state = new state(numJunkPiles);
        cleanable = cleaner.register(this, state);
    }
    
    @Override public void close() {
    	cleanable.clean();
    }       
}

State 클래스는 cleaner가 방을 청소할 때 수거할 자원을 담고 있음.(numJunkPiles) State는 Runnable을 구현하고 그 안의 run메서드는 cleanable에 의해 딱 한 번만 호출 될 것임. cleanable 객체는 Room생성자에서 Room과 state를 등록할 때 얻는다. run이 호출되는 상황은 보통 Room에서 close를 호출하거나 GC가 Room을 회수할 때까지 클라이언트가 close를 호출하지 않는다면, cleaner가 run 메서드를 호출해주기를 기대할 수 있다.

 

state 인스턴스는 절대 Room인스턴스를 참조하면 안된다. (순환 참조의 문제) GC가 인스턴스를 회수해 가지 않음. state가 static 중첩 클래스인 이유이기도 함. static이 아닌 중첩 클래스는 자동으로 바깥 객체를 참조하기 때문.

 

만약, 사용자가 try-with-resource구문을 사용하면 자동 청소는 전혀 필요하지 않다.(클라이언트가 잘 해주면됨)

 

 

 

gc가 메모리를 관리해주는데 굳이 close나 finalizer등을 사용해야하는 의문이 처음에 들었다. 여기서 close나 finalizer의 목적은 메모리가 아니라 시스템 자원이다. 자세한 내용은 아래 첨부

okky.kr/article/401102

 

OKKY | Close() 메소드에 대해서..

최근 Stream을 사용하면서 궁금한 것이 생겼습니다. 바로 Close()에 대한 내용인데요. 지금까지 객체를 사용 후 close() 라는 메소드를 호출 한 적이 없었는데  Stream은 열심히 close()를 해주고 있습니

okky.kr

 

-결론-

안전망 역할이나 중요하지 않은 네이티브 자원 회수용으로만 사용하자. 이런 경우에도 불확실 성과 성능 저하에 주의해야함

 

Comments