JEP로 본 Java 21 New Feature

겔로그·2023년 11월 26일
1

Java 21 에 새롭게 도입되는 여러 기능들 중 인상깊은 몇가지 JEP 에 대해 공유해보고자 합니다.

목차

추가로 참고하면 좋은 내용

읽어보면서 가장 중요한 것은 Virtual Thread와 Generational ZGC 인 것 같은데 JEP에서는 간략하게 나와 이해가 어려운 부분이 있었던 것 같습니다.
아래 글을 추가로 읽어보시는 것을 추천드립니다.

JEP 431: Sequenced Collections

기존 Collection 요소 접근 방식

첫번째 요소 접근마지막 요소 접근
Listlist.get(0)list.get(list.size() – 1)
Dequedeque.getFirst()deque.getLast()
SortedSetsortedSet.first()sortedSet.last()
LinkedHashSetlinkedHashSet.iterator().next()// missing
  • 첫 요소, 마지막 요소를 가져오는 방식이 클래스마다 각기 다름
  • 일부는 역방향 호출을 지원하지 않음

Sequenced Collection

  • 컬렉션의 첫번째 요소와 마지막 요소에 손쉽게 액세스할 수 있는 인터페이스 추가
  • 기존 Collection Framework와 호환되는 특징이 있습니다.

interface SequencedCollection<E> extends Collection<E> {

    // new method
    SequencedCollection<E> reversed();

    // methods promoted from Deque
    void addFirst(E);
    void addLast(E);

    E getFirst();
    E getLast();

    E removeFirst();
    E removeLast();
}

interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
    SequencedSet<E> reversed();    // covariant override
}

interface SequencedMap<K,V> extends Map<K,V> {
    // new methods
    SequencedMap<K,V> reversed();
    SequencedSet<K> sequencedKeySet();
    SequencedCollection<V> sequencedValues();
    SequencedSet<Entry<K,V>> sequencedEntrySet();
    V putFirst(K, V);
    V putLast(K, V);
    // methods promoted from NavigableMap
    Entry<K, V> firstEntry();
    Entry<K, V> lastEntry();
    Entry<K, V> pollFirstEntry();
    Entry<K, V> pollLastEntry();
}
  • 위와 같이 첫번째 요소, 마지막 요소에 접근하기 쉽게 인터페이스를 추가

주의사항

  • 몇몇의 메소드는 도입된 새로운 메소드와 충돌 가능성이 존재함 (일부 소스코드 비호환)
  • override 재정의 필요: List와 Deque에서 동일하게 reversed() 메소드를 override하고 있어 두 인터페이스를 모두 구현하는 구현체에서는 컴파일 에러 발생

JEP 439: Generational ZGC

개발 동기 (Motivation)

  • 일반적으로 기존 ZGC를 사용하는 것 만으로도 GC 관련 지연 문제를 충분히 해결함
  • 현재 ZGC는 모든 객체를 함께 저장함으로 실행될 때마다 모든 개체를 수집해야 함
  • 오래된 객체(old)보다 젊은 객체(young)가 빨리 죽는 경향이 있음
    • 이에 비해, 젊은 객체들이 정말 많이 사용됨
    • 젊은 객체를 따로 수집하면 더 적은 리소스, 더 많은 메모리가 생성됨
    • 논리적으로 두 개의 영역으로 분리해 관리하고 젊은 객체들을 더 자주 수집하는 것이 성능에 더 뛰어나다는 weak generational hypothesis 를 참고해 고안됨

기존 ZGC의 특징 보존

  • 일시중지 시간은 1ms를 초과하지 않음
  • 수백MB ~ nTB까지 힙 크기가 지원됨
  • 최소한의 수동 구성

