Java LTS 버전 한판정리 - Java 8부터 25까지

2026-01-10 14:32:00
#Java#JDK#LTS#GC#JVM

LTS(Long-Term Support)란?

Oracle은 2017년 9월부터 6개월 주기로 새로운 Java 버전을 릴리스하는 정책을 도입했다. 하지만 모든 버전에 장기 지원을 제공하는 것은 아니며, LTS(Long-Term Support) 버전만 최소 8년의 상용 지원을 받는다.

LTS 버전 출시일 Premier Support Extended Support
Java 8 2014년 3월 2022년 3월 2030년 12월
Java 11 2018년 9월 2023년 9월 2032년 1월
Java 17 2021년 9월 2026년 9월 2029년 9월
Java 21 2023년 9월 2028년 9월 2031년 9월
Java 25 2025년 9월 2030년 9월 2033년 9월

프로덕션 환경에서는 LTS 버전을 사용하는 것이 안정적이다. 비-LTS 버전은 다음 버전이 나오면 지원이 종료되기 때문에 지속적인 업그레이드가 필요하다.


JVM 메모리 구조

각 LTS 버전에서는 JVM 메모리 관리 방식과 GC 알고리즘이 지속적으로 개선되어 왔다. Java 8의 PermGen → Metaspace 변경, Java 9의 G1 GC 기본 채택, Java 21의 Virtual Thread 도입 등은 모두 JVM 내부 구조와 밀접하게 연결되어 있다. 버전별 변화를 제대로 이해하려면 JVM 메모리 구조의 기본 개념을 먼저 알아야 한다.

JVM 메모리 영역 전체 구조

JVM Memory Structure

이미지 출처: GeeksforGeeks - Java Memory Management

Heap 메모리

모든 객체 인스턴스가 저장되는 영역이다. GC의 주요 대상이 된다.

Young Generation

새로 생성된 객체가 할당되는 영역. 대부분의 객체는 금방 사라진다는 관찰(Weak Generational Hypothesis)에 기반하여 설계되었다.

Eden Space

  • 새 객체가 최초로 할당되는 공간
  • Eden이 가득 차면 Minor GC 발생
  • 살아남은 객체는 Survivor 영역으로 이동

Survivor Space (S0, S1)

  • Minor GC에서 살아남은 객체가 이동하는 공간
  • 두 개의 공간(S0, S1) 중 하나는 항상 비어 있음
  • GC마다 살아남은 객체는 S0 ↔ S1 사이를 이동
  • 각 객체는 "age" 카운터를 가지며, 이동할 때마다 증가

객체의 생존 과정:

  1. 객체 생성 → Eden에 할당
  2. Eden 가득 참 → Minor GC 발생
  3. 살아남은 객체 → S0으로 이동 (age = 1)
  4. 다음 Minor GC → 살아남으면 S1으로 이동 (age = 2)
  5. age가 임계값(기본 15) 도달 → Old Generation으로 승격

Old Generation (Tenured)

Young Generation에서 오래 살아남은 객체가 승격되는 영역.

  • Young보다 크기가 크고, GC 빈도가 낮음
  • Old 영역 GC는 Major GC 또는 Full GC라고 부름
  • Minor GC보다 시간이 오래 걸림 (수십 ms ~ 수 초)
# 승격 임계값 설정 (기본값: 15, 최대: 15)
java -XX:MaxTenuringThreshold=10 -jar app.jar

# Young/Old 비율 설정 (Old/Young = 2:1이면 NewRatio=2)
java -XX:NewRatio=2 -jar app.jar

# Eden/Survivor 비율 (Eden:S0:S1 = 8:1:1이면 SurvivorRatio=8)
java -XX:SurvivorRatio=8 -jar app.jar

왜 세대를 구분하는가?

이유 설명
Weak Generational Hypothesis 대부분의 객체는 생성 직후 금방 죽는다
GC 효율성 Young은 자주, 빠르게 / Old는 드물게, 천천히
Stop-the-World 최소화 전체 힙 스캔보다 Young만 스캔이 훨씬 빠름
메모리 단편화 방지 영역별로 다른 할당/압축 전략 사용 가능

