JVM 메모리 할당 및 객체 생존 전략

JVM 힙 메모리 구조

Java 가상 머신(JVM)의 메모리 관리는 자바 애플리케이션의 성능과 안정성에 매우 중요합니다. 효율적인 메모리 할당과 객체 승격 메커니즘은 가비지 컬렉션(GC) 빈도를 줄이고 메모리 사용 효율을 높여 전체 프로그램 성능을 향상시킵니다. 여기서는 JVM의 메모리 할당 전략과 객체 승격 메커니즘을 심층적으로 다룹니다. JVM의 힙 메모리는 주로 영(Young) 영역과 올드(Old) 영역으로 나뉩니다. 영 영역은 다시 이든(Eden) 영역과 두 개의 서바이버(Survivor) 영역(일반적으로 From 영역과 To 영역으로 불림)으로 구성됩니다.

영(Young) 영역

새롭게 생성되는 객체들이 주로 할당되는 공간입니다. 대부분의 객체는 생성 직후 이든 영역에 배치됩니다. 수명이 짧은 객체가 많기 때문에 영 영역에서는 Minor GC라고 불리는 빈번한 가비지 컬렉션이 발생합니다.

올드(Old) 영역

수명이 긴 객체들을 저장하는 공간입니다. 특정 조건을 만족하는 객체는 영 영역에서 올드 영역으로 승격됩니다. 올드 영역의 가비지 컬렉션은 Major GC 또는 Full GC라고 불리며, Minor GC보다 훨씬 큰 비용이 소요됩니다.

서바이버(Survivor) 영역

영 영역의 일부로, Minor GC 과정에서 살아남은 객체들을 임시로 보관하는 역할을 합니다. Minor GC가 발생할 때마다 살아남은 객체들은 두 서바이버 영역 사이를 이동하며, 이때 객체의 '나이(Age)'가 증가합니다.

JVM 메모리 할당 전략

1. 객체는 이든(Eden) 영역에 우선 할당

대부분의 새로 생성된 객체는 이든 영역에 먼저 할당됩니다. 이든 영역 공간이 부족해지면 Minor GC가 트리거됩니다.
public class MemoryAllocationDemo {
    private static final int MEGA_BYTE = 1024 * 1024; // 1MB 정의

    public static void main(String[] args) {
        // 이 객체들은 주로 Eden 영역에 순차적으로 할당됩니다.
        byte[] firstBlock = new byte[2 * MEGA_BYTE];
        byte[] secondBlock = new byte[2 * MEGA_BYTE];
        byte[] thirdBlock = new byte[2 * MEGA_BYTE];
        System.out.println("다수의 객체가 Eden 영역에 할당될 준비가 되었습니다.");
        // Eden 영역이 부족해지면 Minor GC가 발생할 수 있습니다.
    }
}
위 코드에서는 세 개의 2MB 바이트 배열 객체를 연속적으로 생성합니다. JVM 힙의 영(Young) 영역 중 이든(Eden) 영역이 이들을 모두 수용할 수 없다면 Minor GC가 발생하게 됩니다.

2. 대용량 객체는 올드(Old) 영역으로 직행

크고 연속적인 메모리 공간을 필요로 하는 객체(예: 긴 배열이나 대형 문자열)는 대용량 객체로 분류됩니다. 이러한 객체가 영 영역에서 빈번하게 복사되고 이동하는 것을 방지하기 위해, JVM은 이들을 바로 올드 영역에 할당합니다. `-XX:PretenureSizeThreshold` 매개변수를 통해 대용량 객체의 기준 크기를 설정할 수 있으며, 이 크기를 초과하는 객체는 올드 영역에 직접 할당됩니다.
public class LargeObjectDirectAllocation {
    private static final int MEGABYTE = 1024 * 1024; // 1MB 정의

    public static void main(String[] args) {
        // JVM 실행 시 -XX:PretenureSizeThreshold=5242880 (5MB) 설정 가정
        // 이 객체는 5MB를 초과하므로 Old Generation으로 직접 할당됩니다.
        byte[] hugeDataArray = new byte[6 * MEGABYTE];
        System.out.println("설정된 임계값을 초과하는 대용량 배열이 Old 영역에 할당되었습니다.");
    }
}

