Hotspot의 CDS와 AppCDS

Hotspot의 CDS와 AppCDS (CDS and AppCDS in Hotspot)

CDS는 사용하기 쉽고 견고한 HotSpot JVM 기능으로, 시작 성능을 개선하는 데 도움을 줘요. 이 문서에서는 CDS의 배경, 아키텍처, 사용법, 디버깅 방법을 함께 살펴봅니다.

출처: CDS and AppCDS in Hotspot

본문

CDS란 무엇인가요?

CDS는 JDK 5의 업데이트 릴리스에서 HotSpot JVM에 추가된 기능이에요. 이후 JDK 릴리스에서 계속해서 개선·확장됐어요. CDS의 목표는 초기화 과정에서 사용되는 Java 클래스와 JVM 메타데이터의 사전 처리된 아카이브(pre-processed archive)를 로드해서 JVM 시작 시간을 줄이는 거예요.

이 문서에서는 CDS의 이점, 동작 방식, Java 애플리케이션에서 CDS를 사용하는 방법, 그리고 CDS를 디버깅하고 문제를 해결하는 방법을 다룰게요.

JVM 초기화 (JVM Initialization)

초기화 과정에서 HotSpot JVM은 일련의 핵심 클래스들을 로드하고 초기화해야 해요. 예를 들어 java.lang 패키지 안에 있는 많은 클래스들이죠. 이 과정은 JVM에서 실행되는 애플리케이션과 무관하게 거의 달라지지 않아요. 이렇게 반복되는 과정이 바로 최적화할 여지가 있는 부분이에요.

JDK 5부터 -Xshare:dump 명령으로 HotSpot이 시작 시 보통 로드하는 핵심 클래스들의 사전 처리된 아카이브를 만들 수 있었어요. 이 공유 아카이브는 $JAVA_HOME/lib/server/classes.jsa (Windows: $JAVA_HOME/bin/server/classes.jsa )에 위치해요.

초기화 시 HotSpot JVM은 지시가 있으면 이 디렉터리에서 공유 아카이브를 찾고, 찾으면 그 아카이브를 읽기 전용의 메모리 매핑된 위치에 로드해요. 사전 처리된 아카이브를 로드하는 것이 클래스를 로드하는 것보다 빠르기 때문에 시간이 절약돼요. 아카이브 형식에서 파일을 압축 해제하고, 검증하고, 바이트코드를 생성하는 등의 단계가 생략되거든요.

기본 CDS 아카이브 (Default CDS Archive)

JDK 12 릴리스부터 64비트 JDK 이미지 빌드를 위해 아키텍처별 기본 CDS 아카이브가 제공돼요. 이전 JDK 버전에서 CDS를 활용하려면 -Xshare:dump 명령을 실행해야 했는데, 이제 그럴 필요가 없어진 거예요. 기본 CDS 아카이브는 $JAVA_HOME/lib/server/classes.jsa (Windows: $JAVA_HOME/bin/server/classes.jsa )에 위치해요.

"Class Data-Sharing"에 대하여

역사적으로 "CDS"는 Class Data-Sharing을 뜻하는 약어였고, CDS 관련 문서에서 여전히 자주 쓰여요. 이 이름은 사전 처리된 아카이브가 같은 머신에서 실행되는 다른 JVM 프로세스들과 메모리 매핑되어 공유되는 방식과 관련이 있어요. 이렇게 하면 JVM 프로세스들의 전체 메모리 사용량을 줄일 수 있었죠.

메모리 비용이 크게 줄고 메모리 가용성은 크게 늘면서, CDS의 이 공유 기능은 더 이상 적극적으로 지원되지 않아요. 게다가 서버에서 실행되는 애플리케이션의 배포 모델은 JVM이 유일하게 실행 중인 프로세스인 꽉 찬 컨테이너(fitted container) 안에 두는 것이어서, 메모리 매핑 공유의 이점이 사라졌어요. 그래서 이제 "CDS"는 더 이상 Class Data-Sharing을 뜻하지 않아요.

CDS 사용하기 (Using CDS)