Non-Heap 메모리

Metaspace (Java 8+)

클래스 메타데이터를 저장하는 영역. Java 8에서 PermGen을 대체했다.

PermGen → Metaspace 변경 이유:

구분 PermGen (Java 7 이하) Metaspace (Java 8+)
위치 JVM Heap 내부 Native Memory (OS 영역)
기본 크기 64MB (32-bit) / 82MB (64-bit) 제한 없음 (OS 한도까지)
크기 조정 고정 (-XX:MaxPermSize) 동적 자동 확장
OOM 에러 OutOfMemoryError: PermGen space OutOfMemoryError: Metaspace
# Metaspace 크기 제한 (무제한 방지)
java -XX:MaxMetaspaceSize=256m -jar app.jar

# 초기 Metaspace 크기
java -XX:MetaspaceSize=128m -jar app.jar

저장되는 내용:

  • 클래스 구조 정보 (필드, 메서드, 상수 풀)
  • 메서드 바이트코드
  • 어노테이션 정보
  • 런타임 상수 풀

Code Cache

JIT 컴파일러가 생성한 네이티브 코드를 저장하는 영역.

# Code Cache 크기 설정
java -XX:ReservedCodeCacheSize=256m -jar app.jar

스레드별 메모리

스레드마다 독립적으로 할당되는 영역으로, 다른 스레드와 공유되지 않는다.

영역 용도
Stack 메서드 호출 시 지역 변수, 파라미터, 리턴 주소 저장
PC Register 현재 실행 중인 JVM 명령어 주소
Native Method Stack JNI를 통한 네이티브 코드 실행 시 사용
# 스레드 스택 크기 설정 (기본: 512KB ~ 1MB)
java -Xss512k -jar app.jar

메모리 관련 주요 JVM 옵션

# Heap 크기 설정
java -Xms2g -Xmx4g -jar app.jar   # 초기 2GB, 최대 4GB

# 전체 메모리 모니터링
java -XX:+PrintFlagsFinal -version | grep -i heap

# GC 로그 출력 (Java 9+)
java -Xlog:gc*:file=gc.log:time -jar app.jar

# 메모리 부족 시 힙 덤프 생성
java -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/var/log/java/heapdump.hprof \
     -jar app.jar

GC(Garbage Collection) 알고리즘의 진화

Java LTS 버전을 이해하려면 GC 알고리즘의 변화를 함께 알아야 한다. 각 LTS 버전에서 기본 GC와 사용 가능한 GC 옵션이 달라진다.

GC 핵심 용어

GC 알고리즘을 이해하기 위해 알아야 할 핵심 용어들이다.

Stop-the-World (STW)

GC가 실행될 때 애플리케이션의 모든 스레드가 일시 정지되는 현상이다. GC 스레드만 동작하고, 사용자 요청 처리도 멈춘다.

[애플리케이션 실행] ──────────┬────────────────────┬──────────────> 시간
                              │    Stop-the-World   │
                              │   (GC 작업 수행)    │
                              │  모든 스레드 정지   │
                              └────────────────────┘
  • 왜 필요한가?: 객체 참조 관계가 GC 도중 변경되면 잘못된 객체를 수집할 수 있다
  • 문제점: STW 시간이 길면 응답 지연(latency) 발생
  • GC 발전 방향: STW 시간을 최소화하는 것이 핵심 목표

Concurrent GC

애플리케이션 스레드와 GC 스레드가 동시에 실행되는 방식이다. STW 시간을 줄이기 위해 가능한 많은 작업을 동시에 처리한다.

구분 STW GC Concurrent GC
동작 방식 애플리케이션 정지 후 GC 애플리케이션과 GC 동시 실행
STW 시간 김 (수백 ms ~ 수 초) 짧음 (수 ms 이하)
CPU 사용 GC 중 100% GC 전용 GC와 애플리케이션이 CPU 공유
대표 GC Parallel GC ZGC, Shenandoah

Compaction (압축)

GC 후 살아남은 객체들을 메모리 한쪽으로 모아 정리하는 작업이다. 메모리 단편화(fragmentation)를 방지한다.