특징

  • 두 개의 영역으로 분리해 메모리를 관리
  • Young한 객체들을 자주 수집해 애플리케이션 성능을 향상시킴
  • 기존 ZGC에서 사용하던 multi-mapped memory가 존재하지 않음
    • Load Barrier의 오버헤드를 줄이기 위해 ZGC는 가상 메모리를 사용해 객체를 매핑함
    • 이 때 기존 힙 메모리보다 3배의 가상 메모리를 추가로 사용했는데 Generational ZGC는 매핑할 필요가 없어져 불필요해짐
  • 이 외에도 많은 차이점이 존재하지만... 나중에 좀 더 알아보겠습니다

장점

  • 기존 ZGC에 비해 월등한 벤치마크 표
    • 메모리 1/4로 관리 가능
    • 처리량 4배 증가
    • 중지 시간 동일

사용 방법

# Enable ZGC (defaults to non-generational)
$ java -XX:+UseZGC

# Use Generational ZGC
$ java -XX:+UseZGC -XX:+ZGenerational

주의사항

  • 기존 ZGC를 이용할 경우, Generational ZGC 옵션을 활성화하면 성능이 무조건 좋아지는 것은 아님
  • GC의 동작 방식이 아예 다르다는 것을 인지해야 함
  • 일반적으로는 성능이 좋아지겠지만 일부 시스템에서는 안좋아질 수 있으니 모니터링을 하고 적용하는 것을 권장

추후 기존 ZGC는 deprecated되며 Generation ZGC가 기본 버전이 될 것이라고 함

  • default GC가 G1 GC에서 바뀔지도..?

JEP 440: Record Patterns

데이터에 접근하기 쉽게 개선한 패턴

기존 Java에서의 Record Pattern

private static void singleRecordPatternOldStyle() {
    Object o = new GrapeRecord(Color.BLUE, 2);
    if (o instanceof GrapeRecord grape) {
        System.out.println("This grape has " + grape.nbrOfPits() + " pits.");
    }
}

grape.nbrOfPits()를 통해 값에 접근

개선된 Java 21 Record Pattern

  • 개선된 record Pattern에서는 직접 값 참조가 가능
private static void singleRecordPattern() {
    Object o = new GrapeRecord(Color.BLUE, 2);
    if (o instanceof GrapeRecord(Color color, Integer nbrOfPits)) {
        System.out.println("This grape has " + nbrOfPits + " pits.");
    }
}

중첩 레코드 지원

  • 중첩된 레코드에서도 값 직접 참조가 가능
private static void nestedRecordPattern() {
    Object o = new SpecialGrapeRecord(new GrapeRecord(Color.BLUE, 2), true);
    if (o instanceof SpecialGrapeRecord(GrapeRecord grape, boolean special)) {
        System.out.println("This grape has " + grape.nbrOfPits() + " pits.");
    }
    if (o instanceof SpecialGrapeRecord(GrapeRecord(Color color, Integer nbrOfPits), boolean special)) {
        System.out.println("This grape has " + nbrOfPits + " pits.");
    }
}

JEP 441: Pattern Matching for switch

  • 기존 switch문을 좀 더 편하게 사용할 수 있습니다.
  • switch문은 Java 버전마다 지속적으로 개선되어옴
//java16
// Prior to Java 16
if (obj instanceof String) {
    String s = (String)obj;
    ... use s ...
}

// As of Java 16
if (obj instanceof String s) {
    ... use s ...
}

Java 21 switch 문

**비교성공시 바로 사용 가능 **

static String formatterPatternSwitch(Object obj) {
    return switch (obj) {
        case Integer i -> String.format("int %d", i);
        case Long l    -> String.format("long %d", l);
        case Double d  -> String.format("double %f", d);
        default        -> obj.toString();
    };
}

null인 경우도 case로 처리 가능

