1. 개요

이 예제에서는 루트 Object 클래스 에서 제공 하는 finalize 메소드인 Java 언어의 핵심 측면에 초점을 맞출 것 입니다.

간단히 말해서 이것은 특정 개체에 대한 가비지 수집 전에 호출됩니다.

2. 종료자 사용

마무리 () 메소드는 파이널 불린다.

JVM이 이 특정 인스턴스를 가비지 수집해야 한다고 판단하면 종료자가 호출됩니다. 이러한 종료자는 개체에 다시 생명을 불어넣는 것을 포함하여 모든 작업을 수행할 수 있습니다.

그러나 종료자의 주요 목적은 메모리에서 제거되기 전에 개체에서 사용하는 리소스를 해제하는 것입니다. 종료자는 정리 작업의 기본 메커니즘으로 작동하거나 다른 방법이 실패할 때 안전망으로 작동할 수 있습니다.

종료자의 작동 방식을 이해하기 위해 클래스 선언을 살펴보겠습니다.

public class Finalizable {
    private BufferedReader reader;

    public Finalizable() {
        InputStream input = this.getClass()
          .getClassLoader()
          .getResourceAsStream("file.txt");
        this.reader = new BufferedReader(new InputStreamReader(input));
    }

    public String readFirstLine() throws IOException {
        String firstLine = reader.readLine();
        return firstLine;
    }

    // other class members
}

Finalizable 클래스 에는 닫을 수 있는 리소스를 참조하는 필드 리더가 있습니다. 이 클래스에서 객체가 생성 되면 클래스 경로의 파일에서 읽는 BufferedReader 인스턴스를 생성합니다 .

이러한 인스턴스는 readFirstLine 메서드에서 지정된 파일의 첫 번째 줄을 추출하는 데 사용 됩니다. 주어진 코드에서 리더가 닫히지 않았음을 주목하십시오.

종료자를 사용하여 이를 수행할 수 있습니다.

@Override
public void finalize() {
    try {
        reader.close();
        System.out.println("Closed BufferedReader in the finalizer");
    } catch (IOException e) {
        // ...
    }
}

종료자가 일반 인스턴스 메서드처럼 선언되었음을 쉽게 알 수 있습니다.

실제로 가비지 수집기가 종료자를 호출하는 시간은 JVM의 구현 및 시스템 조건에 따라 달라지며 이는 우리가 통제할 수 없습니다.

가비지 수집이 그 자리에서 일어나도록 하기 위해 System.gc 방법 을 이용할 것입니다 . 실제 시스템에서는 다음과 같은 이유로 명시적으로 호출해서는 안 됩니다.

  1. 비싸다
  2. 가비지 수집을 즉시 트리거하지 않습니다. JVM이 GC를 시작하도록 하는 힌트일 뿐입니다.
  3. JVM은 GC를 호출해야 할 때를 더 잘 압니다.

강제 GC가 필요한 경우 jconsole사용할 수 있습니다 .

다음은 종료자의 동작을 보여주는 테스트 케이스이다.

@Test
public void whenGC_thenFinalizerExecuted() throws IOException {
    String firstLine = new Finalizable().readFirstLine();
    assertEquals("baeldung.com", firstLine);
    System.gc();
}

첫 번째 문에서 Finalizable 객체가 생성된 다음 해당 readFirstLine 메서드가 호출됩니다. 이 개체는 어떤 변수에도 할당되지 않으므로 System.gc 메서드가 호출 될 때 가비지 수집에 적합합니다 .

테스트의 어설션은 입력 파일의 내용을 확인하고 사용자 정의 클래스가 예상대로 작동하는지 증명하는 데만 사용됩니다.

제공된 테스트를 실행하면 종료자에서 닫히는 버퍼링된 판독기에 대한 메시지가 콘솔에 인쇄됩니다. 이는 finalize 메서드가 호출되어 리소스를 정리 했음을 의미합니다 .

이 시점까지 종료자는 사전 파괴 작업을 위한 좋은 방법처럼 보입니다. 그러나 그것은 사실이 아닙니다.

다음 섹션에서는 사용을 피해야 하는 이유를 살펴보겠습니다.

3. 종료자 피하기

그들이 가져오는 이점에도 불구하고 종료자에는 많은 단점이 있습니다.

3.1. 종료자의 단점

종료자를 사용하여 중요한 작업을 수행할 때 직면하게 될 몇 가지 문제를 살펴보겠습니다.

