Tagged: JIT Compiler

유용한 JVM 플래그 – Part 1 (JVM 타입들과 컴파일러 모드들)

현대의 JVM들은 효율적이고 안정적인 방법으로 자바 애플리케이션을(혹은 JVM과 호환되는 프로그래밍 언어들) 실행시키는 놀라운 일을 한다. 맞춤 메모리 관리(Adaptive memory management), 가비지 컬렉션(garbage collection), just-in-time compilation, 동적 클래스로딩(dynamic classloading), 락 최적화(lock optimization) – 이러한 것이 마법처럼 인용되지만 일반적인 프로그래머들에게 직접적으로 영향을 주진 않는다. 실행 시점에서, JVM은 지속적인 측정과 프로파일링을 기반으로 애플리케이션이나 그것의 일부를 핸들링하는 방법을 최적화한다.

여전히 JVM이 자동화 수준과 같은 것이나 그보다 못한 것들에 대해서 외부 모니터링이나 수동 튜닝을 위한 충분한 설비를 제공하고 있다는 것은 중요하다. 에러나 낮은 퍼포먼스의 경우에는 반드시 전문가가 개입하는 것이 가능해야 한다. 게다가, 수면 아래에서 일어나는 모든 마법같은 일 외에도, 아주 폭 넓은 수동 튜닝 같은것은 현대 JVM이 가지는 강력한 것중에 하나다. 특히 흥미로운 것은 JVM이 시작시 그들에게 전달되어 질수 있는 커맨드 라인 플래그들이다. 몇몇 JVM은 수백개의 이러한 플래그들을 제공하지만 JVM에 대한 적절한 지식이 없이는 잊어버리기 쉽다.이 시리즈의 목표는 매일 사용하는 적절한 플래그들을 조명하고 그들이 장점들에 대해서 설명하는 것이다. 다른 인기있는 JVM들에 아주 유사한 플래그가 존재하지만 우리는 Java 6으로 Sun/Oracle HotSpot에 집중할 것이다.

-server 와 -client

HotSpot JVM에는 두개의 타입이 있다. 이름하야 “server” 와 “client”. 서버(server) VM은 기본적으로 아주 큰 힙(Heap), 패러럴 가비지 컬렉터(parallel garbage collector)를 사용하고 실행타임에서 좀 더 공격적으로 코드를 최적화 한다. 클라이언트(client) VM은 좀 더 보수적인데, 그 결과 좀 시작 타임이 짧아지고 메모리를 좀 더 적게 사용한다. “JVM 인체공학(ergonomic)” 이라 불리는 컨셉 덕분에 JVM의 타입은 JVM이 시작될때에 운영체제와 활용할수 있는 하드웨어를 고려한 기준에의해서 자동적으로 선택되어진다. 추가적인 기준(혹은 규격)은 여기서 찾을 수 있다. 규격(기준) 테이블로부터, 우리는 클라이언트 VM은 오직 32bit 시스템에서만 활용할 수 있다는 것을 알 수 있다.

만약 미리 정의된 JVM이 불만이라면, 우리는 서버와 클라이언트 VM 사용을 규정하기 위해 -server 와 -client 플래그를 사용할 수 있다. 비록 서버 VM이 기본적으로 장시간 실행되는 서버 프로세스들에 초점이 맞춰졌지만, 오늘날 그것은 아주 많은 독립 애플리케이션 VM에서 클라이언트 VM 보다 훨씬 높은 성능을 종종 보여준다. 나는 애플리케이션이 빠른 실행시간이 중요하다고 할때에 -server 플래그를 세팅함으로써 서버VM을 선택할 것을 권장한다. 일반적으로, 32-bit 시스템에서, HotSpot JDK는 모두 서버VM으로 동작하도록 할 필요가 있다. – 32bit JRE만 클라이언트VM을 탑재한다.

-version 과 -showversion

어떻게 우리는 자바가 설치되어 있고 JAVA를 호출했을때에 JVM 타입이 어떤건지를 알 수 있을까? 시스템에 하나 이상의 JAVA가 설치되어 있다면 아무런 알림없이 잘못된 JVM이 실행될 약간의 위험성이 항상 있다. 이러한 관점에서,비록 내가 해를 거듭할수록 좋았졌다는 것을 인정한다해도, 인기있는 다양한 리눅스 배포판에는 JVM이 미리 설치되었다.

운 좋게, 우리는 -version 플래그를 활용할 수 있다. 사용되어진 JVM에 대한 간단한 정보를 표준출력으로 출력할 수 있다. 예를들면,

