박종훈 기술블로그
close menu

JDK-8382582: 실험적 기능 JVMCI 제거 동향 정리

요즘 OpenJDK 커뮤니티에서 가장 큰 화두 중 하나가 JVMCI 제거다. JDK-8382582 “Remove the experimental JVMCI feature” 이슈가 그 중심에 있고, 호환성 검토를 담당하는 JDK-8385105 CSR이 이미 Approved 상태이며, 실제 코드 변경은 openjdk/jdk#30834 PR에서 진행되고 있다. 릴리즈 노트 초안은 JDK-8385142에 정리되었다.

JVMCI는 HotSpot 내부에서 10년 넘게 실험적 기능으로 존재해왔던, 그러나 동시에 Graal과 GraalVM Native Image의 근간을 이루었던 인터페이스다. 이 글에서는 JVMCI가 무엇이었고, 왜 이 시점에 제거 결정이 나오게 되었는지를 정리해본다.

JVMCI란 무엇인가

JVMCI는 JVM Compiler Interface의 약자다. 한 줄로 요약하면, “Java로 JIT 컴파일러를 작성해서 HotSpot에 끼워 넣을 수 있게 하는 인터페이스”다.

HotSpot의 기존 JIT 컴파일러(C1, C2)는 모두 C++로 작성되어 있다. C1은 빠르게 컴파일하지만 최적화 수준이 낮고, C2는 시간이 더 걸리지만 더 공격적인 최적화를 수행한다. 두 컴파일러 모두 수십 년 동안 HotSpot 안에서 성장해온 만큼 코드베이스가 거대하고, 변경에 보수적이다.

JVMCI는 여기에 “Java로도 JIT 컴파일러를 작성할 수 있게 하자” 는 아이디어로 추가된 인터페이스다. JEP 243의 표현을 빌리면, JVMCI API는 다음 세 가지 메커니즘으로 구성된다.

  1. VM 자료구조 접근: 클래스, 필드, 메서드, 프로파일링 정보 등 바이트코드-기계어 컴파일러가 필요로 하는 VM 내부 자료에 Java에서 접근할 수 있게 한다.
  2. 기계어 설치(install): Java 측에서 생성한 기계어를 HotSpot에 설치하면서, GC가 필요로 하는 oop map과 디옵트(deoptimization)용 메타데이터까지 함께 넘길 수 있게 한다.
  3. 컴파일 시스템에 플러그인: HotSpot의 compile broker에서 “이 메서드를 컴파일해 주세요”라고 요청이 들어왔을 때, Java로 작성된 컴파일러가 그 요청을 직접 처리하게 한다.

쉽게 말해, JVMCI를 통해 외부에서 작성된 Java 컴파일러가 마치 HotSpot 내부의 C2처럼 동작할 수 있게 된다.

주요 사용자: Graal과 Native Image

JVMCI의 가장 대표적인 소비자는 Graal 컴파일러다. Graal은 Java로 작성된 JIT 컴파일러로, JVMCI를 통해 HotSpot에 플러그인되어 C2를 대체할 수 있다. JEP 243 자체에서도 “Graal이 광범위한 벤치마크에서 C2와 대등한 peak performance를 보여주었다”고 JVMCI 활용의 모범 사례로 언급되어 있다.

그리고 이 Graal 컴파일러가 다시 GraalVM Native Image의 AOT(Ahead-of-Time) 컴파일에도 활용된다. 즉, JVMCI는 단순히 한 실험이 아니라:

JVMCI (HotSpot 인터페이스)
  └── Graal (Java로 작성된 JIT 컴파일러)
        └── Native Image (AOT 컴파일러)

이런 식의 생태계를 떠받치는 기반 기술이었다.

왜 JVMCI가 도입되었었나

JVMCI는 John Rose가 2014년 10월 제안한 JEP 243: Java-Level JVM Compiler Interface을 통해 JDK 9에 정식 도입되었다(이슈 JDK-8062493).

동기: “컴파일러를 Java로 작성하면 좋지 않을까?”

