자바는 더 이상 참조되지 않는 객체를 자동으로 수거해 자원을 해제하는 GC를 보유하고 있다.
그리고 앞으로 설명할 finalizer 와 cleaner는 객체를 소멸시키는 기능을 제공한다.
소멸이란 더 이상 유효하지 않은 해당 객체를 GC가 해당 객체를 수거하고 메모리를 해제하는 것을 의미한다.
교재에서는 이 finalizer 와 cleaner 의 사용은 일반적으로 불필요 하니 사용을 자제하라고 하는데 그 이유는 다음과 같다.
언제 호출될지 알 수 없다. (제때 자원 해제가 필요하다면 사용하면 안됨)
GC는 finalize()가 정의된 객체를 처리할 때 별도의 큐에 저장하는 등 추가적인 오버헤드가 발생하는 성능이슈가 있다.
finalize()에서 발생하는 예외는 무시된다.
(즉 예외가 발생해도 프로그램은 종료되지 않는다. 경고도 안뜬다. 디버깅 개빡세다는 뜻)
여전히 언제 호출될지 알 수 없다. (제때 자원 해제가 필요하다면 사용하면 안됨)
finalizer보다는 좋겠지만 그래도 성능적인 이슈가 발생한다.
예외를 좀 더 명시적으로 처리할 수 있다.
👨💻정리
finalizer의 문제점을 개선한 cleaner 또한, 결국에는 실행시점이 예측 불가능하고 성능 이슈가 있어 사용을 자제해야한다.
import java.io.FileWriter;
import java.io.IOException;
import java.lang.ref.Cleaner;
public class CleanerExample {
private static final Cleaner cleaner = Cleaner.create(); //Cleaner 객체 생성
//이너 클래스로 Runnable 작업 생성
//이 작업은 FileWriter의 자원을 해제하는 역할을 함
private static class State implements Runnable {
private FileWriter writer;
...생성자생략
@Override
public void run() {
if (writer != null) {
try {
writer.close(); // FileWriter 자원 해제
} catch (IOException e) {
System.err.println("자원해제 실패");
}
}
}
}
//자원를 해제(클린업) 할 작업을 등록 및 관리 , 실행하기 위한 객체
private final Cleaner.Cleanable cleanable;
public CleanerExample(String filePath) throws IOException {
FileWriter writer = new FileWriter(filePath);
State state = new State(writer); //FileWriter를 State에 주입
this.cleanable = cleaner.register(this, state); //해당 State를 클린 작업에 등록!
}
//쓰기 작업을 하는 메서드
public void writeToFile(String data) throws IOException {
State state = (State) cleanable.get();
state.writer.write(data); //state가 가진 FileWriter의 write로 쓰기 기능 구현
}
public void close() {
cleanable.clean(); // ★ 등록된 자원 해제 작업을 수행
}
public static void main(String[] args) {
try {
CleanerExample cleanerExample = new CleanerExample("example.txt");
cleanerExample.writeToFile("안녕하세요");
cleanerExample.close(); //과연 언제 파일이 닫힐까?????
} catch (IOException e) {
e.printStackTrace();
}
}
}
출력 결과
example.txt 파일에 "안녕하세요"를 쓴다.
그리고 clean() 메서드가 실행이 되어 cleaner에 등록한 state객체의 run 메서드가 실행되어
FileWriter 객체가 닫힌다.
try-with-resource는 사용한 자원을 자동으로 닫아주는 기능을 하며
try 블록이 끝나면 자동으로 자원이 닫힌다
try-with-resource는 AutoCloseable 인터페이스를 구현한 자원만 사용할 수 있는데
아래 FileWriter 나 FileReader 등은 해당 인터페이스를 구현하였으므로 사용이 가능하다.
그리고 성능도 try-with-resource가 finalizer 등에 비해 훨씬 개선된다.
import java.io.FileWriter;
import java.io.IOException;
public class TryWithResourcesExample {
public static void main(String[] args) {
try (FileWriter writer = new FileWriter("example.txt")) {
writer.write("Hello, try-with-resources!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
👨💻정리
Cleaner를 사용한 파일쓰기 예시의 문제점은 언제 run()은 GC시점에 호출되지 때문에 언제 자원이 해제될지 예측할 수가 없다. 게다가 별도의 해제 로직을 Runnable로 구현해야 해서 코드가 길어진다.
👨💻정리2
- 반대로 try-with-resource는 try 종료 시점에 자동으로 자원을 해제하니 즉시 해제가 가능하다.
단, AutoClosable 인터페이스를 구현한 클래스만 사용할 수 있다.- 참고로 AutoClosable의 close() 메서드를 오버라이딩 해야하며 try 종료시점에 close() 메서드가 자동 호출된다.
👨💻정리3
- 만약 DB의 lock해제 같은 즉시처리가 필요한 작업을 finalizer로 구현한다면?
자칫 , lock은 해제되지 않고 계속 잠기기만 하여 전체 시스템이 멈춰버릴 수도 있다..!
스레드 우선순위가 낮다는 말은 자원을 회수하는 작업이 다른 우선순위가 높은 작업에 의해 지연될 수 있다는 의미이다. Cleaner는 본인의 스레드를 제어할 수 있다고는 하지만 결국 문제는 언제 실행될지 모른다는 것이다.
finalizer 의 가장 큰 문제점은 "언제 실행될지 모른다는 것이다"
그럼 이걸 명시적으로 "실행해줘" 라고 하면 어떨까?
바로 System.gc() 와 System.runFinalizersOnExit() 가 해당 역할을 할 수 있다.
System.gc()
GC에게 가능한 빨리 실행하라고 요청한다. 하지만 즉시 처리된다는 보장은 없다.
System.runFinalization
JVM종료시점에 모든 finalizer 메서드를 실행하도록 한다.
하지만 종료시점에 강제로 finalizer를 실행하면 자원이 예상치 못한 시점에 해제되는 등 심각한 결함이 될 수 있어 현재는 deprecated 되었다.
👨💻정리
- 결국 자원 해제는 JVM에게 맡기는 것이 좋다.
- 아니면 try-with-resource 등으로 즉시 해제 가능하도록 코드를 작성한다.
- 자원의 사용범위를 줄인다. 👉(item7 참고)
만약 사용자가 자원을 쓰고 close() 처리를 하지 않았다면 해당 자원은 계속 누수 상태일 것이다.
최소한 close() 호출을 안하는 것보다는 안전빵으로 finalizer을 만들어놓아 해제 처리 되도록 한다는 것이다. (늦게라도 하는 것이 안하는 것보다는 좋다.)
네이티브란 java언어가 아니라 다른 프로그래밍 언어 (보통 C언어) 로 구현되어 JNI(java native Interface) 등을 통해 사용할 수 있게 해주는 기능이다.
네이티브 피어란 네이티브 코드에서 실제 작업을 수행하는 네이티브 객체를 의미한다.
중요한 점은 네이티브 피어는 네이티브 코드에서 관리되는 객체이므로 GC가 관여할 수 없다
이런 경우에 finalizer 등을 활용해볼 수 있겠다.
cleaner (자바 8까지는 finalizer) 는 안전망 역할이나 네이티브 자원 회수용으로 제한적으로 사용하자. (이때도 불확실성과 성능 저하를 신경써서 조심히 사용해야한다.)