출력된 내용을 보면 JAVA 버전 넘버(1.6.0_24)와 사용된 정확한 JRE의 빌드ID(1.6.0_24-b07) 를 보여준다. 또, 우리는 이름을(HotSpot) 볼수 있고, JVM의 빌드ID(19.1-b02) 와 타입(Client)도 볼 수 있다. 거기에 더해, 우리는 JVM 이 믹스드 모드(mixed mode)로 동작한다는걸 알 수 있다. 이 실행 모드는 기본적인 HotSpot 모드이고 실행 타임에 동적으로 바이트 코드(byte code)를 네이티브 코드(nate code)로 컴파일 한다는 걸 의미한다. 또, 우리는 클래스 데이터 공유(class data sharing)가 활성화 되었다는 것도 알 수 있다. 클래스 데이터 공유는 모든 JAVA 프로세스들이 클래스로더에 의해서 자원을 공유하는데 사용되어지는 읽기전용 캐쉬에 JRE 의 시스템 클래스들을 저장하는 기법이다. 클래스 데이터 공유는 매번 jar archive들로부터 모든 클래스 데이터를 읽어들이는 것과 비교해볼때 성능면에서 대체로 이득이 있다.

-version 플래그는 위 데이터를 출력한 후에 즉각 JVM을 종료한다. 그러나, 같은 출력결과를 만드는데 사용되어질 수 있는 -showversion 는 유사한 플래그지만 주어진 자바 애플리케이션을 실행하고 처리한다. 따라서 -showversion 은 거의 모든 자바 애플리케이션의 커맨드 라인에 유용하게 추가되었다. 여러분은 갑자기 어떤 정보가 필요할때 특별한(깨진) 자바 애플리케이션에서 사용된 JVM에 대해서 알수가 없다. 시작시에 -showversion 을 추가함으로써, 우리는 우리가 필요로할지 모르는 시점에서 활용가능한 이러한 정보를 얻는것을 보장받을 수 있다.

-Xint, -Xcomp, 그리고 Xmixed

두개의 플래그 -Xint, -Xcomp 는 우리가 매일 하는일과 관련이 없지만 JVM에 대해서 무언가를 배우기 위한 아주 큰 주제가 있다. -Xint 는 JVM에게 모든 바이트코드(Bytecode)를, 통상적으로 10배 이상 아주 느려지는 것이 수반되는, 인터프리터 모드로 실행하도록 강제한다. 이와 대조적으로, 플래그 -Xcomp 는 명시적으로 정반대로 동작하도록 강제하는데 그것은 JVM이 처음 사용시에 모든 바이트코드를 네이티브코드(Native code)로 컴파일하는데 결국 최고의 최적화 레벨을 적용하게 된다. 이것은 아주 듣기좋은 소리인데, 왜냐하면 인터프리터의 느림을 피하는 완벽한 방법이기 때문이다. 하지만 많은 애플리케이션들은 -Xinit 가 성능저하가 발생한다는 단 하나의 이유와 비교하더라도 -Xcomp 의 사용은 적은 성능 차이를 겪게 된다. 그 이유는 -Xcomp 세팅은 JVM에게 JIT 컴파일러(JIT Compiler)가 효율적으로 네이티브 코드를 만들어내는 것을 방해하게 한다. JIT 컴파일러는 실행타임에 메소드 사용 프로파일들을 생성한 다음에 실제 애플리케이션 동작을 위해 차례대로 그들의 일부나 혹은 추론을 해서 싱글 메소드들을 최적화한다. 이러한 최적화 테크닉의 일부들은, 예를들어 optimistic branch prediction, 맨 처음에 애플리케이션의 프로파일링 없이 효율적으로 적용되어질 수 없다. 또 다른 관점으로 메소드는 그들 스스로가 애플리케이션에서 어떤 종류의 지점을 구성하는데 연관되어 있다는 것이 증명되었을때 전체가 컴파일되어 진다. 오직 한번이나 아주 적게 호출되어지는 메소드들은 인터프리터 모드로 실행되는 것을 지속하게 되고 따라서 compilation 과 최적화(optimzation) 비용을 절약하게 된다.

우리는 -Xmixed 플래그를 가지는 mixed 모드를 주목하자. 최신의 HotSpot 버전에서, mixed mode는 기본값이 됐고 우리는 더 이상 이 플래그를 지정하지 않아도 된다.

해쉬맵(HashMap)에 객체를 채워넣고 그것을 다시 받는것을 반복하는 샘플 벤치마크 예제의 결과를 살펴보자. 각각의 벤치마크가 보여주는 실행시간은 수 많은 샘플 실행의 평균 값이다.

