Project Valhalla 설명: 10년 작업이 JDK 28에 도착하는 방식

6 hours ago 1
  • Oracle 엔지니어 Lois Foltan이 JEP 401: Value Classes and Objects의 OpenJDK 메인 저장소 통합과 JDK 28 타깃을 확인하며, Valhalla가 실제 JDK preview로 들어가는 단계에 도달함
  • 핵심 목표는 Java 객체를 “클래스처럼 코딩하고 int처럼 동작”하게 만들어 객체 헤더·힙 할당·GC·포인터 간접 참조 비용을 줄이는 것임
  • JDK 28의 value class는 아직 null 가능한 참조 타입이며, non-null 타입·전문화 제네릭·128비트 인코딩은 포함되지 않고 --enable-preview가 필요함
  • JVM은 value object를 스칼라화하거나 필드·배열에 힙 평탄화할 수 있지만, erased generic이나 Object 같은 상위 타입에서는 힙 객체로 materialize될 수 있음
  • Java 개발자는 identity와 value의 차이를 코드 설계에 반영해야 하며, ==, synchronized, primitive wrapper, 배열 성능, 향후 제네릭 전문화까지 영향이 이어짐

JDK 28에 들어오는 Valhalla의 범위

  • 6월 15일 Oracle 엔지니어 Lois Foltan이 JEP 401: Value Classes and Objects의 OpenJDK 메인 저장소 통합과 JDK 28 타깃을 확인함
  • 관련 pull request1,816개 파일에 걸쳐 19만 7천 줄 이상을 추가함
  • 변경 규모가 커서 통합 중 다른 커미터들에게 큰 커밋을 잠시 보류해 달라는 요청이 있었음
  • JEP 401은 기본 비활성화된 preview 기능
    • 문법을 사용하려면 --enable-preview가 필요함
    • Brian Goetz는 이를 “Valhalla의 첫 번째 부분”이라고 선을 그음
  • JDK 28은 2027년 3월 릴리스 예정이며, mainline 통합은 2026년 7월쯤으로 계획됨

Valhalla가 겨냥한 Java 객체 모델의 비용

  • Valhalla의 표어는 “codes like a class, works like an int”임
    • 메서드, 생성자 검증, 의미 있는 필드 이름을 가진 일반 클래스를 쓰면서도 JVM이 primitive처럼 효율적으로 다룰 수 있게 하는 것이 목표임
  • Java에서는 8개 primitive를 제외하면 거의 모든 것이 참조 타입
    • Point p = new Point(1, 2)에서 p는 point 자체가 아니라 힙 객체를 가리키는 포인터임
    • 필드를 읽을 때마다 JVM은 포인터를 따라가야 함
  • 객체 수가 늘어나면 비용이 급격히 커짐
    • 각 객체에는 타입, 동기화 상태 등을 위한 객체 헤더가 있음
    • 객체는 힙에 할당되고 이후 GC 대상이 됨
    • Point 백만 개 배열은 실제로 백만 개 포인터와 힙 곳곳의 백만 개 객체로 구성됨
  • Brian Goetz의 “State of Valhalla”는 이런 메모리 배치를 fluffy라고 부름
    • Valhalla가 원하는 것은 데이터가 나란히 놓이는 dense한 배치임

하드웨어 격차와 escape analysis의 한계

  • 밀도 높은 메모리 배치가 중요한 이유는 CPU와 메모리 속도 격차 때문임
    • 1995년에는 메모리 접근 비용이 CPU 연산과 비슷했음
    • 현재 CPU는 메인 메모리보다 두 자릿수 배 빠르며, 이 차이를 캐시가 메움
  • CPU는 보통 64바이트 cache line 단위로 메모리를 읽음
    • 데이터가 조밀하고 순서대로 있으면 한 번에 유용한 값을 많이 가져옴
    • 포인터를 따라 흩어진 객체에 접근하면 cache miss가 발생할 수 있고, hit보다 훨씬 느릴 수 있음
  • JVM의 escape analysis는 일부 객체 할당을 제거할 수 있음
    • 객체가 로컬 코드 조각 밖으로 “escape”하지 않는다고 판단되면 힙에 할당하지 않고 필드를 변수나 레지스터로 펼칠 수 있음
  • 다만 escape analysis는 예측 가능성이 낮고 취약함
    • 객체가 다른 클래스의 필드에 들어가거나 배열에 저장되거나 복잡한 메서드로 전달되거나 JIT가 분석할 수 없는 경계를 넘으면 최적화가 멈출 수 있음
    • 작은 리팩터링, JDK 업데이트, 코드 구조 변경만으로 객체가 다시 힙에 올라갈 수 있음
  • 성능을 위해 객체를 포기하고 r, g, b 같은 raw byte로 직접 인코딩하면 속도는 얻을 수 있지만 안전성·가독성·검증·메서드를 잃음

