반응형
자바는 두 가지 객체 소멸자를 제공
- `finalizer`
- finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요
- 오동작, 낮은 성능, 이식성 문제의 원인이 되기도 함
- 자바 9에서는 finalizer를 deprecated API로 지정
- 기본적으로 `쓰지 말아야`한다
- `cleaner`
- finalizer보다 덜 위험하다
- 예측할 수 없다 (언제 실행되는지)
- 느리고, 일반적으로 불필요
`finalizer와 cleaner의 문제점
- 불확실성 (수행 보장 X)
- 자바에서는 접근할 수 없게 된 객체를 회수하는 역할을 GC가 담당
- finalizer나 cleaner를 수행할지는 적적으로 `GC 알고리즘`에 달렸고, 구현마다 천차만별
- 객체에 접근할 수 없게 된 후 finalizer나 cleaner가 실행되기까지 얼마나 걸릴지 알 수 없다
- 자원회수가 지연되어 `OutOfMemoryError`와 같은 문제가 발생하기도 함
- 자바 언어 명세에는 finalizer나 cleaner의 수행 시점뿐만 아니라 수행 여부조차 보장 X
- finalizer와 cleaner로는 제때 실행되어야 하는 작업은 절대 할 수 없다
- 프로그램 생애주기와 상관없는, 상태를 영구적으로 수정하는 작업에서는 절대 finalizer나 cleaner에 의존해서는 안 된다.
- 자바에서는 접근할 수 없게 된 객체를 회수하는 역할을 GC가 담당
- 예외 발생 시, 예외를 무시 후 바로 종료
- finalizer 동작 중 발생한 예외는 무시되고, 처리할 작업이 남았더라도 그 순간 종료
- 잡지 못한 예외 때문에 해당 객체는 자칫 마무리가 덜 된 상태로 남을 수 있음
- 훼손된 객체는 어떻게 동작할지 예측할 수 없음
- 보통의 경우엔 잡지 못한 예외가 스레드를 중단시키고 스택 추적 내역을 출력하겠지만, finalizer에서는 경고조차 출력되지 않음
- cleaner를 사용하는 라이브러리는 자신의 스레드를 통제하기 때문에 이러한 문제가 발생하지는 않음.
- finalizer 동작 중 발생한 예외는 무시되고, 처리할 작업이 남았더라도 그 순간 종료
- 성능 문제
- finalizer와 cleaner를 사용하여 객체를 생성하고 파괴하는 것은 심각한 성능 문제를 동반
- AutoCloseable 객체를 생성하고 GC가 수거하기까지 12ns가 걸림(try-with-resources로 자신을 닫음)
- finalizer, cleaner를 사용하면 550ns, 500ns정도 걸림
- 안전망 방식을 사용하면 약 66ns가 걸림
- 안전망을 설치하는 대가로 성능이 약 5배 정도 느려진다는 뜻
- finalizer와 cleaner를 사용하여 객체를 생성하고 파괴하는 것은 심각한 성능 문제를 동반
- 보안 문제
- finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제가 일어날 수 있음
- 생성자나 직렬화 과정(readObject와 readResolve 메서드)에서 예외가 발생하면, 이 생성되다 만 객체에서 악의적인 하위 클래스의 finalizer가 수행될 수 있게 됨
- 이 finalizer는 정적 필드에 자신의 참조를 할당하여 GC가 처리하지 못하도록 할 수 있음
- 이런 일그러진 객체가 만들어지면, 이 객체의 메서드를 호출해 허용되지 않는 작업을 수행하게 할 수 있음
- 객체 생성을 막으려면 생성자에서 예외를 던지면 되지만 finalizer가 있다면 그렇지 않다.
- final 클래스들은 그 누구도 하위 클래스를 만들 수 없으니 이 공격에서 안전
- final이 아닌 클래스의 경우 아무 일도 하지 않는 finalizer 메서드를 만들고 final로 선언하자
- 생성자나 직렬화 과정(readObject와 readResolve 메서드)에서 예외가 발생하면, 이 생성되다 만 객체에서 악의적인 하위 클래스의 finalizer가 수행될 수 있게 됨
- finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제가 일어날 수 있음
AutoCloaseable을 구현해 주고 finalizer나 cleaner를 대신하여 사용하자
- 클라이언트에서 인스턴스를 종료할 때 `close`메서드를 호출
- 일반적으로 예외가 발생해도 제대로 종료되도록 `try-with-resources`를 사용해야 한다
- 각 인스턴스는 자신이 닫혔는지를 추적하는 것이 좋다
- `close` 메서드에서 이 객체는 더 이상 유효하지 않음을 필드에 기록
- 다른 메서드는 이 필드를 검사해서 객체가 닫힌 후에 불렀다면 IllegalStateException을 던짐
finalizer와 cleaner의 쓰임새
- 자원의 소유자가 `close`메서드를 호출하지 않는 것에 대비한 안전망 역할
- cleaner나 finalizer가 즉시 호출되리라는 보장은 없지만, 클라잉너트가 하지 않은 자원 회수를 늦게라도 해주는 것이 아예 안 하는 것보다는 낫다
- 네이티브 피어(native peer)와 연결된 객체에서의 사용
네이티브 피어란, 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체를 말한다.
네이티브 메서드(Native Method)는 C, C++와 같은 네이티브 프로그래밍 언어로 작성한 메서드를 말한다.
그리고 자바 프로그램에서 네이티브 메서드를 호출하는 기술을 JNI(Java Native Interface)라고 한다.
-
- 네이티브 피어는 자바 객체가 아니기 때문에 GC가 처리하지 못함
- 자바 피어를 회수할 때 네이티브 객체까지는 회수하지 못함
- cleaner나 finalizer가 나서서 처리하기에 적당한 작업
- 성능 저하와 즉시 회수되지 않아도 될 때에만 사용
- 그렇지 않다면 `close`메서드를 사용해야 한다
cleaner를 안전망으로 활용하는 AutoCloseable 클래스 예시
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 클래스
- 정적 중첩 클래스
- 자동으로 바깥 객체의 참조를 갖게 됨(GC의 처리를 받을 수 있음)
- cleaner가 방을 청소할 때 수거할 자원을 담고 있음
- Runnable을 구현하고, 그 안의 run 메서드는 cleanable에 의해 딱 한 번만 호출될 것
- State 인스턴스는 절대로 Room 인스턴스를 참조하면 안 된다
- 순환참조에 의해 GC가 처리하지 못하게 됨
위의 클래스를 사용하는 클라이언트 코드 예시
public class Adult {
public static void main(String[] args) {
try (Room myRoom = new Room(7)) {
System.out.println("안녕~");
}
}
}
- 앞서 말한 대로 cleaner는 단지 안전망으로만 쓰임
- Room의 생성을 `try-with-resources` 블록으로 감쌌다면 자동 청소(GC)는 전혀 필요하지 않다
위 프로그램은 "안녕~"을 출력하고 나서 "방 청소"를 출력할 것
`try-with-resources` 블록으로 감싸지 않은 코드 예시
public class Teenager {
public static void main(String[] args){
new Room(99);
System.out.println("아무렴");
}
}
"아무렴"에 이어 "방 청소"가 출력되리라 기대하겠지만, "방 청소"는 출력할 수도, 출력하지 않을 수도 있다.
앞에 말한 `예측할 수 없는` 상황이다
정리
- cleaner는 안전망 역할이나 중요하지 않은 네이티브 자원회수용으로만 사용하자
- 물론 이런 경우에도 불확실성과 성능 저하에 주의하여야 한다
`try-with-resources`를 사용해서 자원을 회수해 주는 게 제일 베스트인 것 같고, cleaner의 사용법을 잘 알고 사용해야 한다는 것을 전하는 것 같다.
반응형
'Java' 카테고리의 다른 글
자바 Type-Erasure (0) | 2023.04.30 |
---|---|
Item9 try-finally보다는 try-with-resources를 사용하라 (0) | 2023.02.02 |
Item7 다 쓴 객체 참조를 해제하라 (1) | 2023.01.12 |
Item6 불필요한 객체 생성을 피하라 (0) | 2022.12.09 |
Item5 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2022.12.07 |