9. JVM의 코드 실행
JVM이 제공하는 가장 중요한 서비스는 메모리 관리와 사용하기 쉬운 애플리케이션 코드 실행 컨테이너입니다.
이 장에서는 바이트코드 해석을 알아보고 인터프리터와 핫스팟의 차이점에 대해 알아봅니다. 그리고 프로파일 기반 최적화의 기초 개념을 다루고 코드 캐시 및 핫스팟 컴파일 서비시스템의 기본적인 내용을 다룹니다.
#
바이트코드 해석JVM 인터프리터는 일종의 스택 머신처럼 작동하므로 물리적 CPU와는 달리 계산 결과를 보관하는 레지스터리는 없습니다. 대신 작업할 값은 모두 평가 스택에 놓고 스택 머신 명령어로 스택 최상단에 위치한 값을 모두 평가 스택에 놓고 스택 머신 명령어로 스택 최상단에 위치한 값을 변환하는 식으로 작동합니다.
JVM은 다음 세 공간에 주로 데이터를 담아 높습니다.
- 평가 스택 : 메서드별로 하나씩 생성됩니다.
- 로컬 변수 : 결과를 임시 저장합니다.
- 객체 힙 : 메서드끼리, 스레드끼리 공유됩니다.
#
JVM 바이트코드 개요자바는 처음부터 이식성을 염두에 두고 설계된 언어입니다. 따라서 big endian, little endian 하드웨어 아키텍처 모두 바이트 코드 변경없이 실행 가능하도록 명세에 규정되어 있습니다.
다음과 같은 명령어가 있습니다.
- 로드/스토어 카테고리
- load, store, ldc, const, pop, dup, getField, putField
- 산술 카테고리
- add, sub, div, mul
- 흐름 제어 카테고리
- if, goto
- 메서드 호출 카테고리
- invokevirtual, invokespecial, ...
- 플랫폼 카테고리
- new, newarray, anewarray, ...
#
단순 인터프리터#
핫스팟에 특정한 내용핫스팟
- 상용 제품급 JVM이며 완전한 구현체
- 인터프리팆드 모드에서 빠르게 실행될 수 있는 여러 고급 확장 기능을 가지고 있음
참고 코드
#
AOT와 JIT 컴파일#
AOT 컴파일- C/C++에서 사용하는 방법
- 프로그램 소스 코드를 외부 프로그램에 넣고 바로 실행 가능한 기계어를 뽑아내는 과정
- 소스코드를 AOT 컴파일하면 어떤 식으로 최적화할 기회는 단 한번입니다.
- CPU 기능을 최대한 활용하지 못하는 경우가 많고 성능향상의 숙제가 있습니다.
#
JIT 컴파일- 런타임에 프로그램을 고도로 최적화한 기계어로 변환하는 기법
- 대부분의 상용 JVM이 이방식으로 작동됩니다
- 런타임 실행 정보를 수집해서 어느 부분이 자주 쓰이고, 어느 부분을 최적화해야 가장 효과가 좋은지 결정합니다.
- 이를 프로파일 기반 최적화(PGO, Profile-guided optimization)
- JIT 서브시스템은 실행 프로그램과 VM리소스를 공유하므로 프로파일링 및 최적화 비용 및 성능 향상 기대치 사이의 균형을 맞춰야합니다.
- 바이트코드를 네이티브 코드로 컴파일하는 비용은 런타임에 지불됩니다.
- 핫스팟은 프로파일링 정보를 보관하지 않고 VM이 꺼지면 일체 폐기됩니다.
#
AOT 컴파일 vs JIT 컴파일AOT 컴파일
- 이해하기 쉬움
- 최적화 결정에 대한 정보가 없어서 런타임 정보를 포기하는 만큼 장점이 상쇄됨
- 특정 프로세서에서만 사용 가능한 실행 코드가 만들어짐
- 확장성에서 문제가 있습니다.
JIT 컴파일
- 새로 릴리즈마다 프로세스 기능에 최적화 코드를 추가할 수 있습니다.
- 신기능을 활용할 수 있습니다.
자바 9부터 핫스팟 VM은 JDK 코어 클래스를 대상으로 AOT 컴파일 옵션을 제공합니다.
#
핫스팟 JIT 기초- 핫스팟의 기본 컴파일 단위는 전체 메서드입입니다.
- 한 메서드에 해당하는 바이트 코드는 한꺼번에 네이티브 코드로 컴파일됩니다.
- 핫스팟은 핫 루프를 온-스택 치환(OSR, on-stack replacement)라는 기법을 이용해 컴파일 기능을 제공합니다.
#
klass 워드, vtable, 포인트 스위즐링- 핫스팟은 멀티스레드 C++ 애플리케이션
- JIT 컴파일 서브시스템을 구성하는 스레드는 핫스팟 내부에서 가장 중요한 스레드
하나의 메서드를 단순 컴파일하는 과정
#
JIT 컴파일 로깅성능 엔지니어라면 다음 JVM 스위치를 반드시 기억해야합니다.
-XX:+PrintCompilation
#
핫스팟 내부의 컴파일러- 핫스팟 JVM에는 C1, C2라는 두 JIT 컴파일러가 있습니다.
- C1, C2 컴파일러 모두 핵심 측정값, 즉 메서드 호출 횟수에 따라 컴파일이 트리거링됩니다.
- 컴파일 프로세스는 가장 먼저 메서드의 내부 표현형을 생성한 다음 인터프리티드 단계에서 수집한 프로파일링 정보를 바탕으로 최적화 로직을 적용합니다.
- 같은 코드라도 C1과 C2가 생성한 내부 표현형은 전혀 다릅니다. (일반적으로 C1이 C2보다 컴파일 시간도 더 짧고 단순합니다.)
#
핫스팟의 단계별 컴파일자바 6부터 JVM은 단계별 컴파일 모드를 지원합니다.
- 레벨 0 : 인터프리터
- 레벨 1 : C1 - 풀 최적화
- 레벨 2 : C1 - 호출 카운터 + 백엣지 카운터
- 레벨 3 : C1 - 풀 프로파일링
- 레벨 4 : C2
#
코드 캐시JIT 컴파일드 코드는 코드 캐시라는 메모리 영역에 저장됩니다. 이곳에는 인터프리터 부속 등 VM 자체 네이티브 코드가 들어있습니다.
코드 캐시는 미할당 영역과 프리 블록 연결 리스트를 담은 힙으로 구현됩니다.
다음의 경우, 네잍티브 코드가 코드 캐시에서 제거됩니다.
- 역최적화될 때
- 다른 컴파일 버전으로 교체됐을 때
- 메서드를 지닌 클래스가 언로딩될 때
풀어설명하면, 추측해서 코드를 최적화했는데 그렇지 않은 경우에 최적화 이전의 형태로 되돌려놓는 행위입니다.
#
단편화C1 컴파일러를 거친 중간 단계의 컴파일드 코드가 C2 컴파일로 치환된 후 삭제되는 일이 잦아지면 코드 캐시는 단편화되기 쉽습니다.
#
간단한 JIT 튜닝법코드 튜닝 시 애플리케이션이 JIT 컴파일을 십분 활용하도록 만드는 것은 그리 어렵지 않습니다.
단순 JIT 튜닝의 대원칙은 간단합니다. 이는 컴파일을 원하는 메서드에게 아낌없이 리소스를 베풉니다.
다음의 항목을 점검합니다.
- 먼저 PrintCompilation 스위치를 켜고 애플리케이션을 실행합니다.
- 어느 메서드가 컴파일됐는지 기록된 로그를 수집합니다.
- ReservedCodeCacheSize를 통해 코드 캐시를 늘립니다.
- 애플리케이션을 재실행합니다.
- 확장된 캐시에서 컴파일드 메서드를 살펴봅니다.
이를 명심하면 두가지 사실을 알 수 있습니다.
- 캐시 크기를 늘리면 컴파일드 메서드 규모가 유의미한 방향으로 커지는가.
- 주요 트랜잭션 경로상에 위치한 주요 메서드가 모두 컴파일되고 있는가.
위 두개를 고려하면서 JIT를 튜닝하는 것이 중요합니다.