모든 자바 객체가 실제 데이터 앞에 다는 관리 정보.
[ mark word ][ klass pointer ][ 인스턴스 필드 ][ 정렬 패딩 ]
- mark word — 해시코드 · GC 나이 · 락 상태 (용도가 상태에 따라 바뀐다)
- klass pointer — 이 객체가 어느 클래스인지 (압축 시 4바이트)
64비트 + 압축 참조 기준 헤더 12바이트, 8바이트 정렬이므로 필드가 없어도 객체 하나는 16바이트다
왜 알아야 하나 — 메모리 감각
-
Integer 한 개 — 헤더 12 + int 4 = 16바이트
-
int 한 개 — 4바이트
-
박싱이 4배 + 참조(4~8) 추가 + 캐시 지역성 상실
- 1,000만 개 컬렉션에서 List<Integer> 와 int[] 의 차이가 수백 MB 로 벌어진다
mark word 의 겸업
같은 64비트를 상황에 따라 다르게 쓴다
- identity hashCode 를 처음 부르면 여기에 저장된다
- 락이 걸리면 락 레코드 포인터로 바뀐다
- GC 가 살아남은 횟수(age) 를 여기에 센다
그래서 "hashCode 를 부르면 특정 락 최적화가 풀린다" 같은 겉보기에 무관한 상호작용이 생긴다
Compact Object Headers
헤더를 12 → 8바이트로 줄인다 작은 객체가 많은 워크로드에서 힙 사용량이 10~20% 줄어든 사례가 보고됐다
- JDK 24 — JEP 450 — 실험적 기능으로 도입
- JDK 25 — JEP 519 — 정식(product) 기능으로 승격
- -XX:+UseCompactObjectHeaders 로 활성화한다
배열은 하나 더 붙는다
[ mark ][ klass ][ length ][ 원소들 ] 길이를 헤더에 들고 있어 arraylength 명령이 O(1) 이고, 인덱스 검사(경계 검사)도 여기서 읽는다
필드 정렬과 실제 크기 계산
JVM 은 필드를 선언 순서가 아니라 크기 순으로 재배치한다
- long/double(8) → int/float(4) → short/char(2) → byte/boolean(1) → 참조
class A { boolean a; long b; boolean c; }
- 선언대로면 1 + 패딩7 + 8 + 1 + 패딩7 = 24
- 재배치하면 헤더12 + long8 + boolean1 + boolean1 + 패딩2 = 24
- (여기서는 같지만 필드가 많아질수록 차이가 벌어진다)
실측은 JOL(Java Object Layout) 로 한다
- ClassLayout.parseClass(A.class).toPrintable()
압축 참조
64비트 JVM 이 참조를 4바이트로 줄이는 최적화
- 객체가 8바이트 정렬이므로 하위 3비트가 항상 0 → 그만큼 시프트해 32비트로 표현
- 최대 32GB 까지 주소 지정 가능
힙이 32GB 를 넘으면 자동으로 꺼진다 참조가 전부 8바이트가 되어 실효 용량이 오히려 줄 수 있다 31GB 로 두는 것이 33GB 보다 나은 경우가 실제로 있다
면접 함정
- ❌ "빈 객체는 0바이트" → 헤더 + 정렬로 16바이트다.
- ❌ "헤더는 상수 정보" → mark word는 실행 중 계속 바뀐다.