[Java] 4. 4 개체의 생성과 소멸, 4.4.1 개체의 생과 사

4.4 개체의 생성과 소멸

앞에서 개체를 생성할 때 new 키워드와 함께 생성할 개체 형식 명과 생성자 메서드의 입력 인자를 전달한다는 것을 소개하였습니다. 이번에는 구체적으로 개체의 생성과 소멸에 관하여 살펴보기로 합시다.

4.4.1 개체의 생과 사

C언어나 C++언어에서는 동적으로 메모리를 할당하면 힙에 할당하고 개발자 코드에 의해 해제해야 합니다. 이에 반해 Java 언어에서는 개발자 코드에 의해 개체를 생성하지만 개발자 코드에 의해 개체를 소멸하지 않습니다.

이와 같이 개체의 생성은 개발자 코드에 의해 결정하고 소멸하는 코드를 작성하지 않는 이유는 Java 가상 머신의 관리화 힙(Managed Heap)에 개체를 할당하여 관리하기 때문입니다.

Java에서 개체를 생성하면 관리화 힙(Managed Heap)에 할당합니다. 관리화 힙은 Java가상 머신에서 개체를 참조하는 변수의 개수를 카운팅하여 관리합니다. 그리고 가상 머신의 쓰레기 수집기(Gabage Collector)에 의해 관리화 힙의 개체를 참조하는 변수의 카운터가 0이 되면 개체의 세대를 변경하여 수거 대상임을 표시하여 제거할 수 있음을 표시합니다. 만약 개발자가 특정 변수로 참조하고 있는 개체를 사용할 필요가 없으면 가상 머신에서는 참조 개수를 1 감소하여 효과적으로 개체를 관리할 수 있게 도움을 줄 수 있습니다.

쓰레기 수집기는 주기적으로 가동하며 개체를 할당할 때 메모리가 부족할 때도 가동합니다. 그리고 System 클래스의 정적 메서드 gc를 호출해도 쓰레기 수집기가 동작합니다.

쓰레기 수집기가 가동하면 관리화 힙의 개체를 참조하는 변수의 카운터가 0인 것이 있는지 확인하여 세대를 부여하는 작업을 수행합니다. 처음으로 발견한 개체는 세대 0으로 마킹하고 세대 0인 개체가 여전히 남아 있으면 1세대, 1세대인 것이 여전히 남아 있으면 2세대로 마킹합니다. 이와 같은 방식으로 세대 조사를 하는 이유는 개체를 참조하는 변수가 없을 때 바로 메모리를 해제하는 방식은 너무 많은 비용이 들기 때문입니다.

Java 가상 머신에서는 주기적인 세대 조사를 통해 관리화 힙의 메모리가 필요하면 세대 관리 정책에 따라 필요없는 개체를 수거합니다. 만약 개발자 코드에서 System 클래스의 정적 메서드 runFinalization을 호출하면 수집 대상 개체의 finalize 메서드를 수행합니다. 따라서 여러분이 정의하는 클래스에 finalize 메서드를 정의하면 개체를 수거할 때 수행합니다.

그리고 System 클래스의 runFinalizersOnExit 메서드를 호출하면 프로세스가 끝날 때 프로세스의 개체의 finalize 메서드를 호출하는 것을 활성화 혹은 비활성화할 수 있습니다. 가상 머신은 기본적으로 자동으로 호출하지 않도록 비활성화 상태입니다.

물론 Java 가상 머신은 매우 정교하게 만들어졌기 때문에 개발자 코드에서 의도적인 쓰레기 수집을 가동하거나 수거할 필요는 거의 없습니다. 여러분이 작성하는 프로그램이 서버 시스템의 주요한 서버 프로그램이며 자원 활용을 효과적으로 할 필요가 있는 특수한 프로그램일 때 이를 사용할 수 있지만 일반적인 프로그래밍에서는 사용할 필요가 없습니다.

System 클래스의 쓰레기 수집 관련 정적 메서드

  1. public static void gc()

쓰레기 수집기 가동

  1. public static void runFinalization()

쓰레기 수거(수거 대상 개체의 finalize 메서드를 호출함)

  1. public static void runFinalizersOnExit(boolean value)

프로세스가 끝날 때 모든 개체를 수거함

여기에서는 대략적으로 쓰레기 수집기가 동작하는 것을 확인하는 테스트 코드를 작성합시다.

먼저 프로젝트를 생성하여 Unit 클래스를 추가하세요.