CDS는 -Xshare:<value> 인자로 제어되며, 다음과 같은 값을 받아요.

  • auto : 공유 아카이브가 있으면 CDS를 사용할 수 있게 해줘요. 기본값이에요. 참고: auto 는 JDK 12부터 모든 64비트 빌드에서 기본값이 되었고, 공유 아카이브가 $JAVA_HOME/lib/server/classes.jsa (Windows: $JAVA_HOME/bin/server/classes.jsa )에 있다고 가정해요.
  • on : CDS 사용을 강제해요. JVM이 공유 아카이브를 로드하는 중 문제를 만나면 에러 메시지를 출력하고 종료해요. 참고: 이것은 테스트 목적으로만 사용하고, 절대 프로덕션 환경에서는 쓰지 마세요.
  • off : CDS를 비활성화해요.
  • dump : CDS 아카이브를 생성해요.

CDS의 성능 이점 (Performance Benefits of CDS)

CDS의 성능 향상은 간단한 "Hello World" 애플리케이션으로 테스트했을 때 약 33% 정도예요. 아래 테스트 결과를 보면:

$ time java -Xshare:off HelloWorld
Hello world!
java -Xshare:off HelloWorld  0.08s
$ time java -Xshare:on HelloWorld
Hello world!
java -Xshare:on HelloWorld  0.05s

-Xshare:off 일 때 애플리케이션 실행 시간은 0.10초였고, -Xshare:on 일 때는 0.05초였어요.

핵심 JDK 클래스만 사용하는 CDS에서 측정 가능한 성능 이점은 실행되는 Java 프로세스의 크기와 복잡성이 커질수록 작아져요. 이것이 AppCDS가 도입된 동기 중 하나예요.

AppCDS

AppCDS는 JEP 310: Application Class-Data Sharing으로 JDK 10 릴리스에서 HotSpot JVM에 추가됐어요. AppCDS는 CDS의 이점을 애플리케이션 클래스까지 확장하는 것을 목표로 해요. AppCDS는 JDK 13과 19 릴리스에서 성능과 사용 용이성을 크게 개선하는 등 더 많은 개선을 거쳤어요. AppCDS는 Java 애플리케이션의 크기와 복잡성이 커짐에 따라 CDS 사용의 일관된 이점을 가능하게 해줘요.

AppCDS는 다음 위치를 지원해요.

  • 런타임 이미지의 플랫폼 클래스
  • 런타임 이미지의 애플리케이션 클래스
  • 클래스패스의 애플리케이션 클래스
  • 모듈 경로의 애플리케이션 클래스

AppCDS 사용하기 (Using AppCDS)

CDS는 JDK에 포함된 핵심 Java 클래스만 다뤘기 때문에, 사전 처리된 공유 아카이브를 JDK의 일부로 포함시키는 것이 가능했어요. 그래서 개발자들은 별다른 조작 없이 바로 CDS를 쓸 수 있었죠(JDK 12부터는 모든 플랫폼에서요). 하지만 AppCDS를 사용하려면 개발자가 어느 정도 적극적으로 개입해야 해요.

동적 공유 아카이브 생성 (Generating a Dynamic Shared Archive)

JDK 13에 추가된 동적 공유 아카이브 기능은 대부분의 사용 사례에서 AppCDS를 더 쉽게 쓰게 하고, 사용자 정의 클래스 로더를 쓰는 애플리케이션을 더 잘 지원하려고 설계됐어요. 동적 공유 아카이브를 생성하려면 -XX:ArchiveClassesAtExit=<name of archive file> 명령을 쓰면 되는데, 애플리케이션 종료 시 공유 아카이브를 생성해요. 이 명령을 쓰는 구체적인 예를 보면:

java -XX:ArchiveClassesAtExit=petclinic-dynamic-archive.jsa -jar target/spring-petclinic-2.5.1.jar

참고: 공유 아카이브 생성은 JVM 초기화와 종료 과정에서 상당한 성능 영향을 줘요.

생성된 아카이브를 이후 실행에서 사용하려면 -XX:SharedArchiveFile=<name of archive file> 을 쓰면 돼요. 앞선 예에서 생성한 아카이브를 사용하는 모습은 이렇습니다:

java -XX:SharedArchiveFile=petclinic-dynamic-archive.jsa  -jar target/spring-petclinic-2.5.1.jar