static void testFooBarNew(String s) {
    switch (s) {
        case null         -> System.out.println("Oops");
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
}

enum 타입일 경우

static void exhaustiveSwitchWithBetterEnumSupport(CardClassification c) {
    switch (c) {
        case Suit.CLUBS -> {
            System.out.println("It's clubs");
        }
        case Suit.DIAMONDS -> {
            System.out.println("It's diamonds");
        }
        case Suit.HEARTS -> {
            System.out.println("It's hearts");
        }
        case Suit.SPADES -> {
            System.out.println("It's spades");
        }
    }
}

복수 조건 사용 가능

static void testStringEnhanced(String response) {
    switch (response) {
        case null -> { }
        case "y", "Y" -> {
            System.out.println("You got it");
        }
        case "n", "N" -> {
            System.out.println("Shame");
        }
        case String s
        when s.equalsIgnoreCase("YES") -> {
            System.out.println("You got it");
        }
        case String s
        when s.equalsIgnoreCase("NO") -> {
            System.out.println("Shame");
        }
    }
}

JEP 444: Virtual Threads

JDK 19에서 처음 소개된 Virtual Thread는 2개의 JDK 버전을 넘어오면서 개념 정립이 완료되었습니다.

  • Virtual Thread는 항상 thread 로컬 변수를 지원(가질 수 없는 virtual thread는 생성 불가능)
  • Thread.Builder를 통해 직접 생성된 virtual thread는 기본적으로 전체 수명동안 모니터링됨
  • Observing virtual threads 섹션에서 thread dump를 통해 관찰 가능

목표

  • 하드웨어 스펙에 최적화된 애플리케이션 개발
  • ThreadAPI를 사용해 최소한의 변경으로 virtual thread를 채택할 수 있도록 함
  • 기존 JDK 도구를 활용해 virtual thread에 대한 문제 해결, 디버깅 및 프로파일링 수행

목표가 아닌 것

  • Virtual Thread 를 통해 기존 스레드 사용 방식을 마이그레이션 하는 것
  • Java의 기본 동시성 모델을 변경하는 것
  • Java 언어나 라이브러리에서 새로운 데이터 병렬성 구조를 제공하는 것

개발 동기 (Motivation)

1. 스레드당 요청 스타일 (thread-per-request style)

  • 요청당 전용 스레드를 할당해 요청을 처리
  • 동시성이란, 애플리케이션에서 여러 요청을 동시에 처리하는 것
  • 동시성과 처리량의 관점에서 요청기간동안 처리량을 더 높이려면 스레드의 수를 증가시켜야 함
    • 하지만..
      • JDK에서는 OS의 스레드를 wrapper로 감싸서 제공하기 때문에 사용 가능한 스레드 수에 제약이 존재함
      • 하나의 요청의 전체 주기에 OS 스레드가 바인딩 되기 때문에 Blocking으로 인한 리소스 낭비 발생

2. 비동기 스타일로 확장성 향상 (Improving scalability with the asynchronous style)

  • 몇몇 개발자들은 하드웨어를 최대한 활용하고 싶은 개발자들로 인해 thread-per-request style이 아닌 thread-sharing style을 선호함
  • thread가 다른 요청을 처리할 수 있게 I/O  처리가 완료될 때 까지 thread를 thread pool에 반환하는 방식
    • 소위 말하는 비동기 프로그래밍 방식의 개발이 필요
  • 단점으로는, 요청의 각 단계가 다른 스레드에서 실행될 수 있어 디버깅 및 프로파일링에 어려움을 겪음

Virtual thread로 얻는 이점

1. 스레드당 요청 스타일을 가상 스레드로 유지

  • 운영체제의 virtual memory 방식과 유사하게 virtual thread를 런타임간 제공해 많은 스레드를 가지고 있는 것처럼 보이게 할 수 있음

2. 개발 동기였던 2.비동기 스타일로 확장성 향상 에서 말한 모든 문제가 개선

  • virtual thread는 특정 OS 스레드에 바인딩 되지 않은 java.lang.Thread의 인스턴스
  • 기존 Thread는 요청의 전체 기간동안 특정 OS의 스레드를 소비함
  • Virtual thread는 CPU에서 계산을 수행하는 동안에만 OS 스레드를 소비
  • 이를 통해 2. 비동기 스타일의 문제 해결 방법이 자연스럽게 녹아 비동기 스타일과 동일한 확장성을 가지게 됨

가상 스레드의 시사점(Implications of virtual threads)

  • Java의 스레드당 요청 스타일을 보존하면서 사용 가능한 하드웨어를 최적으로 활용 가능하게 만듬
  • 스레드 고비용 문제(high cost of threads)를 대응하기 위해 만든 개념
  • 기존 ThreadAPI 사용 방식과 차이가 없기에 사용성에 있어 러닝 커브가 없음

사용시 주의 사항

  • virtual thread는 값이 매우 싸기 때문에 pool에 넣으면 안됨
  • 어플리케이션 태스크간 새롭게 생성하는 방식으로 사용되어야 함
  • 기존 API 와의 호환성 문제 주의
    • ex) java.io.BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter, PrintStream및 클래스에 사용된 내부 잠금 프로토콜에서 영향을 받을 수 있음 (I/O method 동기화 과정)
  • 기존 코드와 virtual thread의 코드를 혼합할 때 몇가지 동작 차이가 관찰될 수 있음
    • The Thread.setPriority(int) method has no effect on virtual threads, which always have a priority of Thread.NORM_PRIORITY.
    • The Thread.setDaemon(boolean) method has no effect on virtual threads, which are always daemon threads.
    • Thread.getAllStackTraces() now returns a map of all platform threads rather than a map of all threads.
    • The blocking I/O methods defined by java.net.Socket, ServerSocket, and DatagramSocket are now interruptible when invoked in the context a virtual thread. Existing code could break when a thread blocked on a socket operation is interrupted, which will wake the thread and close the socket.
    • Virtual threads are not active members of a ThreadGroup. Invoking Thread.getThreadGroup() on a virtual thread returns a dummy "VirtualThreads" group that is empty.
    • Virtual threads have no permissions when running with a security manager set. See JEP 411 (Deprecate the Security Manager for Removal) for information about running with a security manager on Java 17 and later.
    • In JVM TI, the GetAllThreads and GetAllStackTraces functions do not return virtual threads. Existing agents that enable the ThreadStart and ThreadEnd events may encounter performance issues since they lack the ability to limit the events to platform threads.
    • The java.lang.management.ThreadMXBean API supports the monitoring and management of platform threads, but not virtual threads.
    • The -XX:+PreserveFramePointer flag has a drastic negative impact on virtual thread performance.