Unit 클래스에는 개체를 구분하기 위한 멤버 필드 num을 캡슐화하고 생성자에서 입력 인자로 받아 num 멤버 필드의 값을 설정할게요. 그리고 finalize 메서드를 정의하여 수거할 때 어떤 개체인지 출력하기로 할게요.

[소스 4.6] 쓰레기 수집 시기를 확인하기 위한 Unit 클래스 정의

public class Unit {
    int num;
    public Unit(int num){
        this.num = num;
    }
    public int getNum(){
        return num;
    }
    protected void finalize(){
        System.out.println(num+"번 개체 정리");
    }
}

첫 번째 테스트는 쓰레기 수집에 관한 코드를 작성하지 않는 예제입니다.

Program 클래스를 추가하여 진입점 메서드에 두 개의 유닛 개체를 생성하는 코드를 작성하여 실행해 보세요. 두 개의 Unit 개체를 생성하지만 수거하지 않음을 알 수 있습니다.

[소스 4.7] 쓰레기 수집에 관한 코드를 작성하지 않은 예

public class Program {
    public static void main(String[] args){
        Unit unit1 = new Unit(1);
        System.out.println("유닛 생성"+unit1.getNum());
        Unit unit2 = new Unit(2);
        System.out.println("유닛 생성"+unit2.getNum());
    }
}

실행 결과

유닛 생성1
유닛 생성2

두 번째 테스트에서는 진입점 main 메서드에 System.runFinalizersOnExit(true); 코드를 추가하여 프로세스가 끝날 때 수거하지 않은 개체를 수거하도록 설정합니다. 실행하면 프로세스가 끝날 때 모든 개체를 수거하므로 Unit 클래스에 정의한 finalize 메서드가 동작하는 것을 확인할 수 있습니다.

[소스 4.8] 쓰레기 수집에 관한 코드를 작성하지 않은 예

public class Program {
    public static void main(String[] args){
        Unit unit1 = new Unit(1);
        System.out.println("유닛 생성"+unit1.getNum());
        Unit unit2 = new Unit(2);
        System.out.println("유닛 생성"+unit2.getNum());
        System.runFinalizersOnExit(true);
    }
}

실행 결과

유닛 생성1
유닛 생성2
2번 개체 정리
1번 개체 정리

세 번째 테스트는 unit1 변수에 null을 설정하고 System의 정적 메서드 gc를 호출하는 코드를 추가하고 runFinalizersOnExit 메서드 호출은 제거합니다. 그리고 바로 개체를 수거하는지 확인하기 위해 출력문을 작성합니다. 실행하면 gc를 호출한 직후 바로 unit1을 수거하지 않고 나중에 수거하는 것을 알 수 있습니다. 이를 통해 gc를 호출하면 세대 조사를 수행함을 알 수 있습니다.

[소스 4.9] System.gc()를 호출한 예

public class Program {
    public static void main(String[] args){
        Unit unit1 = new Unit(1);
        System.out.println("유닛 생성"+unit1.getNum());
        Unit unit2 = new Unit(2);
        System.out.println("유닛 생성"+unit2.getNum());

        unit1 = null;
        System.gc();
        System.out.println("음");
    }
}

실행 결과

유닛 생성1
유닛 생성2
음
1번 개체 정리

네 번째 테스트는 unit1 변수에 null을 대입하고 System 클래스의 정적 메서드 gc를 호출하는 코드 뒤에 System 클래스의 정적 메서드 runFinalization을 호출하는 코드를 추가합니다. 그리고 unit2 변수에 null을 대입하고 System 클래스의 정적 메서드 gc를 호출하는 코드를 작성하고 수거 시기를 확이하기 쉽게 화면에 출력문을 작성합니다.

실행해 보면 unit1 개체는 runFinalization 호출에 의해 수거함을 알 수 있습니다. 하지만 unit2 개체는 프로세스가 끝날 때 수거함을 알 수 있습니다.

[소스 4.10] System.gc()와 System.runFinalization()을 호출한 예

public class Program {
    public static void main(String[] args){
        Unit unit1 = new Unit(1);
        System.out.println("유닛 생성"+unit1.getNum());
        Unit unit2 = new Unit(2);
        System.out.println("유닛 생성"+unit2.getNum());
        
        unit1 = null;
        System.gc();
        System.runFinalization();
        unit2 = null;
        System.gc();
        System.out.println("음");
    }
}

실행 결과

유닛 생성1
유닛 생성2
1번 개체 정리
음
2번 개체 정리