Category: Java

자바 프로그래밍에 대해 정리한 카테고리.

유용한 JVM 플래그들 – Part 5 (Young Generation Garbage Collection)

이번 시간에 우리는 주요한 힙 영역의 하나인 “young generation” 에 집중한다. 첫째로, 우리는 우리의 애플리케이션의 성능에 아주 중요한 young generation의 알맞은 설정이 무엇인지 논의한다. 그리고나서 우리는 적절한 JVM 플래그들에 대해서 알아보도록 하자.

순수하게 기능적인 관점에서, JVM은 young generation 을 전혀 필요하지 않는다. – 그것은 하나의 힙 영역으로만으로도 동작한다. 첫위치에 young generation 을 가져야할 유일한 이유는 가비지 컬랙션(Garbage Collection, GC) 성능을 최적화하는데 있다. 구체적으로, young generation 과 old generation 으로 힙의 분리는 두가지 장점을 가진다. 새로운 객체의 할당을 간소화해주고 (왜냐하면 메모리 할당은 young generation 에 영향을 주기 때문) 더 이상 필요하지 않은 객체를 좀 더 효과적으로 클린업 해준다.(두 generation 에 서로 다른 GC 알고리즘을 사용해)

객체 지향 프로그램의 넓은 범위에 걸쳐 광범위 측정은 많은 응용 프로그램이 공통 특성을 공유하는 것으로 나타났다: 대부분의 객체는 젊었을때 죽는다. 예를들어 그들이 생성되고 난 후에 프로그램 흐름상 오랫동안 참조(reference) 되지 않았을때. 또, 이것은 젊은 객체들은 좀 더 늙은 객체들에 드물게 참조되어졌다는 것이 관찰되었다. 지금바로 우리가 이러한 두가지 관점을 조합한다면, GC가 young 객체에 빠른 접근을 가질수 있도록 하는것이 바람직하다는게 분명해 진다. – 예를들어 이 힙 영역안에 “young generation” 으로 불리우는 분리된 힙 영역에서, GC 는 그들을 판별할 수 있고 오랜시간동안 힙에서 여전히 살고 있는 모든 old 객체들사이에서 그들을 찾을 필요없이 빠르게 죽은 young 객체를 수집한다.

더나가 Sun/Oracle HotSpot JVM은 young generation은 세개의 서브 영역들로 나뉜다: “Eden” 으로 이름붙여진 하나의 큰 영역과 “From”과 “To”로 이름붙여진 두개의 좀 더 작은 “survivor spaces”. 규칙처럼, 새로운 객체는 “Eden”에 할당되이진다. (예외적으로 만약 새로운 객체가 “Eden” 공간에 들어가기에 너무나 크다면 old generation 에 직접 할당되어진다) GC 동안, “Eden”에 살아있는 객체들은 첫째로 survivor 영역으로 이동되고 그들이 특정한 시점에 도달할때까지 그곳에 머물고나서 (in terms of numbers of GCs passed since their creation) old generation 으로 전달된어진다. 따라서, survivor 영역의 롤(role)은 나중에 그들이 죽는 순간에 그들을 빠르게 수집하는게 가능하게 하기위해서 그들의 첫번재 GC보다 좀더 오랫동안 young generation 에 young 객체를 유지시키는 것이다.

대부분의 young 객체들이 GC 중에 삭제되어질 것이라는 사실에 기반해, 복사하기 전략(“copy collection”) 은 young generation GC 를 위해서 사용되어 진다. GC가 시작되면, survivor 공간 “To”는 비어지고 객체들은 오직 “Eden” 혹은 “From”에만 존재할 수 있다. 그때, GC 중에, “Eden”의 여전히 참조되어지고 있는 모든 객체들은 “To”로 이동되어진다. “From” 에서는 이 공간에서 여전히 참조되고 있는 객체들은 그들의 나이에 의존해 다루어진다. 만약 특정 나이에 도달하지 않으면(“tenuring threshold“”), 그들 또한 “To”로 이동되지만 그렇지 않은 경우에는(특정한 나이에 도달하면) old generation 으로 이동되어진다. 이 복사하기 과정이 끝날때에, “Eden” 과 “From”은 빈 공간으로 여겨지고(왜냐하면 그들은 오직 죽은 객체들만 가지고 있으니까.) 모든 young generation 에서 살아있는 객체들은 “To”에 있게 된다. GC 중에 특정시점에 “To”를 채워야하는데, 남아 있는 모든 객체들은 old generation 으로 이동된다.(그리고 결코 되돌아오지 않는다). 마지막으로, “To”가 다음 GC에 다시 비워지고 “From”이 모든 남은 young 객체들을 가지고 있기 위해서 “From”과 “To”는 그들의 롤들을 바꾼다.(더 정확하게는 그들의 이름을 바꾼다.)

요약을 하면, 객체는 보통 “Eden” 에서 태어나고 각 young generation GC 때마다 servivor 공간들 사이를 왔다갔다 한다. 만약 객체들이 많은 young generation GC를 통과할때까지 생존한다면, 그것은 최종적으로 old generation 으로 옮겨지고 다른 살아있는 객체들과함께 머물게 된다. 마지막으로 객체가 old generation 에서 죽었을때, 아주 무거운 GC 알고리즘중에 하나때문에 좀 더 많은 노력으로 그것들을 수집하게된다. (단순한 복사 컬렉션은 여기서 사용될 수 없다 - there simply is no place to copy to)
요약을 하면, 객체는 보통 “Eden” 에서 태어나고 각 young generation GC 때마다 servivor 공간들 사이를 왔다갔다 한다. 만약 객체들이 많은 young generation GC를 통과할때까지 생존한다면, 그것은 최종적으로 old generation 으로 옮겨지고 다른 살아있는 객체들과함께 머물게 된다. 마지막으로 객체가 old generation 에서 죽었을때, 아주 무거운 GC 알고리즘중에 하나때문에 좀 더 많은 노력으로 그것들을 수집하게된다. (단순한 복사 컬렉션은 여기서 사용될 수 없다 – there simply is no place to copy to)