[Compaction 전]                    [Compaction 후]
┌───┬───┬───┬───┬───┬───┐         ┌───┬───┬───┬───┬───┬───┐
│ A │   │ B │   │   │ C │   →     │ A │ B │ C │           │
└───┴───┴───┴───┴───┴───┘         └───┴───┴───┴───────────┘
    ↑       ↑   ↑                               ↑
  빈공간  빈공간 빈공간                     연속된 빈 공간
  • 왜 필요한가?: 큰 객체 할당 시 연속된 메모리 공간이 필요
  • 단점: Compaction 중에는 STW 발생
  • GC별 차이: ZGC, Shenandoah는 Concurrent Compaction 지원 (STW 최소화)

Minor GC vs Major GC vs Full GC

구분 대상 영역 발생 빈도 STW 시간
Minor GC Young Generation 자주 (초~분 단위) 짧음 (수~수십 ms)
Major GC Old Generation 드물게 김 (수백 ms~초)
Full GC 전체 Heap + Metaspace 매우 드물게 매우 김 (수 초)

Full GC가 자주 발생하면 애플리케이션 성능에 심각한 영향을 준다. GC 튜닝의 주요 목표 중 하나가 Full GC 빈도를 줄이는 것이다.

GC 알고리즘 변천사

JDK 버전 기본 GC 주요 변경사항
JDK 8 Parallel GC G1 GC 정식 지원, CMS 사용 가능
JDK 9 G1 GC G1이 기본 GC로 변경
JDK 11 G1 GC ZGC 실험적 도입, CMS Deprecated
JDK 14 G1 GC CMS 완전 제거
JDK 15 G1 GC ZGC/Shenandoah 정식 지원
JDK 17 G1 GC ZGC, Shenandoah 안정화
JDK 21 G1 GC Generational ZGC 정식 지원
JDK 25 G1 GC Generational Shenandoah 정식 지원

주요 GC 알고리즘 비교

G1 GC (Garbage-First)

Java 9부터 기본 GC. 힙을 여러 Region으로 나누어 관리하며, 예측 가능한 pause time을 목표로 한다.

# G1 GC 활성화 (JDK 9+ 기본값)
java -XX:+UseG1GC -jar application.jar

# 목표 pause time 설정 (기본 200ms)
java -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar application.jar

특징:

  • Heap 크기: 4GB ~ 64GB에 적합
  • Pause Time: 수십 ~ 수백 ms
  • Throughput과 Latency의 균형

ZGC (Z Garbage Collector)

Oracle이 개발한 초저지연 GC. Java 15에서 정식 지원, Java 21에서 Generational 모드 추가.

# ZGC 활성화
java -XX:+UseZGC -jar application.jar

# Generational ZGC (JDK 21+)
java -XX:+UseZGC -XX:+ZGenerational -jar application.jar

특징:

  • Heap 크기: 수백 GB ~ 16TB 지원
  • Pause Time: 1ms 미만 (힙 크기와 무관)
  • 대규모 힙에서도 일관된 저지연

Shenandoah GC

Red Hat이 개발한 저지연 GC. Concurrent compaction을 지원한다.

# Shenandoah 활성화
java -XX:+UseShenandoahGC -jar application.jar

# Generational Shenandoah (JDK 25+)
java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -jar application.jar

특징:

  • Heap 크기: 8GB ~ 64GB에 적합
  • Pause Time: 10ms 미만
  • Oracle JDK에는 미포함 (OpenJDK 전용)

GC 선택 가이드

요구사항 권장 GC
범용 (균형잡힌 성능) G1 GC
대용량 힙 + 초저지연 ZGC
중간 힙 + 저지연 (OpenJDK) Shenandoah
최대 처리량 (배치 작업) Parallel GC

Java 8 (2014년 3월)

Java 역사상 가장 큰 패러다임 변화가 있었던 버전이다. 함수형 프로그래밍이 도입되었으며, 현재까지도 많은 엔터프라이즈 시스템에서 사용되고 있다.

기본 GC: Parallel GC

Lambda Expression