2014년 시작과 Q World에서 L World로의 전환

  • Project Valhalla는 공식적으로 2014년에 시작됨
  • James Gosling은 당시 이를 “six PhDs tied into a single knot”라고 표현함
  • Java 창시자들은 Java 1.0 시절부터 value type을 원했지만, 1995년에는 문제가 너무 어려워 포기했음
  • 초기 목표는 프로그래밍 모델과 현대 하드웨어 성능 특성의 정렬을 회복하는 것이었음
    • 사용자가 직접 primitive처럼 flat하고 dense한 타입을 선언하되, 일반 클래스처럼 보이고 동작하게 만드는 방향임
  • 초기 prototype은 Q World 방향이었음
    • 새 value type을 객체와 근본적으로 다른 존재로 보고, 별도 type descriptor, bytecode, top type을 두는 방식임
    • JVM 타입 시스템 전체가 두 가지 변형을 가져야 해 복잡성이 커졌음
  • 2019년쯤 등장한 L World가 전환점이 됨
    • value type이 일반 reference와 같은 “L carrier”를 공유함
    • 팀은 이 통합이 어렵다고 예상했지만, 큰 타협 없이 동작했고 이전 prototype의 여러 문제를 해결함
  • L World에서 중요한 분리가 생김
    • JVM 모델과 언어 모델이 100% 겹칠 필요는 없음
    • JVM에는 L World 모델을 두고, 프로그래머에게는 더 편한 언어 모델을 제공할 수 있음
  • 이후 작업은 value class와 전문화 제네릭의 두 단계로 나뉨

이름과 모델의 변화

  • Valhalla 용어는 여러 번 바뀌었고, 단순한 명칭 변경이 아니라 모델 변경을 반영함
  • 초기 용어는 value types였음
    • 당시에는 이 타입들이 정확히 어떤 존재인지 아직 명확하지 않았음
  • 2019~2020년쯤 inline classes 모델이 자리 잡음
    • 기존 클래스는 identity classes로, 새 클래스는 identity가 없는 inline classes로 구분됨
    • inline class는 기본적으로 final이고, 필드는 final이며, 동기화할 수 없는 제약이 잡힘
  • 2021년 “State of Valhalla”는 primitive classes와 두 projection 모델을 다룸
    • 하나의 타입이 flat하고 null이 불가능한 value variant와 null을 허용하는 reference variant를 갖는 구상이었음
    • Point.val / Point.ref, 이후 Point! / Point? 같은 문법도 실험됨
  • 이 모델은 강력했지만 인지 부담이 컸음
    • 프로그래머가 같은 타입의 두 형태와 변환 시점을 일상적으로 이해해야 했음
    • 결국 사용자 모델을 단순화하기 위해 dualism이 축소됨
  • 현재 JEP 401value classvalue object를 사용함
    • value modifier로 value class를 선언함
    • 인스턴스는 identity가 없는 value object임
    • value class는 여전히 reference type임
  • non-nullability는 별도 optional JEP인 Null-Restricted Value Class Types로 분리됨
    • JDK 28에는 포함되지 않음
  • 이전 “primitive classes” 모델을 설명하는 오래된 글은 현재 OpenJDK 기준과 다를 수 있음
  • JEP 401에는 preview인 JEP 402: Enhanced Primitive Boxing도 함께 있음
    • primitive와 wrapper 사이 변환을 더 부드럽게 만드는 방향임
    • 완성된 형태로 JEP 401과 함께 모두 들어온다고 가정하면 안 됨

JDK 28의 value class 모델

  • value class는 value modifier로 선언함
