2주차 Unit 4.4 — 거대 리전 (Humongous Region)

Psj·2026년 5월 15일

F-lab

목록 보기
67/230

Unit 4.4 — 거대 리전 (Humongous Region)

F-LAB JAVA · 2주차 · Phase 4 · G1 GC 심화


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • 거대 객체(Humongous Object) 의 정확한 기준은?
  • 거대 객체를 별도 리전에 두는 이유는?
  • 거대 객체가 G1의 효율을 어떻게 떨어뜨리나?
  • 연속된 거대 리전(Humongous Continued) 의 의미는?
  • ILIC 코드에서 어떤 객체가 거대 객체가 될 수 있나?
  • 운영에서 Humongous Allocation 을 어떻게 모니터링하나?
  • Java 8u40 이전과 이후의 Humongous 처리 차이는?

🎯 핵심 한 문장

거대 객체는 G1의 리전 모델을 깨는 "골치 아픈 손님"이다.
리전의 절반보다 큰 객체는 일반 리전에 못 들어가서 Humongous 리전을 차지한다.
연속된 메모리가 필요해서 단편화가 발생하고, GC 효율이 떨어진다.
ILIC 운영에서는 이런 객체를 만들지 않거나 작게 쪼개는 것이 정답.

비유 — 호텔 객실의 거대 손님

시스템비유
일반 객체일반 손님 — 한 객실(리전) 사용
거대 객체VIP 단체 — 여러 객실을 연속으로 합쳐서 사용
단편화객실 1, 2, 3은 비어있는데 4번 손님 있어서 단체에게 못 줌
Old gen 직행VIP는 처음부터 스위트 룸 (Eden 거치지 않음)

→ 호텔 운영자(GC)는 일반 손님 청소가 쉽지만, VIP 단체는 한 번 들어오면 청소 어려움.


🧭 9개 섹션 로드맵

1. 거대 객체의 정의
2. Humongous 리전의 동작
3. 왜 별도 처리하는가
4. GC 효율에 미치는 영향
5. ILIC 실무 — 거대 객체 발생 패턴
6. 거대 객체 회피 전략
7. 운영 모니터링
8. 흔한 실수 + 디버깅
9. 면접 + 자기 점검

1️⃣ 거대 객체의 정의

1.1 기준 — 리전 / 2

거대 객체 (Humongous Object) 정의:
  객체 크기 > 리전 크기 / 2

리전 크기별 거대 객체 기준:

리전 크기거대 객체 기준
1 MB> 512 KB
2 MB> 1 MB
4 MB> 2 MB
8 MB> 4 MB
16 MB> 8 MB
32 MB> 16 MB

→ ILIC 일반 환경 (Heap 4GB → 리전 2MB)에선 1MB 초과 객체가 거대 객체.

1.2 왜 정확히 절반?

리전 절반이 기준인 이유:

일반 리전의 동작:
  - 객체 여러 개가 같은 리전에 함께 들어감
  - Eden 리전 = 작은 객체들의 모음

만약 객체 크기가 리전의 절반보다 크면:
  - 한 리전에 거의 1개만 들어감 (나머지 절반 낭비)
  - 메모리 효율 ↓
  → 차라리 별도 처리하자

1.3 거대 객체의 예시

4MB 리전 환경에서:

이런 객체가 거대 객체:
  byte[] data = new byte[3_000_000];     // 3MB 배열 (> 2MB)
  
  String huge = veryLongQuery;            // 2MB 넘는 String
  
  Map<Long, Shipment> cache = new HashMap<>();
  // ... 100만 항목 추가
  // → HashMap 내부 배열이 거대해질 수 있음
  
  ArrayList<Shipment> all = new ArrayList<>();
  // ... 수십만 항목
  // → 내부 배열이 거대화

1.4 객체 자체 크기 vs 참조하는 데이터

주의: "거대 객체"는 객체 자체의 메모리 크기.

// 객체 본체는 작음 (참조만 보유)
List<Shipment> shipments = new ArrayList<>(1_000_000);
shipments.add(new Shipment());
// ... 1만 건