JEP 456: Unnamed Variables & Patterns

  • 사용하지 않는 변수에 대해 생략할 수 있음
  • '_'를 활용한 unnamed variable 생성

표현식의 결과가 필요하지 않는 로직

  • 간단한 for문, 사용하지 않는 지역변수에 사용
  • 사용하지 않는 변수에 사용
static int count(Iterable<Order> orders) {
    int total = 0;
    for (Order _ : orders)    // Unnamed variable
        total++;
    return total;
}

for (int i = 0, _ = sideEffect(); i < 10; i++) { ... i ... }

Queue<Integer> q = ... // x1, y1, z1, x2, y2, z2, ...
while (q.size() >= 3) {
   var x = q.remove();
   var y = q.remove();
   var _ = q.remove();        // Unnamed variable
   ... new Point(x, y) ...
}

Exception 이름을 지정하지 않을 경우

try { ... }
catch (Exception _) { ... }                // Unnamed variable
catch (Throwable _) { ... }                // Unnamed variable

switch 문에서 비교만 하는 걍우

switch (box) {
    case Box(RedBall _), Box(BlueBall _) -> processBox(box);
    case Box(GreenBall _)                -> stopProcessing();
    case Box(_)                          -> pickAnotherBox();
}

# Reference * [Virtual Thread란 무엇일까? (1)](https://findstar.pe.kr/2023/04/17/java-virtual-threads-1/) * [Virtual Thread란 무엇일까? (2)](https://findstar.pe.kr/2023/07/02/java-virtual-threads-2/) * [Generational ZGC and Beyond](https://www.youtube.com/watch?v=YyXjC68l8mw)
profile
Gelog 나쁜 것만 드려요~

0개의 댓글