JVM 메모리 구조를 이해해야 성능 문제의 원인을 찾을 수 있다

성능 문제가 터졌을 때 원인을 빠르게 찾으려면 JVM 메모리 구조를 알아야 한다. 단순히 “OutOfMemoryError가 났다”로 끝나는 게 아니라, 어떤 영역에서 왜 문제가 생겼는지를 설명할 수 있어야 한다.

메모리 영역 3가지

JVM 메모리는 크게 Method Area, Stack Area, Heap Area로 나뉜다.

Method Area에는 클래스 정보와 static 변수, Runtime Constant Pool이 저장된다. 애플리케이션이 뜰 때 클래스를 로드하면서 여기에 채워진다.

public class MyClass {
    static int count = 0;        // Method Area에 저장
    static final int MAX = 100;  // Constant Pool에 저장
}

Stack Area는 메서드 호출마다 프레임을 쌓는 구조다. 지역 변수와 매개변수가 이 영역에 산다. 메서드가 끝나면 프레임이 pop되고 그 안의 데이터도 사라진다.

public void method(int param) {
    int local = 10;  // 스택 프레임에 저장, 메서드 종료 시 자동으로 사라짐
}

Heap Areanew 키워드로 만든 객체들이 사는 곳이다. 가비지 컬렉션의 대상 영역이기도 하다. 대부분의 메모리 이슈는 여기서 비롯된다.

String str = new String("Hello");       // Heap에 저장
List<Integer> list = new ArrayList<>();  // Heap에 저장

실제 문제 상황들

OutOfMemoryError는 대부분 Heap Area가 꽉 찼을 때 발생한다. GC가 수거하려고 해도 살아있는 참조가 너무 많으면 공간을 확보하지 못한다. 순환 참조가 의심스러울 때는 힙 덤프를 떠서 어떤 객체가 얼마나 메모리를 점유하고 있는지 확인해야 한다.

컬렉션 객체의 초기 용량 설정도 종종 놓치는 부분이다. ArrayListHashMap을 기본 생성자로 만들면 내부적으로 리사이징이 반복적으로 일어나면서 GC 부담이 늘어난다. 예상 데이터 크기를 안다면 초기 용량을 지정해주는 편이 낫다.

StackOverflowError는 Stack Area에서 발생한다. 재귀 호출이 종료 조건 없이 계속되거나, 메서드 depth가 지나치게 깊어질 때 나타난다.

메모리 구조를 알고 있으면 에러 메시지를 봤을 때 “어떤 영역에서 무슨 이유로 터진 건지”를 추론할 수 있다. 그 추론 능력이 트러블슈팅 속도를 결정한다.

GC 튜닝의 기본 방향

힙 메모리가 부족해서 GC가 자주 돌면 애플리케이션 응답 시간에 직접 영향을 준다. -Xms-Xmx를 동일하게 설정하면 힙 리사이징이 일어나지 않아 GC 오버헤드를 줄일 수 있다. G1GC는 Java 9 이후 기본 GC로, 대용량 힙 환경에서 일시 정지 시간을 줄이는 데 유리하다.

연결 (이유)

출처(참고문헌)