// ArrayList 객체 자체는?
// - Header 16 bytes
// - elementData 참조 8 bytes
// - size 등 + Padding
// → 작은 객체 (수십 bytes)

// 그러나 elementData 배열은?
// - Object[1_000_000] 크기 = 약 8MB
// → 이 배열이 거대 객체!

→ "List가 거대" 아니라 "List 내부 배열이 거대".

1.5 자기 점검 — 거대 객체 확인

ILIC 코드에서 거대 객체 후보 식별:

// 후보 1: 큰 byte 배열
byte[] csvData = readCsvFile(largFile);    // 5MB CSV → 거대 객체

// 후보 2: 큰 컬렉션 내부 배열
List<Cargo> manyCargoes = new ArrayList<>(500_000);  // 내부 배열 4MB+

// 후보 3: 큰 HashMap
Map<Long, Shipment> cache = new HashMap<>();
for (Shipment s : loadAll()) {              // 수십만 건
    cache.put(s.getId(), s);                // 내부 table이 거대화
}

// 후보 4: 큰 String
StringBuilder report = new StringBuilder();
for (...) {
    report.append(...);                     // 거대 String 생성 가능
}
String result = report.toString();         // 거대 String 객체

→ 박승제씨가 ILIC 코드에서 의심할 패턴들.


2️⃣ Humongous 리전의 동작

2.1 한 리전을 초과하는 객체

3MB 객체를 2MB 리전 환경에 할당:

  ┌────────┐ ┌────────┐
  │ Region │ │ Region │
  │  2MB   │ │  2MB   │
  └────────┘ └────────┘
  
  → 객체가 한 리전에 안 들어감
  → 두 리전을 연속으로 차지

2.2 Humongous + Humongous Continued

표기:
  H  = Humongous Start (거대 객체 시작)
  HC = Humongous Continued (연속)

3MB 객체 (2MB 리전 환경):
  ┌────┬────┬────┐
  │ H  │HC  │... │  ← H 리전이 시작, HC가 이어짐
  └────┴────┴────┘

5MB 객체:
  ┌────┬────┬────┐
  │ H  │HC  │HC  │  ← 3개 리전 연속
  └────┴────┴────┘

2.3 핵심 제약 — 연속된 메모리

거대 객체는 반드시 연속된 리전에 저장.

힙 상태:
  ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
  │E│O│ │E│ │O│ │ │ │E│  ← 빈 리전이 흩어져 있음
  └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
  
5MB 객체 할당 시도:
  - 3개 연속 빈 리전 필요
  - "2, 3, 4번 리전"이 연속이지만 2번이 차있음
  - "6, 7, 8번 리전" 연속 → 여기 할당

만약 연속된 빈 리전 없으면?
Full GC 발동 → 메모리 정리 후 재시도.
→ G1의 약속 깨짐.

2.4 동적 역할 — Old gen 취급

거대 객체는 일반 객체와 다른 라이프사이클:

일반 객체:
  Eden → Survivor → ... → Old (승격)

거대 객체:
  바로 Old gen으로 (Eden 안 거침)
  → 짧은 수명 가정 무시

이유:

  • 거대 객체를 Eden → Survivor 복사하면 비용 폭증
  • "거대한 건 어차피 오래 살 거다" 가정
  • Concurrent Marking 대상 (Old gen)

2.5 Java 8u40 이전 vs 이후

Java 8u40 이전:
  - 거대 객체는 Concurrent Cycle에서만 회수
  - Young GC에서 안 건드림
  - 빠르게 누적 → Full GC 위험 ↑

Java 8u40 이후 (현재):
  - 거대 객체도 Young GC에서 회수 가능
  - 참조 없으면 즉시 정리
  - "Eager Reclamation" 도입

박승제씨가 Java 17 쓴다면 Eager Reclamation 활성화됨.
→ 거대 객체 누수 위험 줄어듦.


3️⃣ 왜 별도 처리하는가

3.1 일반 리전과의 차이

일반 리전:
  - 여러 객체가 함께 거주
  - Eden 리전 = 수십~수백 개 객체
  - 복사(evacuation) 가능
  - 압축(compaction) 가능

