Reboot JAVA = GraalVM

1995년 JAVA 발표되고 이제 햇수로 27년이 되었다. 사람 나이로 27살이면 이제 청년이다. 여러 버전의 JAVA가 출시됐고 계속해서 새로운 기술이 추가되고 있고 있지만 어느 순간 정체되었다는 느낌을 떨칠 수가 없다. 한창이어야 할 청년이 급 늙어 버린 것 같은 느낌이다. 다이내믹하게 변하고 있는 NodeJS가 나온 JavaScript 진영이나 네이티브 개발에 쓰이는 GO, 하물며 JVM에서 돌아가는 Scala 같은 언어들에 비해 점점 죽어가는 느낌이다. 이런 상황에 변화를 가져올 것 같은 하나의 플랫폼을 오늘 소개하고자 한다.

느린 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만 있으면 한번 만들어진 ByteCode를 어디서든 이론적으로 실행 가능하다. 개발자 입장에서 하나의 프로그램만 만들면 Windows이든 Linux이던 핸드폰이든 모두 실행 가능한 것이다. 동일 플랫폼이라고 해도 버전과 사용자의 환경 차이로 인해서 개발단계에서 엄청난 예외 조건을 설정하거나 컴파일 시 수많은 옵션을 넣어서 별도의 바이너리를 배포했어야 했으나, 이론상으로 JVM만 정상적으로 동작한다면 아무 문제 없이 하나의 실행파일로 모든 플랫폼에서 실행 가능한 세상이 온 것이다.
이런 JAVA는 크로스플랫폼과 다양한 기능을 지원하기 위해서 ByteCode를 JVM내 내장된 interpreter가 한 줄 한 줄 번역하고 번역된 명령을 실행하는 구조로 만들어져야만 했다. 기본적으로 interpreter는 많은 이점이 있지만 느리다. JAVA 역시 interpreter를 사용함에 이 단점을 벗어날 수 없는 것이다.


<JAVA 소스 파일 실행 구조>


<JAVA 1.6 reaches ~95% performance of C++ Fiehn Lab - JAVA vs C++ Benchmark>

JAVA 속도 개선을 위한 노력 시작 HotSpot

JAVA가 만들어지고 얼마 되지 않아 여러 업체가 JAVA의 강점을 알아보고 JAVA 시장에 뛰어들었다. 각자 JAVA 표준에 맞추어 JDK와 JRE를 만들어 냈다. 우리가 잘 아는 IBM도 그중 하나이며, Longview Technologies LLC라는 듣도 보도 못한 회사가 Hotspot JVM라는 JVM를 발표한다. 이 회사는 다시 SUN 인수되고 JAVA 1.3부터 기본 JVM으로 사용된다. 이때부터 JAVA 성능은 급속도로 개선되기 시작되었으며 1.6 버전이 나오면서 많은 도약을 이루었다. JAVA 성능 개선은 GC의 개선이나 기타 영역도 있겠지만 오늘은 Hotspot JVM 개선만 보고자 한다.

Hotspot

Hotspot은 단어는 Hotspot JVM의 동작 방식에서 따온 것이다. 코드에서 많은 부하를 발생하거나 자주 실행되는 HotCode가 몰려있는 Hotspot을 최적화하기에 붙여진 이름이다.
기본 동작 원리를 간단히 알아보면 아래와 같다.
먼저 코드 전체를 읽어 불필요하고 개선할 수 있는 부분은 개선한다. 그리고 코드를 실행 후 실행 과정에서 개선점을 계속 모니터링하고 다시 코드를 수정한다. 이렇게 인터프린터의 매번 코드를 번역한다는 약점을 이용해서 그때그때 최적화된 코드 변경해가며 계속 최적화 코드를 찾아가는 것이다.
예를 들면 사용하지 않는 코드를 삭제하거나 아주 작은 크기에 Method가 자주 호출될 경우 Method 코드 자체를 실행 코드에 삽입시켜 스택이 생성되는 것을 없앤다든지 하는 식으로 소스코드 자체를 최적화한다.
그리고 자주 사용되는 코드를 미리 컴파일해서 인터프린팅 시 매번 번역하지 않고 캐시 된 번역 코드를 사용하게 한다.
Hotspot JVM은 인터프린트의 강점과 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 컴파일된 프로그램보다 느릴까?

우리는 여기서 잠깐 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
2
3
4
5
6
/**
 * may be (but is not guaranteed to be) intrinsified by the HotSpot VM. 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. The {@code @IntrinsicCandidate} annotation is internal to the
 ...
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 JAVA가 HotSpot VM을 도입하고 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