3. 오래 살아남은 객체는 올드(Old) 영역으로 승격

JVM은 각 객체에 대해 '객체 나이(Age)' 카운터를 유지합니다. 이든 영역에서 생성된 객체가 첫 Minor GC 이후에도 살아남아 서바이버 영역에 수용되면, 그 객체의 나이는 1로 설정됩니다. 이후 서바이버 영역에서 Minor GC를 거칠 때마다 살아남으면 나이가 1씩 증가합니다. 객체의 나이가 특정 임계값(기본값은 15이며, `-XX:MaxTenuringThreshold` 매개변수로 조정 가능)에 도달하면 해당 객체는 올드 영역으로 승격됩니다.

4. 동적인 객체 나이 판단

JVM은 프로그램의 메모리 사용 패턴에 더 유연하게 대응하기 위해, `MaxTenuringThreshold`에 도달하지 않아도 객체를 올드 영역으로 승격시킬 수 있습니다. 만약 서바이버 영역 내에서 특정 나이를 가진 모든 객체 크기의 합이 서바이버 영역 전체 공간의 절반을 초과하면, 해당 나이 이상인 객체들은 즉시 올드 영역으로 이동할 수 있습니다.

객체 승격 메커니즘

Minor GC 시 객체 승격 과정

이든 영역 공간 부족으로 Minor GC가 발생하면, 이든 영역과 From 서바이버 영역에서 여전히 살아있는 객체들은 To 서바이버 영역으로 복사됩니다. 이 과정에서 객체의 나이는 1 증가합니다. 만약 To 서바이버 영역이 모든 살아남은 객체를 수용하기에 부족하거나, 객체 나이가 임계값에 도달하면 해당 객체들은 올드 영역으로 직접 승격됩니다. 이후 From 영역과 To 영역의 역할이 바뀝니다.

올드 영역으로 승격의 영향

객체가 올드 영역으로 승격되면 올드 영역의 메모리 사용량이 증가합니다. 올드 영역의 공간이 부족해지면 Major GC 또는 Full GC가 트리거됩니다. 올드 영역에는 일반적으로 많은 객체가 존재하므로, 이러한 가비지 컬렉션은 상당한 시스템 자원을 소모하며 프로그램에 눈에 띄는 일시 정지(Stop-The-World)를 유발하여 성능에 부정적인 영향을 미칠 수 있습니다.

메모리 할당 및 객체 생존 전략 최적화

1. JVM 매개변수 합리적 설정

애플리케이션의 특성과 실행 환경에 맞춰 `-XX:PretenureSizeThreshold` 및 `-XX:MaxTenuringThreshold`와 같은 매개변수를 적절히 조정해야 합니다. 예를 들어, 대용량 객체가 많은 프로그램의 경우 `PretenureSizeThreshold` 값을 높여 대용량 객체가 영 영역에서 불필요하게 복사되는 것을 줄일 수 있습니다. 반면, 객체 수명이 짧은 프로그램은 `MaxTenuringThreshold`를 낮춰 객체가 더 빠르게 회수되도록 할 수 있습니다.

2. 대용량 객체 생성 최소화

가능한 한 대용량 객체 생성을 피하거나, 하나의 큰 객체를 여러 개의 작은 객체로 분할하는 것을 고려합니다. 예를 들어, 매우 큰 배열 대신 분할된 배열(segmented array) 방식을 사용하여 개별 객체의 크기를 줄일 수 있습니다.

3. 객체 생명주기 최적화

객체의 생명주기를 최대한 짧게 유지하고, 더 이상 사용되지 않는 객체 참조를 제때 해제해야 합니다. 이는 객체가 메모리에 머무르는 시간을 줄여 가비지 컬렉션 부담을 경감시키는 데 도움이 됩니다.

태그: JVM GarbageCollection HeapMemory EdenSpace SurvivorSpace

7월 21일 22:03에 게시됨