Humongous 리전:
  - 객체 1개 (또는 연속 리전 N개에 1개)
  - 복사 비용 매우 큼
  - 압축 어려움

3.2 Evacuation 비용

일반 객체 복사:
  - 100 byte 객체 1개 복사: 100ns
  - Young 리전 1개 (1MB) 복사: ~1ms

거대 객체 복사:
  - 5MB 객체 복사: ~50ms (50,000배!)
  - Young GC 시간 폭발

거대 객체는 복사하지 말고 그 자리에 둔다.

3.3 메모리 단편화 위험

시간 흐름에 따른 힙 상태:

T1: 거대 객체 A (3리전) 할당
  ┌────┬─┬─┬─┬─┬─┐
  │ A  │ │ │ │ │ │
  └────┴─┴─┴─┴─┴─┘

T2: 거대 객체 B (2리전) 할당
  ┌────┬─┬───┬─┬─┐
  │ A  │ │ B │ │ │
  └────┴─┴───┴─┴─┘

T3: A 회수됨
  ┌────┬─┬───┬─┬─┐
  │ .  │ │ B │ │ │  ← A 자리 비었지만 다른 위치
  └────┴─┴───┴─┴─┘

T4: 4리전 연속 필요한 거대 객체 C 할당 시도
  - 빈 리전 5개 있지만 4개 연속 X
  - → Full GC 발동

외부 단편화 문제.

3.4 G1의 약속이 깨지는 시점

G1의 약속:
  "Pause < 200ms"

거대 객체 때문에 약속 깨지는 경우:
  1. 거대 객체 evacuation
     → 큰 데이터 복사 → STW 길어짐
  
  2. 연속 리전 부족 → Full GC
     → STW 수 초
  
  3. Humongous 누적 → Concurrent Cycle 따라가지 못함
     → Mixed GC 효율 ↓

→ 거대 객체가 G1을 무너뜨리는 3가지 경로.


4️⃣ GC 효율에 미치는 영향

4.1 직접적 영향 5가지

1. STW 시간 증가
   - 거대 객체 처리 비용

2. 빈 공간 낭비
   - 거대 객체 < 리전 절반 + 일부 = 일부 낭비
   - 예: 3MB 객체 + 2MB 리전 = 2개 리전 사용 (4MB)
   - 1MB는 낭비

3. 외부 단편화
   - 연속 리전 부족
   - Full GC 위험

4. Mixed GC 효율 ↓
   - Humongous는 우선순위 회수 대상이지만
   - 다른 Old 리전 회수 효과 분산

5. Concurrent Cycle 부담
   - 거대 객체 마킹 비용

4.2 측정 — GC 로그에서 보기

일반 Young GC:
  [gc] GC(100) Pause Young (Normal) (G1 Evacuation Pause) 100M->10M(256M) 15ms

Humongous Allocation 발생 시:
  [gc] GC(101) Pause Young (Concurrent Start) (G1 Humongous Allocation) ...
       ↑ "Humongous Allocation" 표시!
       
  → 거대 객체 할당으로 GC 발동
  → 일반보다 STW 길 수 있음

4.3 사례 — 잘못된 캐시 설계

// ❌ 거대 캐시
public class ShipmentCache {
    private static final Map<Long, Shipment> cache = new HashMap<>();
    
    public static void putAll(List<Shipment> all) {
        for (Shipment s : all) {
            cache.put(s.getId(), s);   // 100만 건 추가
        }
    }
}

문제:

  • HashMap 내부 table이 100만 슬롯 = 8MB+
  • 거대 객체!
  • Concurrent Cycle 시 마킹 비용 큼
  • Mixed GC에서 회수 어려움

해결:

// ✓ Caffeine 같은 라이브러리 (내부적으로 분할)
private static final Cache<Long, Shipment> cache = 
    Caffeine.newBuilder()
        .maximumSize(100_000)        // 한정된 크기
        .expireAfterWrite(Duration.ofHours(1))
        .build();

Caffeine은 내부적으로 여러 segment로 분할 → 거대 객체 발생 X.

4.4 자기 점검 답변

거대 객체가 많으면 G1의 효율이 어떻게 되는가?