함수형 인터페이스의 구현을 간결하게 표현할 수 있다.

// Before Java 8: 익명 클래스
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello");
    }
};

// Java 8: Lambda Expression
Runnable runnable = () -> System.out.println("Hello");

// 파라미터가 있는 경우
Comparator<String> comparator = (s1, s2) -> s1.compareTo(s2);

// Method Reference
Comparator<String> comparator = String::compareTo;

Stream API

컬렉션 데이터를 선언적으로 처리할 수 있는 API. 내부 반복(internal iteration)을 사용하여 병렬 처리도 쉽게 적용 가능하다.

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

// 필터링 + 변환 + 수집
List<String> result = names.stream()
    .filter(name -> name.length() > 3)
    .map(String::toUpperCase)
    .sorted()
    .collect(Collectors.toList());

// 병렬 처리
long count = names.parallelStream()
    .filter(name -> name.startsWith("A"))
    .count();

// 집계 연산
int totalLength = names.stream()
    .mapToInt(String::length)
    .sum();

Optional

null을 명시적으로 처리하기 위한 컨테이너 클래스.

Optional<String> optional = Optional.ofNullable(getValue());

// 값이 있으면 처리, 없으면 기본값
String result = optional.orElse("default");

// 값이 있으면 처리, 없으면 예외
String result = optional.orElseThrow(() ->
    new IllegalStateException("Value not present"));

// 조건부 처리
optional.ifPresent(value -> System.out.println(value));

// Java 9+에서 추가된 메서드
optional.ifPresentOrElse(
    value -> System.out.println(value),
    () -> System.out.println("Empty")
);

Date/Time API (java.time)

Joda-Time의 저자 Stephen Colebourne이 설계한 새로운 날짜/시간 API. 불변(immutable)이며 스레드 안전하다.

// 현재 날짜/시간
LocalDate date = LocalDate.now();
LocalTime time = LocalTime.now();
LocalDateTime dateTime = LocalDateTime.now();
ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul"));

// 날짜 연산
LocalDate nextWeek = date.plusWeeks(1);
LocalDate lastMonth = date.minusMonths(1);

// 날짜 간격
Period period = Period.between(startDate, endDate);
Duration duration = Duration.between(startTime, endTime);

// 포맷팅
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = dateTime.format(formatter);

Default Method in Interface

인터페이스에 구현을 가진 메서드를 정의할 수 있다. 기존 인터페이스에 새 메서드를 추가해도 구현체를 수정할 필요가 없다.

public interface Vehicle {
    void drive();  // 추상 메서드

    default void start() {
        System.out.println("Starting vehicle...");
    }

    static Vehicle create(String type) {
        // 정적 팩토리 메서드
        switch (type) {
            case "car": return new Car();
            case "bike": return new Bike();
            default: throw new IllegalArgumentException();
        }
    }
}

다이아몬드 문제 해결: 여러 인터페이스에서 같은 시그니처의 default 메서드가 있으면 구현 클래스에서 명시적으로 선택해야 한다.

interface A { default void hello() { System.out.println("A"); } }
interface B { default void hello() { System.out.println("B"); } }

class C implements A, B {
    @Override
    public void hello() {
        A.super.hello();  // 명시적으로 A의 구현 선택
    }
}

Java 11 (2018년 9월)

Java 8 이후 첫 번째 LTS 버전. Oracle JDK의 상용 라이선스 정책 변경으로 OpenJDK 기반 배포판(Temurin, Corretto, Zulu 등)이 활성화되기 시작했다.

기본 GC: G1 GC 실험적 GC: ZGC (Linux/x64), Epsilon GC

HTTP Client API

java.net.http 패키지에 새로운 HTTP 클라이언트가 추가되었다. HTTP/2와 WebSocket을 지원한다.

HttpClient client = HttpClient.newBuilder()
    .version(HttpClient.Version.HTTP_2)
    .connectTimeout(Duration.ofSeconds(10))
    .build();

// 동기 요청
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users"))
    .header("Content-Type", "application/json")
    .GET()
    .build();

HttpResponse<String> response = client.send(request,
    HttpResponse.BodyHandlers.ofString());

