자바는 두 가지 객체 소멸자는 finalizer와 cleaner가 있다.

이때, finailzer는 예측할 수 없고, 상황에 따라 위험할 수 있어 쓰지 않는 것이 좋다. (오동작, 낮은 성능, 이식성 문제)

cleaner 또한 finalizer 보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.

 

1) finalizer와 cleaner는객체에 접근을 할 수 없게 된 후, 이 것이 실행되기까지 얼마가 걸릴지 알 수 없다. 심지어 수행 시점뿐 아니라 수행 여부조차 보장하지 않는다.

=> 즉, 제때 실행되어야 하는 작업은 절대 할 수 없다. (ex. file close, db 공유자원의 영구 lock 해제 등의 인스턴스 자원 회수)

왜냐면 finalizer와 cleaner은 GC에 의해 실행되고, GC 알고리즘마다 다르게 실행되기 때문이다.

 

자바 언어 명세는 어떤 스레드가 finalizer를 수행할지 명시하지 않는 반면, cleaner는 자신을 수행할 스레드를 제어할 수 있다는 면에서 조금 낫다. 하지만 여전히 백그라운드에서 수행되며 가비지 컬렉터의 통제하에 있으니 즉각 수행되리라 는 보장은 없다.

 

item07에서 예시로 사용해 보았던

`System.gc` 와 `System.runFinalization` 또한 gc를 건드려서 finalizer와 cleaner가 실행 될 가능성을 높여줄 수는 있으나, 보장해주진 않는다.

(단, `System.runFinalizersOnExit`와 그 쌍둥이 인 `Runtime. runFinalizersOnExit` 다. 하지만 이 두 메서드는 심각한 결함 때문에 수십 년간 지탄받아 왔다.[ThreadStop])

  • System.gc
    • 명시적으로 가비지 컬렉션이 일어나도록 하는 코드이다
    • 모든 스레드가 중단되기 때문에 코드단에서 호출하면 안된다
  • System.runFinalization
    • method는 종료 대기중인 모든 개체의 종료 방법을 실행하지만, 언제 실행될지/실행될지 여부는 보장하지 못한다
  • System.runFinalizersOnExit & Runtime.runFinalizersOnExit
    • 프로세스가 끝날 때 수거하지 않은 개체를 수거하도록 설정한다

 

 

2) finalizer 동작 중 발생한 예외는 무시되고, 처리할 작업이 남았더라도 그 순간 종료된다

=> 만약 다른 스레드가 이처럼 훼손된 객체를 사용하려 한다면 어떻게 동작할지 예측할 수 없다.

일반적으로는 잡지 못한 예외가 스레드를 중단시키고 스택 추적 내역을 출력하겠지만, finalizer에서는 경고조차 출력하지 않는다.

c.f.) cleaner는 자신의 스레드를 통제하기 때문에 이 문제에 대해서는 괜찮다.

 

 

3) finalizer와 cleaner는 심각한 성능 문제도 동반한다

=> finalizer가 가비지 컬렉터의 효율을 떨어뜨리기 때문이다

 

 

4) finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수도 있다.

finalizer 공격 원리는 생성자나 직렬화 과정 (readObject와 readResolve 메서드)에서 예외가 발생하면,

이 생성이 되다 만 객체에서 악의적인 하위 클래스의 finalizer가 수행될 수 있게 된다.

=> 이 경우 finalizer는 정적 필드에 자신의 참조를 할당하여 가비 지 컬렉터가 수집하지 못하게 막을 수 있다.

해결 방법1 : 클래스를 final로 만들자(final 클래스들은 그 누구도 하위 클래스를 만들 수 없기 때문)
해결 방법2: final이 아닌 클래스를 finalizer 공격으로부터 방어하려면 아무 일도 하지 않는 finalize 메서드를 만들고 final로 선언

 

 


finalizer나 cleaner를 대신 할 수 있는 방법은 무엇이 있을까?

 

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

(일반적으로 예외가 발생해도 제대로 종료되도록 try-withresources를 사용해야 한다. 아이템 9 참조).

추가적으로 각 인스턴스는 자신이 닫혔는지 추적하는 것이 좋다. 즉, close 메서드에서 이 객체는 더 이상 유효하지 않음을 필드에 기록하고, 다른 메서드는 이 필드를 검사해서 객체가 닫힌 후에 불렸다면 IllegalStateException을 던지록 하자.


그렇다면 finalizer이나 cleaner를 써야하는 경우는 언제일까?

1) 자원의 소유자가 close 메서드를 호출하지 않는 것에 대비한 안전망 역할로서 사용할 때