:
1. STW 시간 증가 (복사 비용)
2. 외부 단편화 → Full GC 위험
3. Concurrent Cycle 부담
4. Mixed GC 효율 저하
5. 시간 예측 어려움 (약속 깨짐)

거대 객체는 가능한 한 만들지 말 것.


5️⃣ ILIC 실무 — 거대 객체 발생 패턴

5.1 패턴 1 — 대용량 파일 처리

// ❌ 5MB CSV 전체를 메모리에
public void importCsv(File csvFile) throws IOException {
    byte[] data = Files.readAllBytes(csvFile.toPath());  // 거대 객체!
    String content = new String(data, UTF_8);            // 또 거대 객체
    
    for (String line : content.split("\n")) {
        process(line);
    }
}

해결:

// ✓ 스트림으로 한 줄씩
public void importCsv(File csvFile) throws IOException {
    try (Stream<String> lines = Files.lines(csvFile.toPath())) {
        lines.forEach(this::process);
    }
}

→ 1주차 NIO Unit과 연결. Buffer로 한 줄씩 처리.

5.2 패턴 2 — 큰 리스트 메모리 적재

// ❌ 100만 건 한꺼번에
public List<Shipment> findAll() {
    return entityManager.createQuery("SELECT s FROM Shipment s", Shipment.class)
        .getResultList();   // 100만 건 → ArrayList 내부 배열 거대화
}

해결:

// ✓ 페이징 + 스트림
public void processAll() {
    int page = 0;
    int size = 1000;
    
    while (true) {
        List<Shipment> batch = repository.findAll(
            PageRequest.of(page, size)).getContent();
        
        if (batch.isEmpty()) break;
        
        batch.forEach(this::process);
        page++;
    }
}

5.3 패턴 3 — 큰 String 빌딩

// ❌ 큰 보고서 생성
public String generateReport(List<Shipment> shipments) {
    StringBuilder sb = new StringBuilder();
    
    for (Shipment s : shipments) {       // 10만 건
        sb.append(s.toCsvLine());
    }
    
    return sb.toString();                 // 거대 String!
}

해결:

// ✓ Stream + Writer
public void writeReport(List<Shipment> shipments, Writer out) throws IOException {
    try (BufferedWriter writer = new BufferedWriter(out)) {
        for (Shipment s : shipments) {
            writer.write(s.toCsvLine());
            writer.newLine();
        }
    }
}

→ 메모리에 안 쌓고 즉시 출력. 거대 객체 안 생김.

5.4 패턴 4 — JSON 직렬화의 함정

// ❌ 100만 건을 한 JSON으로
public String toJson(List<Shipment> all) {
    return objectMapper.writeValueAsString(all);   // 거대 String!
}

해결:

// ✓ Streaming Writer
public void toJson(List<Shipment> all, OutputStream out) throws IOException {
    try (JsonGenerator gen = objectMapper.createGenerator(out)) {
        gen.writeStartArray();
        for (Shipment s : all) {
            objectMapper.writeValue(gen, s);
        }
        gen.writeEndArray();
    }
}

→ 메모리 압박 없이 큰 JSON 생성.

5.5 패턴 5 — 이미지/문서 캐싱

// ❌ 이미지 메모리에 저장
public class ImageCache {
    private static final Map<String, byte[]> cache = new HashMap<>();
    
    public static byte[] get(String id) {
        return cache.computeIfAbsent(id, ImageCache::load);
    }
    
    private static byte[] load(String id) {
        return Files.readAllBytes(Paths.get(id));  // 5MB 이미지!
    }
}

해결:

  • 파일 시스템 캐시 (메모리 안 씀)
  • S3 등 외부 스토리지
  • Redis (네트워크 캐시)
  • 또는 약한 참조 + 크기 제한

5.6 ILIC에서의 진짜 위험

ILIC가 마주칠 진짜 거대 객체 후보:

1. PDF 생성 결과 (몇 MB)
2. Excel 일괄 다운로드 (XLSX 파일)
3. 대량 배치 처리의 중간 결과
4. 외부 API 응답 (큰 JSON)
5. 보고서 캐싱

