자바와 JIT 컴파일러 버전/ 컴파일러 튜닝 법 정리

25gStroy·2022년 5월 31일
0

JAVA

목록 보기
4/18

JIT 세가지 버전

  • 32-bit 클라이언트 버전

  • 32-bit 서버 버전

  • 64-bit 서버 버전

64-bit 서버 컴파일러는 티어드 컴파일을 지원하기 위해 클라이언트 컴파일도 가지고 있습니다.

32bit 운영체제를 가지고 있다면 JVM도 32bit를 사용해야 합니다.

64bit 운영체제를 가지고 있다면 JVM은 32bit 또는 64bit를 사용할 수 있습니다.

만약 힙 크기가 3GB보다 작다면 32bit 버전의 자바가 더 빠르게 동작할 수 있습니다. 당연하게 메모리 참조가 32bit 이므로 64bit 보다는 비용이 싸기 때문입니다.

만약 long이나 double 같은 64bit 변수를 광범위하게 사용하는게 아니고 전체 프로세스 크기가 4GB 미만이라면 32bit 컴파일러를 사용하는게 더 성능상의 우위가 있습니다.

컴파일러는 여러개 설치하고 사용할 수 있지만 호환성을 위해 어떤 컴파일러를 사용할지 명시하는 인자는 엄격하게 따지지 않습니다.

예시로 64bit JVM을 가지고 있다면 클라이언트 컴파일러를 사용하겠다고 -client 명령어를 붙혀도 서버 컴파일러를 사용하게 됩니다.

32bit JVM을 가지고 있다면 -d64 명령어를 통해 64bit 컴파일러를 사용하겠다고 하면 에러를 낼 것 입니다.

자바 8의 경우 서버 컴파일러가 디폴트일 때 티어드 컴파일도 디폴트로 사용하게 됩니다.

컴파일러 중급 튜닝

대개 컴파일러 튜닝이란 대상 머신에 설치하기에 알맞은 JVM과 컴파일러 모드( -client, -server, -XX:+TieredCompilation)를 선택하는 일입니다.

티어드 컴파일은 보통 오래 수행되는 어플리케이션에서 가장 최선의 선택입니다. 주기가 짧은 어플리케이션에서는 클라이언트 컴파일러를 선택해 성능의 이점을 누릴 수 있습니다.

그리고 이제 추가로 튜닝이 필요한 경우가 있습니다.

코드 캐시 튜닝

JVM이 코드를 컴파일할 때 코드 캐시 내에는 어셈블리 언어 명령 세트가 들어있습니다.

이 코드 캐시는 고정 크기이며 일단 가득 차게 되면 JVM은 더 이상 코드를 추가적으로 컴파일 할 수 없습니다.

이 말은 애플리케이션의 많은 양의 부분이 인터프리터로 실행될 수 있다는 말입니다.

이와 같은 이슈는 클라이언트 컴파일러나 티어드 컴파일러를 사용할 때 발생할 수 있습니다. 서버 컴파일러의 경우에는 몇개의 소수 클래스만 컴파일 되므로 코드 캐시를 채울 일은 그다지 없습니다.

코드 캐시가 가득 차게되면 JVM은 다음과 같은 경고를 발생합니다.

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=

java 8 코드 캐시의 디폴드 값 목록

  • 32bit 클라이언트 자바 8 : 32MB
  • 티어드 컴파일을 하는 32bit 자바8 : 240MB
  • 티어드 컴파일을 하는 64bit 자바8 : 240MB

특정 애플리케이션이 필요로 하는 코드 캐시가 어느 정도인지 알아내기에 적합한 매커니즘은 따로 없습니다.

코드 캐시의 최대 크기를 -XX:ReservedCodeCacheSize=N 플래그를 통해 디폴트의 두 배 또는 네 배로 늘려보는 방법밖에 없습니다.

이런 캐시 리사이징은 백그라운드에서 일어나기 때문에 성능에는 실제로 영향을 주진 않습니다.

단 머신의 메모리성능과 비교해서 적절히 할당하는 것이 중요합니다.

컴파일 임계치

코드 컴파일을 유발하는 요소는 얼마나 자주 코드를 실행했는가 입니다. 일정한 횟수만큼 실행되고 나면 컴파일 임계치에 도달하고 컴파일러는 코드를 컴파일 하기에 충분한 정보가 쌓였다고 생각합니다.

컴파일은 JVM 내에 있는 메소드가 호출된 횟수, 메소드가 루프를 빠져 나오기까지 돈 횟수에 대한 카운터(counter) 두 개를 기반으로 합니다.

루프 자체의 끝에 이른경우 또는 continue와 같은 분기문을 만난 경우도 실행했기 때문에 루프 실행을 완료한 횟수로 생각할 수 있습니다.

JVM은 자바 메소드를 실행할 때 두 카운터의 합게를 확인하고 메소드가 컴파일 될 자격이 있는지 결정합니다.

자격이 있다면 메소드는 컴파일되기 위해 큐에서 대기합니다. 이런 컴파일을 정식 명칭은 없지만 일반 컴파일(standard compilation)이라고 합니다.

만약 정말 긴 루프가 있거나 빠져나오기 힘든 구조로 되어 있는 경우라면 어떨까요? 이 경우에 JVM은 메소드 호출을 기다리지 않고 루프를 컴파일할 필요가 있습니다.

따라서 루프의 실행이 완료될 때마다 분기 카운터가 증가하고 감지됩니다. 분기 카운터가 개별적인 임계치를 넘었다면 루프는 컴파일 대상이 됩니다.

이 종류의 컴파일은 루프가 컴파일되고 스택상의 교체(on-stack replacement, OSR)를 통해 컴파일된 루프를 실행하도록 동작합니다.

이 방식으로 인해 JVM은 스택상의 코드를 교체해 더 빠르게 실행될 수 있도록 합니다.

일반 컴파일은 -XX:CompileThreshold=N 플래그의 값에 따라 트리거됩니다.

클라이언트 컴파일러의 경우 N의 디폴트 값은 1500이고 서버 컴파일러는 10,000 입니다.
CompileThreshold 값을 더 낮게 변경한다면 더 빨리 컴파일 될 수 있습니다. 이 값은 메소드 엔트리 카운터(method entry counter)와 백 엣지 루프 카운터(back-edge loop counter)를 더한 합계로 계산됩니다.

임계치를 낮추는 이유

  1. 애플리케이션 워밍업하는데 필요한 시간을 약간 절약한다.
  2. 컴파일 임계치를 낮추지 않으면 절대로 컴파일되지 않을 메소드들이 있다.

두번째 이유에서 컴파일 임계치에 도달하지 않았기 때문이 아닙니다. 메소드와 루프가 실행될 때 마다 카운터 값이 증가하지만 시간이 지남에 따라 감소하기 때문에 컴파일 임계치에 도달하지 못합니다.

JVM은 주기적으로 세이프 포인트에 이르렀을때 각 카운터 값은 감소합니다. 이를통해 나름 사용한 메소드라고 하더라도 컴파일 되지 않을 수 있습니다. 이를 lukewarm 메소드라고 합니다.

컴파일 프로세스 점검

컴파일러의 작업에 가시성을 제공하는 jvm 플래그로는 -XX:+PrintCompilation 입니다. 디폴트로는 false 입니다.
PrintCompilation이 활성화돼있다면 메소드가 컴파일될 때마다 JVM은 방금 컴파일된 대상에 대한 정보를 한 줄씩 출력할 것입니다.

결과물은 자바 릴리즈에 따라 다릅니다.

profile
애기 개발자

0개의 댓글