일단 Java 에서 LTS 는 현재까지
- JDK8
- JDK11
- JDK17
- JDK21
- JDK25
가 있다.
우리는 기본적으로 JDK21 을 쓰고 있는데, 그래서 이걸 써서 뭐가 좋아졌어? 하면 뭔가 피상적인 대답밖에 할 수 없었어서… 무엇이 좋아졌는지를 조금 더 상세히 보려 한다.
JDK8
아마 책에서 modern java 라고 하면 얘를 많이 대상으로 했을 것이다.
이전 java 랑은 차원이 다르게 많이 바뀌기도 했고 자바를 쓸 일이 있으면 최소한 이거는 써야 한다는 생각이 든다.
JDK8의 특징
특징은 다음과 같다.
- Lambda Expression
- Stream API
- Optional
- LocalDate
이미 워낙 유명해서 딱히 뭐 설명할만한 특성은 없다.
한가지 재미있는 부분은 람다'식' 과 스트림'API' 라는 이름인데 저게 다른 이름으로 되어있는 이유가 있다.
- Lambda Expression은 Expression(식) 이라고 표현한 이유
- 식 : 프로그래밍에서 어떤 값으로 평가될 수 있는 코드 조각
- 람다는 그냥 하나의 메서드를 간결하게 표현하는 방식이다. 뭔가 다른 역할을 하지는 않는다.
- 그래서 함수형 인터페이스의 인스턴스라는 역할로 식 이라 표현하는 것
- Stream API은 API 라고 표현한 이유
- API : Application Programming Interface
- API 는 개발자가 기능/라이브러리를 사용할 수 있도록 미리 정의된 약속이나 규칙 혹은 도구의 모음이다.
- Stream 은 데이터 흐름(Stream) 을 다루기 위한 기능을 뭉쳐놓은 친구이다.
- Collection API 를 생각해보면 좋음
- 그래서 이거는 다양한 역할을 하는 것이 추가되어 API 라는 용어를 사용한다.
JDK8의 성능개선 포인트
그래서 이전 JDK 에 비해 이게 뭐가 좋아짐? 이라 하면
Metaspace 가 도입되었다.
JDK7 까지는 PermGen 을 사용했었다.
얘는 클래스 메타데이터(클래스 구조, 필드, 메서드 데이터 등), 메서드의 바이트코드, Constant Pool, Static 변수 을 저장하는 친구였는데 문제는.. PermGen 이 JVM heap 영역의 일부였고 크기 동적 변경이 불가능했다는 것. 그래서 OOM 이 발생할 일이 많았다. (Spring 처럼 클래스의 로드/언로드 작업이 계속 일어나면 고정 크기가 금방 꽉차버리기 때문)
그래서 Metaspace 가 도입된 이후 이게 어떻게 해결되었을까?
일단 Metaspace 는 Heap 영역에 속하지 않는 OS 네이티브 메모리 공간이다. 그래서 Heap 영역과 GC 대상이 되는 조건이 다르다.
Metaspace 에 있는 데이터는 해당 클래스를 로드한 ClassLoader 이 GC 대상이 될 떄에만 GC 대상이 된다. 근데 ClassLoader 이 아무래도 거의 모든 클래스랑 메서드가 스프링 시작 시점에 Main Application 의 ClassLoader 에 의해 로드되기 때문에… 거의 왠만하면 애플리케이션이 실행되는 내내 살아있는 경우가 많다.
Metaspace 는 그래서 클래스 메타데이터(클래스 구조, 필드, 메서드 데이터 등), 메서드의 바이트코드, Constant Pool 는 관리하지만 Static 변수 는 관리하지 않는다.
이 때부터 Static 변수는 얘가 아니라 Heap 영역에서 관리한다. (이유는 Static 변수의 경우는 참조 객체가 없어지면 GC 대상이 되어야 하기 때문. ) 물론 Static 변수 관리 여부가 성능에 큰 영향을 주지는 않는다.
중요한 점은 이녀석은 JVM 시작 시에 크기를 지정할 필요가 없었다는 것. OS 의 가용 메모리 내에서 크기가 유동적으로 늘어났기 때문에 위처럼 꽉 차서 터지는 일이 확실히 줄어들게 되었다.
JDK8 기본 GC
기본적으로는 parallel GC 를 사용한다.
이름 그대로 GC 를 parallel하게 병렬로 처리하는 것.
이전에 사용하던 serial GC 를 그냥 여러 스레드에서 할 수 있게 한 것이다.
이렇게 한 이유는 처리율을 올리기 위함인데, GC의 step-the-world 가 길어지지만 그 외의 시간에는 최대한 애플리케이션 동작을 빠르게 하기 위해 작업량을 최대한으로 하는 것이 목표이다.
JDK11
JDK11의 특징
특징은 다음과 같다.
- 지역 변수에 var 키워드 활용 가능 (이거는 JDK10 에서부터 추가됨)
- 컴파일러가 변수 타입을 추론할 수 있게 됨.
- interface 에 private 메서드 추가
- API 나 library 같은 것에 자잘한 기능 및 메서드 추가
- of(), java.net.http, …
- 참고로 이중에 java.net.http 의 경우는 HTTP/2 와 WebSocket 을 지원하고 비동기랑 논블로킹 요청을 지원한다.
- MSA 환경에서의 중요한 성능상 이점
- 참고로 이중에 java.net.http 의 경우는 HTTP/2 와 WebSocket 을 지원하고 비동기랑 논블로킹 요청을 지원한다.
- of(), java.net.http, …
JDK11의 성능개선 포인트
- Compact Strings (JDK9부터 도입)
- 특징
- String 내부 저장소가 char[] 이 아니라 byte[] 를 사용하도록 변경
- String 객체 내부에 coder flag 를 둬서 문자가 어떤 형태인지 표현
- UTF-16 형태의 2바이트 문자가 포함되는 경우 2바이트가 하나의 문자를 의미 -> 기존 char 방식과 동일
- LATIN-1 문자로만 구성된 경우 1바이트가 하나의 문자를 의미
- 장점
- LATIN-1 으로 표현하는 문자열의 힙 메모리 사용량이 절반으로 감소
- 단점
- coder 으로 인한 오버헤드?(사실상 없는거랑 똑같음)
- 특징
- App CDS (Application Class-Data Sharing) (JDK9 부터 도입)
- 특징
- 기존 JVM 애플리케이션 부트 과정
- classpath 에서 .class 파일 찾음 (Disk I/O)
- 클래스 파일 로딩
- 클래스 내용 파싱 및 검증
- JVM 이 사용하는 내부 자료구조(메타데이터)로 변환
- 변경된 방식
- 애플리케이션을 실행해서 실제 로드되는 클래스 목록을 뽑아냄. 즉 만들어두고 호출되지 않는 클래스는 공유 아카이브에 들어가지 않음
- 클래스를 미리 로드한 후에 공유 아카이브 파일(app.jsa) 생성
- 공유 아카이브 파일을 활용해서 애플리케이션 실행
- 참고로, 공유 아카이브 파일에서 저장된 메타데이터를 통해 검증하고, 실행 환경이랑 다르면 이거 안쓰임
- 예를 들어 JAR 파일의 버전이 다른 경우 등
- 기존 JVM 애플리케이션 부트 과정
- 장점
- 쿠버네티스 환경처럼 하나의 노드에 여러 서버를 띄우는 경우, 각각에 대해 클래스 파일 로딩부터 검증을 여러 번 할 필요 없이 한번에 만든 아카이빙 파일을 활용해 시작 가능
- 이를 통해 시작 시간 감소 가능
- 클래스 메타데이터가 메모리에 한 벌만 올라가서 서버가 여러개 있을 때 이 메타데이터의 점유율 감소 가능
- 쿠버네티스 환경처럼 하나의 노드에 여러 서버를 띄우는 경우, 각각에 대해 클래스 파일 로딩부터 검증을 여러 번 할 필요 없이 한번에 만든 아카이빙 파일을 활용해 시작 가능
- 단점
- 아카이브 파일과 현재 JVM 의 실행 환경이 다른 경우 + 하나의 노드에서 하나의 서버만 돌리는 경우는 의미 없는 공유 아카이브 파일을 만들기 때문에 메모리 공유 이득이 없어짐
- 그대신 재시작이 잦은 경우는 클래스 로딩이 생략되기 때문에 시간적 이득이 있음
- 아카이브 파일과 현재 JVM 의 실행 환경이 다른 경우 + 하나의 노드에서 하나의 서버만 돌리는 경우는 의미 없는 공유 아카이브 파일을 만들기 때문에 메모리 공유 이득이 없어짐
- 특징
JDK11 기본 GC
G1GC 를 사용한다(JDK9부터 이거를 default 로 채택)
이름에서부터 알 수 있듯 (Garbage First) 얘는 쓰레기를 먼저 처리하는 친구이다.
이전의 parallel GC 에 비해서는 처리율에서는 손해가 있지만 응답 시간에서 이점이 있다.
이걸 참고하면 좋음
JDK17
JDK17의 특징
개인적인 생각인데 JDK17부터는 뭔가 'kotlin스러운' 느낌의 추가가 많아진 것 같다.
위기감을 느껴서 그럴까?
- Records 클래스 (JDK16 부터 정식 도입)
- record 로 선언하면 getter, equals(), hashCode(), toString(), 내부 로직 등의 보일러플레이트를 컴파일러가 자동 생성
- Sealed 클래스 (JDK17 부터 정식 도입)
- 혀용된 자식 클래스가 아니면 상속이 불가능(Permit)
- 기존에 리플랙션을 통해 가능했던 JDK 내부 API 에 대한 접근이 차단됨 (뭔가 바뀐건 없지만 이상한 짓을 할 가능성 차단)
- instanceof 패턴매칭 가능 (JDK16 부터 정식 도입)
- switch 표현식 간결하게 추가, Text Block 등 코드 작성 시 불필요한 내용 줄이기 가능
JDK17의 성능개선 포인트
- JIT 최적화
- Sealed Class 의 경우는 상속 가능 자식의 종류를 미리 알 수 있기 때문에 인터페이스의 메서드 호출 단계에서 가상 호출이 필요 없이 바로 직접 호출할 수 있음
- Records 는 불변 데이터 객체 전달용 객체임이 보증되기 때문에 내부 필드를 private final 로 만들고 구조를 단순히 만들 것이 확정되어 성능에 필요한 최적화를 JIT가 알아서 매우 잘 해줌.
JDK17 기본 GC
기본적으로는 G1GC 를 활용하지만, ZGC, Shenandoah GC 가 들어와서 이걸 상황에 따라 활용 가능
JDK21
JDK21의 특징
아마 자바맨들은 한번씩 들어본 Virtual Thread 가 도입된 것이 이때부터라 이거 위주로 생각해도 무방하다.
- 가상 스레드 추가 (JDK21 부터 정식 도입)
- 이거 하나만으로 설명 엄청 길어질 수 있어서.. 상세한 설명은 따로 글로 대체
- Sequenced Collection (JDK21 부터 정식 도입)
- List 나 Deque처럼 순서가 있는 Collection 에서 활용할 수 있는 인터페이스 추가됨
- switch 패턴 매칭, 레코드 패턴 (JDK21 부터 정식 도입)
- 코드 개선 관련 내용 정도?
가상 스레드에 묻혔지만 개인적으로 Sequenced Collection 이 실제로 자바를 쓰면서 굉장히 와닿는 장점이었던 것 같다. 뭐 좋아지는건 아니지만 코드가 이뻐짐
JDK21의 성능개선 포인트
- 가상 스레드 도입
- I/O bound 작업의 동시 처리량이 엄청 좋아짐.
JDK21 기본 GC
기본적으로는 G1GC 를 활용
JDK25
JDK25의 특징
JDK21이 워낙 변화가 커서 주목을 받았지만, 개인적으로 JDK25도 엄청나게 좋은 점이 많아 보인다.
- Structured Concurrency (아직 fifth preview이기는 한데 중요해 보여서 일단 작성)
- 개인적으로 가상 스레드를 활용할 때 매우 중요한 개선점으로 보인다.
- 기존 virtual thread 의 문제점
- 기존 virtual thread 사용 중 작업 중 하나에서 에러가 발생하는 경우
- 다른 작업들은 그냥 수행이 되어서 상태가 꼬일 가능성이 있었음
- 이는 기본적으로 멀티스레딩이 fire-and-forget 발식으로 되어있었기 때문이다.
- 다른 작업들은 그냥 수행이 되어서 상태가 꼬일 가능성이 있었음
- 여러 작업이 동시에 수행되는데 끝나는 것을 기다리기 위해 Future, CountDownLatch 같은 걸로 구현해서 확인해야 했다.
- 코드의 흐름을 파악하기 어려웠음(물론 비동기에 비해서는 훨씬 쉬었지만 그래도)
- 기존 virtual thread 사용 중 작업 중 하나에서 에러가 발생하는 경우
- Structured Concurrency의 해결 방법
- StructuredTaskScope 라는 API를 통해 동시성 작업을 부모-자식으로 묶어서 처리
- 작업들이 모두 성공해야 하는 경우 등 뭔가 태스크의 성공 여부가 중요한 경우 이를 처리해 줄 수 있다 (ShutdownOnFailure)
- 자식의 모든 작업이 성공적으로 끝나면 결과의 취합이 가능하다. (ShutdownOnSuccess)
- 생명 주기가 명확해진다(부모 작업은 모든 자식 작업이 끝날 때까지 종료되지 않는다)
- StructuredTaskScope 라는 API를 통해 동시성 작업을 부모-자식으로 묶어서 처리
- Scoped Value (JDK24부터 정식 도입)
- 가상 스레드 환경 상에서 ThreadLocal 을 대체하는 메커니즘
- 특정 코드 블록 내에서만 유효한 불변 데이터를 공유한다.
- 특정 코드 블록 내에서만 활용하므로 가상 스레드에 최적화된다.
- 불변 데이터를 공유하는 것이라 사이드이펙트 발생 여지가 없다.
- 위의 Structered Concurrency 의 부모-자식 관계에서 부모의 ScopedValue 를 모든 자식이 자동으로 물려받을 수 있다.
- Stream Gatherers (JDK 22부터 정식 도입)
- 이를 통해 내부적으로 상태를 가지고 있을 수 있어서 이전의 모든 데이터를 활용한 기능 수행이 가능해졌다. (기존 map, filter 같은 애들은 상태가 없으니..)
JDK25의 성능개선 포인트
- Vector API (JDK22 부터 정식 도입)
- CPU는 내부적으로 여러 개의 데이터를 한 번의 명령으로 처리할 수 있는 SIMD(Single Instruction, Multiple Data) 를 가지고 있는데 이전에는 이게 불가능했음.
- Vector API 를 활용하면 병렬 연산이 가능(이를 쓰면 JVM 이 알아서 SIMD 코드 생성해줌)
- Project Valhalla (아직 도입은 안됨)
- 아직은 명확하게 결정된 건 없으니 그냥 참고만 하자. 설명은 이거 참조
- 이거 도입이 되면 진짜 미친 변경일듯 싶다.
- 객체를 primitive type 으로 실행할 수 있게 됨
- 즉 List 에 100개의 객체를 담으면 그 주소값이 아니라 데이터 자체가 메모리에 배치됨
- 이렇게 되면 쓸데없이 찾아갈 필요가 없고 CPU 캐시 적중률이 엄청 올라간다.
- 기본적으로 CPU 가 찾아온 데이터 주변 애들을 가져오기 때문에 주소로 되어 있으면 히트되지 않는 것들이 연속적으로 있어서 캐시 히트됨
- 이렇게 되면 쓸데없이 찾아갈 필요가 없고 CPU 캐시 적중률이 엄청 올라간다.
- 즉 List 에 100개의 객체를 담으면 그 주소값이 아니라 데이터 자체가 메모리에 배치됨
- 위의 Scoped Value, Structured Concurrency 가 일단 엄청 큰 성능개선
- 다만 이거는 가상 스레드에 한함
- ZGC 에 세대별 GC 개념 도입 (JDK23 부터 정식 도입)
- 기존 ZGC는 그냥 단일 세대로 힙 메모리에 더이상 메모리를 할당하기 힘든 상황이 되면 GC가 돌아갔다.
- 세대별 region 을 활용하기 때문에 CPU 사용량이 줄고 처리량이 좋아짐
- 기존 ZGC는 그냥 단일 세대로 힙 메모리에 더이상 메모리를 할당하기 힘든 상황이 되면 GC가 돌아갔다.
- Shenandoah GC 에 세대별 GC 개념 도입 (JDK25 부터 정식 도입)
- 기존 Shenandoah GC는 그냥 단일 세대로 힙 메모리에 더이상 메모리를 할당하기 힘든 상황이 되면 GC가 돌아갔다.
- 세대별 region 을 활용하기 때문에 CPU 사용량이 줄고 처리량이 좋아짐
- 기존 Shenandoah GC는 그냥 단일 세대로 힙 메모리에 더이상 메모리를 할당하기 힘든 상황이 되면 GC가 돌아갔다.
JDK25 기본 GC
기본적으로는 G1GC 를 활용
ZGC, Shenanhoad GC 에도 세대별 GC가 도입되어 이것도 활용 가능
'이론 정리 > java' 카테고리의 다른 글
간략한 Shenandoah GC 방식 설명 (0) | 2025.10.10 |
---|---|
간략한 ZGC 방식 설명 (0) | 2025.10.10 |
간략한 G1GC 방식 설명 (0) | 2025.10.10 |
간략한 Serial, Parellal GC 방식 설명 (0) | 2025.10.09 |
모던 자바 인 액션 4장 간략정리 (2) | 2024.11.26 |