이제 young generation 크기가 왜 중요한지 확실해졌다. 만약 young generation 이 아주 작으면, 짧게 생존하는 객체들은 그들이 수집되기 좀 더 힘든 old generation 으로 빠르게 옮겨진다. 반대로 young generatin이 아주 크면, 우리는 어쨌거나 나중에 old generation 으로 옮겨지게될 오랫동안 생존한 객들을에대해서 불필요하게 아주 많은 복사가 이루어진다. 따라서 우리는 작은 young generation 크기 와 큰 young generation 크기 사이에 접점을 찾을 필요가 있다. 불행하게도, 특정한 애플리케이션에 대해 올바른 접점 찾기는 의미론적 측정과 튜닝에 의해서만 이루어진다. 그리고 그것은 JVM 플래그 설정으로 된다.

-XX:NewSize and -XX:MaxNewSize

전체 힙 크기와 유사하게(-Xms and -Xmx 가지는) 이것도 young generation 의 크기의 양을 명시적으로 하한과 상한으로 지정이 가능하다. when setting -XX:MaxNewSize we need to take into account that the young generation is only one part of the heap and that the larger we choose its size the smaller the old generation will be. 이것은 old generation 보다 you gerneration 크기를 좀 더 크게 선택하지 말아야 하는데, 모든 객체들을 young generation 에서 old generation 으로 이동하기 위해 GC가 필요할 경우에 최악의 상황이 된다. 따라서 -Xmx/2 는 -XX:MaxNewSize 에 대한 상한이다.

성능상의 이유로 우리는 -XX:NewSize 플래그를 사용함으로써 young generation 의 초기 크기를 지정할 수 있다. 우리가 어린 개체가 할당되는 속도를 알고 천천히 시간이 지남에 따라 그 크기로 젊은 세대의 성장에 필요한 비용의 일부를 절약 할 수 있는 경우에 유용하다.

-XX:NewRatio

이것은 또한 old generation 의 상대적인 비율로 young generation 크기를 지정하는게 가능하다. 이러한 접근의 잠재적인 이득은 JVM이 런타임으로 전체 힙크기를 동적으로 변경할때에 young generation 은 자동적으로 자라고 줄어든다. -XX:NewRatio 는 old generation 이 young generation 보다 더 크게 지정되도록 해준다. 예를들어, -XX:NewRatio=3 일때 old generation 는 young generation 보다 3배 더 크게 된다. old generation 은 힙의 3/4 을 young generation 은 힙의 1/4 를 얻게 된다.

만일 우리가 young generation 크기의 절대치와 상대치를 혼용한다면, 절대 값들이 늘 우선한다. 아래의 예를 생각해보자.

이러한 세팅들은, JVM 은 old generation 의 1/3 로 young generation 크기로 유지하기 위해 애쓸테지만 young generation 이 결코 32MB 이하나 512MB 이상을 초과하지 않도록 한다.

절대 또는 상대적인 young generation 크기가 존재한다면 바람직한 일반적인 룰은 없다. 만약 애플리케이션의 메모리 사용량을 잘 알고 있다면, 전체 힙과 young generation 양쪽 모두 고정된 크기로 지정하는 것이 유리할 수 있고 이것은 비율을 지정하는데도 유용할 수 있다. 만약 우리가 조금만 알거나 애플리케이션에 대해서 전혀 아는게 없다면, 이러한 접근은 단순하게 JVM 을 동작하게하고 플래그 주변을 지저분하게 하지 않게 한다. 만약 애플리케이션이 부드럽게 동작한다면, 우리는 아무것도 필요하지 않은 경우 여분의 노력을 하지 않아도된다는 것에 행복해질 수 있다. 그리고 우리는 성능 문제나 OutOfMemoryErrors가 발생하면 우리는 여전히 튜닝을 하기전에 문제의 근본 원인을 좁힐 수 있는 의미있는 일련의 측정이 필요하다.

-XX:SurvivorRatio

플래그 -XX:SurvivorRatio 는 -XX:NewRatio 유사하지만 young generation 내부 영역에 적용된다. -XX:SurvivorRatio 의 값은 두 survivor 공간중 하나에 상대적으로 얼마나 크게 Eden 을 정할건지를 지정한다. 예를들어, -XX:SurvivorRatio=10는 “To”에 비해서 “Eden”이 10배 큰 치수를 가리킨다. (그리고 같은 시점에서 “From”에 비해 10배 크게). 결과적으로, “Eden”은 young generation 에서 10/12 를 가지는 반면에 “To”와 “From”은 각각 1/12 를 가진다. 주목할 것은 두개의 survivor 공간은 늘 같은 크기다.

survivor 공간의 크기가 주는 영향은 무엇일까? survivor 공간은 “Eden”과 비교해서 아주 작다고 가정한다. 그리고 우리는 “Eden”에 새롭게 할당된 객체를 아주 많이 가지고 있다고 생각해보자. 만약 이러한 모든 객체들이 다음번 GC 동안 모두 수집되어질 수 있다면, “Ede”은 다시 비워지고 모든것은 괜찮아진다.하지만, 만약 이러한 young 객체들중에 몇몇이 여전히 참조되어지는 상태라면, 우리는 survivor 공간에 그들을 수용하기 위해 아주 작은 공간을 가진다. 결론적으로, 이러한 대부분의 객체들은(아직도 참조되어지고 있는 객체들) 첫번째 GC 이후에 old generation 으로 옮겨질 겁니다. 이제 반대 상황을 생각해보자. survivor 공간의 크기가 상대적으로 크다고 가정해보자. 그리고 그들은 자신들의 목적을 수행하기 위해서 많은 공간을 가지고 있다면 객체를 수용하기위해서 하나 이상의 GC들을 발생시키지만 여전히 young 객체는 죽는다. 그러나, 아주 작은 “Eden” 공간은 좀 더 빠르게 고갈될 것이고, 실행되는 young generation GC들의 양은 증가한다.

요약하면, 우리는 너무 빠르게 old generation 으로 이동되어지는 짧은 삶을 사는 객체들의 숫자를 최소화하길 원한다. 하지만 우리는 또 young generation GC 시간과 발생 숫자를 최소화하길 원한다. 다시 말해서 우리는 우리들 스스로 애플리케이션의 특성에 따라서 타협점을 찾을 필요가 있다. 알맞은 타협점을 찾기위한 좋은 시작점은 특정 애플리케이션에 객체들의 연령 분포에 대해서 아는 것이다.

-XX:+PrintTenuringDistribution

-XX:+PrintTenuringDistribution 플래그로 우리는 각각 young generation GC 의 survivor 공간에 포함된 모든 객체들의 연령 분포(age distribution)를 출력하게 할 수 있다. 아래 예제를 보면,

