본문 바로가기
JAVA/[JAVA] 바구니

Increasing Code Cache

by oncerun 2023. 2. 20.
반응형

Increasing the Code Cache Size

 

tier 4로 코드를 컴파일하는 경우 해당 코드가 이제 Code Cache에 추가되는 것을 안다.

다만 Code Cache의 크기는 제한적이라는 문제가 하나 있다.

 

Julio Falbo에 따르면 많은 양의 메서드가 tier 4에 추가되는 경우, 다음 코드를 위해서 몇 개의 코드는 Code Cache에서 제거된다는 것이다. 제거된 코드블록은 다시 다른 코드블록을 제거하고 다시 추가되는 반복적인 상황이 발생한다.

( 해당 글에는 code cache size가 가득 찬 경우 코드블록이 추가될 때 어떤 원리나 순서로 삭제된다는 설명글이 없다. )

 

다른 말로 하면 거대한 애플리케이션을 운영하면서 level 4로 컴파일된 코드블록들은 지속적으로 code cache에 추가되고 삭제되는 행위를 반복한다고 설명한다.

 

code cache가 가득 찼는지 어떻게 확인할 수 있는지 방법을 알려준다.

 

아마 code cahe가 가득 찬 경우 console log로 다음과 같은 로그가 발생한다고 한다.

 

VM warning: CodeCache is full. The compiler has been disabled.

 

해당 로그는 tier 4의 조건이 맞는 메서드나 코드블록이 발생하여 CodeCache에 추가하려고 하는데 CodeCache가 가득 차고 전부 actvie 상태이기 때문에 메모리에서 해제할 코드블록이 없어 할당하지 못하는 경우를 나타낸다.

(이 말에 따르면 무조건적으로 삭제하지는 않는 것 같다. 사용여부를 체크하여 메모리에서 해제하는 것으로 보인다.)

 

이러한 경고로 인해 애플리케이션이 중지되지는 않으니 걱정하지는 말자.

 

이 문제를 해결할 수 있는 방법은 무엇일까?

 

간단하다 그냥 code cache의 사이즈를 기본사이즈에서 변경하면 된다.

 

단지 이러한 변경으로 인해 애플리케이션의 성능을 크게 향상할 수 있다.

 

우선 현재 CodeCache를 위해 JVM 옵션을 켜보자.

 

필자도 테스트를 위해 다음과 같은 VM Option을 사용하여 디버깅하였으며 이때 적용한 옵션은 다음과 같다.

  1. -XX:+LogFile은 java 9 and later부터 사용할 수 있으며 -XX:+UnlockDiagnosticVMOptions와 같이 사용해야 한다.
  2. XX:+PrintCompilation

해당 option은 JIT 컴파일러의 컴파일 정보를 확인할 수 있다. 관련 내용은 CodeCache 글에서 확인할 수 있다.

  1. XX:+PrintCodeCache

실제 CodeCache 로그를 가져왔다.

CodeHeap 'non-profiled nmethods': size=238336Kb used=15257Kb max_used=15257Kb free=223078Kb

bounds [0x00000207 ea580000, 0x00000207 eb470000, 0x00000207 f8 e40000]

CodeHeap 'non-nmethods': size=7424Kb used=1378Kb max_used=1378Kb free=6045Kb

bounds [0x00000207e9e40000, 0x00000207ea0b0000, 0x00000207ea580000]

total_blobs=10039 nmethods=9345 adapters=619

compilation: enabled

stopped_count=0, restarted_count=0

full_count=0

 

실제 모니터링할 때 non-profiled nmethods 부분이 있었는데 이제 어떤 정보를 모니터링하는지 알 것 같다.

해당 위젯이 코드캐시 부분이었고 이를 활성화하려면 sprign actuactor 어떤 option을 별도로 활성화시켜야 할 것 같다.

 

우선 코드 캐시 크기에 대한 제한이 있으며 이는 우리가 사용하는 자바 버전을 기반으로 한다고 한다.

 

java -XX:+PrintCommandLineFlags -version

 

다음 명령어를 커맨드 창에 입력하면 ReservedCodeCacheSize관련 옵션에 바이트 크기로 로그가 출력된다. 필자의 경우 java 11로 출력결과는 다음과 같다.

 

XX:ReservedCodeCacheSize=251658240 → 약 240MB..

 

만약 -XX:-TieredCompilation 옵션으로 tiered compilation을 비활성화하면 기본 크기는 48mb로 줄어들게 됩니다.

 