요청 처리 중에는 가능한 한 스트림으로.
저장이 필요하면 외부 스토리지.


6️⃣ 거대 객체 회피 전략

6.1 전략 1 — 스트리밍 처리

원칙: 데이터를 메모리에 다 적재하지 말 것

도구:
  - Files.lines() (텍스트)
  - InputStream / OutputStream
  - Stream API
  - Spring Data JPA Stream
  - JSON Streaming (Jackson)

6.2 전략 2 — 페이지네이션

// 데이터베이스에서 한 번에 가져오지 말 것
@Query("SELECT s FROM Shipment s")
List<Shipment> findAll();   // ❌

@Query("SELECT s FROM Shipment s")
Page<Shipment> findAll(Pageable pageable);   // ✓

6.3 전략 3 — 청크 처리

// ❌ 한꺼번에
public void processAll(List<Shipment> all) {
    all.forEach(this::process);
}

// ✓ 청크 단위
public void processAll(Iterable<Shipment> all) {
    Iterables.partition(all, 1000).forEach(chunk -> {
        chunk.forEach(this::process);
    });
}

6.4 전략 4 — 적절한 컬렉션 초기 크기

// 거대 컬렉션이 예상되면 미리 크기 지정
List<Shipment> list = new ArrayList<>(1_000_000);

이게 정말 좋은가? 상황에 따라.

장점:

  • 내부 배열 재할당 안 함
  • 성능 향상

단점:

  • 즉시 거대 객체 생성
  • 단편화 위험

→ 데이터가 진짜 1백만 건 들어갈 거면 OK.
→ 막연한 예상이면 기본 크기로.

6.5 전략 5 — Off-Heap 저장

// 큰 데이터를 JVM Heap 밖에 저장
ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);  // 10MB Off-heap

// 또는 Chronicle, Mapdb 같은 라이브러리

장점:

  • GC 부담 0 (Heap 밖)
  • 거대 객체 X

단점:

  • 직접 메모리 관리 필요
  • 복잡성 ↑

6.6 전략 6 — 리전 크기 증가

# 큰 객체가 정상 사용 사례라면
-XX:G1HeapRegionSize=32m

장점:

  • 16MB 객체도 일반 객체로 처리
  • Humongous 발생 안 함

단점:

  • 작은 객체에 대한 효율 손실
  • 리전 수 줄어듦

→ 매우 큰 객체가 정상 패턴일 때만.

6.7 ILIC 권장 접근

우선순위:
  1. 스트리밍 / 페이지네이션 (코드 수정)
  2. Off-Heap 또는 외부 스토리지
  3. 마지막 수단: 리전 크기 조정

박승제씨가 ILIC에서 만나는 대부분 거대 객체는
1번으로 해결.


7️⃣ 운영 모니터링

7.1 GC 로그에서 Humongous 검출

# Java 11+ 통합 로그
java -Xlog:gc+humongous=trace:file=gc.log -jar app.jar

# 또는 일반 GC 로그에서
grep -i "Humongous" /var/log/gc.log

발견 패턴:

[gc] GC(123) Pause Young (Concurrent Start) (G1 Humongous Allocation) ...
[gc,heap] GC(123) Humongous regions: 5->5

7.2 JFR로 분석

# JFR 시작
jcmd <PID> JFR.start duration=120s filename=app.jfr

# JDK Mission Control로 분석
# - "Memory Allocation" 탭에서 Humongous 확인

7.3 Heap dump로 거대 객체 찾기

jmap -dump:format=b,file=heap.hprof <PID>

# Eclipse MAT:
# 1. Histogram → "Shallow Heap" 정렬
# 2. 가장 큰 객체 식별
# 3. 어떤 클래스인지 확인 (보통 byte[], char[], Object[])
# 4. "List Objects → outgoing references"로 추적

7.4 운영 알림 설정

# Prometheus + JMX Exporter 사용
# G1 Humongous 메트릭:
#   jvm_gc_g1_humongous_regions
#   jvm_gc_g1_humongous_objects

# 알림 규칙:
#   Humongous 리전 수 > 10
#   → 경고 발송