첫번째 라인은 “To” survivor 공간의 타켓 사용률(target utilization)이 약 75MB 임을 말해준다. 또, old generation 으로 이동되지 전에 객체가 young generation 에 머무는 GC의 수를 나타내는 “임계 보유기간”에 대한 정보도 보여준다. (i.e., 이것이 추진되기 전에 객체의 최대 수명) 이 예제에서, 우리는 현재 “임계 보유기간”은 15이고 최대값도 15임을 알 수 있다.

다음 라인은, “임계 보유기간” 보다 훨씬 어린 각각의 객체, 현재 그 나이를 가지는 모든 객체의 총 바이트 수를 보여준다 (만약 객체가 현재 특정 연령에 대해 존재하지 않는 경우, 그 라인은 생략된다). 예를들어, 약 19MB 는 한번 GC에서 살아남은 양, 약 79KB는 두번 GC로 살아남은 양, 약 3MB는 세번째 GC에서 살아남은양이다. 각 라인에 끝에는, 그 연령(age)에 올라온 모든 객체들의 총 양(byte) 이다. 따라서 , 마지막 라인에 “Total” 값은 “To” survivor 공간에 현재 약 22MB 의 객체 데이터를 포함한다는 것을 가리킨다. “To”의 타켓 사용률(target utilization)은 75MB 이고 현재 “임계 보유기간”은 15, 우리는 현재 young generation GC 로인해서 old generation 으로 승진시켜야할 객체가 없다는 결론을 내릴 수 있다. 이제 다음 GC가 다음과 같은 출력을 이끌어내는 것을 가정해보자.

이전에 보유기간 분포의 출력과 비교해보자. 이전 출력에서 2, 3세대(age) 의 모든 객체들은 여전히 “To” 에 있다. 왜냐하면 우리는 3, 4세대에 대해 같은 양이 출력된것을 정확하게 볼 수 있다.(역, 뭔 소린지… total 값이 비슷하다는 건지) 그래서 우리는 GC로 인해서 “To” 에 있는 특정 객체들이 성공적으로 수집되었다고 결론을 내릴 수 있다. 왜냐하면 현재 우리는 2 세대의 객체가 12MB만 가지고 있는 반면에 이전 출력에서 1세대는 19MB를 가졌었기 때문이다. 마지막으로 우리는 약 68MB의 새로운 객체들은, 1세대 부분에서, 마지막 GC 동안 “Eden” 에서 “To”로 이동되었다는 것을 알 수 있다.

주목할 것은 “To” 에 총 bytes 수는 – 여기서는 거의 84MB – desired number of 75MB 보다 크다. 결론적으로 JVM은 보유기간 임계값(tenuring threshold)을 15에서 2로 줄였고, 그래서 다음 GC에 특정 객체들은 “To”로 강제로 이주될 것이다. 이러한 객체들은 만약 특정시간에 죽게되면 수집되어지거나 여전히 참조되고 있다면 old generation 으로 이동되어진다.

-XX:InitialTenuringThreshold, -XX:MaxTenuringThreshold and -XX:TargetSurvivorRatio

-XX:+PrintTenuringDistribution 출력을 보여준 튜닝 노브(knobs)는 다양한 플래그들로 인해서 조절될 수 있다. -XX:InitialTenuringThreshold 과 -XX:MaxTenuringThreshold 는 보유기간 임계값의 초기값과 최대값을 지정할 수 있다. 추가로 우리는 young generation GC 끝 시점에서 “To” 타켓 사용률(target utilization)를 지정하기 위해 -XX:TargetSurvivorRatio 사용할 수 있다. 예를들어, -XX:MaxTenuringThreshold=10 -XX:TargetSurvivorRatio=90 조합은 보유기간 임계값에 대해 10 의 상한과 “To” survivor 공간에 대해 90% 의 타켓 사용률을 지정한다.

young generation 동작을 조정하기 위해 이러한 플래그들을 사용하는 서로다른 접근법이 존재하지만 일반적인 가이드라인은 활용할 수 없다. 명확하게 두가지 경우를 제한한다.

  • 만약 최종적으로 최대 보유기간 임계값에 도달하기전 보유기간 임계값 많은 객체들은 오래됐고 늙었다걸 보여주고 있다면 이것은 -XX:MaxTenuringThreshold 는 아주 크다는 걸 말해준다.
  • -XX:MaxTenuringThreshold 의 값이 1보다 크지만 대부분의 객체가 1보다 큰 세대에 도달하지 못할거라면 우리는 “To” 타켓 사용률(the target utilization)을 찾아봐야 한다. Should the target utilization never be reached, then we know that all young objects get collected by the GC, which is exactly what we want. 그러나, 만약 타켓 사용률이 빠르게 도달한다면, 적어도 1세대 이후에 혹은 조기에 특정 객체들이 old generation 으로 이동되어진다. 이 경우에, 우리는 타켓 사용률과 그들이 크기를 증가시킴으로써 survivor 공간을 개선시도할 수 있다.

-XX:+NeverTenure 과 -XX:+AlwaysTenure

마지막으로, 나는 young generation GC 동작의 두 극단을 테스트하는데 사용할 수 있는 두개의 이국적인 플래그들을 언급하고 싶다. 만약 -XX:+NeverTenure를 지정하면, 객체들은 결코 old generation 으로 옮겨지지 않는다. 이 동작은 old generation 이 필요가 없다고 확신할때 가능하다. 그러나, 그러한 플래그는 분명히 위험하고 예약한 힙 메모리의 절반을 낭비하게 한다. 반대 동작으로 -XX:+AlwaysTenure 는 작동할 수 있다. 첫번째 GC 에서 모든 young 객체들은 즉각 old generation 으로 옮겨 survivor 공간을 사용하지 않게 한다. 이 플래그의 적절한 사용 케이스를 찾기는 어렵다. 테스트 환경에서 어떤 일이 발생할지를 찾기위해서 재미로 할 수 있지만, 나는 이 두 플래그의 사용을 추천하진 않는다.

Conclusion

young generation 에 대한 적절한 구성과 응용 프로그램을 실행하는 것은 중요하며 튜닝을 위해 몇개의 플래그들이 있다. 그러나 old generation 고려없이 young generation 의 튜닝은 성공을 이끌지는 못한다. 힙을 튜닝하거나 GC 세팅을 튜닝할때에 우리는 늘 you generation 과 old generation 의 상호작용을 살펴야 한다.

