이번 포스팅에서는 Java 26 새 기능에 대해서 정리하고자 한다. 2026년 3월 17일 GA(General Availability)된 Java 26은 총 10개의 JEP를 포함하고 있다. 전작 JDK 25가 LTS로서 대규모 기능을 쏟아냈다면, Java 26은 그 기반 위에서 성능 최적화와 기존 프리뷰 기능의 안정화에 집중한 릴리스다. HTTP/3 지원이 정식으로 들어왔고, G1 GC 처리량이 참조가 많은 애플리케이션에서 5~15% 개선되었으며, AOT 객체 캐싱이 ZGC를 포함한 모든 GC에서 동작하게 되었다. 아래에서 10개 JEP를 하나씩 살펴본다.
Java 26 한눈에 보기 — 10개 JEP 전체 목록

먼저 Java 26에 포함된 JEP 전체를 표로 정리한다. 정식 확정(Finalized) 5개, 프리뷰(Preview) 4개, 인큐베이터(Incubator) 1개로 구성되어 있다.
| JEP | 이름 | 상태 |
|---|---|---|
| 500 | Prepare to Make Final Mean Final | 정식 |
| 504 | Remove the Applet API | 정식 |
| 516 | Ahead-of-Time Object Caching with Any GC | 정식 |
| 517 | HTTP/3 for the HTTP Client API | 정식 |
| 522 | G1 GC: Improve Throughput by Reducing Synchronization | 정식 |
| 524 | PEM Encodings of Cryptographic Objects | 2차 프리뷰 |
| 525 | Structured Concurrency | 6차 프리뷰 |
| 526 | Lazy Constants | 2차 프리뷰 |
| 529 | Vector API | 11차 인큐베이터 |
| 530 | Primitive Types in Patterns, instanceof, and switch | 4차 프리뷰 |
전체 목록은 OpenJDK JDK 26 프로젝트 페이지에서 확인할 수 있다. 정식 확정된 5개 기능이 당장 프로덕션에서 사용 가능한 변경사항이고, 프리뷰 기능은 --enable-preview 플래그를 켜야 쓸 수 있다.
G1 GC 처리량이 5~15% 빨라졌다 (JEP 522)

Java 26에서 체감 효과가 가장 큰 변경사항은 G1 GC의 처리량 개선이다. JEP 522는 애플리케이션 스레드와 GC 최적화 스레드 사이의 동기화 오버헤드를 줄여 참조가 많은 워크로드에서 5~15%의 처리량 향상을 달성했다.
핵심 원리는 듀얼 카드 테이블(dual card-table) 도입이다. 기존 G1 GC에서는 하나의 카드 테이블을 애플리케이션 스레드와 GC 스레드가 공유했기 때문에 write barrier 실행 시 락이 필요했다. Java 26부터는 테이블을 두 개로 분리하여 각 스레드가 독립적으로 기록한다.
// Java 26 G1 GC 개선 확인 — JVM 옵션으로 로그 활성화
// G1 GC 동작 로그를 확인하여 처리량 변화를 측정할 수 있다
public class G1GcBenchmark {
public static void main(String[] args) {
// 참조가 많은 객체 그래프를 생성하여 G1 GC에 부하를 건다
var nodes = new java.util.ArrayList<Object[]>();
for (int i = 0; i < 1_000_000; i++) {
// 각 배열이 다른 배열을 참조하는 그래프 구조
var refs = new Object[10];
if (i > 0) refs[0] = nodes.get(i - 1);
nodes.add(refs);
}
System.out.println("노드 수: " + nodes.size());
}
}Java위 코드를 아래 JVM 옵션과 함께 실행하면 G1 GC의 동기화 개선 효과를 로그에서 확인할 수 있다.
# Java 26에서 G1 GC 로그 활성화
java -XX:+UseG1GC \
-Xlog:gc*=info \
-Xmx512m \
G1GcBenchmarkShellScript이 옵션은 G1 GC의 수집 단계별 소요 시간과 처리량을 출력한다. Java 25와 동일 워크로드를 비교하면 참조 처리(Reference Processing) 구간에서 시간이 줄어든 것을 확인할 수 있다. 네이티브 메모리 사용량이 소폭 증가하지만 카드 테이블 하나 분량이므로 대부분의 환경에서 무시 가능하다.
AOT 객체 캐싱, 이제 ZGC에서도 된다 (JEP 516)

