홈 › 고급 › 10 / 10

JVM 메모리·GC·OOM 진단

힙은 어떻게 생겼고, 무엇이 남고, 터지면 어디를 보는가
섹션 7진행 0 / 10

6. 연습 문제

문제 1

예제 4 의 자식 JVM 옵션에서 -Xmx64m 을 -Xmx64m -XX:+UseSerialGC 로 바꿔 실행하세요. GC 로그의 정지 종류 이름이 어떻게 달라지는지, OOM 직전 줄에서 -> 앞뒤 숫자가 왜 같은지 설명하세요.

정답 보기

Pause Young (Allocation Failure) 와 Pause Full (Allocation Failure) 로 바뀝니다. G1 의 Humongous Allocation·Concurrent Start 같은 G1 전용 표현이 사라집니다.

62M->62M 처럼 앞뒤가 같은 것은 Full GC 가 돌았는데 치울 것이 하나도 없었다는 뜻입니다. 리스트가 모든 배열을 참조하고 리스트는 스택의 지역 변수가 참조하므로 전부 도달 가능합니다. GC 는 도달 가능한 것을 절대 치우지 않으므로 다음 할당에서 OOM 이 납니다.

문제 2

childChurn 의 "살아남는 20MB" 를 200MB 로 올리고(-Xmx256m 그대로) 네 GC 를 다시 비교하세요. 어느 GC 에서 Old 수집이 나타나고 전체 시간이 어떻게 달라지는지 예측한 뒤 실행하세요.

정답 보기

힙 256MB 중 200MB 가 살아 있으면 Young 영역이 좁아져 Young GC 횟수가 크게 늘고, Serial·Parallel 에서는 MarkSweepCompact·PS MarkSweep 이 1회 이상 돕니다. G1 은 G1 Old Generation 또는 Concurrent GC 가 나타납니다. ZGC 는 정지 시간은 여전히 0ms 근처지만 Cycles 시간이 더 늘어납니다.

전체 시간은 모두 늘어나며, 특히 Full GC 가 도는 Serial 이 가장 큰 폭으로 느려집니다. 살아 있는 객체가 많을수록 "복사할 것" 이 많아지는 것이 세대별 GC 의 비용 구조입니다.

문제 3

예제 3 의 static CACHE 를 "최근 100개만 유지" 하는 캐시로 바꾸세요. LinkedHashMap 의 removeEldestEntry 를 사용합니다. 1000개를 넣은 뒤 힙 사용량이 약 10MB 에 머무는 것을 확인하세요.

정답 보기
java
static final Map<String, byte[]> CACHE = new LinkedHashMap<>(128, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, byte[]> eldest) {
        return size() > 100;
    }
};

세 번째 생성자 인자 true 는 접근 순서로 정렬하라는 뜻이라 LRU 가 됩니다. 1000개를 넣어도 100개만 남아 100KB × 100 = 약 10MB 입니다. 실무에서는 시간 만료와 통계까지 주는 Caffeine 을 쓰지만, 뼈대는 이 열 줄입니다.