이 시리즈의 다음 두번에 걸쳐 우리는 HotSpot JVM 에서 제공되는 두개의 기본적인 old generation GC 전략에 대해서 배울 것이다. 우리는 “Throughput Collector” 과 “Concurrent Low Pause Collector” 를 알게될 것이고 그들의 기본적인 원칙, 알고리즘, 튜닝 플래그들을 살펴볼 것이다.

유용한 JVM 플래그들 – Part 4 (힙 튜닝)

이상적으로, 자바 애플리케이션은 아무런 플래그를 지정하지 않은채 기본 JVM 세팅만으로도 잘 동작한다. 하지만 운이 나쁘게도 성능적인 문제에 직면했을때, 관련 JVM 플래그에 대한 지식들은 훌륭한 안내서가 된다. 이번시간에는 메모리 관리 영역에 대한 몇몇 JVM 플래그들을 살펴볼 것이다. 이러한 플래그들에 대해서 알고 있는 것과 이해하는 것은 개발자나 운영자로서의 높은 가치를 증명한다.

모든 설치된 HotSpot 메모리 관리와 가비지 컬렉션 알고리즘은 힙(Heap)의 동일한 기본 분할에 기초한다: “young generation” 은 새롭게 메모리가 할당된 것과 짧은 삶을 사는 객체를 가진다. 반면에 “old generation” 은 특정연령 이상 수명이 긴 객체를 포함한다. 그 외에, “permanent generation” 전체 JVM 라이프사이클을 통해서 살아가야할 객체를 포함하는데 예를들면 로드 된 클래스의 객체 표현이나 문자열 인터 캐쉬등이다. 다음시간부터 우리는 힙이 permanent, old, young generation 들이 전통적인 전략에 따라 분할되어 있다고 가정할 것이다. 그러나, 다른 전략들도 유망하다, 한가지 눈에 뛰는 새로운 G1 가비지 컬랙터의 존재는 young 과 old generation 사이의 구분을 흐리게 한다. 또,  HotSpot JVM 의 미래버전일 수 있는 현재 개발버전에서는 더 이상 old 와 permanent generation들을 구분하지 않을 것이다.

-Xms and -Xmx (or: -XX:InitialHeapSize and -XX:MaxHeapSize)

아주 유명한 JVM 플래그들로 최대 JVM 힙 크기와 초기 JVM 힙 크기를 각각 지정할 수 있도록 해주는 -Xms 와 -Xmx가 있다. 양쪽 모두 기본적으로 bytes 용량단위의 값을 가지지만 짧은 용량 표기법을 지원하는데 “kilo”는 “k”나 “K”, “mega”는 “m”나 “M” 그리고 “giga”는 “g”나 “G”로 표기할 수 있다. 예를들어, 다음의 커맨드 라인은 초기 힙 크기를 128 megabytes 와 최대 힙 크기를 2 gigabytes 로 세팅하고 “MyApp” 자바 클래스를 시작한다.

실제로, the initial heap size turns out to also be a lower bound for the heap size,i.e., 최소 힙 크기(minimum heap size). JVM이 런타임에 동적으로 힙의 크기를 조정할 수 있으며, 따라서 이론적으로 우리는 힙의 크기가 초기 힙 크기 아래로 떨어지는 것을 관찰할 수 있는 것은 사실이지만, 나는 실험적으로 아주 낮은 힙을 사용할때조차도 이러한 것을(이론상으로 가능한 것) 목격한 적은 없다. 이러한 행동은 개발자와 운영자에게 편리한데, 단순하게 -Xms 와 -Xmx 를 같은 값으로 세팅함으로써 정적 힙 크기를 지정할 수 있도록 해주기 때문이다.

-Xms 와 -Xmx 가 짧은 약어이며 이것은 내부적으로 -XX:InitialHeapSize 와 -XX:MaxHeapSize 로 매핑된다. 이러한 두개의 XX 플래그들은 같은 효과을 주기위해서 직접 사용할 수 있다.

주목할 것은 초기 및 최대 힙 크기에 관한 모든 JVM 출력이 독점적으로 긴 이름을 사용한다. 따라서 동작중인 JVM 의 힙 크기에 대한 정보를 찾을때에는, -XX:+PrintCommandLineFlags 의 출력으로 체킹을 하거나 JMX를 통해 JVM 에 질의를한다거나, 우리는 “Xms” 나 “Xmx” 가 아닌 “InitialHeapSize” 나 “MaxHeapSize”를 찾아야 한다.

-XX:+HeapDumpOnOutOfMemoryError and -XX:HeapDumpPath

만약 우리가 -Xmx 세팅을 적절한 값으로 하지 않을 경우에, JVM을 다룰 때 우리가 직면 할 수있는 가장 무서운 짐승의 하나인 OutOfMemoryError 와 마주칠 위험을 동반한채 JVM이 동작한다. 이 주제에대해 이 블로그에서 상세하게 다루었듯이, OutOfMemoryError의 근본원인을 신중하게 다룰 필요가 있다. 종종, 특별히 만약 JVM 이미 크래쉬(Crash) 되었고 애플리케이션이 몇 시간 또는 며칠 동안 부드럽게 실행 후에 오류가 프로덕트 시스템에만(Product system. 역) 실제 서비스를 하고 있는 시스템을 말한다) 나타나는 않았다면 깊이 있는 분석에 대한 좋은 방법은 힙 덤프(Heap Dump)이다. – 만약 사용할 수 없다면 최악이다

운좋게도, -XX:+HeapDumpOnOutOfMemoryError 플래그를 세팅함으로써 OutOfMemoryError 발생했을때에 자동적으로 힙 점프를 생성하도록 JVM에 지시할 수 있다. 단지 그러한 경우에 대한 플래그를 세팅함으로써 예기치못한 OutOfMemoryError 에 직면했을때에 많은 시간을 절약할 수 있다. 기본적으로, 힙 덤프는 JVM이 시작된 디렉토리에 java_pid<pid>.hprof(여기서 <pid>는 JVM 프로세스의 프로세스 ID이다) 파일에 저장된다. 이 기본값을 변경하기 위해서, 우리는 -XX:HeapDumpPath=<path> 플래그를 사용해 다른 위치를 지정할 수 있으며, <path> 는 힙 덤프를 저장할 파일에대해 상대적이거나 절대적인 경로가 될 수 있다.