JEP 516은 JDK 24에서 도입된 AOT(Ahead-of-Time) 클래스 캐싱의 확장판이다. JDK 24의 JEP 483이 G1 GC에서만 동작했다면, Java 26에서는 ZGC를 포함한 모든 GC에서 AOT 객체 캐싱을 사용할 수 있다.
변경의 핵심은 메모리 주소 대신 논리적 인덱스로 캐시된 객체를 참조하는 방식이다. 기존에는 캐시된 힙 객체가 특정 GC의 메모리 레이아웃에 의존했지만, 인덱스 기반 참조를 사용하면 GC 구현과 무관하게 객체를 복원할 수 있다.
# 1단계: AOT 캐시 생성 트레이닝 실행
java -XX:AOTMode=record \
-XX:AOTConfiguration=app.aotconf \
-cp myapp.jar com.example.MyApp
# 2단계: AOT 캐시 파일 생성
java -XX:AOTMode=create \
-XX:AOTConfiguration=app.aotconf \
-XX:AOTCache=app.aot \
-cp myapp.jar
# 3단계: AOT 캐시와 ZGC를 함께 사용
java -XX:AOTCache=app.aot \
-XX:+UseZGC \
-XX:+AOTStreamableObjects \
-cp myapp.jar com.example.MyAppShellScript1단계에서 애플리케이션이 사용하는 클래스와 객체 정보를 수집하고, 2단계에서 이를 AOT 캐시 파일로 저장한다. 3단계에서 -XX:+AOTStreamableObjects 플래그를 켜면 캐시된 객체가 ZGC 환경에서도 즉시 로드된다. JDK 24에서는 이 옵션을 ZGC와 함께 쓸 수 없었다. 스타트업 시간이 중요한 마이크로서비스나 서버리스 환경에서 GC 선택의 자유도가 높아진 셈이다.
HTTP/3가 드디어 Java에 온다 (JEP 517)

Java 26에서 가장 오래 기다려온 기능 중 하나다. JEP 517은 기존 java.net.http.HttpClient에 HTTP/3(QUIC) 지원을 추가한다. 새로운 API를 만든 것이 아니라 기존 HttpClient에 버전 옵션 하나를 추가하는 방식이라 코드 변경이 거의 없다.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Http3Example {
public static void main(String[] args) throws Exception {
// HTTP/3를 사용하는 HttpClient 생성
var client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_3) // Java 26에서 추가된 열거값
.build();
var request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com"))
.GET()
.build();
// 서버가 HTTP/3를 지원하면 QUIC으로 통신한다
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("프로토콜 버전: " + response.version());
System.out.println("응답 코드: " + response.statusCode());
}
}JavaHttpClient.Version.HTTP_3를 지정하면 QUIC 프로토콜로 서버에 연결을 시도한다. 서버가 HTTP/3를 지원하지 않으면 자동으로 HTTP/2나 HTTP/1.1로 다운그레이드된다.
Java 26은 네 가지 프로토콜 협상 전략을 제공한다.
| 전략 | 동작 |
|---|---|
| HTTP/3 우선 시도 | HTTP/3로 연결, 실패 시 HTTP/2로 다운그레이드 |
| 경쟁 연결 | HTTP/3과 HTTP/2를 동시에 시도, 먼저 성공한 쪽 사용 |
| HTTP/2 시작, 업그레이드 | HTTP/2로 시작, 서버가 Alt-Svc 헤더로 HTTP/3를 광고하면 전환 |
| HTTP/3 강제 | HTTP/3만 사용, 미지원 서버는 연결 실패 |
기본 동작은 첫 번째 전략(HTTP/3 우선 + 자동 다운그레이드)이다. QUIC은 TCP와 달리 UDP 기반이라 연결 설정 왕복(RTT)이 줄어들고, TLS 1.3 핸드셰이크가 프로토콜에 내장되어 있어 별도 설정 없이 암호화 통신이 된다. JEP 517 공식 명세에서 세부 동작을 확인할 수 있다.
final이 진짜 final이 되는 날이 온다 (JEP 500)