7.5 일일 점검 체크리스트

ILIC 운영 정기 체크 (GC 측면):

☐ GC 로그에서 "Humongous Allocation" 빈도
☐ Full GC 발생 여부 (이상적: 0회)
☐ Old gen 사용률 추세
☐ Mixed GC 빈도 정상 (시간당 1~3회)
☐ Young GC 평균/P99 STW
☐ Concurrent Cycle 정상 완료
☐ Metaspace 사용률

7.6 실전 — 운영 사고 대응

시나리오: 갑자기 Full GC 발생

1. GC 로그 확인:
   tail -50 gc.log | grep "Pause"
   
   결과:
   GC(456) Pause Full (G1 Humongous Allocation)
            ↑ Humongous 때문에 Full GC!

2. 원인 추적:
   - 어떤 코드가 거대 객체 만드는지?
   - Heap dump로 식별
   - Histogram에서 byte[] 등 큰 객체 확인

3. 해결:
   - 코드 수정 (스트림 처리로 변경)
   - 또는 리전 크기 늘림 (임시)

4. 검증:
   - 재배포 후 GC 로그 모니터링
   - Humongous Allocation 없어졌는지 확인

8️⃣ 흔한 실수 + 디버깅

실수 1 — "큰 리스트"가 거대 객체로 직결되지 않음

List<Shipment> list = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
    list.add(new Shipment());
}

거대 객체는?:

  • ArrayList 객체 자체: 작음
  • 내부 Object[] 배열: 거대 객체 (8MB+)
  • 각 Shipment: 작음

→ 거대 객체는 내부 배열.
→ HashMap도 마찬가지.

실수 2 — Humongous 무시

GC 로그에 Humongous Allocation 자주 나옴
→ "어 이게 정상인가?" → 무시

❌ 절대 무시 X. 잠재적 시한폭탄.

  • 누적 시 단편화
  • Concurrent Cycle 부담
  • Mixed GC 효율 ↓

→ 즉시 코드 검토.

실수 3 — 리전 크기 무리한 조정

# ❌ Humongous 회피하려고
-XX:G1HeapRegionSize=32m

장점: 16MB 객체도 정상 처리
단점: 일반 객체 효율 ↓, 정지시간 변동 ↑

→ 코드 수정이 먼저. 마지막 수단으로만 리전 조정.

실수 4 — 큰 객체를 빈번 생성

// ❌ 매 요청마다
@GetMapping("/report")
public byte[] downloadReport() {
    byte[] hugeReport = generateReport();   // 5MB
    return hugeReport;
}

요청 100건/초 = 초당 500MB 거대 객체 생성.
→ GC 폭발.

해결:

  • 스트리밍 (ResponseEntity)
  • 청크 응답

실수 5 — Java 버전 무시

Java 8u40 이전:
  거대 객체 GC 어려움
  Concurrent Cycle만 회수

Java 8u40 이후 (현재):
  Eager Reclamation
  Young GC도 거대 객체 회수

→ 박승제씨가 Java 17 쓰면 자동 활성화.
→ Java 8 구버전 쓰는 시스템 마이그레이션 검토.

실수 6 — Direct ByteBuffer 무시

ByteBuffer buf = ByteBuffer.allocateDirect(10 * 1024 * 1024);
  • Off-Heap 메모리
  • GC 부담 0
  • 거대 객체 회피
  • 그러나 명시적 해제 필요 (또는 finalize)

→ 적극 활용. 1주차 NIO Unit과 연결.

디버깅 도구

# 1. GC 로그 (가장 기본)
-Xlog:gc*,gc+humongous=trace:file=gc.log

# 2. JFR
jcmd <PID> JFR.start duration=60s filename=hum.jfr

# 3. Heap dump + MAT
jmap -dump:live,format=b,file=heap.hprof <PID>

# 4. 실시간 통계
jstat -gc <PID> 1000   # 1초마다 GC 통계