아주 멋진 말인데, 우리가 명심해야할 게 있다. 힙 덤프는 특히나 OutOfMemoryError 가 발행했을때에 아주 큰걸 얻을 수 있다. 따라서 디스크 사용이 아주 넉넉한 위치를 지정하기 위해 -XX:HeapDumpPath 를 사용할 것을 권장한다.

-XX:OnOutOfMemoryError

우리는 OutOfMemoryError 가 발생했을때 임의 명령행을, e.g., 관리자에게 메일을 보낸다든가 어떤 클린업(Cleanup) 작업을 실행시키다든가하는, 실행시킬 수도 있다. 이것은 명령어들의 리스트와 그들의 파라메터들을 가지는 -XX:OnOutOfMemoryError 플래그로 인해서 가능하다. 우리는 이것에 대해 자세히 나가진 않고 설정예제를 보여주겠다. 다음 명령어 라인 상태에서 OutOfMemoryError 가 발생하면 우리는 /tmp/heapdump.hprof 파일에 힙 덤프를 작성하고 동작중인 JVM 의 사용자 홈 디렉토리에 쉘 스크립트 cleanup.sh 가 실행된다.

-XX:PermSize and -XX:MaxPermSize

Permanent 세대는 JVM에 의해서 로드되는 모든 클래스의 객체표현을 포함하는 것으로 힙 영역과 분리된다. 많은 클래스들을(e.g., because they depend on lots of third-party libraries, which in turn depend on and load classes from even more libraries) 로드하는 애플리케이션을 성공적으로 동작시키기 위해서는 아마도 permanent 크기를 증가시킬 필요가 있을 것이다. 이것은 -XX:PermSize 와 -XX:MaxPermSize 플래그를 사용해 가능하다. 여기서 -XX:MaxPermSize 는 permanent 세대의 최대 크기를 지정하는 것이고 반면에 -XX:PermSize 는 JVM 시작시에 초기화할 크기이다. 예를들어,

주의할 것은 permanent 세대 크기는 -XX:MaxHeapSize 로 지정되어지는 힙 크기의 일부로 계산되지 않는다. -XX:MaxPermSize 의해서 정한 permanent 세대 메모리의 양은 -XX:MaxHeapSize 에의해서 정해진 힙 메모리에 더해서 필요하게 되어질 수 있다.

-XX:InitialCodeCacheSize and -XX:ReservedCodeCacheSize

자주 JVM 메모리 영역에서 무시되어지는 재미있는 영역은 “코드 캐쉬(code cache)” 인데, 이것은 컴파일된 메소드의 네이티브 코드 생성을 저장하는 데 사용된다. 코드 캐쉬는 거의 성능문제를 발생시키지 않지만, 한번 코드 캐쉬 문제가 생기면 그 효과는 어마어마할 것이다. 만약 코드 캐쉬 메모리를 모두 사용하게 되면, JVM 은 경고 메시지를 출력하고 “interpreted-only mode” 로 전환한다. JIT 컴파일러는 정지되고 더이상 바이트코드(bytecode)는 네이티브 코드로 컴파일 될지 않을 것이다. 따라서 애클리케이션이 동작은 계속되지만 크기의 순서에따라 점점 느려진다.

다른 메모리 영역과 같이 우리는 코드 캐쉬 크기를 정할 수 있다. 관련 플래그로 -XX:InitialCodeCacheSize 과 -XX:ReservedCodeCacheSize이 있고 위에서 소개한 플래그들처럼 byte 용량 값을 가진다.

-XX:+UseCodeCacheFlushing

만약 코드 캐쉬가 지속적으로 증가한다면, 예를들어 hot deployment 로 인해서 발생된 메모리 릭으로 인해서, 코드 캐쉬를 늘리는 것은 단지 필연적으로 발생될 오버플로우(OverFlow)를 지연시킬 뿐이다. 오버플로우를 비하기 위해, 코드 캐쉬 메모리가 가득찼을때에 JVM이 몇몇 컴파일된 코드를 정리하도록 시키기위해 우리는 재미있고 비교적 새로운 옵션을 시도할 수 있다. 우리는 -XX:+UseCodeCacheFlushing 플래그를 지정하면 된다. 이 플래그를 사용함으로 인해서 우리는 적어도 코드 캐쉬 문제에 직면했을때에 “interpreted-only mode” 전환되는 것을 적어도 피할 수 있다. 그러나 나는 여전히 코드 캐쉬 문제가 발생되면 가능한 빨리 근본 원인을 차단하길 권장한다. 예를들어, 메모리 릭을 규명하고 그것을 고치는 방법.

유용한 JVM 플래그들 – Part 3 (모든 XX 플래그들과 값들을 프린팅하기)

최근 java 6 의 업데이트 (반드시 20이나 21로 업데이트되 있어야하는), HotSpot JVM은 JVM이 시작되자마자 커맨드라인에 모든 XX 플래그들과 그들의 값을 테이블로 출력하도록 하는 두 개의 새로운 커맨드라인 플래그들을 제공한다. 많은 HotSpot 사용자들은 첫번째 자바버전 이후부터 이러한 기능들을 원했었는데, 그래서 나는 이 글의 주제를 이것으로 하기로 했다.

-XX:+PrintFlagsFinal and -XX:+PrintFlagsInitial

바로 새로운 플래그들에 대한 출력물들을 살펴보도록 하자. -XX:+PrintFlagsFinal 사용해 클라이언트 VM 을 시작하면 알파벳으로 정렬된 590개의 XX플래그 테이블을 얻게된다. (주의할 것은 각 HotSpot버전마다 플래그 숫자는 다를 수 있다.)

각 테이블 행은 XX 플래그들을 나타내고 다섯개의 열을 포함한다. 첫번째 열은 플래그의 데이터 타입을 보여주고, 두번째는 플래그 이름, 네번째는 플래그 값, 다섯번째는 플래그의 카테고리다. 세번째 열에서 “=“는 네번째 플래그의 값이 기본값(Default Value)이라는 걸 의미하며, 반면에 ”:=“는 사용자 혹은 JVM 인체공학에 의해서 지정된 값이라는 것을 의미한다.

