공공부하자개발 · 영어 학습 노트
자바
고급모던 자바와 성능0/10 완료
  • 01제네릭과 와일드카드
  • 02멀티스레드와 동기화
  • 03람다식과 함수형 인터페이스
  • 04Stream API와 병렬 처리
  • 05Optional로 NPE 방지
  • 06메서드 활용 패턴 (고급)
  • 07Java 21 모던 문법
  • 08어노테이션·리플렉션·동적 프록시
  • 09CompletableFuture 심화와 가상 스레드 실전
  • 10JVM 메모리·GC·OOM 진단
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 고급 › 10 / 10

JVM 메모리·GC·OOM 진단

힙은 어떻게 생겼고, 무엇이 남고, 터지면 어디를 보는가
섹션 7진행 0 / 10
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

3. 코드 예제

소스: java-src/advanced/10_jvm_gc/Main.java 하나입니다. 4·5절은 자기 자신을 자식 JVM 으로 다시 띄웁니다.

bash
cd java-src/advanced/10_jvm_gc
javac -encoding UTF-8 *.java && java -Dstdout.encoding=UTF-8 Main

예제 1: 지금 JVM 의 메모리 영역

java
Runtime rt = Runtime.getRuntime();
rt.maxMemory();                                        // -Xmx
for (MemoryPoolMXBean p : ManagementFactory.getMemoryPoolMXBeans())
    System.out.println(p.getName() + " " + p.getType() + " " + p.getUsage().getUsed());
for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans())
    System.out.println(gc.getName() + " " + Arrays.toString(gc.getMemoryPoolNames()));
text
힙: 현재 254 MB / 최대 4042 MB (물리 메모리의 1/4 이 기본 -Xmx)
  풀 Metaspace              비힙       사용   879 KB
  풀 G1 Eden Space          힙        사용  2048 KB
  풀 G1 Old Gen             힙        사용     0 KB
  풀 G1 Survivor Space      힙        사용     0 KB
  GC G1 Young Generation    담당 풀 [G1 Eden Space, G1 Survivor Space, G1 Old Gen]
  GC G1 Old Generation      담당 풀 [G1 Eden Space, G1 Survivor Space, G1 Old Gen]

이 MXBean 들이 Actuator 의 /actuator/metrics/jvm.memory.used 와 Grafana 대시보드가 읽는 원천입니다. 물리 메모리 16GB 라 최대 힙이 4GB 로 잡혔습니다.

예제 2: 단명 객체 1GB

java
for (int i = 0; i < 100_000; i++) {
    byte[] garbage = new byte[10_000];     // 만들고 바로 버림
    sum += garbage.length;
}
text
  G1 Young Generation    7회 (누적 22 ms)
  G1 Concurrent GC       0회 (누적 0 ms)
  G1 Old Generation      0회 (누적 0 ms)
  할당 합계 953 MB, 지금 힙 사용 1 MB ← 남은 게 없습니다

1GB 를 만들었는데 Young GC 7번, 합계 22ms 입니다. 살아남는 객체가 없어서 복사할 것이 없기 때문입니다. "객체를 많이 만들면 느리다" 는 절반만 맞습니다. 많이 만들고 오래 붙잡을 때 느립니다.

예제 3: 누수 재현

java
static final Map<String, byte[]> CACHE = new HashMap<>();     // 만료 없음
for (int i = 0; i < 1000; i++) CACHE.put("key" + i, new byte[100_000]);

Map<String, byte[]> weak = new WeakHashMap<>();
for (int i = 0; i < 1000; i++) weak.put(new String("key" + i), new byte[10_000]);
text
  시작 힙 사용 1 MB
  static Map 에 100KB × 1000 저장 후 GC → 100 MB (안 줄어듦, 엔트리 1000)
  clear() 후 GC → 1 MB
  WeakHashMap 에 10KB × 1000 저장 직후 엔트리 184, 힙 5 MB
  GC 후 엔트리 0, 힙 1 MB ← 키를 아무도 안 잡으면 사라짐

static Map 은 GC 를 아무리 돌려도 100MB 그대로입니다. clear() 로 참조를 끊어야 돌아옵니다. WeakHashMap 은 넣는 도중에 이미 Young GC 가 돌아 184개만 남았고, 명시적 GC 후에는 0개입니다.

예제 4: OOM 재현과 힙 덤프

부모 JVM 이 자신을 -Xmx64m 으로 다시 띄웁니다. 자식은 1MB 배열을 리스트에 계속 넣습니다.