첫 번째 눈에 띄는 문제는 신속성 부족입니다. 가비지 수집은 언제든지 발생할 수 있으므로 종료자가 언제 실행되는지 알 수 없습니다.

종료자가 조만간 계속 실행되기 때문에 그 자체로는 문제가 되지 않습니다. 그러나 시스템 리소스는 무제한이 아닙니다. 따라서 정리 작업이 수행되기 전에 리소스가 부족할 수 있으며 이로 인해 시스템 충돌이 발생할 수 있습니다.

종료자는 프로그램의 이식성에도 영향을 미칩니다. 가비지 수집 알고리즘은 JVM 구현에 의존하기 때문에 프로그램은 한 시스템에서는 매우 잘 실행되지만 다른 시스템에서는 다르게 동작할 수 있습니다.

성능 비용은 종료자와 함께 제공되는 또 다른 중요한 문제입니다. 특히 JVM은 비어 있지 않은 종료자를 포함하는 객체를 생성하고 파괴할 때 더 많은 작업을 수행해야 합니다 .

우리가 이야기할 마지막 문제는 종료 중 예외 처리가 부족하다는 것입니다. 종료자가 예외를 throw하면 종료 프로세스가 중지되고 알림 없이 개체가 손상된 상태로 남습니다.

3.2. 종료자의 효과 시연

이론을 제쳐두고 실제 종료자의 효과를 볼 때입니다.

비어 있지 않은 종료자를 사용하여 새 클래스를 정의해 보겠습니다.

public class CrashedFinalizable {
    public static void main(String[] args) throws ReflectiveOperationException {
        for (int i = 0; ; i++) {
            new CrashedFinalizable();
            // other code
        }
    }

    @Override
    protected void finalize() {
        System.out.print("");
    }
}

finalize() 메서드를 주목하세요 . 빈 문자열을 콘솔에 출력합니다. 이 메서드가 완전히 비어 있으면 JVM은 종료자가 없는 것처럼 개체를 처리합니다. 따라서 이 경우 거의 아무것도 하지 않는 구현과 함께 finalize() 를 제공해야 합니다 .

기본 메서드 내에서 for 루프 의 각 반복에서 새로운 CrashedFinalizable 인스턴스가 생성됩니다 . 이 인스턴스는 어떤 변수에도 할당되지 않았으므로 가비지 수집에 적합합니다.

// 다른 코드표시된 줄에 몇 가지 명령문을 추가하여 런타임에 메모리에 얼마나 많은 객체가 있는지 확인합니다.

if ((i % 1_000_000) == 0) {
    Class<?> finalizerClass = Class.forName("java.lang.ref.Finalizer");
    Field queueStaticField = finalizerClass.getDeclaredField("queue");
    queueStaticField.setAccessible(true);
    ReferenceQueue<Object> referenceQueue = (ReferenceQueue) queueStaticField.get(null);

    Field queueLengthField = ReferenceQueue.class.getDeclaredField("queueLength");
    queueLengthField.setAccessible(true);
    long queueLength = (long) queueLengthField.get(referenceQueue);
    System.out.format("There are %d references in the queue%n", queueLength);
}

주어진 명령문은 내부 JVM 클래스의 일부 필드에 액세스하고 백만 번 반복할 때마다 개체 참조 수를 인쇄합니다.

main 메소드 를 실행하여 프로그램을 시작해보자 . 무기한 실행될 것으로 예상할 수 있지만 그렇지 않습니다. 몇 분 후 다음과 유사한 오류와 함께 시스템 충돌이 표시됩니다.

...
There are 21914844 references in the queue
There are 22858923 references in the queue
There are 24202629 references in the queue
There are 24621725 references in the queue
There are 25410983 references in the queue
There are 26231621 references in the queue
There are 26975913 references in the queue
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
    at java.lang.ref.Finalizer.register(Finalizer.java:91)
    at java.lang.Object.<init>(Object.java:37)
    at com.baeldung.finalize.CrashedFinalizable.<init>(CrashedFinalizable.java:6)
    at com.baeldung.finalize.CrashedFinalizable.main(CrashedFinalizable.java:9)

Process finished with exit code 1

가비지 수집기가 제대로 작동하지 않은 것 같습니다. 시스템이 충돌할 때까지 개체 수가 계속 증가했습니다.

종료자를 제거하면 참조 수는 일반적으로 0이 되고 프로그램은 계속해서 계속 실행됩니다.