주의할 것은 나는 이 예제를 위해서 이전 시간에 사용했던 “Benchmark” 클래스만 사용했다. 여러분은 -version 파라메터를 넣어 자바를 실행시킴으로써 메인 클래스 없이 같은 출력물을 얻을 수 있다.

이제 서버 VM이 얼마나 많은 플래그들을 제공하는지 체크해보자. 모든 추가적인 숨은 플래그들을 풀기위해 우리는 -XX:+UnlockExperimentalVMOptions 와 -XX:+UnlockDiagnosticVMOptions 플래그를 지정해야 한다.

그 결과는 아주 많은 724 플래그들이다. 기본값이 아닌 값이 지정된 플래그들을 살펴보자.

우리는 플래그들 중에 하나만(-XX:+PrintFlagsFinal) 지정 했을 뿐이다. 다른 플래그들은 시스템을 고려해 알맞은 힙의 크기와 가비지 컬렉터 세팅을 가지고 실행하기 위해서 서버 VM에 의해서 지정되었다.

만일 모든 XX 플래그들의 기본값들만 보길 원한다면, 우리는 -XX:+PrintFlagsInitial 플래그를 사용할 수 있다. -XX:+PrintFlagsInitial 에서, 출력은 세번째 열에서 원조 ”=” 엔트리만 보여준다.(also for those flags that were set to other values) 그런데, 주의해서보면 몇몇 플래그들은 -XX:+PrintFlagsFinal의 출력물과 비교해보면 빠져있다는 것이 있는데, 이러한 플래그들은 동적으로 생성되어지기 때문인것으로 추정된다.

클라이언트 VM과 서버 VM의 동작을 비교하기 위해서 이러한 테이블 컨텐츠를 공부하는것은 재미있는 일이다. 또, 커맨드라인에서 지정되었을때에 플래그들이 다른 플래그 값들에 영향을 미치는 것을 알수 있도록 해준다. 관심있는 독자라면, Inspecting HotSpot JVM Options 훌륭한 블로그 포스트를 참고하라. 이 포스트는 다섯번째 열에 나타나는 플래그 카테고리에 대한 의미를 짧게 설명하고 있다.

-XX:+PrintCommandLineFlags

이 토픽의 결론을 위해서, 오랫동안 사용해온 플래그 하나를 생각해보자: -XX:+PrintCommandLineFlags. 이 플래그는 JVM에게 시작시에 사용자나 JVM 인체공학에 의해서 지정된 XX 플래그들의 정확한 이름과 값을 출력하도록 한다. 다른 말로하면, 이것은 -XX:+PrintFlagsFinal 출력물의 세번째 열에서 “:=“를 가지는 플래그 리스트들이다. 이렇게 보면, 수정되는 플래그들에만 관심을 기울릴때에 -XX:+PrintCommandLineFlags 는 -XX:+PrintCommandLineFlags의 단순 버전으로 여겨질 수 있다. 위 예제로 돌아가보면,

이제 우리가 매번 -XX:+PrintCommandLineFlags 를 지정하고 자바 프로그램을 시작시키고 출력을 로그에 기록한다면 우리는 애플리케이션 성능측면에서 JVM 플래그 세팅의 효과를 문서화할 수 있다. 따라서 -showversion 과 유사하게(첫번째 시간에서 살펴봤듯이) 나는 JVM이 시작시 늘 지정해야하는 것처럼 -XX:+PrintCommandLineFlags 플래그로 생각한다. It comes for free, and you never know when you are going to need that information.

-XX:+PrintCommandLineFlags 에서 리스트된 최대 힙 사이즈는 -XX:+PrintFlagsFinal 에서 보여주는 상응한 갑보다 약간 작다는 것에 의문을 가지고 있다. 왜 이렇게 차이가 나는지 알고 있는 사람이 있다면 나에게 좀 알려달라.

유용한 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 동작에 대해 아주 짧은 출력 예제를 살펴보자.