기본 아카이브 없이 동적 아카이브 (Dynamic Archive without Default Archive)

동적 아카이브가 생성될 때는 기본 아카이브에 포함된 핵심 클래스들이 포함되지 않아요. 대신 동적 아카이브는 기본 아카이브의 기본 위치를 참조해요. 그런데 아카이브 파일이 없거나 손상됐다면, JDK 핵심 클래스와 메타데이터가 평소대로 로드되면서 시작 성능에 나쁜 영향을 주게 돼요(또는 -Xshare:on 을 쓰면 시스템이 종료돼요).

동적 아카이브 자동 생성 (Autogenerating a Dynamic Archive)

JDK 19 릴리스에서 JDK-8261455가 JVM이 -XX:+AutoCreateSharedArchive 로 공유 아카이브를 자동 생성하는 기능을 도입했어요. 이 명령은 JVM에게 -XX:SharedArchiveFile 로 정의된 공유 아카이브를 찾으라고 알려줘요. 아카이브가 없거나 유효하지 않은 상태라면 아카이브를 생성해요. 이 기능의 핵심 이점은 애플리케이션을 시작하는 데 쓰는 것과 같은 java 명령이 아카이브를 생성할 수 있어서 유지보수 작업이 줄어든다는 거예요.

기존 아카이브가 손상됐거나, 더 오래된 JDK 버전으로 생성됐거나, 의존하는 JAR 중 하나가 바뀌었다면 새 아카이브가 생성돼요.

정적 아카이브 (Static Archives)

대부분의 사용 사례에서는 동적 아카이브로 충분해요. 하지만 정적 아카이브가 유리한 경우도 있어요. 예를 들면:

  • 추가적인 심볼·문자열 데이터를 저장할 때
  • 일부 시나리오에서 시작 성능이 약간 더 좋을 때

정적 아카이브를 만들려면 먼저 -XX:DumpLoadedClassList=<classlist name> 명령으로 클래스 목록(classlist)을 생성해야 해요. 이 과정에서 CDS를 꺼야 해요( -Xshare:off ). 앞선 Spring Boot Petclinic 앱 예시를 이어서, 클래스 목록을 만들려면 다음 명령을 실행해요:

java -Xshare:off -XX:DumpLoadedClassList=petclinic.classlist -jar target/spring-petclinic-2.5.1.jar

다음으로 -Xshare:dump 와 함께 -XX:SharedArchiveFile=<name of archive file> 명령으로, 이전 단계에서 생성한 클래스 목록을 -XX:SharedClassListFile=<classlist name> 으로 지정해 공유 아카이브를 생성해야 해요. 예를 들면:

java -Xshare:dump -XX:SharedArchiveFile=petclinic-static-archive.jsa -XX:SharedClassListFile=petclinic.classlist

생성된 아카이브를 이후 실행에서 사용하려면 -XX:SharedArchiveFile=<name of archive file> 을 쓰면 돼요. 앞선 예에서 생성한 아카이브를 사용하는 모습은 이렇습니다:

java -XX:SharedArchiveFile=petclinic-static-archive.jsa -jar target/spring-petclinic-2.5.1.jar

공유 클래스의 정적 아카이브 (Static Archive of Shared Classes)

아래는 여러 Java 애플리케이션에서 함께 쓰는 공유 클래스·라이브러리의 아카이브를 정적 아카이브 기능으로 만드는 예시예요.

hello.jar와 hi.jar의 클래스를 포함하려면, .jar 파일을 -cp 파라미터로 지정된 클래스패스에 추가해야 해요.

Hello 애플리케이션이 사용하는 모든 클래스와 Hi 애플리케이션이 사용하는 모든 클래스의 목록을 만듭니다:

java -XX:DumpLoadedClassList=hello.classlist -cp common.jar:hello.jar Hello
java -XX:DumpLoadedClassList=hi.classlist -cp common.jar:hi.jar Hi

공유 아카이브 파일을 공유할 모든 애플리케이션이 사용하는 클래스들의 단일 목록을 만듭니다.

Linux와 macOS: 다음 명령은 hello.classlist와 hi.classlist 파일을 하나의 common.classlist 파일로 합칩니다:

cat hello.classlist hi.classlist > common.classlist