// 비동기 요청
CompletableFuture<HttpResponse<String>> future = client.sendAsync(request,
    HttpResponse.BodyHandlers.ofString());

// POST 요청
HttpRequest postRequest = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"John\"}"))
    .build();

Local Variable Type Inference (var)

지역 변수 선언 시 var 키워드로 타입 추론이 가능하다. Java 10에서 도입되었고, Java 11에서 람다 파라미터에도 사용 가능해졌다.

// 지역 변수 (Java 10+)
var list = new ArrayList<String>();
var stream = list.stream();

// 람다 파라미터 (Java 11+)
// 어노테이션을 붙일 수 있어서 유용함
list.stream()
    .map((@NonNull var item) -> item.toUpperCase())
    .collect(Collectors.toList());

String API 추가

String text = "  Hello World  ";

// 공백 체크
text.isBlank();       // false
"   ".isBlank();      // true (공백만 있으면 true)

// 공백 제거 (유니코드 공백 포함)
text.strip();         // "Hello World"
text.stripLeading();  // "Hello World  "
text.stripTrailing(); // "  Hello World"

// 반복
"Ha".repeat(3);       // "HaHaHa"

// 라인 분리
"line1\nline2\nline3".lines()
    .forEach(System.out::println);

단일 파일 소스 코드 실행

컴파일 없이 Java 소스 파일을 직접 실행할 수 있다.

# 기존 방식
javac HelloWorld.java
java HelloWorld

# Java 11+
java HelloWorld.java

# Unix에서 shebang 사용
#!/usr/bin/java --source 11
public class Script {
    public static void main(String[] args) {
        System.out.println("Hello from script!");
    }
}

제거 및 Deprecated 기능

기능 상태
Java EE 모듈 (JAX-WS, JAXB 등) 제거 (별도 의존성 추가 필요)
CORBA 제거
Nashorn JavaScript 엔진 Deprecated (JDK 15에서 제거)
CMS GC Deprecated (JDK 14에서 제거)
<!-- JAXB 사용 시 Maven 의존성 추가 -->
<dependency>
    <groupId>jakarta.xml.bind</groupId>
    <artifactId>jakarta.xml.bind-api</artifactId>
    <version>4.0.0</version>
</dependency>

Java 17 (2021년 9월)

3년 만의 LTS 버전. Preview로 제공되던 많은 기능들이 정식 채택되었다. 모던 Java 프로그래밍의 시작점으로 볼 수 있다.

기본 GC: G1 GC 정식 지원 GC: ZGC, Shenandoah

Sealed Classes

상속 가능한 클래스를 제한할 수 있다. permits 키워드로 허용된 하위 클래스를 명시한다.

public sealed class Shape permits Circle, Rectangle, Triangle {
    public abstract double area();
}

public final class Circle extends Shape {
    private final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

public final class Rectangle extends Shape {
    private final double width, height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() {
        return width * height;
    }
}

public non-sealed class Triangle extends Shape {
    // non-sealed: 다른 클래스가 상속 가능
    @Override
    public double area() { /* ... */ }
}

Sealed Classes + Pattern Matching

Sealed class의 모든 하위 타입을 switch에서 처리하면 default 절이 필요 없다. 새 하위 타입이 추가되면 컴파일 에러가 발생하여 누락을 방지한다.

// 모든 케이스를 처리하면 default 불필요 (exhaustive)
double calculateArea(Shape shape) {
    return switch (shape) {
        case Circle c    -> Math.PI * c.radius() * c.radius();
        case Rectangle r -> r.width() * r.height();
        case Triangle t  -> t.base() * t.height() / 2;
        // default 없어도 컴파일 OK
    };
}

// Pentagon 추가 시 -> 컴파일 에러 발생 (누락 방지)

Pattern Matching for instanceof

instanceof 연산자에서 타입 체크와 캐스팅을 동시에 수행한다.

// Before Java 17
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
}

// Java 17+
if (obj instanceof String s) {
    System.out.println(s.length());
}

// 조건 추가 가능
if (obj instanceof String s && s.length() > 5) {
    System.out.println(s.toUpperCase());
}

