유용한 JVM 플래그 – Part 2 (플래그 카테고리들과 JIT 컴파일러 진단들)
두번째 시간으로, HotSpot JVM에서 제공하는 플래그의 다른 카테고리들을 소개한다. 또한, 나는 JIT 컴파일러 진단(diagnostics)와 연관된 몇가지 흥미로운 플래그들에 대해서 이야기할 것이다.
JVM 플래그 카테고리들
HotSpot JVM은 세개의 플래그 카테고리들을 제공한다. 첫번째 카테고리는 표준 플래그(stand flag)들을 포함한다. 이름에서도 알수 있듯이, 기능적인부분과 표준 플래그들의 출력 모두 안정적이며 미래에 릴리즈 되는 JVM에서 잘 바뀌지 않을 것이다. java 실행시에 아무런 파라메터를 주지않으면 모든 표준 플래그 리스트들을 얻을 수 있다.(혹은 표준 출력이 있는 -help 파라메터를 사용하거나) 우리는 첫번째 시간에 몇몇 표준 플래그들을 이미 봤었다. 예를들어 -server.
두번째 카테고리는 비표준 플래그들의 행동이거나 X 플래그들로, 앞으로 릴리즈에서 바뀔 가능성이 있다. 이 카테고리의 모든 플래그들은 “-X” 로 시작되고 java -X 를 통해 리스트를 볼 수 있다. 주의할 것은 이 리스트는 완벽하게 존재하는지 보증하지 않는다. -Xcomp 가 대표적으로 누락된 플래그다.
세번째 카테고리는(가장 큰) XX 플래그들로 구성되는데, 이 또한 비표준이고 오래전부터 리스트조차 된적이 없다. (현재 이것은 바뀌었고, 우리는 이 시리즈의 세번째 시간에 이 토픽에 대해서 돌아 볼 것이다.) 그러나 실용적으로 X 플래그들과 XX 플래그들은 실제로 차이가 없다. XX 플래그들이 실험적인 반면에 X 플래그들의 행동은 XX 플래그들에 비해 아주 안정적이다. (디버깅이나 JVM 구현 자체의 튜닝을 위해서 JVM 개발자들에 의해서 주요하게 사용된다.) 비표준 플래그들에 대해서 부주의하게 사용되어서는 안될 XX 플래그들에 대해 명확하게 어떤 상태를 가지는지 HotSpot JVM 문서를 읽어 볼만 하다. 이것은 중요한 것으로, 이것은 내가 봤을때 X 플래그들에 대해서도 동일하게 적용된다. (물론 몇몇 표준 플래그들에도 적용된다.) 카테고리와 상관없이 이것을 사용하기 전에 플래그가 의도하는 행동과 가능한 사이드 이펙트(side effects)를 이해하기 위해서 노력해야 한다.
XX 플래그 문법(syntax)에 대해 한마디. 모든 XX 플래그들은 “-XX” 로 시작하지만 문법이 의존하는 플래그 타입이 다르다.
- 불린 플래그(boolean flag)에서, “+“나 ”-” 둘다 가지며 플래그를 지정하기 위해서 JVM 옵션의 실제 이름만 가진다. 예를들어, -XX:+<name> 은 옵션 <name> 을 활성화하고 -XX:-<name> 은 이 옵션을 비활성화 한다.
- 텍스트 문자열처럼 불린(boolean)이 아닌 값을 가지거나 정수를 가지는 플래그들에서, “=” 에 따라오는 플래그 이름을 가지고 값을 지정한다. 예를들어, -XX:<name>=<value> 는 옵션 <name> 에 값 <value>을 지정한다.
이제 JIT 컴파일 구역에 속하는 몇몇 XX 플래그들을 살펴보도록 하자.
-XX:+PrintCompilation and -XX:+CITime
이것은 자바 애플리케이션이 동작하는 중에 JIT compiler 작업을 상세히 보여준다. -XX:+PrintCompilation 플래그를 세팅함으로써 우리는 바이트코드가 네이티브 코드 컴파일과 연관된 몇몇 간단하고 단순한 출력을 활성화 할 수 있다. Server VM 동작에 대해 아주 짧은 출력 예제를 살펴보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$ java -server -XX:+PrintCompilation Benchmark 1 java.lang.String::hashCode (64 bytes) 2 java.lang.AbstractStringBuilder::stringSizeOfInt (21 bytes) 3 java.lang.Integer::getChars (131 bytes) 4 java.lang.Object::<init> (1 bytes) --- n java.lang.System::arraycopy (static) 5 java.util.HashMap::indexFor (6 bytes) 6 java.lang.Math::min (11 bytes) 7 java.lang.String::getChars (66 bytes) 8 java.lang.AbstractStringBuilder::append (60 bytes) 9 java.lang.String::<init> (72 bytes) 10 java.util.Arrays::copyOfRange (63 bytes) 11 java.lang.StringBuilder::append (8 bytes) 12 java.lang.AbstractStringBuilder::<init> (12 bytes) 13 java.lang.StringBuilder::toString (17 bytes) 14 java.lang.StringBuilder::<init> (18 bytes) 15 java.lang.StringBuilder::append (8 bytes) [...] 29 java.util.regex.Matcher::reset (83 bytes) |
메소드가 컴파일 되어질때 마다, 하나의 라인(line)은 -XX:+PrintCompilation 의 출력을 찍는다. 각 라인은 동작숫자(running number, 유일한 compiler task ID) 와 이름 그리고 컴파일된 메소드의 크기로 구성된다. 따라서 라인 1 은 String::hashCode 메소드를 컴파일한 네이티브 코드를 나타낸다. 메소드 타입과 컴파일러 태스크 유형에 따라, 추가적인 출력 특징들이 찍힌다. 예를들어, 네이티브 랩퍼 메소드의 생성은, 위 예제에 System::arraycopy 처럼, “n”으로 표시된다. 주의할 것은 어떤 라인은 동작숫자와 메소드 크기를 가지지 않는데, 실제로 아무것도 네이티브 코드로 컴파일 되지 않았기 때문이다. 또 11 ~ 15 라인에 StringBuilder::append 에 대한 출력을 보면, 재컴파일된(recompiled) 메소드들을 보는것이 가능하다. 출력은 총 29라인에서 멈추었는데, 자바 애플리케이션이 동작하는 동안에 총 29개의 메소드를 컴파일 했다는 것을 의미한다.
-XX:+PrintCompilation 에 대한 공식적인 문서는 없지만, 여기 내용은 이 플래그 출력에 대한 훌륭한 자원중에 하나다. 나는 이것에 대해서 좀더 공부하기를 강력하게 권유한다.
JIT 컴파일러 출력은 클라이언트 VM 과 서버 VM 사이에 몇몇 다른점을 이해하는데 도움을 준다. 서버 VM에서, 예제 애플리케이션은 29 라인 컴파일 출력을 만들었지만 클라이언 VM 사용 결과 55 라인 컴파일 출력을 만들었다. 이것은 서버 VM이 클라이언트 VM 보다 좀 더 컴파일을 하기로 되어 있었기 때문에 이상하게 여기진다.(역, 서버 VM이 컴파일을 좀 더하기 때문에 출력 라인수가 클라이언트 VM보다 많아야 할거라 추측하지만 그러지 않았다.) 그러나, 각각 주어진 기본 세팅 측면에서, 서버 VM은 메소드가 hotspot 인지 아닌지와 전체 컴파일되채 존재할 필요가 있는지 없는지를 결정하기 전에 클라이언 VM 보다 아주 오랫동안 메소드를 관찰한다. 따라서, 그것은 놀랄일이 아니다, 서버 VM에서, 몇몇 잠재적인 메소드 컴파일은 최종 단계에서만 일어난다.
추가적으로 -XX:+CITime 플래그 세팅에 의해서 우리는 JVM 셧다운시에 출력되어질 수 있는 컴파일에 대한 다양한 통계정보를 요청할 수 있다. 통계정보의 특정한 한 부분을 살펴보자.
1 2 3 4 5 6 7 8 |
$ java -server -XX:+CITime Benchmark [...] Accumulated compiler times (for compiled methods only) ------------------------------------------------ Total compilation time : 0.178 s Standard compilation : 0.129 s, Average : 0.004 On stack replacement : 0.049 s, Average : 0.024 [...] |
(29 컴파일러 태스크를 위해) 총 0.178 초를 소비했다. 물론, “on stack replacement” 를 가지는 0.049초는 스택에서 현재 메소드의 컴파일 시간이다. 이 기술은 성능 기준에 맞는 유형을 구현하기위해서 단순하지 않지만 실제로 아주 중요하다. “on stack replacement” 없이 오랜 실행 시간을 가지는 메소드들은 그들의 컴파일된 카운터파트(counterpart, 짝 혹은 또 다른 부분 )로 즉각 교체되어질 수 없다.
다시말하면, 클라이언트 VM 와 서버 VM 과 비교는 흥미롭다. 클라이언트 VM에 해당하는 통계는 비록 55개 메소드를 컴파일했다는 것을 나타냈지만 이들을 컴파일하는데 총 0.021초만 소비했다. 따라서, 서버 VM 은 클라이언트 VM보다 적게 컴파일을 했지만 시간은 더 많이 소비했다. 이러한 행동의 이유는 서버 VM은 네이티브 코드를 생성할때에 좀 더 최적화를 수행하기 때문이다.
첫째 시간에 우리는 -Xint 와 -Xcomp 플래그에 대해서 배웠다. -XX:+PrintCompilation 과 -XX:+CITime 와함께 이제 우리는 두 가지 경우에(역, 서버 VM 과 클라이언트 VM) 대한 JIT 컴파일러가 행동을 어떻게 하는지에 대한 좀 더 깊게 알 수 있다. -Xint 와 함께, -XX:+PrintCompilation 은 두가지 경우에 대해서 정확하게 아무런 라인도 출력하지 않는다.(zero lines of output) 또, -XX:+CITime 은 컴파일하는데 시간을 전혀 소비하지 않았다는 것을 확인시켜준다. -Xcomp 경우는 다르다. 클라이언트 VM 은 프로그램 시작이후에 즉각 726 라인은 출력하고 모든 관련 메소드들은 컴파일되었기 때문에 더 이상 출력되지 않는다. 서버 VM 에서는 993 라인 출력을 볼 수 있는데 이는 좀 더 공격적 최적화를 수행했다는 것을 말해준다. 또, JVM이 셧다운시에 출력되는 통계에서도 둘 VM 사이에서는 아주 큰 차이를 보여준다. 서버 VM에서 실행 결과을 살펴보자.
1 2 3 4 5 6 7 8 |
$ java -server -Xcomp -XX:+CITime Benchmark [...] Accumulated compiler times (for compiled methods only) ------------------------------------------------ Total compilation time : 1.567 s Standard compilation : 1.567 s, Average : 0.002 On stack replacement : 0.000 s, Average : -1.#IO [...] |
-Xcomp 를 사용해 컴파일하는데 소비한 시간 1.567 초는 기본 세팅 값(ex, mixed mode) 보다 약 10배나 많은 시간을 소비했다. 여전히, 애플리케이션은 mixed 모드보다 더 느리게 동작한다. 클라이언트 VM 은 -Xcomp 를 사용했을 경우 726개의 메소드들을 컴파일하는데 0.208 초를 소비했다. 이것은 -Xcomp 를 사용한 서버 VM보다 더 느린 것이다. (역, 말이 이상하다. server VM 에서는 993 라인에 1.567 초를 client VM 에서는 726 라인에 0.208 초를 소비했는데 어째서 server VM 보다 느리다고 한 걸까?)
모든 메소드는 처음 실행 시점에서 호출되어질때에 컴파일되어지기 때문에 ‘on stack replace“가 발생하지 않는다. 손상된 출력 “Average: -1.#IO”는 (정확하게는 0) 비표준 플래그의 출력이 아주 많이 의존하는게 없다는 것을 다시 한번 보여준다.(역, 말이 이상함. The corrupted output “Average: -1.#IO” (correct would be: 0) demonstrates once more that the output of non-standardized flags is nothing to rely on too much)
-XX:+UnlockExperimentalVMOptions
특정 JVM 플래그를 세팅하면 JVM은 시작하자마자 “Unrecognized VM option” 메시지를 출력하고 곧바로 중단되곤 한다.만일 이런 일이 발생하면, 혹시 오타를 치지지 않았나 체크해야 한다. 하지만, 오타없이 정확하게 입력했는데도 여전히 JVM이 그 플래그를 인식하지 못한다면 아마도 -XX:+UnlockExperimentalVMOptions 세팅을 통해서 플래그를 풀어줄(unlock) 필요가 있다. 이것은 보안 메커니즘도 영향을 주기 때문에 명확하진 않지만 JVM이 올바르게 사용되어지지 않았을 경우에 이러한 방법으로 플래그를 안내하는 것이 JVM의 안전성에 영향을 주는 성향이라 생각하고 있다. (예를들어, 그들이 어떤 로그 파일에 아주 과도한 디버그 출력을 쓰도록 했다든지..)
어떤 플래그들은 실제로 자바 애플리케이션에서 사용하지 않고 오직 JVM 개발을 위해서 사용되도록 해놨다. 만일 플래그가 -XX:+UnlockExperimentalVMOptions 로도 활성화가 되지 않지만 어떤 이유에선지 그 플래그를 반드시 필요로한다면, 여러분은 JVM 의 디버그 빌드를 가지는 행운을 누릴 수 있다.(역, 놀리는거 같다. ㅡ.ㅡ 무슨 얼어죽을 행운이냐!!) java 6 HotSpot JVM 에 대한 디버그 빌드는 여기서찾을 수 있다.
-XX:+LogCompilation and -XX:+PrintOptoAssembly
만일 -XX:+PrintCompilation 로 제공되어지는 정보가 충분히 상세하지 않다면, “hotspot.log” 파일에 확장된 컴파일단계를 출력하도록 -XX:+LogCompilation 플래그를 사용할 수 있다. 덧붙여서 컴파일된 메소드에 대한 아주많은 상세한 정보들 중에 작업이 시작된 컴파일러 쓰레드를 볼 수 있다. 주의해야 할것은 -XX:+LogCompilation 는 -XX:+UnlockExperimentalVMOptions로 풀어줘야 한다.
JVM 은 바이트코드 컴파일로 생성된 네이티브 코드 결과들을 살펴볼 수 있도록 해준다. -XX:+PrintOptoAssembly 플래그로 인해서 컴파일러 스레드에 의해 생성 된 네이티브 코드는 표준 출력과 “hotspot.log” 파일에 모두 기록됩니다. 이 플래그를 사용은 서버 VM의 디버그 빌드를 실행하도록 요구한다. 우리는, 죽은 코드 제거하기처럼, JVM이 실제 어떤 종류의 최적화를 수행하는지를 이해하기 위해서 -XX:+PrintOptoAssembly 의 출력을 연구할 수 있다. 예제를 제시하는 흥미로운 기사는 여기에서 찾울 수 있다.
Further information about XX flags
만약 이글이 당신의 상상력을 자극한다면, HotSpot 의 XX 플래그를 스스로 찾아봐라. 아주 좋은 시작점은 이 리스트다.