JEP 243의 Motivation 섹션은 의외로 단순하다. 요지는 “최적화 컴파일러는 복잡한 소프트웨어인데, Java가 제공하는 다음과 같은 장점들이 컴파일러 개발에 매우 잘 맞는다”는 것이다.

  • 자동 메모리 관리(GC)
  • 예외 처리(exception handling)
  • 동기화(synchronization)
  • 훌륭한(그리고 무료인) IDE
  • 잘 갖춰진 유닛 테스트 인프라
  • 서비스 로더를 통한 런타임 확장성

반면 컴파일러는 바이트코드 인터프리터나 GC처럼 저수준 언어 기능이 꼭 필요한 영역도 아니다. 따라서 “JVM 컴파일러를 Java로 작성하면 C/C++로 된 기존 컴파일러보다 유지보수와 개선이 더 쉬울 것” 이라는 가설이 JVMCI의 출발점이었다.

목표와 비목표(Non-Goal)

JEP 243의 Goal과 Non-Goal을 보면, 처음부터 어떤 선을 그어두고 시작한 실험이었음을 알 수 있다.

Goal:

  • JVMCI를 대상으로 작성된 Java 컴포넌트가 런타임에 로드되어 JVM의 compile broker에 사용될 수 있게 한다.
  • JVMCI를 대상으로 작성된 Java 컴포넌트가 런타임에 로드되어, 신뢰된 Java 코드가 기계어를 JVM에 설치하고 그것을 Java reference로 호출할 수 있게 한다.

Non-Goal:

  • Graal 같은 동적 컴파일러 자체를 JDK에 통합하는 것은 목표가 아니다.

즉, JVMCI는 어디까지나 “인터페이스”이며, 그 인터페이스 위에서 동작할 컴파일러(Graal 등)는 JDK 본류와 별도로 관리한다는 원칙이 명시되어 있었다.

“experimental”이라는 단서

JEP 243의 Risks and Assumptions 섹션은 JVMCI의 성격을 매우 명확히 못 박는다.

“JVMCI will be experimental in JDK 9 and such will require extra command line options to enable it.”

JDK 9에서 JVMCI를 활성화하려면 다음과 같은 플래그들이 필요했다.

-XX:+UnlockExperimentalVMOptions
-XX:+EnableJVMCI
-XX:+UseJVMCICompiler
-Djvmci.Compiler=<컴파일러 이름>

-XX:+UnlockExperimentalVMOptions을 먼저 풀어야 EnableJVMCI를 켤 수 있었고, 컴파일러 이름을 시스템 프로퍼티로 명시해야 했다. 이 진입 장벽 자체가 “보장된 API가 아니다”라는 신호였다.

또한 JEP 243의 Success Metrics는 다음과 같다.

“The ability of a JVMCI based dynamic compiler to run on the unmodified JVM. The performance of compiler and its generated code are not of high concern since JVMCI will be an experimental feature in JDK 9…”

성공 기준은 “성능”이 아니라 “수정되지 않은 JVM 위에서 JVMCI 기반 동적 컴파일러가 동작하느냐” 였다. 안정성과 성능에 대한 보장은 처음부터 약속된 적이 없다.

보안 모델: Unsafe와 동급

JEP 243은 JVMCI의 보안 위험에 대해서도 언급한다. VM 내부에 접근하고 기계어를 설치/실행하는 API는 본질적으로 위험하며, “JVMCI는 Unsafe 클래스와 같은 수준으로 보호되어야 한다” 고 명시되어 있다. JDK 9에서는 모듈 시스템(JEP 261)의 접근 제어를 통해 신뢰되지 않은 코드로부터 JVMCI를 격리했다.

그 외 배경: Project Galahad와 Metropolis

JEP 243의 표면적 동기는 위와 같았지만, 이후 OpenJDK 차원에서는 JVMCI를 발판으로 삼는 두 가지 큰 흐름이 더 있었다.

  • Project Galahad: GraalVM의 일부 기능(특히 Graal JIT)을 OpenJDK 본류에 통합하려는 프로젝트.
  • Project Metropolis: HotSpot의 일부를 Java로 재작성하려는 장기 프로젝트.

이 두 프로젝트가 살아 있을 때는, JVMCI를 본류에 두는 비용을 “미래의 통합을 위한 투자”로 정당화할 수 있었다.