Records

불변 데이터 클래스를 간결하게 정의한다. 생성자, getter, equals(), hashCode(), toString()이 자동 생성된다.

public record Point(int x, int y) {}

// 사용
Point p = new Point(1, 2);
int x = p.x();  // getter (getX()가 아님)
int y = p.y();

// Compact Constructor로 검증 로직 추가
public record User(String name, int age) {
    public User {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
        name = name.trim();  // 값 정규화
    }
}

// 추가 메서드 정의 가능
public record Rectangle(double width, double height) {
    public double area() {
        return width * height;
    }
}

Records의 제약사항

제약 설명
상속 불가 암묵적으로 final이며 다른 클래스 상속 불가
필드 불변 모든 필드가 final, setter 없음
JPA Entity 불가 기본 생성자, setter가 없어서 JPA와 호환 안됨

Text Blocks

여러 줄 문자열을 삼중 따옴표로 작성한다. 들여쓰기가 자동으로 처리된다.

String json = """
    {
        "name": "John",
        "age": 30,
        "city": "Seoul"
    }
    """;

String sql = """
    SELECT u.id, u.name, u.email
    FROM users u
    WHERE u.status = 'ACTIVE'
      AND u.created_at > :date
    ORDER BY u.name
    """;

// 이스케이프 문자도 그대로 사용 가능
String html = """
    <html>
        <body>
            <h1>Hello, "World"!</h1>
        </body>
    </html>
    """;

Switch Expression

switch를 표현식으로 사용하여 값을 반환할 수 있다.

// 표현식으로 값 반환
String result = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> "6 letters";
    case TUESDAY                -> "7 letters";
    case THURSDAY, SATURDAY     -> "8 letters";
    case WEDNESDAY              -> "9 letters";
};

// yield로 블록에서 값 반환
int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY -> 7;
    case THURSDAY, SATURDAY -> 8;
    case WEDNESDAY -> {
        System.out.println("Processing Wednesday...");
        yield 9;
    }
};

제거 및 Deprecated 기능

기능 상태
Applet API 제거
RMI Activation 제거
Security Manager Deprecated for Removal
실험적 AOT/JIT 컴파일러 제거

Java 21 (2023년 9월)

Virtual Thread 도입으로 Java의 동시성 프로그래밍 패러다임이 크게 변화한 버전. 클라우드 네이티브 환경에서 Java의 경쟁력을 높였다.

기본 GC: G1 GC 정식 지원 GC: Generational ZGC

Virtual Threads

경량 스레드로 대규모 동시성 처리가 가능하다. Platform Thread와 달리 OS 스레드와 1:1 매핑되지 않으며, JVM이 스케줄링한다.

// Virtual Thread 생성
Thread vThread = Thread.ofVirtual()
    .name("virtual-", 0)
    .start(() -> {
        System.out.println("Running in virtual thread");
    });

// ExecutorService 사용
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 100_000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        });
    });
}

Spring Boot에서 Virtual Thread를 활성화하려면 다음 설정을 추가하면 된다.

# application.yml
spring:
  threads:
    virtual:
      enabled: true

Platform Thread vs Virtual Thread

구분 Platform Thread Virtual Thread
생성 비용 높음 (~1MB 스택) 낮음 (~수 KB)
스케줄링 OS 커널 JVM
동시 실행 수 수천 개 수백만 개
Blocking 영향 심각 최소화

주의: Pinning 문제

Virtual Thread가 carrier thread(실제 OS 스레드)에 고정되어 Virtual Thread의 이점이 사라지는 현상이다.

// Pinning 발생 - synchronized 블록에서 blocking
synchronized (lock) {
    Thread.sleep(1000);  // carrier thread가 묶임
}

// 해결책 1: ReentrantLock 사용
private final ReentrantLock lock = new ReentrantLock();

lock.lock();
try {
    Thread.sleep(1000);  // carrier thread 해제 가능
} finally {
    lock.unlock();
}

// 해결책 2: Pinning 발생 지점 진단
java -Djdk.tracePinnedThreads=short -jar app.jar