3.3. 설명

가비지 수집기가 객체를 폐기하지 않은 이유를 이해하려면 JVM이 내부적으로 어떻게 작동하는지 살펴봐야 합니다.

종료자가 있는 참조 대상이라고도 하는 객체를 생성할 때 JVM은 java.lang.ref.Finalizer 유형의 동반 참조 객체를 생성합니다 . 참조 대상이 가비지 수집 준비가 된 후 JVM은 참조 개체를 처리 준비가 된 것으로 표시하고 참조 큐에 넣습니다.

java.lang.ref.Finalizer 클래스 의 정적 필드 대기열통해 이 대기열에 액세스할 수 있습니다 .

한편 Finalizer 라는 특수 데몬 스레드는 계속 실행되고 참조 대기열에서 개체를 찾습니다. 하나를 찾으면 대기열에서 참조 개체를 제거하고 참조 개체에서 종료자를 호출합니다.

다음 가비지 수집 주기 동안 참조 대상은 더 이상 참조 개체에서 참조되지 않을 때 삭제됩니다.

스레드가 이 예에서 발생한 것처럼 고속으로 객체를 계속 생성하는 경우 Finalizer 스레드는 따라갈 수 없습니다. 결국 메모리는 모든 개체를 저장할 수 없으며 결국 OutOfMemoryError 가 발생 합니다.

이 섹션에서와 같이 객체가 워프 속도로 생성되는 상황은 실제 생활에서 자주 발생하지 않는 상황에 주목하십시오. 그러나 이는 중요한 점을 보여줍니다 . 종료자는 매우 비쌉니다.

4. 종료자가 없는 예

동일한 기능을 제공하지만 finalize() 메서드를 사용하지 않는 솔루션을 살펴보겠습니다 . 아래 예제가 종료자를 대체하는 유일한 방법은 아닙니다.

대신 중요한 점을 설명하는 데 사용됩니다. 종료자를 피하는 데 도움이 되는 옵션이 항상 있습니다.

다음은 새 클래스의 선언입니다.

public class CloseableResource implements AutoCloseable {
    private BufferedReader reader;

    public CloseableResource() {
        InputStream input = this.getClass()
          .getClassLoader()
          .getResourceAsStream("file.txt");
        reader = new BufferedReader(new InputStreamReader(input));
    }

    public String readFirstLine() throws IOException {
        String firstLine = reader.readLine();
        return firstLine;
    }

    @Override
    public void close() {
        try {
            reader.close();
            System.out.println("Closed BufferedReader in the close method");
        } catch (IOException e) {
            // handle exception
        }
    }
}

새로운 CloseableResource 클래스와 이전 Finalizable 클래스 의 유일한 차이점은 종료자 정의 대신 AutoCloseable 인터페이스 의 구현 이라는 것을 아는 것은 어렵지 않습니다 .

CloseableResourceclose 메소드 본문은 Finalizable 클래스 종료 자의 본문과 거의 동일합니다 .

다음은 입력 파일을 읽고 작업을 마친 후 리소스를 해제하는 테스트 방법입니다.

@Test
public void whenTryWResourcesExits_thenResourceClosed() throws IOException {
    try (CloseableResource resource = new CloseableResource()) {
        String firstLine = resource.readFirstLine();
        assertEquals("baeldung.com", firstLine);
    }
}

위의 테스트에서 CloseableResource 인스턴스는 try-with-resources 문의 try 블록에 생성 되므로 try-with-resources 블록이 실행을 완료하면 해당 리소스가 자동으로 닫힙니다.

주어진 테스트 메소드를 실행하면 CloseableResource 클래스 close 메소드 에서 출력된 메시지를 볼 수 있습니다 .

5 . 결론

이 예제에서는 자바의 핵심 개념인 finalize 메소드 에 초점을 맞추었습니다 . 이것은 문서에서는 유용해 보이지만 런타임에 보기 흉한 부작용이 있을 수 있습니다. 그리고 더 중요한 것은 종료자를 사용하는 것에 대한 대체 솔루션이 항상 있다는 것입니다.

주목해야 할 한 가지 중요한 점은 finalize 가 Java 9부터 더 이상 사용되지 않으며 결국 제거된다는 것입니다.

항상 그렇듯이 이 예제의 소스 코드는 GitHub 에서 찾을 수 있습니다 .

Junit footer banner