본문 바로가기

Java/Java SE, EE

객체 소멸자 finalize() vs 널(null) 할당을 통한 리소스 반환

이 문서의 내용

    finalize()

    JVM의 GC는 힙 영역에서 관리되는 더 이상 사용하지 않는-참조되지 않는 데이터를 객체를 자동으로 메모리에서 제거합니다.

    이때 GC가 실행되며 객체가 소멸되기 직전에 finalize()가 호출됩니다.

    • finalize()는 자바의 Object 클래스에서 정의됩니다.
    • 모든 자바의 클래스는 Object 클래스를 상속합니다.
    • finalize() 내부에서 객체가 사용하고 있던 리소스를 반환 또는 데이터를 저장하는 코드를 구현합니다.
    • 결론적으로 메모리 누수(Leak) 또는 데이터 유실을 방지하기 위한 목적으로 사용합니다.

    Object 클래스에서 정의하는 finalize()는 빈 함수입니다.

    protected void finalize() throws Throwable { }

    필요하다면 finalize()를 오버라이드하여 객체 소멸 코드를 직접 구현합니다.

    public class Main 
    {
       public static void main(String[] _args) 
       {
          Counter counter = null;
          for (int i = 1; i <= 5000; ++i) {
             counter = new Counter(i);
             counter = null;
             System.gc();
          }
       }
       
       static class Counter 
       {
          public int no;
          
          public Counter(int no) { this.no = no; }
          
          @Override
          protected void finalize() throws Throwable {
             System.out.println(no + "번 인스턴스의 객체 소멸자 호출");
          }
       }
    }
    코드 비고
    Line 7:8 counter = new Counter(i) 새로운 인스턴스를 할당합니다.
    counter = null 인스턴스를 다시 null로 초기화하여 더 이상 사용되지 않도록 합니다.
    Line 9 System.gc() GC 수동 실행을 명령합니다.

    예제 코드 테스트를 위해 VM Options에 다음 옵션을 추가합니다.

    추가된 옵션은 GC의 실행과 관련된 로그를 출력합니다.

    -verbose:gc

    예제 코드를 실행하고 콘솔 출력을 확인합니다.

    ...
    [19.436s][info][gc] GC(4996) Pause Full (System.gc()) 1M->1M(8M) 3.971ms
    4987번 인스턴스의 객체 소멸자 호출
    [19.440s][info][gc] GC(4997) Pause Full (System.gc()) 1M->1M(8M) 3.878ms
    4988번 인스턴스의 객체 소멸자 호출
    4996번 인스턴스의 객체 소멸자 호출
    4997번 인스턴스의 객체 소멸자 호출
    4998번 인스턴스의 객체 소멸자 호출
    ...
    • 인스턴스는 순서대로 소멸되지 않습니다.
    • 모든 인스턴스가 소멸되지 않습니다.
    • 이번에 소멸되지 않은 인스턴스는 다음 번 또는 그 이후 GC에서 소멸 될 수 있습니다.
    • System.gc()를 호출하더라도 JVM이 메모리 상태에 따라서 GC 실행 요청을 무시 할 수 있습니다.

    부모 클래스의 finalize()의 오버라이드

    finalize()를 오버라이드하는 클래스가 있고, 이를 상속 받는 자식 클래스가 finalize()를 다시 재정의하면 JVM은 자식 클래스의 finalize()를 호출합니다.

    class CounterWrapper extends Counter 
    {
       public CounterWrapper(int no) 
       {
          super(no);
       }
       
       @Override
       protected void finalize() throws Throwable 
       {
          super.finalize();
       }
    }
    코드 비고
    Line 11 super.finalize() 부모 클래스의 finalize()를 자식 클래스가 호출하도록 합니다.
    더보기

    부모 클래스가 finalize()를 재정의하고 있으나, 이를 상속 받는 자식 클래스가 finalize()를 다시 재정의하지 않으면 JVM은 부모 클래스의 것을 호출합니다.

    널(null) 할당을 통한 리소스 반환

    객체를 참조하고 있던 변수에 null을 할당하면 GC에 의해 힙 메모리에서 자동으로 제거됩니다.

    자바에서는 finalize() 또는 System.gc()를 통해 수동으로 리소스를 반환하는 코드를 지양하고 null 사용을 지향합니다.

    • 부모 클래스에서 오버라이드하는 finalize() 호출이 누락될 위험이 있습니다.
    • System.gc()가 원하는 실행 시간에 동작을 보장하지 않습니다.
    • Full GC가 실행될 경우 순간적으로 성능이 현저히 저하됩니다.
    • null 사용으로 더 이상 사용하지 않는 객체를 반환하는 코딩 습관을 들이는 것이 좋습니다.

    다음은 null을 사용하지 않으면서 계속해서 힙 메모리를 사용하는 예시입니다.

    public class Main 
    {
       public static void main(String[] _args)  
       {
          try {
             System.out.println("trial : " + 1);
             Allocator alloc_0 = new Allocator();
             
             System.out.println("trial : " + 2);
             Allocator alloc_1 = new Allocator();
             
             System.out.println("trial : " + 3);
             Allocator alloc_2 = new Allocator();
          } catch (Exception e) {
             e.printStackTrace();
          }
       }
       
       static class Allocator 
       {
          public byte[] v = new byte[2000000000];
       }
    }

    인스턴스 할당 과정에서 Full GC를 실행하지만 더 이상 사용 가능한 힙 메모리가 없어 OutOfMemoryError: Java heap space가 발생합니다.

    [0.014s][info][gc] Using G1
    [0.109s][info][gc] Periodic GC disabled
    trial : 1
    [0.199s][info][gc] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) 2M->0M(256M) 2.055ms
    [0.199s][info][gc] GC(1) Concurrent Cycle
    [1.189s][info][gc] GC(1) Pause Remark 1908M->1908M(3182M) 9.393ms
    trial : 2
    [1.191s][info][gc] GC(2) Pause Young (Normal) (G1 Humongous Allocation) 1909M->1908M(3182M) 1.488ms
    [1.214s][info][gc] GC(3) Pause Full (G1 Humongous Allocation) 1908M->1908M(4096M) 14.552ms
    [1.215s][info][gc] GC(1) Concurrent Cycle 1016.126ms
    trial : 3
    [2.608s][info][gc] GC(4) Pause Young (Concurrent Start) (G1 Humongous Allocation) 3818M->3817M(4096M) 4.367ms
    [2.608s][info][gc] GC(5) Concurrent Cycle
    [2.609s][info][gc] GC(6) Pause Young (Normal) (G1 Humongous Allocation) 3817M->3817M(4096M) 0.630ms
    [2.647s][info][gc] GC(7) Pause Full (G1 Humongous Allocation) 3817M->3817M(4096M) 37.865ms
    [2.651s][info][gc] GC(8) Pause Full (G1 Humongous Allocation) 3817M->3817M(4096M) 4.324ms
    [2.652s][info][gc] GC(5) Concurrent Cycle 43.936ms
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    	at Main$Allocator.<init>(Main.java:20)
    	at Main.main(Main.java:13)

    예시의 코드에서 Allocator 인스턴스를 사용한 이후 null로 초기화하여 객체가 사용되지 않음을 명시합니다.

    public class Main 
    {
       public static void main(String[] _args)  
       {
          try {
             System.out.println("trial : " + 1);
             Allocator alloc_0 = new Allocator();
             alloc_0.release();
             
             System.out.println("trial : " + 2);
             Allocator alloc_1 = new Allocator();
             alloc_1.release();
             
             System.out.println("trial : " + 3);
             Allocator alloc_2 = new Allocator();
             alloc_2.release();
          } catch (Exception e) {
             e.printStackTrace();
          }
       }
       
       static class Allocator 
       {
          public byte[] v = new byte[2000000000];
          
          public void release() { this.v = null; }
       }
    }

    이전 실행과 달리 Full GC가 실행되지 않았으며, 힙 메모리가 부족할 때마다 GC가 실행됩니다.

    GC가 실행되면 null로 반환된 인스턴스를 힙 메모리에서 제거하여 새로운 힙 메모리 공간을 확보합니다.

    [0.015s][info][gc] Using G1
    [0.109s][info][gc] Periodic GC disabled
    trial : 1
    [0.203s][info][gc] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) 2M->0M(256M) 2.002ms
    [0.203s][info][gc] GC(1) Concurrent Cycle
    [1.190s][info][gc] GC(1) Pause Remark 1908M->1908M(3182M) 9.326ms
    trial : 2
    [1.193s][info][gc] GC(2) Pause Young (Normal) (G1 Humongous Allocation) 1909M->0M(3182M) 2.285ms
    [1.488s][info][gc] GC(1) Pause Cleanup 1908M->1908M(3182M) 0.608ms
    trial : 3
    [1.491s][info][gc] GC(3) Pause Young (Normal) (G1 Humongous Allocation) 1909M->0M(3182M) 2.300ms
    [1.548s][info][gc] GC(1) Concurrent Cycle 1345.026ms

    GC가 실행될 때 null로 반환된 객체를 수집하여 힙 메모리 공간을 확보합니다.

    따라서 null 사용을 습관하하면 메모리를 더 효율적으로 사용 할 수 있습니다.

    정리 및 복습

    • 자바의 GC는 객체가 사용되지 않을 때 finalize()를 호출하며 힙 메모리에서 제거합니다.
    • Object 클래스의 fianlize()는 빈 함수이며 이를 오버라이드하여 구현합니다.
    • 상속 관계에서 오버라이드 된 finalize() 중 가장 자식 클래스의 함수를 호출합니다.
    • GC 수동 실행을 위한 System.gc()는 JVM에 의해서 무시 될 수 있습니다.
    • 객체가 사용되지 않을 때 null로 초기화하면 GC가 수행될 때 메모리를 더 효율적으로 확보 할 수 있습니다.