요약하면 JVMCI는, “성공하면 본격적인 아키텍처 작업을 거쳐 정식화하고, 실패하면 제거하거나 다른 방식을 찾는다” 는 전제 위에서 시작된 실험이었다.

그래서 왜 지금 제거하려는가

JDK-8382582의 설명은 비교적 길지만, 한 문장으로 요약하면 다음과 같다.

JVMCI가 본류 JDK 개발에 부과하는 비용은 크고 지속적인 반면, 그 혜택을 보는 다운스트림 프로젝트는 매우 좁다.

조금 더 풀어보면 다음과 같은 근거들이 있다.

1. JVMCI는 격리된 모듈이 아니라 “교차 관심사”

JVMCI 코드는 한 디렉토리 안에 갇혀 있는 외부 플러그인이 아니다. 다음과 같이 HotSpot 곳곳에 침투해 있다.

  • 컴파일러 공용 코드
  • 런타임 코드
  • 메타데이터 처리
  • 디옵트(Deoptimization)
  • code cache 관리
  • GC 지원 코드
  • Serviceability(JFR, JMX 등)
  • 플래그 정의
  • 테스트 인프라
  • 빌드 시스템

제거 PR에서는 #if INCLUDE_JVMCI 조건부 블록 252개가 한꺼번에 사라지는 것으로 보고되었다. 단순한 죽은 코드가 아니라, 다른 변경들이 매번 “이 변경이 JVMCI에도 영향을 주는가?”를 고려하며 짊어져야 했던 복잡성이다.

2. 1.5%의 부수 비용

이슈에서는 흥미로운 통계 하나를 제시한다.

“1년치 커밋을 분석한 결과, JVMCI나 Graal을 직접 타겟으로 하지 않은 변경 중 약 75건이 JVMCI 관련 경로/테스트/조건부를 건드려야 했다. 전체 기여의 약 1.5% 가 의도치 않게 JVMCI를 신경 써야 했다.”

1.5%는 작아 보이지만, 본류 HotSpot의 변경 빈도를 생각하면 결코 적지 않다. 그리고 그 1.5%는 정작 JVMCI를 사용하지 않는 사람들이 부담하는 비용이다.

3. 구체적인 회귀(Regression) 사례

이슈는 추상적인 주장에 그치지 않고, JVMCI 때문에 본류 변경이 깨졌던 실제 사례들을 든다.

  • JDK-8331087 — nmethod의 immutable 데이터를 CodeCache 밖으로 옮긴 작업. jvmci_data 처리가 명시적으로 포함되어야 했고, 이후 관련 작업에서 JDK-8378195, JDK-8355034 같은 JVMCI 전용 회귀가 따라 나왔다.
  • JDK-8364128 — CPU feature 이름을 모으는 런타임 정리 작업. 그 결과 JDK-8365218에서 Graal이 AArch64 CPU feature를 잘못 인식해 지원하지 않는 명령어를 방출하는 문제가 발생했다.
  • JDK-8301995 — invokedynamic resolution 정보를 ConstantPoolCacheEntry에서 분리한 작업. JVMCI의 HotSpotConstantPool 조회 경로가 JDK-8307588에서 깨졌고, 관련된 libgraal 빌드 문제까지 동반되었다.

공통점은, 이 작업들 중 어느 것도 “JVMCI에 관한” 작업이 아니었다는 것이다. C2나 nmethod, deoptimization 같은 일반적인 HotSpot 진화의 한 부분이었는데, 매번 JVMCI 쪽에서 그에 대응하는 메타데이터 표현/플래그/특수 경로가 있는지 확인하고 수리해야 했다.

이슈에서는 이를 다음과 같이 표현한다.

“If the JVMCI-specific consequence is noticed early, the original change becomes broader. If it is missed, the breakage surfaces later in a real client such as Graal or Native Image.”

(JVMCI 쪽 여파를 일찍 발견하면 원래 변경의 범위가 넓어지고, 놓치면 Graal이나 Native Image 같은 실제 클라이언트에서 뒤늦게 깨진다.)

4. Project Valhalla 같은 진행 중인 프로젝트에 미치는 영향