Windows: 다음 명령은 hello.classlist와 hi.classlist 파일을 하나의 common.classlist 파일로 합칩니다:

type hello.classlist hi.classlist > common.classlist

common.classlist의 모든 클래스를 포함하는 common.jsa라는 공유 아카이브를 만듭니다:

java -Xshare:dump -XX:SharedArchiveFile=common.jsa -XX:SharedClassListFile=common.classlist -cp common.jar:hello.jar:hi.jar

사용된 클래스패스 파라미터는 Hello와 Hi 애플리케이션이 공유하는 공통 클래스패스 접두사예요.

같은 공유 아카이브로 Hello와 Hi 애플리케이션을 실행해요:

java -XX:SharedArchiveFile=common.jsa -cp common.jar:hello.jar:hi.jar Hello
java -XX:SharedArchiveFile=common.jsa -cp common.jar:hello.jar:hi.jar Hi

출처(Source)

기본 CDS 아카이브를 가진 정적 아카이브 (Static Archive with Default CDS Archive)

동적 아카이브와 달리 정적 아카이브는 핵심 JDK 클래스를 포함해요. 그래서 시스템에서 기본 CDS 아카이브가 삭제되거나 손상돼도, 정적 아카이브를 참조하는 JVM 프로세스의 시작에는 영향을 주지 않아요.

CDS 디버깅 (Debugging CDS)

CDS는 장애 방지(failsafe) 방식으로 설계돼서, 백그라운드에서 매끄럽게 동작하는 것이 정상이에요. CDS는 기본 설정인 -Xshare:auto 를 쓰면, 공유 아카이브가 없거나 손상됐거나 유효하지 않을 때 파일 시스템에서 클래스를 로드하는 방식으로 폴백해요.

그런데 실제로 CDS가 JVM을 시작 시 크래시시키는 경우도 있을 수 있어요. 이 섹션에서는 CDS가 실패하게 만드는 흔한 문제들과, CDS 아카이브를 만들려다 생길 수 있는 문제들, 그리고 그것들을 진단·디버깅하는 방법을 다룰게요.

에러 메시지 (Error Messages)

가장 직접적인 디버깅 형태는 CDS가 시작 시 문제를 만나 JVM을 종료시키는 경우예요. 이는 -Xshare:on 을 쓸 때만 발생해야 해요. -Xshare:auto (기본값)로 설정했을 때 공유 아카이브 로드 중 에러가 나면, JVM은 조용히 에러를 무시하고 클래스를 정상적으로 로드해요. 이런 이유로 -Xshare:on 은 프로덕션 환경에서 권장되지 않아요.

유효하지 않거나 없는 공유 아카이브 (Invalid or Missing Shared Archive)

-XX:SharedArchiveFile 로 정의된 공유 아카이브를 찾을 수 없으면 HotSpot JVM은 콘솔에 이 메시지를 출력해요:

An error has occurred while processing the shared archive file.
Specified shared archive not found (<name of the archive>).
Error occurred during initialization of VM

-XX:SharedArchiveFile 로 정의된 공유 아카이브가 손상됐거나 유효하지 않으면 HotSpot JVM은 콘솔에 이 메시지를 출력해요:

An error has occurred while processing the shared archive file.
The shared archive file has a bad magic number.
Error occurred during initialization of VM
Unable to use shared archive.

기본 CDS 아카이브 파일이 없거나 손상됐다면 HotSpot JVM은 콘솔에 이 메시지를 출력해요:

An error has occurred while processing the shared archive file.
Specified shared archive not found (<JAVA_HOME>/Contents/Home/lib/server/classes.jsa).
Error occurred during initialization of VM
Unable to use shared archive.

유효하지 않거나 없는 클래스 목록 (Invalid or Missing Classlist)

정적 공유 아카이브를 생성할 때 -XX: SharedClassListFile 로 정의된 클래스 목록을 찾지 못하면 다음 에러가 출력돼요:

Error occurred during initialization of VM
Loading classlist failed: No such file or directory

정적 공유 아카이브를 생성할 때 -XX: SharedClassListFile 로 정의된 클래스 목록이 손상됐다면 다음 에러가 출력돼요:

An error has occurred while processing class list file HelloMessage-test.classlist 1:9.
Unknown input:
Invalid format
        ^
Error occurred during initialization of VM
class list format error.

아카이브 생성 보고서 (Archive Generation Report)

공유 아카이브를 생성할 때 JVM 프로세스는 기본적으로 콘솔에 많은 진단 정보를 출력해요. 이 정보로 어떤 클래스·라이브러리가 공유 아카이브에 추가되는지(또는 추가되지 않는지)를 확인할 수 있어요.

공유 아카이브에 추가되는 클래스/라이브러리:

[0.007s][info][class,load] java.lang.Object source: jrt:/java.base
[0.007s][info][class,load] java.io.Serializable source: jrt:/java.base
[0.007s][info][class,load] java.lang.Comparable source: jrt:/java.base

공유 아카이브에 추가되지 못한 클래스/라이브러리:

[14.078s][warning][cds] Pre JDK 6 class not supported by CDS: 49.0 jdk/internal/reflect/GeneratedMethodAccessor55
[14.078s][warning][cds] Skipping org/springframework/beans/NotReadablePropertyException: Not linked

생성된 클래스 목록 (Generated Classlist)

정적 아카이브를 생성할 때 클래스 목록 파일을 들여다보면 어떤 클래스와 JVM 메타데이터가 공유 아카이브에 추가될지 알 수 있어요. 생성된 파일 맨 위의 주석에도 적혀 있듯이, 이 파일은 손으로 수정하면 안 돼요. 아래는 클래스 목록 파일이 어떤 모습인지 보여주는 예시예요:

# NOTE: Do not modify this file.
#
# This file is generated via the -XX:DumpLoadedClassList=<class_list_file> option
# and is used at CDS archive dump time (see -Xshare:dump).
#
java/lang/Object
java/io/Serializable
java/lang/Comparable
java/lang/CharSequence
java/lang/constant/Constable
java/lang/constant/ConstantDesc
java/lang/String
java/lang/reflect/AnnotatedElement
java/lang/reflect/GenericDeclaration
java/lang/reflect/Type
java/lang/invoke/TypeDescriptor
java/lang/invoke/TypeDescriptor$OfField

디버그 로깅 (Debug Logging)

HotSpot이 CDS의 내부 동작을 더 자세히 콘솔에 로깅하도록 설정할 수 있는 옵션이 몇 가지 있어요.

CDS 디버그 로깅 (Debug CDS Logging)

  • -Xlog:cds=debug : 공유 아카이브 생성과 로드 시 둘 다 사용해서 아카이브에 추가되는 클래스와 추가 메타데이터에 대한 상세 통계를 보여줘요.
  • -Xlog:cds+lambda=debug : -Xlog:cds=debug 처럼 아카이브 생성·로드 시 둘 다 사용할 수 있어요. 이 옵션은 특히 CDS가 람다를 처리하는 방식에 관한 추가 정보를 제공해요.

클래스 로드 디버그 로깅 (Debug Class Loading Logging)

공유 아카이브를 로드할 때 JVM 인자 -verbose:class 를 쓰면 어떤 클래스가 공유 아카이브에서 로드되는지, 아니면 HotSpot의 일반 클래스 로드 과정을 거치는지 볼 수 있어요. 클래스가 공유 아카이브에서 로드되면 소스가 shared objects file 로 보고돼요. 아래 예시 출력처럼요:

[0.008s][info][class,load] java.lang.Object source: shared objects file
[0.009s][info][class,load] java.io.Serializable source: shared objects file

클래스가 공유 아카이브에서 로드되지 않으면, 여기처럼 java.lang.Objectjava.io.Serializablejrt:/java.base 모듈에서 로드되는 것처럼 원래 소스 위치를 보고해요:

[0.007s][info][class,load] java.lang.Object source: jrt:/java.base
[0.007s][info][class,load] java.io.Serializable source: jrt:/java.base

더 알아보기 (Learn more)

  • 이 튜토리얼의 목차: CDS란 무엇인가요? / CDS 사용하기 / AppCDS / CDS 디버깅 / 더 배우기
  • 더 배우기(More Learning) 항목은 이 튜토리얼을 확장한 별도 페이지에서 확인할 수 있어요.