java
List<String> out = runChild("oom", "-Xmx64m", "-Xlog:gc",
        "-XX:+HeapDumpOnOutOfMemoryError", "-XX:HeapDumpPath=" + dump);
text
  자식 최대 힙 64 MB, 1MB 씩 할당하며 붙잡습니다
  [0.091s][info][gc] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) 30M->28M(64M) 2.018ms
  [0.102s][info][gc] GC(9) Pause Full (G1 Compaction Pause) 62M->62M(64M) 2.089ms
  [0.104s][info][gc] GC(10) Pause Full (G1 Compaction Pause) 62M->62M(64M) 2.178ms
  java.lang.OutOfMemoryError: Java heap space
  Dumping heap to C:\...\Temp\lesson-oom.hprof ...
  Heap dump file created [35702847 bytes in 0.019 secs]
  Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  	at Main.childOom(Main.java:137)
  종료 코드 1
  힙 덤프 생성: lesson-oom.hprof (34 MB) → VisualVM/MAT 로 엽니다

62M->62M 두 번 뒤에 OOM 입니다. Full GC 로도 하나도 못 치웠다는 뜻이고, 덤프를 열면 ArrayList 하나가 byte[] 60개를 쥐고 있는 것이 보입니다. Humongous Allocation 은 G1 영역 절반보다 큰 객체를 뜻하며, 1MB 배열이 여기 해당합니다.

예제 5: GC 네 종류 비교

같은 작업(20MB 유지 + 20KB × 10만 개 단명 할당) 을 -Xmx256m 으로 네 번 돌립니다.

text
  SerialGC     전체 170 ms | Copy 28회/15ms, MarkSweepCompact 0회/0ms
  ParallelGC   전체 179 ms | PS MarkSweep 0회/0ms, PS Scavenge 25회/14ms
  G1GC         전체 203 ms | G1 Young Generation 14회/14ms, G1 Old Generation 0회/0ms
  ZGC          전체 369 ms | ZGC Cycles 8회/138ms, ZGC Pauses 24회/0ms

이 작업은 살아남는 것이 적어 어느 GC 든 Old 수집이 0회입니다. 볼 것은 ZGC 줄입니다. 정지(Pauses) 는 24회 합쳐 0ms 인데 동시 작업(Cycles) 이 138ms 로, 그만큼 CPU 를 애플리케이션과 나눠 썼습니다. 응답 시간을 사면 처리량을 냅니다.

작은 힙·짧은 작업에서는 Serial 이 가장 빠릅니다. GC 스레드를 띄우는 비용조차 아깝기 때문입니다. 컨테이너 코어가 1개면 JVM 은 자동으로 Serial 을 고릅니다.

예제 6: 객체 크기

text
  int[]        4 MB (4바이트 × 100만)
  Integer[]   19 MB (헤더 12 + 값 4 + 참조 4)
  String[]    57 MB (String 24 + byte[] 16+길이)

같은 100만 개 숫자가 박싱하면 5배, 문자열로 들면 14배입니다. 조회 결과를 List<Map<String, Object>> 로 받는 코드가 왜 배치에서 OOM 을 내는지가 여기 있습니다.

예제 7: 스택 깊이

java
Thread t = new Thread(null, () -> deep[0] = maxDepth(), "deep", 64L << 20);   // 64MB 스택
text
  기본 스택(main): 깊이 13430
  64MB 스택 스레드: 깊이 4190969 ← 재귀가 깊으면 -Xss 또는 반복문으로

기본 1MB 스택으로 약 1만 3천 단계입니다. 트리 깊이가 수만이 되는 재귀는 StackOverflowError 가 나므로, 스택을 키우기보다 반복문과 명시적 Deque 로 바꾸는 것이 정석입니다.

예제 직접 실행

아래 폴더를 JDK 21 로 컴파일하고 실행합니다.

cd java-src\advanced\10_jvm_gc
javac -encoding UTF-8 *.java && java Main
코드 예제
  • 예제 1: 지금 JVM 의 메모리 영역
  • 예제 2: 단명 객체 1GB
  • 예제 3: 누수 재현
  • 예제 4: OOM 재현과 힙 덤프
  • 예제 5: GC 네 종류 비교
  • 예제 6: 객체 크기
  • 예제 7: 스택 깊이
이전 섹션2 핵심 원리3 / 7다음 섹션4 응용 변형 예제