지금까지 Java에서 final 필드는 리플렉션으로 수정할 수 있었다. Field.setAccessible(true) 후 Field.set()을 호출하면 final 필드도 값이 바뀐다. JEP 500은 이 동작을 제한하는 첫 단계로, Java 26에서는 final 필드를 리플렉션으로 수정할 때 경고를 출력한다.
import java.lang.reflect.Field;
public class FinalFieldWarningDemo {
private final String name = "원래값";
public static void main(String[] args) throws Exception {
var obj = new FinalFieldWarningDemo();
Field field = FinalFieldWarningDemo.class.getDeclaredField("name");
field.setAccessible(true);
// Java 26에서 아래 코드는 WARNING을 출력한다
// 향후 버전에서는 InaccessibleObjectException을 던질 예정이다
field.set(obj, "변경된값");
System.out.println(obj.name);
}
}Java위 코드를 Java 26에서 실행하면 WARNING: Reflective mutation of final field 메시지가 표준 에러에 출력된다. 코드 자체는 아직 정상 동작하지만, 향후 릴리스에서는 기본적으로 예외를 던지도록 변경될 예정이다.
당장 경고를 억제하려면 --enable-final-field-mutation 플래그를 사용한다.
# 특정 모듈에 대해 final 필드 변경을 허용한다
java --enable-final-field-mutation=com.example.legacy \
-cp myapp.jar com.example.MainShellScriptJackson, Gson 같은 직렬화 라이브러리가 영향을 받을 수 있다. 이미 대부분의 주요 라이브러리는 sun.reflect.ReflectionFactory를 사용하는 방식으로 마이그레이션했지만, 사내에서 직접 작성한 리플렉션 유틸리티가 있다면 점검이 필요하다.
Structured Concurrency, 여섯 번째 프리뷰에서 달라진 것 (JEP 525)
Structured Concurrency는 JDK 21에서 처음 프리뷰로 등장한 이후 Java 26에서 6차 프리뷰를 맞이했다. 프리뷰가 오래 지속되는 이유는 API 설계가 까다롭기 때문이다. Virtual Thread와의 조합, 예외 처리 전략, 취소 전파 메커니즘 등을 정교하게 다듬고 있다.
JDK 25에서 Structured Concurrency는 스레드 누수는 이제 그만! Structured Concurrency 소개 (ft. JDK 25) 참고
핵심 API인 StructuredTaskScope는 여러 비동기 작업을 하나의 스코프 안에서 관리하는 구조다.
import java.util.concurrent.StructuredTaskScope;
public class StructuredConcurrencyDemo {
record UserProfile(String name, int age) {}
record OrderHistory(int count) {}
// 사용자 프로필과 주문 내역을 병렬로 조회한다
static String getUserSummary(long userId) throws Exception {
// ShutdownOnFailure: 하나라도 실패하면 나머지 작업을 취소한다
try (var scope = StructuredTaskScope.open(
StructuredTaskScope.Joiner.awaitAllSuccessfulOrThrow())) {
// 두 작업을 동시에 시작한다 — 각각 Virtual Thread에서 실행된다
var profileTask = scope.fork(() -> fetchProfile(userId));
var orderTask = scope.fork(() -> fetchOrders(userId));
scope.join(); // 모든 작업이 끝날 때까지 대기한다
// 결과를 안전하게 꺼낸다 — 실패한 작업이 있으면 여기 도달하지 않는다
var profile = profileTask.get();
var orders = orderTask.get();
return "%s님의 주문 %d건".formatted(profile.name(), orders.count());
}
}
static UserProfile fetchProfile(long id) { /* DB 조회 */ return new UserProfile("홍길동", 30); }
static OrderHistory fetchOrders(long id) { /* DB 조회 */ return new OrderHistory(42); }
}JavaStructuredTaskScope.open() 안에서 fork()로 작업을 생성하면 각 작업이 Virtual Thread에서 실행된다. Joiner.awaitAllSuccessfulOrThrow()는 모든 작업이 성공할 때까지 기다리되, 하나라도 예외를 던지면 나머지를 즉시 취소하는 전략이다.
Java 26에서 달라진 점은 Joiner 인터페이스의 커스텀 구현이 가능해진 것이다. 기존에는 ShutdownOnFailure와 ShutdownOnSuccess 두 가지 정책만 제공했지만, 이제 직접 Joiner를 구현하여 “3개 중 2개 성공하면 진행” 같은 정책을 만들 수 있다. --enable-preview 플래그를 켜야 사용 가능하다.
Lazy Constants — 초기화 시점을 제어하는 새로운 방법 (JEP 526)

