모든 자바 객체가 달고 다니는 머리말(헤더)을 절반으로 줄이는 JVM 기능. Java 24에서 실험 기능, Java 25에서 프로덕션 기능이 됐다.
객체 헤더란 무엇인가
힙에 놓인 자바 객체는 필드 값만 들어 있는 게 아니다. 앞쪽에 JVM이 쓰는 관리 정보가 붙는다.
[기존 — 64비트 JVM]
┌─────────────────┬─────────────────┬───────────────┐
│ mark word │ class pointer │ 필드들... │
│ 8바이트 │ 4바이트 │ │
└─────────────────┴─────────────────┴───────────────┘
헤더 12바이트 (+ 8바이트 정렬 패딩 → 실제 16바이트)
- mark word — 해시코드, GC 세대 나이, 락 상태(어떤 스레드가 이 객체의 모니터를 쥐고 있는지) 등
- class pointer — 이 객체가 어떤 클래스인지 가리키는 포인터
무엇을 줄였나
두 정보를 재배치해 한 머신 워드(8바이트) 에 담는다.
[Compact Object Headers]
┌─────────────────┬───────────────┐
│ 헤더 8바이트 │ 필드들... │
└─────────────────┴───────────────┘
java -XX:+UseCompactObjectHeaders -jar app.jar
왜 체감이 큰가 — 작은 객체가 압도적으로 많다
Integer, 짧은 문자열, 리스트 노드처럼 필드가 몇 개 안 되는 객체에서는 헤더가 전체 크기의 상당 부분을 차지한다. 필드 8바이트짜리 객체라면 기존엔 헤더 16 + 데이터 8 = 24바이트인데, 헤더가 8이 되면 16바이트다 — 3분의 1이 줄어든다.
효과는 힙 절약에서 끝나지 않는다.
객체가 작아진다
- 같은 캐시 라인(CPU가 메모리에서 한 번에 읽어 오는 단위, 보통 64바이트)에
- 더 많은 객체가 들어간다
- 캐시 적중률 상승 → 메모리 접근 지연 감소
- GC가 훑어야 할 총 바이트 수도 줄어 GC 시간 감소
이게 캐시 지역성(locality) 이 좋아진다는 말의 실제 내용이다.
어디에 쓰나
컨테이너 밀도를 높이려는 환경(같은 메모리로 파드를 더 많이)에서 특히 유효하다. 애플리케이션 코드를 한 줄도 안 고치고 플래그 하나로 얻는다.
배경
자바의 메모리 발자국을 줄이려는 Project Lilliput의 결과물이다.
얼마나 줄어드는지 계산해 보기
-
long 필드 하나짜리 객체 (64비트 JVM, 8바이트 정렬)
-
기존: — 헤더 12B + 필드 8B = 20B → 정렬 패딩으로 24B
-
Compact: — 헤더 8B + 필드 8B = 16B → 그대로 16B
- 약 33% 절감
# 켜기 (Java 25에서 프로덕션 기능)
java -XX:+UseCompactObjectHeaders -jar app.jar
# 실제 객체 크기를 확인하려면 JOL(Java Object Layout) 같은 도구를 쓴다
컬렉션처럼 작은 객체가 대량으로 존재하는 워크로드에서 체감이 크다. HashMap의 노드, ArrayList가 담은 박싱된 숫자, 짧은 문자열이 전부 여기 해당한다.
왜 헤더가 두 워드였나
- mark word — 해시코드·GC 나이·락 상태를 담느라 8바이트가 필요했다 class pointer 어떤 클래스인지 가리키는 포인터 (압축해도 4바이트)
Project Lilliput은 이 둘을 한 워드에 재배치했다. 락 상태 표현 방식까지 함께 손봐야 했기 때문에 실험 기능으로 오래 머물렀다.