G1에서 Region 크기의 절반을 넘는 객체는 별도로 취급된다.
G1 은 힙을 균일한 Region 으로 나눈다 (1~32MB, 힙 크기로 자동 결정) Region 의 50% 를 넘는 객체 = Humongous
- Young 을 거치지 않고 Old 의 연속된 Region 에 바로 놓인다
- 연속된 빈 Region 이 필요하다
- 회수 시점이 제한적이다 (예전에는 Full GC 를 기다렸다)
그래서 생기는 역설
-
증상 — 힙 사용률 60% 인데 OutOfMemoryError
-
원인 — 32MB 배열을 할당하려는데 Region 이 4MB
- 연속된 8개 Region 이 필요하다
- 빈 공간은 많지만 흩어져 있어 연속 확보 실패
-
전형적 발생 지점
- 대용량 파일을 통째로 byte[] 로 읽기
- 큰 결과셋을 List 로 전부 적재 (내부 배열이 계속 2배 확장)
- 이미지·동영상 버퍼
대응
- ① 객체를 쪼갠다 — 스트리밍 처리로 바꾼다 (근본 대응)
- 파일은 InputStream 으로, 쿼리는 커서/페이징으로
- ② Region 크기를 키운다
- -XX:G1HeapRegionSize=16m → 8MB 미만 객체는 더 이상 Humongous 가 아니다
- 단 Region 이 커지면 수집 단위가 커져 정지 시간이 늘 수 있다
- ③ 컬렉션 초기 용량을 미리 지정해 중간 확장 배열을 줄인다
확인 방법
-Xlog:gc+heap=debug 로 humongous 할당 로그를 본다 [gc,heap] Humongous regions: 12->12
- 또는 GC 로그에 (G1 Humongous Allocation) 원인이 찍힌 Young GC 가 잦은지 본다
- 큰 객체 할당이 GC 를 유발하고 있다는 신호
배열 크기 자체의 한계
자바 배열의 최대 길이는 int 범위(약 21억) 다 그보다 커야 하면 배열로는 표현할 수 없다
- 청크 분할 · MappedByteBuffer · 오프힙 자료구조로 간다
Region 크기는 어떻게 정해지나
G1 은 기본적으로 힙을 약 2,048개 Region 으로 나누려 한다 Region 크기는 1·2·4·8·16·32MB 중 하나로 반올림된다
| 힙 4GB | → 2MB | → Humongous 기준 1MB | |
|---|---|---|---|
| 힙 8GB | → 4MB | → 2MB | |
| 힙 32GB | → 16MB | → 8MB |
즉 힙을 줄이면 Humongous 기준도 함께 낮아진다 "힙을 줄였더니 없던 문제가 생겼다" 가 여기서 나온다
회수는 개선됐지만 여전히 특별하다
초기 G1 은 Humongous 를 Full GC 때만 회수했다 이후 도달 불가한 Humongous 는 Young GC 중에도 회수하도록 개선됐다
그래도 남는 성질
- 연속 Region 이 필요하다는 제약은 그대로다
- 할당 자체가 Region 여러 개를 한 번에 요구해 GC 를 유발한다
- 마지막 Region 의 남는 부분은 다른 객체가 쓰지 못한다 (내부 낭비)
면접 함정
- ❌ "힙에 여유가 있으면 OOM은 안 난다" → 연속 공간이 필요하면 난다.
- ❌ "Region을 키우면 항상 이득" → 정지 시간이 늘 수 있다.