Pinning 발생 조건:

  • synchronized 블록/메서드 내에서 blocking 작업
  • Native 코드(JNI) 실행 중

Sequenced Collections

순서가 있는 컬렉션에 대한 통일된 인터페이스가 추가되었다.

// SequencedCollection 인터페이스
SequencedCollection<String> list = new ArrayList<>();
list.addFirst("first");
list.addLast("last");

String first = list.getFirst();
String last = list.getLast();

SequencedCollection<String> reversed = list.reversed();

// SequencedSet
SequencedSet<String> set = new LinkedHashSet<>();
set.addFirst("a");
set.addLast("z");

// SequencedMap
SequencedMap<String, Integer> map = new LinkedHashMap<>();
map.putFirst("first", 1);
map.putLast("last", 99);
Map.Entry<String, Integer> firstEntry = map.firstEntry();

Record Patterns

Record를 패턴 매칭으로 분해하여 내부 컴포넌트에 직접 접근한다.

record Point(int x, int y) {}
record Line(Point start, Point end) {}

// 중첩 패턴 매칭
void printLine(Object obj) {
    if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) {
        System.out.printf("Line from (%d,%d) to (%d,%d)%n", x1, y1, x2, y2);
    }
}

// switch에서 사용
String describe(Object obj) {
    return switch (obj) {
        case Point(int x, int y) when x == 0 && y == 0 -> "Origin";
        case Point(int x, int y) -> "Point at (%d, %d)".formatted(x, y);
        case Line(Point start, Point end) -> "Line from %s to %s".formatted(start, end);
        default -> "Unknown";
    };
}

Pattern Matching for switch (정식)

타입 패턴과 가드 조건을 사용한 switch 표현식.

String format(Object obj) {
    return switch (obj) {
        case Integer i when i > 0 -> "Positive integer: " + i;
        case Integer i when i < 0 -> "Negative integer: " + i;
        case Integer i            -> "Zero";
        case Long l               -> "Long: " + l;
        case String s             -> "String: " + s;
        case null                 -> "null";
        default                   -> "Unknown type";
    };
}

Structured Concurrency (Preview)

여러 병렬 작업을 하나의 단위로 관리한다. 작업 그룹의 생명주기를 구조적으로 관리할 수 있다.

// ShutdownOnFailure: 하나라도 실패하면 전체 취소
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Subtask<User> userTask = scope.fork(() -> fetchUser(userId));
    Subtask<Order> orderTask = scope.fork(() -> fetchOrder(orderId));

    scope.join();              // 모든 작업 완료 대기
    scope.throwIfFailed();     // 실패 시 예외 발생

    User user = userTask.get();
    Order order = orderTask.get();
    return new Response(user, order);
}

// ShutdownOnSuccess: 하나라도 성공하면 나머지 취소
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
    scope.fork(() -> fetchFromPrimary());
    scope.fork(() -> fetchFromSecondary());

    scope.join();
    return scope.result();  // 가장 먼저 성공한 결과
}

Scoped Values (Preview)

ThreadLocal의 대안으로, 불변 값을 스코프 단위로 공유한다.

private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();

void handleRequest(User user) {
    ScopedValue.where(CURRENT_USER, user)
        .run(() -> {
            processRequest();
            // 하위 메서드에서 CURRENT_USER.get()으로 접근 가능
        });
}

void processRequest() {
    User user = CURRENT_USER.get();  // 현재 스코프의 User 획득
    // ...
}

ThreadLocal vs ScopedValue

구분 ThreadLocal ScopedValue
변경 가능 O X (불변)
Virtual Thread 친화 메모리 누수 가능 최적화됨
상속 InheritableThreadLocal 필요 자동 상속

Java 25 (2025년 9월)

현재 최신 LTS 버전. 성능 최적화와 개발자 경험 개선에 초점을 맞췄다.

기본 GC: G1 GC 정식 지원 GC: Generational ZGC, Generational Shenandoah

Scoped Values (정식)

Java 21에서 Preview였던 Scoped Values가 정식 기능이 되었다.

Structured Concurrency (정식)