Project Valhalla(값 타입, primitive class)는 deoptimization 경로에 새로운 정보를 추가하고 있다. 여기서 C2는 배열 속성에 대한 정제된 정보를 제공할 수 있지만, JVMCI는 그 정보를 활용하지 못하고 보수적인 기본값으로 폴백하는 상황이 생겼다.

이슈는 이를 “반복되는 패턴“으로 묘사한다. 새로운 VM 작업이 진행되면, 본류 경로에서 한 번에 잘 정리하고 끝낼 수 없고, JVMCI에도 똑같은 작업을 하거나, 보수적인 폴백을 추가하거나, 불완전한 특수 처리를 남겨야 한다. 이는 Valhalla 같은 큰 프로젝트의 속도를 떨어뜨리고, 실행 모드 간 미묘한 차이의 위험을 키운다.

5. 빌드와 테스트 매트릭스 폭발

JVMCI는 빌드/테스트 측면에서도 비용이 크다.

  • JVMCI 활성/비활성
  • JVMCI 컴파일러 활성/비활성
  • 컴파일러 구성 차이
  • 플랫폼 차이
  • jtreg 속성: vm.jvmci, vm.jvmci.enabled, vm.graal.enabled

이런 조합을 다 관리해야 한다. 이슈에 따르면 tier1의 JVMCI 관련 테스트만 약 42분 분량의 머신 시간을 소비한다. 또한 JDK-8197235 같은 빌드 시스템 특수 처리도 JVMCI 때문에 필요했다.

대부분의 사용자가 JVMCI를 쓰지 않음에도 본류는 이 경로들을 유지하고 빌드하고 테스트해야 한다. 아니면 그것들이 조용히 썩어가는 것을 감수해야 한다.

6. Project Galahad와 Metropolis의 해산

가장 결정적인 변화는, JVMCI의 가장 큰 명분이었던 두 프로젝트가 모두 해산되었다는 점이다.

  • Project Galahad (2022~2026): GraalVM의 Java JIT을 OpenJDK 본류에 통합하려던 시도. 2026년 3월 해산.
  • Project Metropolis (2018~2026): HotSpot의 일부를 Java로 재작성하려던 시도. 2026년 4월 해산.

이 두 프로젝트가 살아 있는 동안에는, JVMCI를 본류에 두는 비용을 “미래의 통합을 위한 투자”로 정당화할 수 있었다. 그러나 두 프로젝트가 연달아 해산되면서, 본류 JDK에 JVMCI를 유지해야 할 가장 큰 이유가 사라졌다.

CSR(JDK-8385105)의 Summary는 이 점을 매우 직설적으로 요약하고 있다.

“After dissolving Galahad, Graal and Metropolis projects there are no consumers of JVMCI experimental API left in OpenJDK.”

(Galahad, Graal, Metropolis 프로젝트가 해산된 이후, OpenJDK 안에는 JVMCI 실험 API의 소비자가 더 이상 남아 있지 않다.)

CSR은 호환성 분류를 다음과 같이 기재하고 있다.

  • Compatibility Kind: behavioral
  • Compatibility Risk: minimal
  • Interface Kind: Java API, add/remove/modify command line option
  • Scope: JDK

“실험적 기능”으로 명시되어 온 API였기 때문에, 본류 입장에서 호환성 리스크는 최소(minimal)로 분류되었다는 점이 눈에 띈다.

무엇이 제거되는가

릴리즈 노트 초안(JDK-8385142)에서 정리된 제거 범위는 다음과 같다.

  • HotSpot JVM 내부의 JVMCI 코드
  • 다음 모듈들
    • jdk.internal.vm.ci
    • jdk.graal.compiler
    • jdk.graal.compiler.management
  • JVMCI 전용 JIT 컴파일 정책
  • configure의 feature selection 플래그
  • 이름에 “JVMCI”가 들어가는 모든 플래그
  • -XX:+UseGraalJIT 플래그

요컨대 본류 JDK 안에서 “Java로 JIT 컴파일러를 작성/등록할 수 있는” 모든 표면이 사라진다.

다운스트림 프로젝트는 어떻게 되는가

이슈와 릴리즈 노트 모두 동일한 안내를 한다.

“Downstream projects that depend on JVMCI should carry and maintain it in their own downstream trees or stay on previous releases of the JDK.”