9️⃣ 면접 질문 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
거대 객체 기준?리전 크기 / 2 초과
거대 객체가 별도 처리되는 이유?일반 리전 비효율. 복사 비용. 압축 어려움
Humongous Continued란?거대 객체가 여러 리전 차지할 때 이어지는 부분
거대 객체의 라이프사이클?바로 Old gen으로. Eden 안 거침
Java 8u40의 변화?Eager Reclamation. Young GC도 거대 객체 회수
거대 객체가 GC 효율에 미치는 영향 5가지?STW 증가, 단편화, Concurrent 부담, Mixed 효율, 예측 불가
ILIC에서 거대 객체 발생 패턴?대용량 파일, 큰 리스트, 빅 String, 캐시
회피 전략 3가지?스트리밍, 페이지네이션, 청크 처리
리전 크기 늘리면?거대 객체 줄지만 일반 객체 효율 ↓
운영 모니터링 방법?GC 로그 Humongous, JFR, Heap dump

9.2 자기 점검 체크리스트

기본 이해

  • 거대 객체 기준 (리전/2)을 안다
  • Humongous + Humongous Continued 구조를 안다
  • 거대 객체가 Old gen 직행하는 이유를 안다
  • Java 8u40의 Eager Reclamation을 안다
  • GC 효율 5가지 영향을 안다

실전 적용

  • ILIC 코드에서 거대 객체 후보를 식별할 수 있다
  • 스트리밍/페이지네이션 전략을 적용할 수 있다
  • Off-Heap (Direct ByteBuffer) 활용할 수 있다
  • GC 로그에서 Humongous Allocation 검출 가능
  • Heap dump로 거대 객체 식별 가능

면접 대비 — 5분 답변

  • 거대 객체의 정의와 처리
  • G1 효율에 미치는 영향
  • 실무 발생 패턴과 회피 전략
  • 운영 모니터링 방법
  • Java 버전별 차이

🎯 핵심 요약 — 3줄 정리

1. 거대 객체 = 리전 / 2 초과 객체

  • 1MB 리전 → 512KB 초과면 거대
  • 2MB 리전 → 1MB 초과면 거대
  • 연속된 리전(H + HC) 차지
  • Old gen 직행 (Eden 안 거침)

2. 5가지 GC 효율 저해

  • STW 시간 증가 (복사 비용)
  • 외부 단편화 → Full GC 위험
  • Concurrent Cycle 부담
  • Mixed GC 효율 ↓
  • 시간 예측 어려움

3. ILIC 실무 — 만들지 말 것

  • 스트리밍 (Files.lines, JsonGenerator)
  • 페이지네이션 (Page)
  • 청크 처리 (Iterables.partition)
  • Off-Heap (Direct ByteBuffer)
  • 마지막: 리전 크기 32MB로

📚 다음으로...

Unit 4.5 — G1의 우선순위 기반 회수

이번 Unit에서 거대 객체의 특수 처리를 봤다면, 다음은 G1의 마지막 핵심 — Garbage First 알고리즘.

  • 회수 효과 계산 (garbage 비율)
  • 정지시간 한도 내에서 최대 효과
  • "Garbage First" 이름의 의미 완전 해독
  • ILIC 운영 시 GC 튜닝 마지막 가이드

→ Phase 4 마지막 Unit.

Phase 4 진행 상황

🚀 Phase 4 — G1 GC 심화
  ✅ Unit 4.1 참조 카운팅의 한계
  ✅ Unit 4.2 G1 GC의 등장 배경
  ✅ Unit 4.3 리전 기반 레이아웃
  ✅ Unit 4.4 거대 리전 ← 여기
  ⏭ Unit 4.5 우선순위 기반 회수 (Phase 4 완주)

2주차 진행 상황

✅ Phase 1 — 자바 변수 ↔ 메모리 매핑 (1.1 ~ 1.6 완주)
✅ Phase 2 — JVM 메서드 실행 메커니즘 (2.1 ~ 2.4 완주)
✅ Phase 3 — 바이트코드와 상수 풀 (3.1 ~ 3.4 완주)
🚀 Phase 4 — G1 GC 심화 (4/5 진행)
⏭ Phase 5 — 컬렉션 내부 구조
⏭ Phase 6 — Reflection & Iterator
⏭ Phase 7 — Buffer
profile
Software Developer

0개의 댓글