static final 필드는 클래스 로딩 시점에 초기화된다. 무거운 리소스를 담고 있으면 애플리케이션 스타트업이 느려지는 원인이 된다. JEP 526의 LazyConstant<T>는 값이 처음 접근될 때 한 번만 초기화되고, 이후에는 상수처럼 동작하는 홀더 타입이다.
import java.lang.LazyConstant; // JEP 526: 2차 프리뷰에서 StableValue → LazyConstant로 이름 변경
public class LazyConstantDemo {
// 처음 .get()이 호출될 때 한 번만 초기화된다
// 이후 호출에서는 캐싱된 값을 즉시 반환한다
private static final LazyConstant<DatabaseConnection> DB_CONN =
LazyConstant.of(() -> {
System.out.println("DB 연결 생성 중...");
return DatabaseConnection.create("jdbc:postgresql://localhost/mydb");
});
// LazyConstant.ofList()로 지연 초기화 리스트도 만들 수 있다
private static final LazyConstant<List<String>> COUNTRY_CODES =
LazyConstant.of(() -> loadCountryCodes());
public static void main(String[] args) {
System.out.println("애플리케이션 시작");
// 이 시점까지 DB_CONN은 초기화되지 않는다
// 첫 접근 시 "DB 연결 생성 중..." 출력 후 연결 생성
var conn = DB_CONN.get();
// 두 번째 접근 시 캐싱된 연결을 즉시 반환
var sameConn = DB_CONN.get();
System.out.println(conn == sameConn); // true
}
}JavaLazyConstant.of()에 Supplier를 전달하면 된다. get()이 처음 호출될 때 Supplier가 실행되고, 그 결과가 내부에 캐싱된다. 두 번째 get() 부터는 저장된 값을 곧바로 반환한다. 스레드 안전이 보장되므로 volatile + 더블 체크 락 패턴을 직접 구현할 필요가 없다.
기존에 같은 목적으로 사용하던 패턴과 비교하면 차이가 명확하다.
// 기존 방식: volatile + 더블 체크 락 (보일러플레이트가 많다)
private static volatile DatabaseConnection dbConn;
private static DatabaseConnection getDbConn() {
if (dbConn == null) {
synchronized (MyApp.class) {
if (dbConn == null) {
dbConn = DatabaseConnection.create("...");
}
}
}
return dbConn;
}
// Java 26 방식: LazyConstant 한 줄
private static final LazyConstant<DatabaseConnection> DB = LazyConstant.of(() -> DatabaseConnection.create("..."));Java더블 체크 락 패턴의 5줄이 LazyConstant.of() 한 줄로 줄어든다. JVM이 constant-folding 최적화를 적용할 수 있어 성능도 동등하거나 더 좋다. 이 기능도 --enable-preview 플래그가 필요하다.
Primitive Patterns로 안전한 타입 변환하기 (JEP 530)