이는 MaxSize를 설정하는 것이다. 따라서 다음과 같이 추가적인 옵션을 사용하여 최적화해야 한다.

 

-XX:InitialCodeCacheSize

InitialCodeCacheSize 옵션은 애플리케이션 시작 시 사용될 CodeCache 크기입니다.

기본값은 매우 작은데 약 180kb 정도밖에 안됩니다.

 

-XX:ReservedCodeCacheSize

ReservedCodeCacheSize 이는 코드 캐시의 최대 값을 말합니다.

 

-XX:CodeCacheExpansionSize

CodeCacheExpansionSize는 코드 캐시의 크기가 증가되는 속도에 대한 옵션입니다. 속도 값은 아니고 코드캐시의 크기가 증가될 때 각 추가될 영역의 크기를 지정합니다.

기본적으로 옵션의 값에 대해선 byte 크기를 지정합니다.

지원되는 옵션의 크기에 대해선 kilobytes, megabytes, gigabytes를 지원하고 이는 문자 k, m, g를 붙여서 표현할 수 있습니다.(대문자 가능)

 

예를 들어 CodeCache의 Max size를 1기가로 설정한다면 다음과 같이 JVM 옵션을 주면 됩니다.

 

-XX:ReservedCodeCacheSize=1g

 

이를 애플리케이션 원격에서 확인하고 싶은 경우 jconsole을 사용할 수 있습니다.

 

java 8을 사용하는 경우 Memory chart 옵션에 “Memory Pool Code Cache”라는 옵션이 출력되며

Java 9+를 사용하는 경우 Memory Pool “CodeHeap non-nmethods”, Memory Pool “CodeHeap non-profiled nmethods”라는 두 가지 옵션이 출력됩니다.

 

Java 9에서는 JEP( JDK Enhancement Proposal) 197 이 존재합니다.

 

이는 Code Cache를 3가지 영역으로 분리하는 정책으로 non-method, profiled, non-profiled이라는 세 가지 영역으로 분리됩니다.

 

non-method

non-method의 code heap에는 컴파일러 버퍼, 바이트코드 인터프리터와 같은 non-method code가 포함됩니다.

이 코드 유형은 코드 캐시에 영구적으로 보관됩니다. 이는 3MB의 고정된 크기를 가지고 있으며, 나머지 코드 캐시는 profiled, non-profiled 코드 힙에 고르게 분포됩니다.

 

profiled

profiled 코드 힙에는 수명이 짧고 가볍게 최적화된 profiled method가 포함되어 있습니다.

 

non-profiled

non-profiled 코드 힙에는 완전히 최적화되고 수명이 긴 non-profiled 메서드가 포함됩니다.

 

나누어진 이유는 다음과 같고 해당 스펙의 URL은 다음과 같다.

https://openjdk.org/jeps/197#:~:text=Instead%20of%20having%20a%20single, internal%20(non%2 Dmethod)%20 code 

 

JEP 197: Segmented Code Cache

JEP 197: Segmented Code Cache OwnerTobias HartmannTypeFeatureScopeImplementationStatusClosed / DeliveredRelease9Componenthotspot / compilerDiscussionhotspot dash compiler dash dev at openjdk dot java dot netEffortLDurationMReviewed byMikael Vidsted

openjdk.org

  • Separate non-method, profiled, and non-profiled code
  • Shorter sweep times due to specialized iterators that skip non-method code
  • Improve execution time for some compilation-intensive benchmarks
  • Better control of JVM memory footprint
  • Decrease fragmentation of highly-optimized code
  • Improve code locality because code of the same type is likely to be accessed close in time
  • Better iTLB and iCache behavior
  • Establish a base for future extensions
  • Improved management of heterogeneous code; for example, Sumatra (GPU code) and AOT compiled code
  • Possibility of fine-grained locking per code heap
  • Future separation of code and metadata (see JDK-7072317)

실제로 Code Cache에 대한 튜닝을 진행하기 전에 32bit-JVM과 64bit-JVM의 차이점에 대해 설명하려고 한다.

 

첫 번째로는 우리가 먼저 알아야 할 것은 OS가 얼마나 많은 bit를 가지고 있는지 확인하는 것이다.

 

만약 32-bit OS를 가지고 있다면 우리는 32bit-JVM을 반드시 사용해야 하는데, 만약 64bit-JVM을 사용하는 경우 32bit-JVM과 64bit-JVM 두 가지를 선택할 수 있다.

 

여기서 드는 의문점은 왜 내가 64bit OS를 가지고 있는데 32bit-JVM을 선택지가 있는 것에 대한 의문입니다.

