티스토리 뷰
[Java] Hotspot VM의 한계(JIT, Just-In-Time 컴파일러)와 이를 극복하기 위한 GraalVM의 등장
망나니개발자 2023. 7. 11. 10:00이번에는 Hotspot VM의 한계와 이를 극복하기 위한 GraalVM에 대해 알아보도록 하겠습니다.
1. Hotspot VM과 JIT 컴파일러(Just-In-Time Compiler)
[ C 언어의 동작 방식 ]
C, C++, GoLang, Rust 등과 같은 컴파일 언어는 컴파일 과정에서 바로 기계어로 번역하고 실행 파일을 만들어낸다. 그리고 컴파일 시에 코드 최적화까지 진행하여 처리 성능이 상당히 뛰어나다. 대신 생성된 기계어가 빌드 환경(CPU 아키텍처)에 종속적이라서, 플랫폼이 바뀐다면 다시 빌드해야 하는 문제가 있다.
[ Java 언어의 동작 방식 ]
자바는 이러한 플랫폼 종속적인 문제를 해결하고자 JVM을 도입하였고, 그래서 동작 과정이 조금 다르다.
만약 우리가 자바 애플리케이션을 실행한다고 하면, 자바 코드는 먼저 바이트 코드로 컴파일된다. 바이트 코드는 자바 코드보다 간결하고 간단하지만, 0과 1만 이해할 수 있는 컴퓨터는 이를 읽어들일 수 없다. 바이트 코드는 JVM을 위한 중간 언어인 것이다. 따라서 JVM은 애플리케이션이 실행되면서 읽어들인 바이트 코드를 실시간으로 기계어로 번역하고, CPU가 번역된 기계어를 처리한다. 이러한 구조 덕분에 Java는 플랫폼에 종속되지 않게 되었다.
하지만 실행 시에 바이트 코드를 기계어로 번역하는 작업 때문에 성능이 느려졌다. 그래서 이러한 문제를 해결하고자 바이트 코드를 기계어로 컴파일하는 JIT 컴파일러를 도입하여 사용하고 있다. JIT 컴파일러의 목표는 빠른 컴파일 및 특정 환경에 맞춤화된 최적화를 제공하는 것이며, 이를 위해 실행 프로파일 정보를 활용한다.
[ JIT 컴파일러의 등장 ]
Java 1.3부터는 Hotspot VM이 추가되었고, Hotspot VM에는 2개의 JIT 컴파일러가 포함되어 있다.
- c1
- 클라이언트 컴파일러(Client Compiler)
- 코드 최적화는 덜하지만 즉시 시작되는 속도는 빠름
- 즉시 실행되는 데스크톱 애플리케이션 등에 적합함
- c2
- 서버 컴파일러(Server Compiler)
- 즉시 시작되는 속도는 느리지만 최적화는 많이 되어 warm-up 후에는 빠름
- 장기 실행되는 서버 애플리케이션 등에 적합함
Java 6에서는 c1 컴파일러와 c2 컴파일러 중 하나를 선택해야 했지만, Java 7부터는 계층형 컴파일을 사용할 수 있는 옵션이 추가되었고, Java 8부터는 이것이 JVM의 기본 동작이 되었다. 이 접근 방식은 c1 컴파일러와 c2 컴파일러를 모두 사용한다.
Hotspot VM은 초기에 인터프리터를 사용해서 최적화 없이 코드를 실행하지만, Hotspot VM은 각 메소드의 호출 여부를 계속해서 주시한다. 각 메서드의 호출 횟수를 추적하고, 호출 횟수가 C1 컴파일러의 임계값을 초과하면 해당 메소드를 C1 컴파일러 대기열에 넣고 재컴파일하여 최적화한다. 이후에도 계속 각 메서드의 호출 횟수를 추적하여 동일하게 최적화를 진행하는데, C1 이후에는 C2 컴파일러를 사용한다. 이렇듯 컴파일러를 순차적으로 적용하는 방식을 계층형 컴파일(Tiered Compliation)이라고 한다.
이때 컴파일되어 최적화된 코드는 코드 캐시라고 하는 특수 힙에 저장된다. 관련 클래스가 언로드될 때까지 또는 코드가 최적화되지 않을 때까지 코드 캐시에 저장된다. 참고로 코드 캐시는 크기가 한정되어 있으므로 공간이 부족하면 더 이상 코드를 컴파일할 수 없어 성능 문제가 발생할 수 있다.
Java HotSpot(TM) 64-Bit Server VM warning: CodeCache is full. Compiler has been disabled.
Java HotSpot(TM) 64-Bit Server VM warning: Try increasing the code cache size using -XX:ReservedCodeCacheSize=
CodeCache: size=3072Kb used=2528Kb max_used=2536Kb free=544Kb
bounds [0x00007f12efae5000, 0x00007f12efde5000, 0x00007f12efde5000]
total_blobs=989 nmethods=629 adapters=272
compilation: disabled (not enough contiguous free space left)
즉, JVM은 대상 아키텍처와 코드의 동적인 동작 방식에 대한 정보를 얻을 때까지 컴파일을 연기한다. 미리 네이티브코드로 컴파일하는 AOT(Ahead-Of-Time) 컴파일러와 다르게 동적으로 적시에 컴파일한다.
- 런타임 정보 및 통계를 고려해 최적화를 함
- 애플리케이션의 시작 시간이 느려지지만 나중에 최고 성능에 도달하게 됨
- 특정 플랫폼에 종속적이게 컴파일할 필요 없이 가상머신이 이를 해결해줌
HotSpot은 99년도에 나왔고, 오랜 연구 결과 끝에 많은 최적화가 구현되어 있다. C2 컴파일러는 극도로 최적화가 많이 되어 있어서 컴파일 언어를 능가하는 성능을 보여주기도 한다. 최적화에는 프로파일링을 통해 얻은 정보들(디바이스 정보, 클럭 수 등)이 사용되며, 자바의 성능 향상은 이러한 프로파일링을 통해 얻어낸 정보를 바탕으로 하는 JIT 컴파일러의 최적화 역할이 크다.
하지만 반대로 이로 인해 애플리케이션이 초기에 느리게 실행되는 웜업 문제가 발생하기도 한다.
[ JIT 컴파일러의 한계 ]
하지만 문제는 HotSpot VM의 C2 컴파일러가 한계에 직면했다는 것이다. C2 컴파일러는 일단 C++로 작성되어 개발자를 구하기도 어렵고, 상당히 오래된 만큼 복잡하다. 이러한 이유로 최근 몇 년 동안 개발된 중요한 최적화가 없었을 뿐만 아니라 전문가들 역시 유지보수가 어렵다고 판단하였다. 즉, C2 컴파일러가 이제 End-of-life에 직면한 것이다.
- C++ 개발자를 구하기 어려움
- 오래된 만큼 상당히 복잡함
- 최근에 중요한 최적화가 거의 없었음
그래서 최적화가 더 이상 어려운 코드들은 HotSpotInstrinsicCandidate 어노테이션을 붙이도록 하는 작업들도 진행되었다. 한계에 마주한 상황을 인정하고 애노테이션으로 명시해두는 것이다.
JIT 컴파일러는 느리지 않으며, 오히려 엄청난 최적화로 인해 컴파일된 언어보다 뛰어난 성능을 보이기도 한다. 대신 더 이상 유지보수하기 어려운 한계에 직면한 상황이다. 그래서 이에 대한 대안으로 등장한 것이 바로 GraalVM이다.
2. GraalVM의 등장과 아키텍처
[ GraalVM의 등장 ]
1999년에 HotSpot VM이 공개된 후에 이는 Oracle JVM의 상징이 되었고, C++로 개발되어 자바 성능을 크게 끌어올렸다. 그러나 이제는 해당 코드가 너무 복잡해져서 추가 개선이 사실상 불가능한 상황이라고 한다.
그래서 JIT 컴파일러들 중에서 C2 컴파일러를 대체할 수 있는 새로운 컴파일러를 만드는 것을 목표로 GraalVM이 시작되었다. GraalVM 프로젝트는 2012년에 Open JDK의 서브 프로젝트로 등록된 지 7년 후인 2019년 5월에 1.0이 공개되었다.
현재 자바는 OpenJDK가 개발을 주체하고, 각각의 회사들에서 이를 확장하여 다양한 자바 구현체들을 만든다. OpenJDK가 사실상 자바의 표준이 되었고, 새로운 자바가 공개되면 이를 따라가고 있다. 오라클은 OpenJDK를 유지보수하는 자바(Oracle JDK)와 OpenJDK를 새롭게 확장 및 변형한 자바를 관리하는데, 클라우드 환경에 맞춘 구현체 중 하나가 바로 GraalVM이라고 볼 수 있다.
앞서 설명하였듯 GraalVM은 Oracle JDK가 아닌 OpenJDK8을 기반으로 만들어졌으며, 이는 새로운 단독 프로젝트가 아닌 기존의 HotSpot VM을 개선하고, 다양한 기능을 추가한 것임을 의미한다. 그 중에서 GraalVM은 JIT 컴파일러들 중에서 기존의 c++로 작성된 C2 컴파일러인 Graal 컴파일러를 Java 기반으로 새롭게 작성하였다. 즉, 자바로 만든 자바(Java on Java) 또는 자바로 만든 자바 컴파일러와 같은 수식어로 GraalVM을 설명할 수 있다.
[ GraalVM의 아키텍처 ]
GraalVM은 크게 아래의 3가지 특징을 갖고 있다고 볼 수 있으며, 이는 GraalVM의 아키텍처를 통해 그 이유를 알 수 있다.
- 고성능 자바(High-Performance)
- 다양한 언어의 통합(Polyglot)
- Native 지원을 통한 빠른 start-up(Native Image)
GraalVM은 Java로 작성된 Advanced JIT(just-in-time) 컴파일러인 GraalVM 컴파일러(GraalVM Compiler)를 HotSpot JVM에 추가하였다. 또한 Language Implementation 프레임워크인 Truffle도 추가하였는데, 이를 통해 JVM 위에서 파이썬, 자바스크립트, 루비 등과 같은 언어를 실행할 수 있게 되었다. 이를 통해 같은 메모리 공간에서 데이터를 주고 받을 수 있는 것이다.
- 핫스팟 JVM
- Graal 컴파일러
- Truffle(Language Implementation 프레임워크)
- GraalVM 업데이터
JVM 런타임 모드(JVM Runtime Mode)
HotSpot JVM에서 프로그램을 실행할 때, GraalVM은 기본적으로 최상위 JIT 컴파일러로 GraalVM 컴파일러를 사용한다. 런타임에 애플리케이션은 JVM에 로드되고 실행된다. JVM은 Java나 다른 JVM native 언어의 바이트코드를 컴파일러에 전달하고, 컴파일러는 이를 머신코드로 컴파일하여 JVM에 돌려준다. 지원되는 언어에 대한 인터프리터는 프러플 프레임워크 위에 작성되며, 그 자체로 JVM에서 실행되는 자바 프로그램이다. 새롭게 만들어진 Graal JIT 컴파일러는 기존의 C2 컴파일러보다 성능이 더 뛰어나다.
네이티브 이미지(Native Image)
네이티브 이미지는 자바 코드를 독립형 네이티브 실행 파일 또는 네이티브 공유 라이브러리로 컴파일하는 혁신적인 기술이다. 네이티브 실행 파일을 빌드하는 동안 처리되는 Java 바이트코드에는 모든 애플리케이션 클래스, 의존성, 외부 라이브러리 및 필요한 모든 JDK 클래스가 포함된다. 생성된 self-contained 네이티브 실행 파일은 각 개별 운영 체제 및 머신의 아키텍처에 따라 다르며 JVM이 필요하지 않는다. 네이티브 이미지는 AoT 컴파일러를 통해 처리된다.
트러플 자바 (Java on Truffle)
트러플 자바는 Truffle 언어 구현 프레임워크로 구축된 JVM 스펙의 구현체이다. 모든 핵심 구성 요소를 포함하고, JRE 라이브러리와 동일한 API를 구현하며, GraalVM의 모든 JAR와 네이티브 라이브러리를 재사용하는 완전한 Java VM이다. 트러플 자바는 Polyglot API를 제공하는데, 이를 통해 런타임 시에 서로 다른 프로그래밍 언어를 결합시킬 수 있다. 즉, JVM 위에서 서로 다른 언어로 작성된 프로그램을 실행시킬 수 있는 것이다.
[ OpenJDK와 GraalVM ]
가장 먼저 Open JDK9에 JEP 243에 따라 JVMCI(자바 컴파일러를 위한 인터페이스)가 추가되었다. 그리고 JEP 295에 따라 Open JDK 9에는 GraalVM의 AoT 컴파일러가 추가되었고, 이후에 Open JDK 10에서는 JEP 317에 따라 Graal JIT 컴파일러가 추가되었다.
하지만 Open JDK 17부터는 Graal JIT 컴파일러와 AoT가 빠지게 되었고, JVMCI만이 그대로 남았다.
이는 Graal이 더 이상 불필요해졌기 때문이 아니다. 대부분의 GraalVM 사용자들이 GraalVM을 직접 설치해서 사용하지, OpenJDK에 내장된 기능을 활용하지 않기 때문이다. GraalVM은 앞으로도 계속해서 발전할 예정이다.
이 중에서 가장 주목받는 부분은 네이티브 이미지(Native Image)이다. 위의 설명에서는 추상적인 내용 만을 작성해뒀으므로, 네이티브 이미지와 관련된 자세한 내용은 다음 포스팅에서 살펴보도록 하자.
관련 포스팅
- Hotspot VM의 한계와 이를 극복하기 위한 GraalVM의 등장
- GraalVM이 제공하는 네이티브 이미지(Native Image)
참고 내용
- https://www.graalvm.org/latest/docs/introduction/
- https://www.graalvm.org/latest/reference-manual/java/compiler/
- https://github.com/oracle/graal/tree/master/compiler
- https://www.graalvm.org/latest/reference-manual/java-on-truffle/
- http://taewan.kim/post/position_of_graalvm/
- https://youtu.be/juTEZZ3v4Ws
- https://www.youtube.com/watch?v=pR5NDkIZBOA
- https://www.baeldung.com/graal-java-jit-compiler
- https://docs.spring.io/spring-boot/docs/current/reference/html/native-image.html
- https://www.graalvm.org/22.1/reference-manual/native-image/Limitations/#reflection
- https://wiki.openjdk.org/display/HotSpot/PerformanceTacticIndex
'Java & Kotlin' 카테고리의 다른 글
[Java] Java 21에 추가될 새로운 기능들(Java 21 Features) (22) | 2023.09.19 |
---|---|
[Java] GraalVM이 제공하는 네이티브 이미지(Native Image) (4) | 2023.07.18 |
[Java] 자바의 컨테이너 환경을 위한 XX:+UseContainerSupport 옵션 (0) | 2023.05.23 |
[Java] JUnit의 진화 과정과 public 접근 제어자 (2) | 2023.03.07 |
[Java] Integer.valueOf(127) == Integer.valueOf(127)가 True인 이유, Integer 캐시 (6) | 2022.11.08 |