(JVMCI에 의존하는 다운스트림 프로젝트는 자체 트리에서 패치를 직접 유지하거나, 이전 JDK 릴리즈에 머물러야 한다.)

핵심은 “이전에는 본류 JDK가 짊어지던 비용을, 그 혜택을 직접 보는 프로젝트에게 돌려준다” 는 것이다. 본류 JDK는 좁은 사용처를 위한 광범위한 비용을 더 이상 떠안지 않고, 대신 그것을 필요로 하는 프로젝트가 자신들의 포크에서 JVMCI를 유지하게 된다.

“그럼 GraalVM이 갑자기 깨지는 것 아닌가?”

이 질문이 자연스럽게 따라온다. Graal이 JVMCI의 대표 소비자였는데, JVMCI가 본류에서 사라지면 GraalVM도 문제가 생기는 것 아닌가? 결론부터 말하면 “문제 없다” 인데, 이유는 다음과 같다.

1. JVMCI는 “코드”이지 “추상적 표준”이 아니다

JVMCI는 OpenJDK 본류 소스 트리에 들어 있는 코드일 뿐, 어떤 추상적 사양이 아니다. 본류에서 빠진다 = 본류 배포본에서 빠진다일 뿐, 누군가가 그 코드를 자기 트리에 그대로 들고 있는 것은 막지 않는다. CSR도 그 점을 명시한다.

2. GraalVM은 원래도 별도 배포본이다

우리가 “GraalVM”이라고 부르는 그 배포본은 사실 다음 같은 구조의 별도 JDK 빌드다.

GraalVM = OpenJDK 본류 + GraalVM 팀의 패치 + Graal 컴파일러 + Native Image + ...

단순히 “Oracle JDK에 Graal 플래그를 켠 것”이 아니다. 실제로 GraalVM CE는 labs-openjdk 같은 별도 포크 위에서 빌드된다. 따라서 본류에서 JVMCI가 빠져도, GraalVM 팀은 자기 포크에 JVMCI 소스를 보존해두고 그 위에서 빌드를 이어가면 그만이다.

3. JVMCI는 원래도 Graal 팀이 만든 코드다

사실 한 단계 더 들어가면 더 흥미로운 그림이 나온다. JVMCI는 OpenJDK가 자체적으로 설계한 것이 아니라, Oracle Labs의 Graal 팀이 자기들 Graal 컴파일러를 HotSpot에 끼우기 위해 만든 인터페이스다. JEP 243의 리뷰어 명단이 이를 잘 보여준다.

  • Douglas Simon — Oracle Labs, Graal 핵심 개발자
  • Thomas Wuerthinger — Oracle Labs, Graal/GraalVM 프로젝트 리드
  • Vladimir Kozlov — HotSpot JIT 리드 (본류 쪽)
  • Mikael Vidstedt — 본류 쪽

이름 구성만 봐도 “Graal 팀이 가져오고, 본류 팀이 받아주는” 구조였다는 게 드러난다. 실제 코드의 상당 부분도 Graal 팀이 작성했고, 이후 유지보수도 주로 그쪽에서 했다.

그래서 이번 변화는 사실상 다음과 같이 정리할 수 있다.

2014~2015 :  Graal 팀이 만든 JVMCI를 OpenJDK 본류에 기증
              "통합의 길로 가자"는 약속과 함께

2026      :  통합의 길이 막혔으니, 그 코드를 다시 Graal 팀이 가져감
              본류는 자기 짐을 내려놓고, Graal 팀은 자기 코드를 다시 안음

말하자면 “잠시 본류가 맡아주던 짐을 원래 주인에게 돌려준 것” 에 가깝다.

4. GraalVM 팀은 이미 본류 패치를 매일 유지하던 사람들이다

JVMCI를 본류에 두고 있을 때도 GraalVM 팀은 이미 다음과 같은 일을 매일 하고 있었다.

  • 본류 HotSpot 변경에 맞춰 Graal 측 코드를 수정
  • libgraal 빌드 시스템 유지
  • 본류와 자기들 트리 간 차이 관리