32bit, 64bit JVM의 큰 차이점 중 하나는 바로 지원되는 최대 힙 크기입니다.

 

32bit JVM의 경우 heap, permgen, native code, native memory the JVM use를 포함한 JVM의 최대 프로세스 크기가 4G입니다.

 

64bit JVM의 경우는 실행 중인 OS 마다 다릅니다.

 

만약 애플리케이션을 실행하는 데 있어서 3G 미만의 heap memory가 필요한 경우는 32-bit JVM이 64-bit JVM 보다 더 빠른 성능을 보장할 수 있습니다.

 

그 이유는 메모리의 객체에 대한 포인터가 작기 때문에 포인터를 처리하는 데 있어서 더욱 빠른 속도를 기대할 수 있기 때문입니다.

 

반대 상황의 예시로는 32bit를 넘는 자료형인 long, double과 같은 타입의 객체가 많이 사용하는 애플리케이션이 존재한다면 객체의 포인터를 처리하는데 더욱 느려질 것입니다.

 

또 비트별 JVM의 JIT 컴파일러의 동작이 다릅니다.

 

일반적으로 두 가지 타입의 애플리케이션이 존재할 수 있습니다.

 

Client application과 Server application입니다.

 

클라이언트 애플리케이션은 생명주기가 매우 짧은 경우가 많고 서버 측 애플리케이션은 생명주기가 더 깁니다.

 

클라이언트 애플리케이션은 시작하는데 드는 시간이 매우 중요합니다. 이는 오래 실행되지 않기 때

문에 JIT 컴파일러는 tiered 컴파일을 실행한 시간이 없습니다.

 

즉 tier 4에 해당되는 메서드가 여러 번 실행되지 않을 수 있습니다.

그렇기 때문에 32bit JVM에는 client compiler 밖에 존재하지 않습니다.

(→ 32bit JVM에 C1 컴파일러 밖에 없다는 이야기는 조금 허무맹랑하다.)

 

반면 웹 서버 애플리케이션인 경우 JIT 컴파일러의 컴파일이 애플리케이션을 시작하는데 드는 시간보다 중요합니다.

 

왜냐하면 JIT 컴파일러는 바이트 코드를 profile 하고 tier 4에 넣은 다음 이를 code cache에 넣을 시간이 있어야 하기 때문입니다.

 

다양한 이유로 인해 32-bit JVM을 사용하지 못하고 64-bit JVM을 사용하게 되는 경우가 있습니다.

하지만 생명주기가 짧아 client application의 이점을 얻고 싶다면 다음 옵션을 추가할 수 있습니다.

우리에게 세 가지 flags가 존재합니다. 바로 -server, -client, -d64이다.

 

-client 플래그는 JIT 컴파일러가 32bit 클라이언트 컴파일러(C1)만 사용해야 한다고 말하는 것입니다.

 

-server 플래그는 JIT 컴파일러가 32bit 서버 컴파일러만 사용해야 한다고 말하는 것입니다.

 

-d64 플래그는 JIT 컴파일러가 64bit 서버 컴파일러를 사용하라고 말하는 것입니다. ( 해당 옵션이 아직까지 있는지 모르겠다. 테스트 시 동작하지 않음)

 

Scott Oasks에 따르면 사용할 컴파일러는 지정하는 인수는 그리 엄격하게 따르지는 않는다고 합니다.

만약 64bit-JVM을 가지고 있고 -client 옵션을 지정한 경우에도 애플리케이션은 64bit 서버 컴파일러를 사용할 수 있습니다.

만약 32-bit JVM을 사용하고 -d64 옵션을 지정한 경우 인스턴스가 64bit JVM을 지원하지는 않는다는 오류가 표시됩니다.

하지만 이러한 역설적인 상황에서도 -client 옵션을 사용하면 시작시간이 더욱 빨라지는 이점을 얻을 수 있습니다. 이는 코드 분석을 더 적게 하기 때문입니다.

 

어디선가 HotSpots JVM은 C1 컴파일러는 우선적으로 사용하고 그다음 C2 컴파일러를 사용한다고 그랬는데 둘 다 사용하는 건가요?

 

맞습니다. 이에 대해서 자바의 역사를 조금 말해보려고 합니다.

 

Tiered compilation은 자바 프로그램의 성능 향상을 위해 도입된 컴파일링 기술입니다.

이 기술은 자바 7부터 추가되었으며, 이전 버전에서는 C1과 C2 컴파일러가 상호 배타적으로 사용되었습니다. 자바 8에는 기본적으로 사용됩니다.