JEP 530은 패턴 매칭을 원시 타입(primitive type)까지 확장한다. instanceof와 switch에서 원시 타입 패턴을 사용할 수 있어 명시적 캐스팅 없이 안전한 타입 변환이 가능하다. 4차 프리뷰에 해당한다.
public class PrimitivePatternDemo {
// 기존 방식: 수동 범위 체크 + 캐스팅
static void oldWay(long value) {
if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) {
byte b = (byte) value; // 수동 캐스팅
System.out.println("byte로 표현 가능: " + b);
}
}
// Java 26 방식: 패턴 매칭으로 안전한 narrowing
static void newWay(long value) {
// value가 byte 범위 안에 있는지 자동으로 검사한 뒤 바인딩한다
if (value instanceof byte b) {
System.out.println("byte로 표현 가능: " + b);
}
}
// switch에서도 원시 타입 패턴을 사용할 수 있다
static String classify(long size) {
return switch (size) {
case byte b -> "아주 작은 값: " + b; // -128 ~ 127
case short s -> "작은 값: " + s; // -32768 ~ 32767
case int i -> "중간 값: " + i; // int 범위
case long l -> "큰 값: " + l; // 나머지
};
}
}Javavalue instanceof byte b는 value가 byte 범위(-128~127) 안에 있는지 검사한 뒤, 참이면 b에 안전하게 바인딩한다. 기존에는 범위 체크와 캐스팅을 분리해서 작성해야 했는데, 패턴 하나로 두 동작이 합쳐진다. switch 표현식에서 원시 타입 패턴을 나열하면 값의 크기에 따라 분기하는 코드를 간결하게 작성할 수 있다.
PEM 인코딩과 Applet 제거 — 보안 관련 변경사항
Java 26의 보안 영역에서는 두 가지 변경이 눈에 띈다.
PEM 암호화 객체 인코딩 (JEP 524, 2차 프리뷰)
PEM(Privacy-Enhanced Mail) 형식은 SSH 키, TLS 인증서 등에서 널리 사용되는 텍스트 인코딩이다. 기존에는 PEM 파일을 Java 객체로 변환하려면 BouncyCastle 같은 서드파티 라이브러리가 필요했는데, JEP 524가 이를 표준 API로 제공한다.
import java.security.KeyPairGenerator;
import java.security.PEMEncoder;
import java.security.PEMDecoder;
import java.security.PublicKey;
public class PemDemo {
public static void main(String[] args) throws Exception {
// RSA 키 쌍 생성
var keyPair = KeyPairGenerator.getInstance("RSA")
.generateKeyPair();
// 공개키를 PEM 문자열로 인코딩
String pem = PEMEncoder.of().encodeToString(keyPair.getPublic());
System.out.println(pem);
// -----BEGIN PUBLIC KEY-----
// MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
// -----END PUBLIC KEY-----
// PEM 문자열을 다시 PublicKey 객체로 디코딩
// decode()의 두 번째 인자로 기대하는 타입을 지정한다
var decoded = PEMDecoder.of().decode(pem, PublicKey.class);
System.out.println("키 알고리즘: " + decoded.getAlgorithm());
}
}JavaPEMEncoder.of().encodeToString()과 PEMDecoder.of().decode() 두 메서드로 PEM 인코딩/디코딩이 완료된다. PKCS #8 개인키, X.509 인증서, CRL까지 지원한다. 2차 프리뷰이므로 --enable-preview가 필요하다.
Applet API 완전 제거 (JEP 504)
java.applet 패키지와 javax.swing.JApplet이 Java 26에서 완전히 제거되었다. JDK 9에서 deprecated, JDK 17에서 deprecated for removal로 표시된 후 마침내 삭제된 것이다. 웹 브라우저들이 이미 수 년 전에 플러그인 지원을 중단했기 때문에 실무 영향은 없다. 다만 레거시 교육 자료의 코드가 Java 26에서 컴파일되지 않으므로 참고해야 한다.
Vector API, 열한 번째 인큐베이터 (JEP 529)
Vector API는 SIMD(Single Instruction, Multiple Data) 연산을 Java에서 표현하기 위한 API로, 11번째 인큐베이터 단계에 있다. x64의 SSE/AVX와 AArch64의 NEON 같은 벡터 명령어를 JVM이 자동으로 활용하게 해주는데, Project Valhalla의 Value Type이 정식 확정되기를 기다리고 있어 인큐베이터가 길어지고 있다.
import jdk.incubator.vector.*;
public class VectorApiDemo {
// 두 float 배열의 요소별 합을 SIMD로 계산한다
static float[] vectorAdd(float[] a, float[] b) {
var species = FloatVector.SPECIES_256; // 256비트 벡터 (float 8개)
float[] result = new float[a.length];
int i = 0;
// 벡터 크기 단위로 루프를 돈다 — 한 번에 8개 float를 처리
for (; i < species.loopBound(a.length); i += species.length()) {
var va = FloatVector.fromArray(species, a, i);
var vb = FloatVector.fromArray(species, b, i);
va.add(vb).intoArray(result, i);
}
// 나머지 요소는 스칼라로 처리
for (; i < a.length; i++) {
result[i] = a[i] + b[i];
}
return result;
}
}JavaFloatVector.SPECIES_256은 256비트(float 8개) 단위로 연산하겠다는 선언이다. fromArray()로 배열 데이터를 벡터에 로드하고, add()로 요소별 합산을 수행한 뒤, intoArray()로 결과를 다시 배열에 쓴다. JVM이 런타임에 CPU의 AVX2 명령어로 컴파일하면 스칼라 루프 대비 수 배의 성능 향상을 얻는다. 머신러닝 추론, 암호화, 금융 계산 등 대량 수치 연산에서 이점이 크다. 인큐베이터 모듈이므로 --add-modules jdk.incubator.vector를 추가해야 한다.
Java 26 업그레이드 전 체크 포인트