Java 21에서 Preview였던 Structured Concurrency가 정식 기능이 되었다.

Flexible Constructor Bodies

super() 호출 전에 코드를 작성할 수 있다. 파라미터 검증이나 변환 작업에 유용하다.

public class Child extends Parent {
    public Child(String value) {
        // super() 호출 전에 검증 로직 가능
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException("Value cannot be null or blank");
        }
        String normalized = value.trim().toUpperCase();

        super(normalized);  // 검증/변환 후 super 호출

        // 이후 추가 초기화 로직
    }
}

Compact Object Headers

객체 헤더 크기가 96-128비트에서 64비트로 감소했다. 힙 메모리 사용량이 줄어들고 캐시 효율이 향상된다.

메모리 절약 효과:

  • 작은 객체가 많은 애플리케이션에서 10-20% 힙 절약
  • 데이터 지역성 향상으로 캐시 hit율 증가
# Compact Object Headers 활성화 (JDK 25+ 기본값)
java -XX:+UseCompactObjectHeaders -jar application.jar

Module Import Declarations

모듈 단위로 import가 가능하다.

import module java.base;  // java.util.*, java.io.*, java.time.* 등 포함

public class App {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();  // import 없이 사용 가능
        LocalDate date = LocalDate.now();
    }
}

Compact Source Files

단순한 프로그램 작성 시 public classpublic static void main 선언을 생략할 수 있다.

// HelloWorld.java - 클래스 선언 없이 바로 실행 가능
void main() {
    System.out.println("Hello, World!");
}

// 인자 처리
void main(String[] args) {
    System.out.println("Arguments: " + Arrays.toString(args));
}

Key Derivation Function API

암호화 키 유도 함수(KDF) API가 추가되었다. Post-quantum cryptography 지원을 위한 기반이다.

KDF kdf = KDF.getInstance("HKDF-SHA256");

byte[] inputKeyMaterial = // ...
byte[] salt = // ...
byte[] info = // ...

SecretKey derivedKey = kdf.deriveKey("AES",
    new HKDFParameterSpec(inputKeyMaterial, salt, info, 256));

Generational Shenandoah (정식)

Shenandoah GC에 세대별 수집 모드가 정식 지원된다. Young generation 객체를 별도로 관리하여 GC 효율이 향상된다.

java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -jar app.jar

제거된 기능

기능 상태
32-bit x86 포트 제거
Security Manager 완전 제거
실험적 Graal JIT 컴파일러 제거

LTS 버전 선택 가이드

상황별 권장 버전

상황 권장 버전 이유
레거시 시스템 유지보수 Java 8 또는 11 기존 코드 호환성
신규 프로젝트 시작 Java 21 또는 25 Virtual Thread, Records 등 최신 기능
대규모 동시성 필요 Java 21+ Virtual Thread 필수
저지연 GC 필요 Java 17+ ZGC, Shenandoah 안정화
최신 기능 + 장기 지원 Java 25 현재 최신 LTS

마무리

Java LTS 버전의 변화를 정리하면:

JVM 메모리 변화:

  • Java 8: PermGen → Metaspace로 변경, Native Memory 사용
  • Java 9+: G1 GC 기본값, Region 기반 힙 관리
  • Java 21+: Generational ZGC로 대용량 힙에서도 저지연 GC 가능

언어 기능 변화:

  • Java 8: 함수형 프로그래밍 패러다임 도입 (Lambda, Stream, Optional)
  • Java 11: HTTP Client API, var 키워드
  • Java 17: 모던 Java의 시작 (Records, Sealed Classes, Pattern Matching)
  • Java 21: 동시성 혁명 (Virtual Thread, Structured Concurrency)
  • Java 25: 성능 최적화 및 안정화 (Generational Shenandoah, Compact Headers)

신규 프로젝트라면 Java 21 이상을 권장한다. Virtual Thread, Record, Pattern Matching 등의 기능으로 코드가 간결해지고 성능도 향상된다.


참고 자료

프로필 이미지
@chani
바둑, 스타크래프트 등 고전 게임을 좋아하는 내향인 개발자입니다

댓글