value class USDCurrency implements Comparable<USDCurrency> { private int cents; // implicitly final public USDCurrency(int dollars, int cents) { this.cents = dollars * 100 + cents; } public USDCurrency plus(USDCurrency that) { return new USDCurrency(0, this.cents + that.cents); } // dollars(), cents(), compareTo(), toString()... }
  • value record도 가능함
  • 주요 규칙은 다음과 같음
    • 모든 instance field는 암묵적으로 final
    • method는 synchronized일 수 없음
    • 클래스는 기본적으로 final임
    • value class와 abstract value class로 구성된 계층은 가능함
    • identity가 있는 클래스를 상속할 수 없음
    • interface 구현은 가능함
  • 핵심 특성은 identity가 없음
    • 일반 객체는 같은 내용을 가져도 new Point(1, 2)로 두 번 만들면 서로 다른 객체임
    • value object는 int 값 4에 “서로 다른 두 개의 4”가 없는 것처럼 identity가 없음

==, synchronized, null의 변화

  • value object에서 ==는 identity 비교가 아니라 substitutability 검사가 됨
    • 같은 클래스이고 같은 필드 값을 가지는지 재귀적으로 비교함
    • primitive field는 bit 단위로 비교하고, object field는 다시 ==로 비교함
    • new USDCurrency(3,95) == new USDCurrency(3,95)는 true가 됨
  • 다만 ==는 내부 상태를 보기 때문에, “같은 데이터를 표현하는가”에는 보통 equals가 더 적합함
  • value object에는 동기화할 identity가 없음
    • 동기화를 시도하면 IdentityException이 발생함
    • identity를 강제 확인해야 할 때는 Objects.requireIdentity와 Objects.hasIdentity를 사용할 수 있음
  • JDK 28의 value class는 여전히 null 가능
    • USDCurrency d = null;은 합법임
    • null을 금지하는 타입은 별도 future JEP이며 JDK 28에는 없음
  • non-nullability는 단순 문법 문제가 아니라 더 큰 value class의 평탄화를 여는 성능 레버가 됨

스칼라화와 힙 평탄화

  • JEP 401은 JVM이 value object를 최적화할 수 있는 자유를 줌
  • 스칼라화(scalarization) 는 value object reference를 필드 집합으로 분해하는 JIT 기법임
    • Color 포인터를 전달하는 대신 r, g, b byte와 null 여부 flag를 전달할 수 있음
    • 할당과 GC 비용이 사라질 수 있음
    • escape analysis와 비슷하지만 더 예측 가능하고, inline되지 않은 method call 경계를 넘어 적용될 수 있음
  • 스칼라화에는 제한이 있음
    • 변수 타입이 value class의 supertype인 Object이거나 erased generic parameter이면 보통 동작하지 않음
    • 이 경우 객체가 힙에 materialize되어야 함
  • 힙 평탄화(heap flattening) 는 value object의 필드 값을 compact bit vector로 인코딩해 필드나 배열 셀에 직접 쓰는 방식임
    • 다른 힙 위치를 가리키는 포인터가 필요 없음
    • 데이터 밀도와 locality가 생김
  • 평탄화된 데이터는 concurrent access에서 tearing을 피하기 위해 atomic하게 읽고 쓸 수 있어야 함
    • 일반 플랫폼에서 “충분히 작은” 크기는 null flag를 포함해 64비트 수준일 수 있음
    • 작은 value class는 잘 평탄화될 수 있지만, 두 개의 int 필드나 하나의 double만 있어도 atomic write 크기에 맞지 않아 일반 힙 객체가 될 수 있음
  • 향후에는 128비트 인코딩과 null-restricted type이 더 큰 value class의 평탄화를 가능하게 할 수 있음

Boxing, wrapper, 배열에서의 효과

  • preview가 켜지면 Integer, Long, Double 같은 primitive wrapper class 자체가 value class가 됨
    • box가 identity를 잃으므로 JVM이 스칼라화하고 평탄화할 수 있음
    • Integer[]는 int[]의 효율에 가까워지고, boxing overhead가 크게 줄어드는 방향임
  • JEP 402: Enhanced Primitive Boxing은 primitive와 box 사이 변환을 더 확장함
    • List<int> 같은 표현으로 가는 길을 열지만, 아직 별도의 성숙 중인 작업임
  • 배열에서 효과가 가장 잘 드러남
    • 기존 Color[]는 백만 개 포인터와 힙에 흩어진 백만 개 객체가 될 수 있음
    • value class Color[]는 연속된 색상 값을 직접 저장하는 contiguous block이 될 수 있음
    • CPU는 cache line 단위로 여러 값을 순차적으로 읽을 수 있음

Point[] 예시로 보는 전후 차이

  • Valhalla 이전의 일반 class 예시는 다음과 같음
final class Point { final int x; final int y; Point(int x, int y) { this.x = x; this.y = y; } } Point[] points = new Point[1_000_000];
  • 이 배열은 백만 개의 포인터를 담음
    • 각 포인터는 힙 어딘가의 별도 Point 객체를 가리킴
    • 각 객체에는 두 int 외에도 객체 헤더가 있음
    • 순회할 때 포인터를 읽고, 해당 주소로 점프하고, 필드를 읽어야 함
  • Valhalla 이후 value class 예시는 다음과 같음
value class Point { final int x; final int y; Point(int x, int y) { this.x = x; this.y = y; } } Point[] points = new Point[1_000_000];
  • 코드 차이는 value 한 단어지만, 메모리 배치는 달라짐
    • JVM은 각 point의 값을 배열 안에 조밀하게 저장할 수 있음
    • element마다 헤더가 없고 포인터도 없음
    • x, y 두 int 기준 8바이트와 가능한 null flag로 연속 배치될 수 있음
  • 유지보수성도 유지됨
    • Point는 여전히 이름, 생성자, 검증, method를 가진 class임
    • int[] xs, int[] ys로 쪼개고 index를 맞추는 방식을 피할 수 있음

전문화 제네릭이 아직 남은 이유

  • Java generics는 type erasure로 구현됨
    • List<String>과 List<Integer>는 runtime에서 같은 List임
    • type parameter T는 Object로 erasure됨
  • erasure는 Java의 기존 코드베이스를 깨지 않고 generics를 도입하기 위한 의도적 선택이었음
    • non-generic class를 generic으로 바꿔도 기존 source file과 compiled class를 깨지 않게 함
  • Valhalla와 erasure는 성능상 충돌함
    • List<Point>에 value object를 넣으면 T가 Object로 erasure되므로 객체가 힙에 materialize되어야 함
    • Point[]에서 얻은 평탄화 이점이 ArrayList<Point>에서는 사라질 수 있음
  • 복구 계획은 두 단계임
    • Universal Generics: 언어 수준에서 type variable이 value type까지 다룰 수 있게 함
      • 여전히 erasure를 사용함
      • T 필드가 기본적으로 null에서 시작하는 문제 때문에 “null pollution” compiler warning이 생길 수 있음
      • 경고를 해결하면 API가 specialization-ready에 가까워짐
    • Specialized Generics: JVM 수준에서 concrete type argument별 전문화된 class layout을 생성함
      • 프로젝트 용어로 species와 type restriction이 관련됨
      • 이 단계에서야 ArrayList<Point>가 실제 flat memory를 쓸 수 있음
  • JDK 28에는 full specialized generics가 없음
    • 컬렉션, stream, API가 value type 위에서 flat하고 allocation-free가 되는 것은 future release의 작업임

JDK 28에 있는 것과 없는 것

  • JDK 28에 들어오는 것은 다음과 같음
    • value class와 value record 선언
    • JDK의 기존 value-based class 중 primitive wrapper 같은 클래스의 value class migration
    • 조건을 만족하는 클래스의 스칼라화와 평탄화
    • 더 저렴한 boxing
  • JDK 28에 없는 것은 다음과 같음
    • null-restricted types
    • full specialized generics
    • 128비트 인코딩
    • 완전히 성숙한 JEP 402
  • preview feature이므로 syntax와 동작은 release마다 feedback에 따라 바뀔 수 있음
  • JDK 28은 LTS가 아님
    • 다음 LTS는 2027년 9월의 JDK 29일 가능성이 큼
    • 많은 회사는 안정화된 Valhalla를 LTS에서 만날 수 있지만, JDK 28 preview가 실제 코드 feedback loop를 시작함

생태계와 코드에 생길 변화

  • 고성능 Java 영역에서 Valhalla는 abstraction을 포기하지 않고 dense data를 다루는 경로가 됨
    • 데이터 처리, vector computation, ML, game development, finance, codec 같은 영역이 해당됨
  • framework와 library는 value-based class migration을 시작할 수 있음
  • identity에 의존하던 코드는 동작 차이를 겪을 수 있음
    • ==가 value object에서 address 비교가 아니라 substitutability 비교가 됨
    • synchronized는 value object에서 IdentityException으로 이어짐
  • Integer가 value class가 되어도 대부분의 경우 binary는 계속 link됨
    • 새 compilation error는 이런 타입에 동기화하려는 경우임
    • Integer identity에 의존한 ==나 synchronized(someInteger)는 영향을 받을 수 있음
  • early-access build는 jdk.java.net/valhalla에서 사용할 수 있음

자주 나오는 질문 정리

  • value class는 record와 다름
    • record는 content가 component라는 선택임
    • value는 identity를 포기하는 선택임
    • 일반 class, record, value class, value record 조합이 모두 가능함
  • value object는 ==로 비교할 수 있음
    • 의미는 address 비교가 아니라 substitutability임
    • 표현하는 데이터의 동등성에는 보통 equals가 더 적합함
  • JDK 28 value class는 null이 가능함
    • non-nullable type은 future JEP임
    • 더 큰 value class의 평탄화에도 중요함
  • 빠른 flat ArrayList<Point>는 아직 아님
    • type erasure 때문에 generic collection 안의 객체는 힙에 materialize됨
    • JDK 28에서 평탄화가 직접 작동하는 대표 사례는 value type의 field와 array인 Point[]임
  • escape analysis가 전부를 대신하지는 못함
    • 객체가 field, array, 분석 경계 밖으로 나가면 최적화가 깨질 수 있음
    • value object의 스칼라화는 더 예측 가능하고 method-call boundary를 더 멀리 넘을 수 있음
  • 전체 Valhalla는 여러 release에 걸쳐 확장됨
    • JDK 28은 value class의 첫 preview임
    • specialized generics, null-restricted types, 128비트 인코딩은 future release에 걸친 작업임
Read Entire Article