자바의 finalize 메소드 가이드

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. 종료 자의 단점

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

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

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

Finalizers also have an impact on the program's portability. Since the garbage collection algorithm is JVM implementation-dependent, a program may run very well on one system while behaving differently on another.

The performance cost is another significant issue that comes with finalizers. Specifically, JVM must perform many more operations when constructing and destroying objects containing a non-empty finalizer.

The last problem we'll be talking about is the lack of exception handling during finalization. If a finalizer throws an exception, the finalization process stops, leaving the object in a corrupted state without any notification.

3.2. Demonstration of Finalizers' Effects

It's time to put the theory aside and see the effects of finalizers in practice.

Let's define a new class with a non-empty finalizer:

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(""); } }

Notice the finalize() method – it just prints an empty string to the console. If this method were completely empty, the JVM would treat the object as if it didn't have a finalizer. Therefore, we need to provide finalize() with an implementation, which does almost nothing in this case.

Inside the main method, a new CrashedFinalizable instance is created in each iteration of the for loop. This instance isn't assigned to any variable, hence eligible for garbage collection.

Let's add a few statements at the line marked with // other code to see how many objects exist in the memory at runtime:

if ((i % 1_000_000) == 0) { Class finalizerClass = Class.forName("java.lang.ref.Finalizer"); Field queueStaticField = finalizerClass.getDeclaredField("queue"); queueStaticField.setAccessible(true); ReferenceQueue 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); }

The given statements access some fields in internal JVM classes and print out the number of object references after every million iterations.

Let's start the program by executing the main method. We may expect it to run indefinitely, but that's not the case. After a few minutes, we should see the system crash with an error similar to this:

... 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.(Object.java:37) at com.baeldung.finalize.CrashedFinalizable.(CrashedFinalizable.java:6) at com.baeldung.finalize.CrashedFinalizable.main(CrashedFinalizable.java:9) Process finished with exit code 1

Looks like the garbage collector didn't do its job well – the number of objects kept increasing until the system crashed.

If we removed the finalizer, the number of references would usually be 0 and the program would keep running forever.

3.3. Explanation

To understand why the garbage collector didn't discard objects as it should, we need to look at how the JVM works internally.

When creating an object, also called a referent, that has a finalizer, the JVM creates an accompanying reference object of type java.lang.ref.Finalizer. After the referent is ready for garbage collection, the JVM marks the reference object as ready for processing and puts it into a reference queue.

We can access this queue via the static field queue in the java.lang.ref.Finalizer class.

Meanwhile, a special daemon thread called Finalizer keeps running and looks for objects in the reference queue. When it finds one, it removes the reference object from the queue and calls the finalizer on the referent.

During the next garbage collection cycle, the referent will be discarded – when it's no longer referenced from a reference object.

If a thread keeps producing objects at a high speed, which is what happened in our example, the Finalizer thread cannot keep up. Eventually, the memory won't be able to store all the objects, and we end up with an OutOfMemoryError.

Notice a situation where objects are created at warp speed as shown in this section doesn't often happen in real life. However, it demonstrates an important point – finalizers are very expensive.

4. No-Finalizer Example

Let's explore a solution providing the same functionality but without the use of finalize() method. Notice that the example below isn't the only way to replace finalizers.

Instead, it's used to demonstrate an important point: there are always options that help us to avoid finalizers.

Here's the declaration of our new class:

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 } } }

It's not hard to see that the only difference between the new CloseableResource class and our previous Finalizable class is the implementation of the AutoCloseable interface instead of a finalizer definition.

Notice that the body of the close method of CloseableResource is almost the same as the body of the finalizer in class Finalizable.

The following is a test method, which reads an input file and releases the resource after finishing its job:

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

In the above test, a CloseableResource instance is created in the try block of a try-with-resources statement, hence that resource is automatically closed when the try-with-resources block completes execution.

Running the given test method, we'll see a message printed out from the close method of the CloseableResource class.

5. Conclusion

이 자습서에서는 Java의 핵심 개념 인 finalize 메서드 에 중점을 두었습니다 . 이것은 종이에 유용 해 보이지만 런타임에 추악한 부작용이있을 수 있습니다. 그리고 더 중요한 것은 항상 파이널 라이저를 사용하는 대체 솔루션이 있다는 것입니다.

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

항상 그렇듯이이 튜토리얼의 소스 코드는 GitHub에서 찾을 수 있습니다.