Tiered compilation을 사용하면, 프로그램이 실행되는 동안 최적의 컴파일러를 선택하여 최상의 성능을 보장합니다.

일반적으로는 C1 컴파일러를 사용하다가 성능이 필요한 부분에서 C2 컴파일러를 사용하는 방식으로 동작합니다. 이를 통해 프로그램의 전반적인 실행 속도를 향상할 수 있습니다.

 

그러나 때로는 tiered compilation을 비활성화하거나, C2 컴파일러를 사용하지 않도록 설정하는 것이 더 나은 성능을 제공하는 경우가 있습니다.

 

이럴 때는 "-server" 또는 "-client" 플래그를 사용하거나,

 

"-XX:-TieredCompilation" 플래그를 사용하여 tiered compilation을 비활성화할 수 있습니다.

 

또한, "-XX:TieredStopAtLevel=<LEVEL>" 플래그를 사용하여 JIT 컴파일러가 C2 컴파일러를 사용하지 않도록 설정할 수도 있습니다.

 

추가적으로 두 가지의 성능 향상 옵션이 존재합니다.

 

첫 번째는 바로 컴파일을 위한 스레드 수를 확인하는 것입니다.

 

-XX:CICompilerCount=<NUMBER>”.

두 번째는 네이티브 컴파일의 임계값을 조정하는 것입니다.

즉 tier 4로 컴파일하기 위해 메서드를 몇 번 실행해야 하는지 확인하는 것입니다.

 

-XX:CompileThreshold=<NUMBER>”.

기본적인 값은 10,000이며, 단순히 메서드 실행 횟수가 아니라 re(compiling) 하기 전에 해석된 메서드의 호출 수입니다.

 

더 많은 성능적인 부분이 궁금하다면 책을 읽어보자

Java Performance, 2nd Edition

 

Java Performance, 2nd Edition

Get full access to Java Performance, 2nd Edition and 60K+ other titles, with free 10-day trial of O'Reilly. There's also live online events, interactive content, certification prep materials, and more.

www.oreilly.com

 

정리.

 

사실 Code Cache 영역에 대해 궁금했던 건 모니터링에 포함여부를 판단하기 위해 공부했던 것인데 생각보다 사이즈가 커진 느낌이다.

 

또한 설명이 올바른지 판단해야 할 것 같다.

 

64bit JVM을 사용하는 환경에서 32bit JVM이 주는 이익은 더 적은 메모리 포인트, 빠른 시작시간이다.

 

-client flag를 통해 Clinet Compiler를 사용해 달라고 요청할 수 있다. 그러면 적극적으로 코드를 컴파일하려고 하지 않을 것으로 예상된다.

 

-server flag를 통해 32bit Server Compiler를 사용해 달라고 요청할 수 있다.

앞에 32bit가 왜 붙는지 모르겠다. 64bit JVM인 경우 기본적으로 64bit Server Compiler를 사용하지 않나? 조금 말이 안 되는 것 같다.

32bit Server Compiler → 64bit Server Compiler가 맞지 않을까 싶다. 예시는 64bit JVM을 사용한다고 했으니까

이 경우 -server 옵션을 사용하면 더 적극적으로 코드를 최적하여 컴파일하려고 한다.

그렇기에 빠른 시작보다는 장기간 실행되는 서버 측 애플리케이션에 사용되면 유리할 수 있다.

 

그런데 여기서 생각해 볼 것은 컴파일에 대한 임계점이다.

 

JVM은 특정 세이브 포인트에서는 메서드의 카운터 값을 감소하는 것으로 알고 있어 실제 JIT 컴파일러가 tier 4까지 가야 Code Cache에 저장된다는 것인데 이 값을 적절히 조절하거나 warmup을 통해 코드 캐시에 저장한 이후 서비스를 시작하는 방법을 사용하는 것으로 보인다.

 

뭐 모니터링에 보이는 각 CodeCache 정보를 파악할 정도의 지식과 부가적으로 JVM, JIT Compiler, 성능향상을 위해서 튜닝하는 방법과 CodeCache 로그까지 알게 된 것에 만족한다.

반응형

'JAVA > [JAVA] 바구니' 카테고리의 다른 글

Comparable  (0) 2022.11.14
equals 정의  (0) 2022.11.10
finalizer와 cleaner 사용을 피하라  (0) 2022.11.07
Exception  (0) 2022.09.25
SSL/TLS 서버 통신 (JSSE, TrustManager)  (1) 2021.11.06

댓글