앞서 정리한 회귀 사례들 — JDK-8307588, JDK-8365218 등 — 이 바로 그 작업의 일환이었다. 즉 JVMCI 제거 이후 GraalVM 팀이 새로 해야 할 일은 “JVMCI 소스 자체를 자기 트리에서 유지”하는 한 가지가 추가될 뿐, 본류와 자기 트리 사이의 적응 작업은 어차피 계속 하고 있었다.

“갑자기 분리”가 아니다 — 5년에 걸친 점진적 분리

타임라인을 펼쳐보면 사실 매우 점진적이었다.

2021     JEP 410       본류 JDK 17에서 Graal JIT/AOT 제거
                       → 이때 이미 "본류 안의 Graal"은 사라짐
                       → 남아 있던 건 JVMCI(인터페이스)뿐

2025/09  Detaching     GraalVM의 방향성을 본류 통합에서 분리

2026/03  Galahad 해산
2026/04  Metropolis 해산
                       → 본류 통합 시도의 공식적 종료

2026/05  JVMCI 제거 시작
                       → 마지막 남은 인터페이스 정리

본류 안에서 Graal 자체는 이미 2021년 JEP 410으로 빠졌다. 그 이후 5년간 본류에 남아 있던 것은 JVMCI라는 “인터페이스의 껍데기”뿐이었고, 이 인터페이스를 실제로 본류 안에서 쓰는 컴파일러는 없었다. 이번에 빠지는 JVMCI는 그러니까 “이미 5년 전에 손님이 떠난 빈집” 같은 상태였던 셈이다.

사용자 시나리오별 영향

사용자변화
GraalVM 배포본 사용자 (Spring Boot Native Image 등)변화 없음. GraalVM이 자체 트리에서 JVMCI 유지
OpenJDK 본류에서 -XX:+UseGraalJIT 쓰던 사람이미 JEP 410(2021)에서 사라졌음
OpenJDK 본류 위에서 JVMCI 기반 컴파일러를 직접 작성하던 사람영향 받음. 단, CSR이 말하듯 그런 사용자는 본류에 거의 없음
HotSpot 본류 기여자큰 이득. #if INCLUDE_JVMCI 252개 제거, 부수 비용 ↓

요약하면 “갑자기 분리”가 아니라, 2021년부터 이어져 온 분리 과정의 마지막 점을 찍는 것에 가깝다. 본류는 빈집을 정리했고, GraalVM 팀은 이미 자기 집을 따로 갖고 있었기에 가능한 시나리오였다.

정리하며

이번 결정의 핵심은 다음과 같이 요약할 수 있다.

  • JVMCI는 실험으로 시작된 기능이었고, 그 실험은 약 10년간 진행되었다.
  • 실험 기간 동안 Graal과 Native Image라는 의미 있는 사용처가 생겼지만, 본류 JDK가 짊어진 유지보수 비용은 누적되었다.
  • 본류 통합을 명분으로 했던 Project Galahad/Metropolis가 해산되면서, 비용 대비 이익의 균형이 무너졌다.
  • 결과적으로, 본류는 정리하고, 필요한 프로젝트는 다운스트림에서 유지하는 쪽으로 결론이 났다.

개인적으로 인상 깊었던 부분은 이 결정이 단순한 “기능이 안 쓰여서 빼겠다”가 아니라, “왜 지금 빼야 하는가”에 대한 정량적/사례 기반 근거를 정성스럽게 정리한 점이다. 252개의 #if INCLUDE_JVMCI, 1.5%의 부수 비용, 42분의 테스트 시간, 그리고 Valhalla 같은 진행 중인 프로젝트에 미치는 영향까지 — 큰 기능을 제거할 때 어떤 식으로 합의를 만들어가는지 보여주는 좋은 사례다.

오랜 시간 OpenJDK의 한 축이었던 JVMCI가 본류에서 사라지는 것은 분명 큰 변화다. 하지만 그만큼 본류 HotSpot은 더 단순해지고, C2, nmethod, deoptimization, GC, Valhalla 같은 핵심 작업들이 신경 쓸 분기점이 줄어든다. 앞으로의 HotSpot 코드를 보는 시야가 한층 깔끔해질 것 같아, 본류 작업에 관심 있는 입장에서는 반가운 변화이기도 하다.

categories: 오픈소스

tags: JDK , OpenJDK , JVMCI , Graal , HotSpot