Reboot JAVA == GraalVM
1995년 JAVA 발표되고 이제 햇수로 27년이 되었다. 사람 나이로 27살이면 이제 청년이다. 여러 버전의 JAVA가 출시됐고 계속해서 새로운 기술이 추가되고 있지만 태생적인 조건때문에 정체되었다는 느낌을 떨칠 수가 없다. 한창이어야 할 청년이 이것 저것 트집을 잡는 꼰대가 되어 버린 느낌이다. 다이내믹하게 변하고 있는 NodeJS가 나온 JavaScript 진영이나 네이티브 개발에 요즘 많이 쓰이는 Go나 Rust, 하물며 JVM에서 돌아가는 Scala/Kotlin 같은 언어들에 비해 점점 죽어가는 느낌이다. 이런 상황에 JAVA 진영에서 변화의 시도로 보이는, 발표된지 조금 시간이 흘렸지만 많이 알려지지 않은 플랫폼 하나를 오늘 소개하고자 한다.
느린 JAVA.
과거 JAVA로 만들어진 프로그램들은 느렸다. 컴파일러로 컴파일된 바이너리 프로그램들보다 느렸고 항상 이 부분이 공격 대상이 되었다. 이에 JAVA 내부적으로 많은 개선 작업이 이루졌고, 하드웨어가 기하급수적으로 발전함에 따라 더 많은 CPU와 더 많은 메모리를 사용할 수 있게 됨에따라 멀티 쓰레드/분산 프로그램 개발이 용의한 JAVA의 강점을 더 부각되면서 JAVA 느린 부분이 더 이상 공격 대상이 되지 않게 되었다. 초기 JAVA가 나왔을 때 JAVA가 대용량 데이터 처리에 쓰인다고 이야기하면 아마 미친놈이라고 놀림당했을 것이지만 이제는 그런가 보다 하고 넘어가는 수준까지 왔다. 하물며 DB도 JAVA로 만들고 있지만 JAVA로 작성됐는지도 모르고 사용하는 세상이 와버렸다(예>Apache Cassandra).
JAVA는 왜 느릴까?
빠른 AOT
일반적으로 C/C++ 같이 소스 코드를 컴파일해서 실행 파일을 만드는 것을 AOT (Ahead-Of-Time) Compiling이라고 한다.
AOT 컴파일러는 소스코드를 전처리하고 컴파일해서 어셈블리 코드를 만들어 낸다. 다시 실행 환경의 하드웨어 어셈블러가 목적 코드를 만들어 낸다. 마지막으로 소스코드에 사용된 라이브러리와 기타 코드를 Linker가 모든 코드를 뭉쳐서 실행 가능한 파일이 만들어내다. 이 일련의 과정은 많은 과정과 많은(?) 시간이 필요하지만 컴파일 과정에 해당 플랫폼에 최적화된 목적코드를 만들어낸다. 이 목적코드는 실행단계에서는 바로 운영체계 레이어 위에서 실행되기 때문에 빠르게 로딩되고 실행 과정에 별도의 처리 과정이 없기에 성능적인 손실도 없다.
예전에 젠투 리눅스라는 미친(?) 리눅스가 있었다. 젠투 리눅스를 설치 과정에서 리눅스 소스 코드를 하나하나 컴파일해서 운영체계를 설치하게 된다. 본인의 하드웨어 플랫폼에 최적화라는 이름 아래 개발자들에게 큰(?) 호응을 받았다. 보통 설치에 하루 이틀 걸린다. 하루 이틀을 날려도 최적화와 성능이라는 이름 아래 사용하는 이들이 많았다. 그만큼 개발자라는 집단에서는 얼마나 속도에 광신하는지 알수 있는 하나의 예이고 그것에 부합하는 것이 AOT 컴파일러로 컴파일된 프로그램이다.
<AOT 컴파일러를 사용한 프로그램 실행 단계>
Interpret
하드웨어 플랫폼에 최적화된 목적 코드를 미리 만들어 두는 AOT 컴파일러와 다르게 Interpret는 소스코드를 Row 단위로 해석(Interpret)하며 실행하여 프로그램을 구동한다. Interpret는 고레벨 언어를 중간 코드(intermediate code)로 변환하고 이를 각 행마다 해석하고 목적 코드를 만들어 낸다.
이런 Interpret는 언어 자체를 설계하기 쉽고, 매번 컴파일하는 게 아니기에 수정 후 쉽게 실행 가능하다(매우 큰 소스를 가지고 프로그램이 있을 경우 단 한 줄 코드라도 수정 될 경우, AOT는 전체를 다시 컴파일해야 수정사항이 반영된 프로그램을 실행 할 수 있음. Interpret 수정 후 바로 실행하면 됨).
반면에 소스 코드를 한 줄씩 기계어로 번역하는 방식이기 때문에 실행 속도는 AOT 컴파일러로 컴파일된 프로그램보다 느리다.
JAVA 속 Interpret
JAVA를 개발한 ‘제임스 고슬링’은 JAVA의 개발할 당시 개발 패러다임으로 “Write Once, Run Anywhere”를 주창하였다. 한번 작성된 코드를 어디에서나 실행할 수 있게 만들겠다는 것을 목표로 한것이다. 초기 개발 목적이 다양한 가전제품에 탑재되는 임베디드 소프트웨어를 제작할 수 있는 언어 만드는것이었기에 이 패러다임은 필수적이었다. JAVA의 “Write Once, Run Anywhere” 패러다임은 JAVA가 만들어지고 실제로 매우 큰 강점이 되었다. 하지만 언제는 하나를 얻으면 하나를 잃는 것이 세상의 이치인 것 같다. JAVA가 크로스 플랫폼을 지원하고 코드와 하드웨어의 종속을 없애기 위해서 JVM이라는 가상머신에서 Interpret를 사용하여 코드를 해석해서 실행하는 구조를 가지게 되었고, 느린 JAVA가 되었다.
JAVA의 JVM
JAVA는 크로스플랫폼을 지원하기 위해서 JVM이라는 가상머신이라는 아키텍처를 도입하였다. 요즘은 알게 모르게 많이 사용하는 기술이지만 그 당시에는 획기적인 구조였다. 소스 코드를 먼저 ByteCode라는 중간 코드로 컴파일하고 이 ByteCode를 JVM(실행하는 하드웨어 플랫폼에 종속적인 실행기)에서 실행하는 방식을 도입하였다. 이 방식은 플랫폼을 지원하는 JVM Runtime만 있으면 한번 만들어진 ByteCode를 어디서든 이론적으로 실행 가능하다. 개발자 입장에서 하나의 프로그램만 만들면 Windows이든 Linux이던 핸드폰이든 모두 실행 가능하다는 것이다. 기존에는 동일 플랫폼이라고 해도 버전과 사용자의 환경 차이로 인해서 개발단계에서 엄청난 예외 조건을 처리해야하고 컴파일 시 수많은 옵션을 넣어서 별도의 바이너리를 배포해야 했으나, 이론상으로 JVM에서만 정상적으로 동작한다면 아무 문제 없이 하나의 실행파일로 모든 플랫폼에서 실행 가능한 세상이 온 것이다.
그래서 JAVA는 크로스플랫폼과 다양한 기능을 지원하기 위해서 ByteCode를 JVM내 내장된 interpreter가 한 줄 한 줄 번역하고 번역된 명령을 실행되는 구조로 동작하게되었고 interpreter의 많은 이점을 가져가지만 이 단점인 느리다는 문제를 벗어날 수 없는 것이다.
<JAVA 소스 파일 실행 구조>
<JAVA 1.6 reaches ~95% performance of C++ Fiehn Lab - JAVA vs C++ Benchmark>
JAVA 속도 개선을 위한 노력 시작 HotSpot
JAVA가 만들어지고 얼마 되지 않아 여러 업체가 JAVA의 비전을 알아보고 JAVA 시장에 뛰어들었다.
; JAVA를 개발한 회사에서 만드는거지 아무나 만들수 있어 하는 의문이 들겠지만 JAVA는 구현체와 표준이 분리되어 있는 구조이다.
; Java Community Process(JCP)라는 표준 스펙만 지키면 JAVA 개발 환경과 런타임 환경을 만들고 인증 받을수 있다
우리가 잘 아는 IBM도 그중 하나 회사고, Longview Technologies LLC라는 듣도 보도 못한 회사도 Hotspot JVM라는 JVM을 가지고 JAVA 시장에 도전장을 던지게 된다. 이 회사는 이후 SUN 인수되고 업체가 개발한 JVM 개발 기술들은 JAVA 1.3 JVM에 흡수 되었다. 이때부터 JAVA 성능은 급속도로 개선되기 시작했으며 1.6 버전이 나오면서 많은 도약을 이루게 된다.
JAVA 성능 개선은 GC의 개선이나 기타 영역도 있겠지만 오늘은 Hotspot JVM 개선만 보고자 한다.
Hotspot
Hotspot은 단어는 Hotspot JVM의 동작 방식에서 따온 것이다. 코드에서 많은 부하를 발생하거나 자주 실행되는 HotCode가 몰려있는 Hotspot을 최적화하기에 붙여진 이름이다.
기본 동작 원리를 간단히 알아보면 아래와 같다.
- 먼저 코드 전체를 읽어 불필요하고 개선할 수 있는 부분은 1차 개선한다.
- 그리고 코드를 실행 후 실행 과정에서 개선점을 계속 모니터링한다.
- 모니터링 결과를 바탕으로 다시 코드를 수정하고 다시 2번 과정을 반복한다.
이렇게 Interpreter의 매번 코드를 번역한다는 약점을 이용해서 그때 그때 최적화된 코드로 실행 코드를 변경해가며 계속 최적화 코드를 찾아가는 것이다. 예를 들면 사용하지 않는 코드를 삭제하거나 아주 작은 크기에 Method가 자주 호출될 경우 Method 코드 자체를 실행 코드에 삽입시켜 스택이 생성되는 것을 없앤다든지 하는 식으로 최적화가 진행된다. 그리고 자주 사용되는 코드를 미리 컴파일해서 인터프린팅 시 매번 번역하지 않고 캐시 된 번역 코드를 사용하기도 한다.
Hotspot JVM은 Interpreter의 강점과 AOP 컴파일러의 강점을 조금씩 차용하여 하이브리드 컴파일러로 JVM을 변화시켰다. 이 부분을 담당하는 것이 HotSpot JVM에서 JIT(just-in-time compilation) 컴파일러이다. 요즘 하이레벨 언어들은 거의 모두 지원한다고 봐도 무리가 아닌 기술이지만 당시에는 획기적인 기술이었다.
자세한 JAVA JIT 내용을 알고 싶으면 아래 링크 문서를 참고하길 바란다.
http://www.ittc.ku.edu/~kulkarni/teaching/EECS768/19-Spring/Idhaya_Elango_JIT.pdf
<JAVA Hotspot Java 코드 실행 구조>
<JAVA Hotspot JVM JIT 최적화 프로세스>
Dynamic Compiler(JIT)는 AOP 컴파일된 프로그램보다 느릴까?
우리는 여기서 잠깐 HotSpot에서 사용한 Dynamic Compiler(JIT)로 구동하는 프로그램과 AOP로 만들어진 프로그램의 성능에 대해서 생각해보자. 대부분의 사람들이 이 두 컴파일러와 실행 프로그램의 성능은 AOP 쪽의 우월할 것이라고 생각할 것이다. 하지만 과연 그럴까?
Python 진영에는 Pypy(PyPy)라는 프로젝트가 있다. 전통적으로 Python의 기본 런타임 환경은 CPython이 사용되어 왔다. CPython이라는 이름에서 알 수 있듯이 C로 만들어졌다. 언어의 실행 속도를 좌우하는 부분이니 C로 작성하여 최대한의 성능을 끌어내기 위함이다. 하지만 고정된 기계 코드의 CPython을 버리고, Python JIT compiler로 Python Runtimer을 만들려는 시도가 이루어지고 있다. 몇천 번의 실행 과정을 거치면서 최적화 코드를 계속 만들어왔고, 근래에 들어서 CPython 보다 더 나은 성능을 발휘하는 결과물을 만들어 냈다.
<PyPy is 4.2 times faster than CPython>
JIT Compiler의 최적화 기능이 얼마나 대단한지 보여주는 예인 것이다.
그럼 다시 한번 묻는다. JIT 컴파일 프로그램이 AOP 프로그램보다 느린가? 실행시간의 실행환경을 적극 반영하고 계속적으로 최적화를 해나가는 Dynamic Compiler(JIT) 프로그램이 AOP 프로그램보다 느릴까? 답은 ‘아닐 수도 있다’ 일 것 같다.
그래서 조금 일찍(2005년)이 JAVA가 아직 Sun Microsystems의 소유일 때 새로운 도전을 시작한다.
Reboot JVM, GraalVM
글쓴이가 어릴 때 PC 잡지에서 “C로 개발된 C, 닭이 먼저인가 계란이 먼저인가?” 제목의 기사를 본적 있다. 해당글의 주 화두는 ‘C는 무슨 언어로 만들었을까?’에 대한 질문이었다. 정답은 최초에는 아니지만 C언어가 처음 만들어지고 난 이후부터는 ‘C언어로 C언어를 개발하고 있다’였다. 그럼 JAVA의 컴파일러와 JVM은 어떤 언어로 만들어졌을까? 위 C언어의 예를 생각하면 JAVA가 답이겠지만 현실적으로 느린 JAVA 특성상 JAVA 작성하기는 역부족였다. JAVA를 실행하는 런타임을 JAVA로 만들 경우 더 느려질 것이 뻔하기에 최대한의 퍼포먼스를 뽑아내기 위해서 C++로 만들어졌다.
JVM에 관심이 있으면 아래 링크 소스를 분석(?)해보길 바랍니다.
http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/prims/jvm.cpp
그리고 관련해서 JDK 소스 중에 아래와 같은 주석을 찾아 볼 수 있다.
https://github.com/openjdk/jdk/blob/739769c8fc4b496f08a92225a12d07414537b6c0/src/java.base/share/classes/jdk/internal/vm/annotation/IntrinsicCandidate.java
1 | /** |
A method is intrinsified if the HotSpot VM replaces the annotated method
with hand-written assembly and/or hand-written compiler IR - a compiler
intrinsic - to improve performance.
결론은 성능개선을 위해서는 어셈블 코드를 작성하라는 것이다.
Oracle이 Sun을 인수하고 새로운 JAVA 버전이 나올 때마다 성능 개선을 이루었다. 하지만 버전이 올라갈 때마다 성능개선 수치는 점점 정채 되어 갔고, 성능개선을 위해서는 “어셈블 코드로 작성하라”라고 할 정도는 더 이상 개선할 수 없는 수준까지 갔다. 그리고 개발자 고용 시장에서 코드 복잡도가 올라갈 수 있는만큼 올라간 코드를 개발/유지 보수할 C++ 개발자를 수급하기란 여간 어려운게 아니였을 것이다.
이런 문제점을 타파하기 위해서 2005년에 썬 마이크로시스템즈는 기존에 C++로 만들어진 JVM을 JAVA로 재 작성하는 Maxine라는 가상머신 프로젝트를 시작하게 되었다. 그리고 그 결실이 GraalVM이다.
([HotSpot JVM] - C2) + JVMCI + Graal JIT = GraalVM
GraalVM은 오라클에서 오픈소스 라이선스와 상업 라이선스로 진행하고 있는 최신 개발 트렌드를 반영한 JAVA로 만든 JVM을 표방하고 있는 JVM이다. 기타 많은 기능을 제공하지만 이번 글에서 우리는 JIT 부분만 초점을 맞추어 알아보아보자.
<GraalVM Architecture : https://www.graalvm.org/22.0/docs/getting-started/>
([HotSpot JVM] - C2)
일반적으로 개발자는 HotSpot JVM의 JIT의 성능이나 이슈를 접할 경우가 거의 없다. 대신 JAVA 프로그램을 CLI로 실행해본 사람이면 아래와 같은 명령을 본 적은 있을 것이다.
java -client agent.jar
java -server daemon.jar
구글링 해보면 클라이언트 프로그램을 실행할 때는 “-client” , 서버 프로그램을 실행할 때는 “-server”라는 JVM 옵션을 사용하라고 되어있다.
이 부분은 HotSpot JVM JIT을 구현 방법 때문에 생긴 옵션이다. HotSpot JVM에는 JIT Compiler의 두 개의 구현체를 들어 있다.
C1
- 서버 컴파일러보다 먼저 컴파일을 시작한다. 적극성이 높다고 볼 수 있다.
- 최적화를 위한 대기시간이 짧다.
- 코드 분석과 컴파일 시간이 서버 컴파일러보다 빠르다.
- 메모리도 더 적게 사용할 수 있다.
- 하지만 코드 실행은 서버가 더 빠르다.
C2
- 컴파일 전에 많은 정보를 수집하여 최적화에 중점을 둔다.
- 서버 컴파일러는 절대로 모든 코드를 컴파일하지 않는다.
- 최적화된 이후는 속도 개선이 있다.
GraalVM은 기존 HotSpot JVM의 C2 JIT를 Graal JIT로 대체한 JVM이다.
단순이 JIT만 바뀐 게 아니라 JIT Compiler Interface인 JVMCI를 추가하여 JIT를 쉽게 교체 가능하게 길을 열어 두었다. Graal JIT만 고집하는 게 아니라 별도의 다른 JIT로도 향후 변경 가능한 것이다.
GraalVM은 OpenJDK8/11 버전을 베이스로 만들어졌다. 표면상으로 JIT가 변경된 부분임에 기존 JAVA 버전과 호환된다.
당연히 성능도 아래 그림과 같이 개선되었다.
<OpenJDK 8/11 vs. GraalVM 20 vs. Amazon Corretto JVM Benchmarks - https://www.phoronix.com/scan.php?page=article&item=openjdk-corretto-graalvm&num=2>
이런 GraalVM에 진심인 업체가 있으니 Twitter이다. GraalVM 배포 초창기부터 서비스 솔루션에 도입하여 사용해왔다.
<Performance tuning Twitter services with GraalVM and Machine Learning (Chris Thalinger, USA) - https://www.youtube.com/watch?v=3lukwqAkz9>
그리고 GraalVM 개발 멤버로 계속 참여하고 있다.
최근 FaceBook 역시 Spark 성능개선을 위해서 사용하는 것으로 알려지고 있다.
더 이상 연구 목적이 아닌 실무에서 사용할 수 있는 혁신적인 제품인 것이다.
마무리
본인만 그런지 모르지만 JAVA에 새로운 GC가 탑재되었을때 쾌자를 부른적이 있었다. 왜냐하면 자바 서비스 운영 시 많은 장애 포인트 중 GC가 있었고, “Stop The World”가 있었고, “OOM”이 있었기 때문이다. 성능은 역시 GC 때문에 CPU가 사용량이 증가하지 않으면 별로 신경 쓰지 않았다. HotSpot VM 따위는 그냥 기본으로 존재하는 공기 같은 것이었다.
그리고 언제부터인가 JAVA 개발자들은 새로운 버전의 JAVA 버전에 무관심해졌다. 그만큼 JAVA가 안정화되었고 완성도가 높아졌다는 증거 일 것이다.
외부적으로도 JAVA 언어 자체가 개발 트렌드를 쫓아가지 못해서 많은 개발자들이 이탈하고 있고 점점 개발자 수도 전 세계적으로 줄어들고 있는 상황이다. 늙어가고 있는 것이다.
이런 상황에서 GraalVM은 새로운 새로운 변화를 가져올 ‘성배’일 수도 있을 것 같다. 이 글에서는 성능에 맞추어 JIT만 보았지만 JIT는 GraalVM의 시작점일 뿐이며 많은 다양한 기능을 품은 선물상자 같은 것이다.
오늘 한번 GraalVM에 도전해보자.
참고 사이트
GraalVM
https://blogs.oracle.com/javakr/post/graalvm-20-3
Fiehn Lab - JAVA vs C++ Benchmark
The JVM Architecture Explained - DZone Java
Understanding JVM Architecture. Understanding JVM architecture and how… | by Thilina Ashen Gamage | Platform Engineer | Medium
https://faun.pub/episode-1-the-evolution-java-jit-hotspot-c2-compilers-building-super-optimum-containers-f0db19e6f19a
Cloud Native Java:GraalVM (@Oracle Developer Meetup)
http://www.ittc.ku.edu/~kulkarni/teaching/EECS768/19-Spring/Idhaya_Elango_JIT.pdf
jdk/IntrinsicCandidate.java at 739769c8fc4b496f08a92225a12d07414537b6c0 · openjdk/jdk · GitHub
Performance tuning Twitter services with GraalVM and Machine Learning (Chris Thalinger, USA)
https://en.wikipedia.org/wiki/Maxine_Virtual_Machine