당연히 벤치마크는 -Xcomp 가 최고라는 것을 보여준다. 하지만 여전히, 그리고 특별히 아주 오랜시간동안 실행되는 애플리케이션에 대해서, 나는 모든 사람들에게 강력하게 JVM 기본 세팅으로 놔두라고 하고 JIT 컴파일러의 다양한 잠재능력을 모두 사용하도록 만들라고 조언한다. 결국, JIT 컴파일러는 JVM의 아주 복잡하고 정교한 컴포넌트(component)중에 하나이다. – 사실, 현재 이 부분의 발전은 오늘날 자바가 더 이상 느리지 않다고 생각하게 만드는 가장 큰 이유다.

댓글

역주) 위 글에 댓글에 아주 흥미로운 댓글이 있어서 같이 번역해 보았습니다.

Tj Says:

조금 헷깔리는게 – 나는 자바 컴파일러가 실행타임에 JVM에 의해서 실행되어지도록
소스 코드를 바이트코드로 바꾸도록한다고 생각했다. 그래서 JVM은 오직 바이트코드만 사용할 수 있는거지.
근데 니가 말하는 -Xint 에 의해서 오직 인터프리터된다는 말은 무슨 뜻이냐?
너는 JVM이 JIT 컴파일러 움직임없이 직접적으로 바이트코드를 이해한다고 생각한거야?

Patrck Peschlow says:

Hi Tj,

그래, 너의 생각이 맞아. JVM의 입력은 바이트코드야. 그 이후에 프로그램이 실행되는 동안에 바이트 코드를
어떻게 다룰지하는 몇가지 선택지가 있어. 실행시점에서 JVM은 처음부터 네이티브코드로 컴파일하지 않고
오직 바이트코드로만 인터프리트(interpret)해. 사실 니가 실행하는 모든 자바 프로그램은 일반적으로 그것이
실행되는 동안 인터프리트된 바이트코드의 일부 조각일뿐이다.

이전에 JVM들은 인터프리터만 가지고 있었지. 그래서 전체 자바 프로그램 바이트코드는 오직 인터프리터되었어.
그것이 이전 몇년동안 자바가 느리다고 여겼던 주요한 이유였어. 지금은 현대의 JVM들은 여전히 커맨드라인에서
-Xint 을 지정하는 것으로 오직 인터프리터된 모드로 사용하는것을 허용해.
간단하게 -Xint 에다가 추가적으로 커맨드라인에 -XX:+PrintCompilation 을 추가하면 바이트코드의
네이티브 코드 컴파일화가 발생되지 않는다는 것을 볼 수 있어.
-Xint 없이 똑같은 자바 프로그램을 실행해서 나오는 결과를 비교해보라고.

JVM이 바이트코드-네이티브 컴파일화를 지원하도록 시작되었을때, 사람들은 맹목적으로 각각 모든 메소드가
네이티브 코드로 컴파일된다는 것이 말이 되지 않는다는걸 깨달았지. 대신 지금은 “just-in-tim-compilation”과
“HotSpot” 으로 알려진 컨셉/기술로 개발되었어.아주 간단하게 말해서, 처음 시작되면 JVM은 전부
바이트코드로 인터프리터하고 프로그램이 실행되는 동안에 네이티브 코드로 컴파일할 방법을 결정을 하지.

오직 “hot” 메소드들에서 아이디어는 효과적인 네이티브 코드를 생산하는데 필요한 컴파일링/최적화 노력은
가치가 있다는 거야. 이와 반대로 “cold” 메소드들은 그들이 “hot”이 될때까지 인터프리터된 모드로
다루어질거야. – 어떤 메소드들은 결코 “hot”이 될일이 없을테지만.

그런데, 니가 실행타임에 instrument 메소들이나 클래스를 다이나믹하게 릴로드할때, 새로운 바이트코드는
오래된 버전이 이미 컴파일되어 있다고 하더라도 일반적으로 얼마동안 인터프리터되서 존재할 거야. 그리고,
모든 새로운 바이트코드의 일부를 보게되면, 일반적으로 JVM은 그것이 “hot” 한지 아닌지를 결정하는데
얼마간의 시간을 소비하게 되지.

이건 말이지 모든 현대의 JVM 실행에서 bytecode interpretation 을 찾을 수 있다는 것을 의미해.
만약 니가 그것을 호출했을 시에 모든 메소드를 컴파일된 네이티브코드로 존해하길 원한다면 커맨드라인에서
-Xcomp 를 지정해주면 돼. 그렇지만 나는 진심으로 이 방법을 추천하지 않아.
오늘날 JVM들은 충분히 똑똑하다구.