cleaner나 finalizer가 즉시 (혹은 끝까지) 호출되리라는 보장은 없지만, 클라이언트가 하지 않은 자원 회수를 늦게라도 해주는 것이 아예 안 하는 것보다는 낫기 때문이다. 그래도 이런 안전망 역할 의 finalizer를 작성할 때는 그럴만한 값어치가 있는지 생각해 보아야 한다.

 

ex) 안전망 역할의 finalizer: FileInputStream, FileOutputStream, ThreadPoolExecutor

(finalize는 java9부터 deprecated 되어서 java11을 사용하는 나는 메소드 내부는 볼 수 없었다)

  • FileInputStream & FileOutputStream: Ensures that the `close` method of this file stream is called when there are no more references to it.

  • ThreadPoolExecutor: Invokes shutdown when this executor is no longer referenced and it has no threads.

@Override
protected void finalize() throws Throwable {
	try {
		... // cleanup subclass state
	} finally {
		super.finalize();
	}
}

기본적으로는 deprecated된 Object.finalize()를 위와 같이 오버라이드 하고 있다.

docs.oracle.com/javase/9/docs/api/java/lang/Object.html#finalize--

 

Object (Java SE 9 & JDK 9 )

Called by the garbage collector on an object when garbage collection determines that there are no more references to the object. A subclass overrides the finalize method to dispose of system resources or to perform other cleanup. The general contract of fi

docs.oracle.com

 

 

c.f. finalizer 와 달리 cleaner는 클래스의 public API에 나타나지 않는다

// 코드 8-1 cleaner를 안전망으로 활용하는 AutoCloseable 클래스
public class Room implements AutoCloseable {
	private static final Cleaner cleaner = Cleaner.create();

	/* static으로 선언된 중첩 클래스인 State는 cleaner가 방을 청소할 때 수거할 자 원들을 담고 있다. */
	// 청소가 필요한 자원.절대 Room을 참조해서는 안 된다! 만약 참조한다면 순환참조가 생김
	private static class State implements Runnable {
		int numJunkPiles; // 방(Room) 안의 쓰레기 수 => 수거할 자원

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

		// close 메서드나 cleaner가 호출한다. => cleanable에 의해 딱 한 번만 호출된다
		@Override
		public void run() {
			System.out.println("방 청소");
			numJunkPiles = 0;
		}

	}

	// 방의 상태. cleanable과 공유한다.
	private final State state;

	// cleanable 객체. 수거 대상이 되면 방을 청소한다. => Room 생성자에서 cleaner에 Room과 State를 등록할 때
	private final Cleaner.Cleanable cleanable;

	public Room(int numJunkPiles) {
		state = new State(numJunkPiles);
		cleanable = cleaner.register(this, state);
	}

	@Override
	public void close() {
		cleanable.clean();
	}

}

run 메서드가 호출되는 상황은 둘 중 하나다. 

1 - 보통은 Room의 close 메서드를 호출할 때이다. close 메서드에서 Cleanable의 clean을 호출하면 이 메서드 안에서 run을 호출

2 - 가비지 컬렉터가 Room을 회수할 때 까지 클라이언트가 close를 호출하지 않는다면, cleaner가 State.run 메서드를 호출

 

이 때, State가 정적 중첩 클래스인 이유는, 정적이 아닌 중첩 클래스는 자동으로 바깥 객체의 참조를 갖게 되기 때문에 GC가 회수할 수 없다.

이와 유사하게 람다 역시 바깥 객체의 참조를 갖기 쉬우니 사용하지 않는 것이 좋다.

 

/* 올바른 예제 */
class Adult {
	public static void main(String[] args) {
		try (Room myRoom = new Room(7)) { // "안녕" "방청소" 순서대로 출력
			System.out.println("안녕〜");
		}
	}
}

/* 틀린 예제 - System.exit을 호출할 때의 cleaner 동작은 구현하기 나름이다. 청소가 이뤄질지는 보장 하지 않는다 */
public class Teenager {
	public static void main(String[] args) {
		new Room(99);
		System.out.println("아무렴"); // "아무렴" 출력 이후 아무 일도 일어나지 않는다 => 예측할 수 없는 상황
	}
}

 

 

2) 네이티브 피어와 연결된 객체에서 사용할 때(단, 성능 저하를 감당할 수 있고 네이티브 피어가 심각한 자원을 가지고 있지 않은 경우)

  • 네이티브 피어(native peer)
    • 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체
    • 네이티브 피어는 자바 객체 가 아니니 가비지 컬렉터슨 그 존재를 알지 못한다. 그 결과 자바 피어를 회수 할 때 네이티브 객체까지 회수하지 못한다.

=> 성능 저하를 감당할 수 없거나 네이티브 피어가 사용하는 자원을 즉시 회수해야 한다면 `close` 메서드를 사용해야 한다.

 

 

+ Recent posts