메소드가 컴파일 되어질때 마다, 하나의 라인(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 셧다운시에 출력되어질 수 있는 컴파일에 대한 다양한 통계정보를 요청할 수 있다. 통계정보의 특정한 한 부분을 살펴보자.

(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에서 실행 결과을 살펴보자.

-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 플래그를 스스로 찾아봐라. 아주 좋은 시작점은 이 리스트다.

유용한 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들은 충분히 똑똑하다구.

 

[발 번역] 자바 가비지 컬렉션

이글은 Garbage Collectors Available In JDK 1.7.0_04 를 발 번역한 것입니다.

Jack Shirazi 씨는 가비지 컬랙터가 무엇이고 오파클 자바 7 업데이트 4 JVM 에서 활용할 수 있는 가비지 컬렉터 조합에 대해서 말해줄 것입니다.

Published June 2012, Author Jack Shirazi

마침내 G1 을 공식적으로 – i.e. 더 이상 실험적 가비지 컬렉터가 아니다 – 1.7.0_04 (Java 7 update 4) 릴리즈에서 지원되며 이것은 이제 Sun JVM 의 가비지 컬렉터 측면에서 활용가능한 가치있는 주식(?)을 가지는 중임을 말한다. 아래에 기술할 자세한 사항은 다른 많은 Sun JVM 버전에 적용가능성이 높지만 특별히 Sun 1.7.0_04 JVM 과 연관된다.

현재 7개의 주요한 가비지 컬렉션 알고리즘이 있고 그중에 하나는(PS Scavenge), 우리가 두가지 다른 알고리즘으로 부르는(adaptive GC 를 가진것과 가지지 않은것), 확실히 다른 두가지 모드(Mode)를 가지고 있고 아주 많은 옵션들을 가지는 다른것들은(concurrent collector) 실질적으로 나머지 절반의 알고리즘을 구성한다. 이것은 우리가 여기서 해야할 가비지 컬렉터 리스트를 위해 유용하다.

첫째로, 나는 실질적으로 다른 주요한 가비지 컬렉터들을 특징지을 것이다. 그것은 7가지다.(나는 G1 컬렉터까지 포함했다.)

Garbage CollectorGeneration
CopyYoung generation
PS ScavengeYoung generation
ParNewYoung generation
G1 Young GenerationYoung generation
PS MarkSweepOld generation
ConcurrentMarkSweepOld generation
G1 Mixed GenerationOld generation

Young generation collectors

Copy (enabled with -XX:+UseSerialGC)

the serial copy collector는 에덴(Eden) 영역에서 살아남은 객체들을 Survivor space로 복사하기 위해서 그리고 Survivor space 사이에서 객체들을 old generation 으로 복사할 시점에서 충분히 오랫동안 객체가 존재하는지 결정될때까지 하나의 쓰레드(thread)를 사용합니다.

PS Scavenge (enabled with -XX:+UseParallelGC)

the parallel scavenge collector 는 복사 컬렉터(the serial Collector)와 유사하지만 병렬로 여러개의 쓰레드를 사용하고 old generation 을 수집하는 방법에 대한 몇가지 지식을 가지고 있다. (기본적으로 serial 과 PS old gen 컬렉터를 함께 동작하도록 작성하라)

ParNew (enabled with -XX:+UseParNewGC)

the parallel copy collector 는 복사 컬렉터(the serial Collector)와 유사하지만 병렬로 여러개의 쓰레드를 사용하고 old generation collector 가 수집하는 객체에서 작동하도록 허용한 내부 콜백(callback)을 허용한다. (실제로 concurrent collector와함께 동작하도록 작성하라)

G1 Young Generation (enabled with -XX:+UseG1GC)

the garbage first collector 는 힙영역을 수 많은 작은 공간으로 나누는 ‘Garbage First’ 알고리즘을 사용하지만 이것은 여전히 G1 을 위해서 young genration 을 Eden 영역과 Survivor space로 나눈다.

Old generation collectors

MarkSweepCompact (enabled with -XX:+UseSerialGC)

the serial mark-sweep collector는, 모든 컬렉터의 아버지, 옵션으로 압축(compaction) 기능을 가지는 시리얼(하나의 쓰레드) 풀 mark-sweep 가비지 컬렉션 알고리즘을 사용한다.

PS MarkSweep (enabled with -XX:+UseParallelOldGC)

the parallel scavenge mark-sweep collector, MarkSweepCompact 의 패러럴 버전이다.

ConcurrentMarkSweep (enabled with -XX:+UseConcMarkSweepGC)

the concurrent collector, 컬렉션이 동작하는 동안 애플리케이션 쓰레드들이 정지 없이 백그라운드로 대부분의 가비지 컬렉션 작업을 하도록 시도하는 가비지 컬렉션 알고리즘이다. (여전히 애플리케이션 쓰레드가 정지되는 단계가 있지만 이러한 단계는 최소로 유지되도록 시도된다.) 주의할 것은 concurrent collector 가 가비지를 수집에 실패한다면, 다음번 GC를 위한 the serial MarkSweepCompact collector 도 실패한다.

G1 Mixed Generation (enabled with -XX:+UseG1GC)

the garbage first collector는 힙(heap)을 아주 많은 작은 공간들로 나누는 ‘Garbage First’ 알고리즘을 사용한다.

ConcurrentMarkSweep 제외한 모든 가비지 컬렉션 알고리즘은 stop-the-world 고, i.e 그들이 동작하는 동안에 모든 애플리케이션 쓰레드들을 정지시킨다 – 정지(the stop)은 ‘멈춤시간(pause time)’ 으로 알려져 있다. ConcurrentMarkSweep 는 모든 작업을 백그라운드로 동작하고 멈춤시간을 최소화하도록 노력하지만 이 컬렉션도 stop-to-world 단계를 가지며 완전한 stop-the-world 를가지는 MarkSweepCompact 가 실패할수 있다.

Combinations of Garbage Collectors

그것은 활용할 수 있는 가비지 컬렉션의 집합이지만, 그들은 두개의 서로다른 힙(heap) 공간에서 운영되고 이것은 우리가 실제로 특정한 JVM 세팅을 가지는 조합이며, 그래서 나는 가능한 조합들을 보여줄 것이다. 이것은 이러한 모든 컬렉션들은 서로함께 동작하기 때문에 여러 묶음 조합이 나올수 없다. G1 다른것들과 동작하지 않는 반사회적(antisocial) 컬렉터이고 the serial collector 는 최후의 보루 컬렉터, ‘PS’ 컬렉터는 다른 것들과 함께 동작하는게 좋고 ParNew 와 Concurrent는 서로함께 동작하는게 좋다. 물론 이렇게 아주 단순화 할수는 없지만, 그래서 여기에 내가 생각하는 가비지 컬렉션 알고리즘 옵션들내에 메인 옵션 리스트들이다. 퀴즈의 비트를 좋아하는 부류의 사람들에게서는, 다양한 JVMS 들에서 -Xincgc 와 -XX:+UseTrainGC 사용해 활용할 수 있는(이러한 플래그는 더 이상 유용하지 않으며 정식 플래그에서는 ParNew 와 Concurrent 를 사용하는것으로 변경되었으며 -XX:+UseTrainGC 플래그는 이 JVM에서 시작시 에러를 발생시킨다) “train” 가비지 컬렉터를 우리는 시간이 지남에따라 잃어버렸다.

GC 알고리즘을 조합해 동작할 수있는 Full list:

Command Options (1)Resulting Collecto Combination
-XX:+UseSerialGCyoung Copy and old MarkSweepCompact
-XX:+UseG1GCyoung G1 Young and old G1 Mixed
-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:+UseAdaptiveSizePolicyyoung PS Scavenge old PS MarkSweep with adaptive sizing
-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:-UseAdaptiveSizePolicyyoung PS Scavenge old PS MarkSweep, no adaptive sizing
-XX:+UseParNewGCyoung ParNew old MarkSweepCompact
-XX:+UseParNewGC -XX:+UseConcMarkSweepGCyoung ParNew old ConcurrentMarkSweep (2)
-XX:-UseParNewGC -XX:+UseConcMarkSweepGCyoung Copy old ConcurrentMarkSweep (2)
(1) 이 리스트들의 모든 조합은 오직 -XX:+UseConcMarkSweepGC 와 조합할 수 있는 -XX:+UseParNewGC를 제외하고 이 리스트에 없는 다른 GC 알고리즘을 추가하하고 JVM을 시작시키면 실패할 것이다.
(2) 알고리즘을 바꾸는 -XX:+UseConcMarkSweepGC 와 함께 사용할 경우 아주 아주 많은 옵션들이 있다. 예를들어
* -XX:+/-CMSIncrementalMode - an incremental concurrent GC algorithm 를 사용 혹은 비활성.
* -XX:+/-CMSConcurrentMTEnabled - parallel (multiple threads) concurrent GC algorithm 을 사용 혹은 비활성.
* -XX:+/-UseCMSCompactAtFullCollection - Full GC가 발생할때 압축(compaction)을 사용 혹은 비활성

위와 똑같은 다른 옵션들

Command Options Used On Their OwnEquivalent To Entry In Table Above
-XX:+UseParallelGC-XX:+UseParallelGC -XX:+UseParallelOldGC
-XX:+UseParallelOldGC-XX:+UseParallelGC -XX:+UseParallelOldGC
-Xincgc-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
-XX:+UseConcMarkSweepGC-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
no option on most Windows-XX:+UseSerialGC (see also this page)
no option on most Unix-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:+UseAdaptiveSizePolicy (see also this page) - 역주) 이것은 잘못된 내용이다. Java 버전에 따라 Default 옵션이 다르다.
-XX:+AggressiveHeapOS와 어떻게 서로 상호작용하고 메모리와 쓰레드의 크기와 연관된 옵션들의 묶음인 -XX:+UseParallelGC -XX:+UseParallelOldGC -XX:+UseAdaptiveSizePolicy

JAVA에서 스크린 Refresh 하기

리눅스 터미널에서 실행되는 각종 모니터링 프로그램들을 보면 화면이 그대로 인채 수치만 바뀌는 형식을 취하는 프로그램들이 많다. 사실 터미널 스크린을 빠르게 Refresh 하는것인데, 자바에서는 다음과 같이 프로그래밍을 하면 된다.

Console 객체를 이용해 구현한다.

Jmxterm – 커맨드 라인 JMX

JAVA 에는 JMX(Java Management Extension) 이라고해서 JAVA 애플리케이션을 관리하기 위한 확장을 제공합니다. JAVA 애플리케이션을 시작할때에 다음과 같이 JVM 옵션을 주게되면 사용할 수 있습니다.

  • -Dcom.sun.management.jmxremote
  • -Dcom.sun.management.jmxremote.port
  • -Dcom.sun.management.jmxremote.authenticate
  • -Djava.rmi.server.hostname
  • -Dcom.sun.management.jmxremote.ssl

hostname, port 그리고 authenticate 을 설정하면 특정 호스트에서 특정포트를 통해서 인증을 통해서 JMX 클라이언트를 통해서 접속할 수 있는데, JMX 클라이언트로 가장 유명한 것이 JConsole 입니다.

jconsole

그런데, 보안상의 이유로 JMX 는 활성화하되 접속은 로컬호스트에서만 되도록 설정을 해놓는 경우가 많습니다. 다음과 같이 말입니다.

대부분의 로컬호스트가 리눅스일 경우에는 JConsole 을 이용하기 위해서는 터미널의 터널링을 해주는 방법등을 동원하는데 JConsole 이 아닌 jmxterm 프로그램을 이용하면 리눅스 터미널에서 jmx 를 활용할 수 있습니다.

jmxterm

jmxterm 은 JMX Terminal 로 자바로 작성된 커맨드 라인 JMX 클라이언트 입니다. 다른 추가적인 라이브러리가 필요없이 jdk 만 있으면 동작하고 사용자와 상호작용을 할수 있도록 Interactive 하게 동작합니다.

또, JMX 서버 설정대로 인증을 할 수 있고 로컬호스트라면 JAVA 애플리케이션의 PID 를가지고 접속을 할수 있습니다.

Interactive 작업시 탭키(Tab key) 를 이용한 자동완성기능(Auto completion)을 제공합니다. 따라서 MBean domain 이나 Beans 들을 전부 외울 필요가 없습니다.

다운로드 

다운로드는 다음의 주소에서 다운받을 수 있습니다.

사용방법.

이제부터 간단한 예제를 통해서 어떻게 사용하는지 알아보겠습니다.  먼저 Tomcat 을 실행하는데, JMX 를 로컬호스트를 통해서만 접속할 수 있도록 옵션을 주고 실행했습니다. 다음과 같이 말이지요.

JMX 포트는 8090 이고 인증은 없으며 로컬호스트에서만 접속되도록 했습니다.

이제 jmxterm 을 실행합니다.

위와같이 $> 프롬프트가 나옵니다. 여기서 이제 JMX로 접속을 해야 합니다. 프롬프트에서 다음과 같이 입력을 합니다.

접속방법은 위와같이 “open 로컬아이피:포트” 형식입니다. 만일 Tomcat 의 PID 를 알고 있다면 “open PID” 형식으로도 접속을 할 수 있습니다.

이제부터는 JMX 의 MBeans Domain, Beans 를 이용해서 JMX 의 속성들을 찾을 수 있습니다. 먼저 Domain 목록은 다음과 같이 조회를 합니다.

domain 리스트를 확인했으니 domain 을 지정할려면 “domain 도메인명” 을 입력해주면 됩니다.

domain을 지정했으니 이제는 bean 들을 볼 수 있습니다. 다음과 같이 합니다.

그럼 Bean 을 지정할려면 어떻게 해야할까요? 짐작대로 “bean 빈명” 해주면 됩니다.

bean 은 각종 속성과 액션들을 가지고 있는 셋트입니다. 이들을 보기 위해서는 info 명령어를 사용합니다.

위와같이 bean 에 속하는 속성들을 볼 수 있고 액션도 볼수 있습니다.(위 예제에서는 액션은 안나왔네요.)

속성이 가지고 있는 값을 보기 위해서는 “get 속성” 을 해주시면 됩니다.

각 속성들은 리턴타입에 맞게 화면에 표시됩니다.

이렇게 리눅스 터미널에서도 얼마든지 JMX 를 이용할 수 있습니다.

Java 디폴트 옵션 알아내기.

Java 로 프로그램을 실행할때에는 많은 옵션들을 줄 수가 있다. 대표적으로 최소, 최대 힙(Heap) 크기와 가비지 컬렉터(Garbage Collector)를 무엇으로 할 것인지 하는 것이다.

그런데, 이러한 옵션들이 없다면 Java 는 기본적으로 어떤 값으로 동작을 하게될까? 이 물음에 대한 답은 다음의 명령어로 알아 볼 수 있다.

위와 같이 실행을 하면 기본적인 값들을 보여준다. 다음은 Linux X86_64 에서 Java 버전별로 가지는 기본값들 이다.

jdk1.6.0_45

jdk1.7.0_75

jdk1.8.0_40

 

tomcat jmx 소스

JMX 소스