지난 9월에 Java25 버전이 정식 릴리즈 됐다. 여러가지 개선과 변화가 있었지만 그 중 눈에 띄는 항목 중 하나가 있었다. 바로 Compact Object Header 기능으로 객체의 헤더 사이즈를 기존 12~16byte에서 8byte로 줄였다는 내용이다. 8byte 라는 수치가 상당히 작게 느껴질 수 있겠지만 수천, 수만개의 객체들이 생성되는 애플리케이션에서 객체당 4~8byte 절약은 메모리 절감 효과를 기대할 수 있다. 이번 포스팅에서는 JOL(Java Object Layer)를 사용하여 정말 객체의 헤더 사이즈가 줄어드는지 확인해 보자.
HotSpot JVM 이란?
JVM은 자바가 어떻게 돌아가야 하는지 적힌 설계도라고 한다면 HotSpot JVM은 실제로 만들어진 소프트웨어(구현체)다. 오라클 JDK나 OpenJDK에서 HotSpot JVM을 기본 엔진으로 사용하기 때문에 보통 우리가 일반적으로 사용하는 표준 JVM은 HotSpot JVM이라고 생각하면 된다. 구현체로써 HotSopt JVM이 아닌 다른 구현체도 존재할 것이다. GraalVM이나 OpenJ9, Azul Platform Prime과 같은 다른 JVM 구현체는 이 포스팅의 대상이 아니다. 이 포스팅의 대상은 HotSpot JVM을 대상으로 한다.
Compact Object Header 기능 히스토리
- JEP450 – Java24 실험 기능으로 처음 Compact Object Header가 제안되었다. 64bit 아키텍처에서 96~128bit 헤더 사이즈를 64bit 크기로 줄이기 위한 목적이다.
- JEP519 – Java25에서 정식 기능으로 릴리즈 되었다.
Compact Object Header JVM Flag
Java25에서 Compact Object Header 기능을 사용하려면 JVM 플래그를 지정해 줘야 한다. 플래그를 지정하지 않으면 Compact Object Header가 작동하지 않는다.
-XX:+UseCompactObjectHeadersJavaJava24 preview 환경에서는 다음 플래그를 추가로 지정해 줘야 한다.
-XX:+UnlockExperimentalVMOptionsJavaJOL(Java Object Layout)을 이용한 확인
JOL은 JVM 내부를 들여다보고 객체가 메모리에 실제로 어떻게 배치되는지(헤더, 필드, 패딩등) 바이트 단위로 보여주는 라이브러리다. 샘플 코드와 JOL 라이브러리를 통해서 실제로 객체 헤더 크기가 줄어드는지 확인해 보자.
JOL github의 ReadMe를 통해서 JOL에 대해서 더 많은 정보를 얻을 수 있다.
샘플 코드
JOL 라이브러리 디펜던시를 추가한다.
implementation 'org.openjdk.jol:jol-core:0.17'Groovy이제 객체 생성 후 JOL을 통해서 화면에 찍어주기만 하면 된다.
public class JolExample {
static class EmptyObject {}
static class SimpleObject {
int id;
boolean active;
}
public static void main( String[] args ) {
// 현재 JVM 환경 정보 출력 (압축 OOP 사용 여부 등 확인)
System.out.println( VM.current().details());
System.out.println("======================================");
// 1. EmptyObject 레이아웃 출력
EmptyObject empty = new EmptyObject();
System.out.println("### 1. EmptyObject Layout ###");
// ClassLayout.parseInstance(객체).toPrintable()이 핵심입니다.
System.out.println( ClassLayout.parseInstance(empty).toPrintable());
System.out.println("======================================");
// 2. SimpleData 레이아웃 출력
SimpleObject data = new SimpleObject();
System.out.println("### 2. SimpleData Layout ###");
System.out.println(ClassLayout.parseInstance(data).toPrintable());
}
}Java빈 객체(EmptyObject)와 int, boolean 타입을 가진 객체(SimpleObject)를 생성하여 JOL을 통해서 메모리 내용을 출력한 코드다.
Java21 에서 실행
Java21 버전으로 main 함수를 실행하면 다음과 같은 결과가 출력된다.
# VM mode: 64 bits
# Compressed references (oops): 3-bit shift
# Compressed class pointers: 3-bit shift
======================================
### 1. EmptyObject Layout ###
JolExample$EmptyObject object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x0101ea50
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
======================================
### 2. SimpleData Layout ###
JolExample$SimpleObject object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x0101fc00
12 4 int SimpleObject.id 0
16 1 boolean SimpleObject.active false
17 7 (object alignment gap)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes totalPlaintextEmptyObject는 빈 객체이므로 헤더 크기 12byte + padding 4byte(object alignment gap)으로 16byte다. 실제 헤더 크기가 header: mark, header: class 사이즈가 12byte인데 이유는 Compressed Oops 기능때문으로 기본적으로 enable 상태인데 이 기능이 활성화되면 class pointer를 압축하여 8 -> 4byte로 줄여주는 역할을 한다. 이러나 저러나 padding 4byte가 추가되므로 16byte가 된다.
padding이 추가되는 이유는 JVM의 정렬 규칙이 ‘모든 객체 크기는 8의 배수로 끝나야 한다’는 규칙이기 때문이다.
Java25 에서 실행
아직 java 25 버전이 설치되어 있지 않고 sdkman을 사용한다면 아래 명령으로 쉽게 설치 가능하다.
sdkman 사용법은 아래 포스팅을 참고하기 바란다.
SDKMAN으로 개발도구 버전 쉽게 관리하자
sdk install java 25.0.1-temBash만약 java 25, gradle 8.14 버전을 사용하는 경우
빌드가 현재 호환되지 않는 Java 25.0.1 및 Gradle 8.14을(를) 사용하도록 구성되었습니다. 프로젝트를 동기화할 수 없습니다.
호환되는 최대 Gradle JVM 버전은 24입니다.Bash와 같은 에러가 발생한다면 gradle을 실행하는 자바 버전을 24 이전 버전으로 설정해야 한다.
IntelliJ를 사용한다면 설정 > 빌드도구 > Gradle > Gradle JVM에서 java 버전을 지정할 수 있다.
java 25 버전에서 main 함수를 실행하면 다음과 같은 결과가 출력된다.
# VM mode: 64 bits
# Compressed references (oops): 3-bit shift
# Compressed class pointers: 3-bit shift
======================================
### 1. EmptyObject Layout ###
JolExample$EmptyObject object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x01055ad8
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
======================================
### 2. SimpleData Layout ###
JolExample$SimpleObject object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x010568b8
12 4 int SimpleObject.id 0
16 1 boolean SimpleObject.active false
17 7 (object alignment gap)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes totalPlaintextJava 21 버전에서 실행한 결과와 동일하다. -XX:+UseCompactObjectHeaders 옵션을 지정하지 않았기 때문이다.
Java25 에서 실행 (-XX:+UseCompactObjectHeaders)
Java25 에서 -XX:+UseCompactObjectHeaders 옵션을 지정하고 실행한 결과는 다음과 같다.
# VM mode: 64 bits
# Lilliput VM detected (experimental)
# Compressed references (oops): 3-bit shift
# Compressed class pointers: 3-bit shift
======================================
### 1. EmptyObject Layout ###
JolExample$EmptyObject object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x010c580000000001 (Lilliput)
Instance size: 8 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
======================================
### 2. SimpleData Layout ###
JolExample$SimpleObject object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x010cd00000000001 (Lilliput)
8 4 int SimpleObject.id 0
12 1 boolean SimpleObject.active false
13 3 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes totalPlaintext‘Lilliput VM detected (experimental)’ 에서 확인할 수 있듯이 아직 실험적 단계이지만 EmptyObject는 header: mark만 존재하고 8byte 사이즈로 줄어들었다. (8byte므로 padding 처리도 없다.)
SimpleObject 역시 24byte -> 16byte로 사이즈가 줄어든 것을 확인할 수 있다.
JEP 519에 따르면 Compact Object Header 활성화가 성능을 향상시킨다는 사실이 입증되었다고 한다.
- 어떤 환경에서의 테스트는 힙 공간 22% 절약 CPU 시간 8% 절약 효과
- 다른 환경에서의 테스트는 GC 횟수가 G1 및 Parallel 컬렉터 모두 15% 감소 효과