Java 26으로 업그레이드할 때 확인할 것들을 정리한다.
가장 먼저 체크할 항목은 Applet API다. javax.swing.JApplet을 상속하는 클래스가 프로젝트에 남아 있으면 컴파일 에러가 발생한다. 드물겠지만 레거시 코드베이스라면 검색해 볼 필요가 있다. final 필드를 리플렉션으로 변경하는 코드도 확인 대상이다. 당장은 경고만 나오지만 로그에서 Reflective mutation of final field 메시지가 보이면 향후 버전에서 깨질 코드라는 뜻이다.
반가운 소식은 G1 GC와 AOT 캐싱 개선이 별도 설정 없이 자동 적용된다는 점이다. JVM을 올리는 것만으로 성능 개선을 얻는다. HTTP/3는 HttpClient.Version.HTTP_3를 직접 지정해야 활성화되므로 기존 코드에는 영향이 없다.
프리뷰 기능(Structured Concurrency, Lazy Constants, Primitive Patterns)은 프로덕션에서 쓰려면 --enable-preview가 필수다. 프리뷰 API는 다음 릴리스에서 바뀔 수 있다는 점은 감안해야 한다. Oracle 공식 릴리스 노트에서 전체 변경 이력을 확인할 수 있다.
마치며
지금까지 Java 26 새 기능에 대해서 정리해 보았다. 10개 JEP 중에서 당장 영향이 큰 것은 G1 GC 처리량 개선(JEP 522)과 HTTP/3 지원(JEP 517), 그리고 final 필드 리플렉션 경고(JEP 500)다. Structured Concurrency와 Lazy Constants는 프리뷰가 계속되고 있지만 매 릴리스마다 API가 다듬어지고 있으므로 정식 확정 전에 미리 익혀 두면 좋다. 다음 릴리스인 JDK 27은 2026년 9월에 예정되어 있으며, Project Valhalla의 Value Classes(JEP 401)가 포함될 것으로 InfoQ에서 보도하고 있다.
