Tagged: 자바

Log4j 취약점, 한국 IT 보안에 대한 단상

몇일 전에 Apache 재단에서 제작해 배포하는 Log4j2 에 대해, 보안 취약점 등급 10등급으로 즉시 취약을 말하면서 보안설정과 패치를 하라는 권고를 했다. 이는 전 세계적인 현상으로 우리나라 뿐만 아니라 미국등에도 매우 심각한 현상임을 인지하고 뉴스에도 나올만큼 큰 뉴스거리가 되었다.

Log4j2 만 문제냐

하지만 이 업계에 10년 넘게 밥벌이를 하는 입장에서 별로 놀랍지도 않고 왜 그렇게 호들갑이냐 하는 생각이 먼저 든다. 모르는 사람이야 뉴스에도 나오고 전문가들이 심각하다고 하는데 ‘호들갑’ 이라고 말하니 내가 뭔가 잘못 말한것처럼 생각이 들겠지만 실상을 알고 나면 그렇지 않다는걸 깨닫게 된다.

먼저, 내가 현재 맡아서 하고 있는 것들을 한번 보자. 대략 다음과 같은 스펙으로 서비스되어지고 있다.

  1. RedHat Enterprise Linux 7.7
  2. Spring Boot 1.5
  3. Java 1.8
  4. WildFly 13

대충 이렇게 나열해 보면 이게 대체 뭐가 문제냐 하는 생각이 들 것이다.

KISA 정보에서 발행한 ‘주요정보통신기반시설 기술적 취약점 분석, 평가 방법 상세가이드’ 라는 것이 있다. 여기에는 특정 요소요소에 대해서 보안 설정을 어떻게 해야하는지를 가이드하고 있는데, 여기에 ‘U-42. 4.1 최신 보안패치 및 벤더 권고사항 적용’ 이란게 있다.

보안 가이드 패치권고

요약을 하자면 항상 최신의 패치 적용을 유지하라는 것이다.

그런데, RedHat Enterprise Linux 7.x 에 최신 버전은 7.9 이다. 더 최신은 RedHat Enterprise Linux 8.5 다. 시스템에 설치된 어떤 프로그램이 보안 취약점을 들어내고 있는지에 대해서 알지도 못하고 어짜피 네트워크가 단절되어 있어서 외부에 침입이 어렵다는 변명으로 운영체제(OS)에 대한 최신 패치 적용을 전혀 하지 않는다.

최신의 RHEL 을 사용하지 않는건 일단 내려놓더라도 쓰지도 않는 각종 프로그램들이 다 설치되어 있다. 대표적인게 gcc 컴파일러이다. 어디 gcc 컴파일러 뿐이랴.. g++ 도 있고 automake, autoconf, bison 등 C 소스코드를 컴파일 할 수 있도록 아주 다 깔려 있다.

크랙커가 가장 좋아라하는 시스템이 뚫어서 들어가보니 각종 컴파일러가 설치되어 있는 시스템이다. 컴파일러만 설치되어 있으면 마음대로 시스템을 가지고 놀수 있다. 그래서 될 수 있으면 컴파일러는 설치를 하지 말아야 한다. 아니 더 정확하게는 사용하지 않거나 불필요한 프로그램들과 명령어는 아예 설치를 하지 말아야 한다.

더 놀라운 것은 Java 시스템인데도 각종 gcc, g++ 컴파일러가 설치되어 있고 각종 라이브러리까지 아주 풍부하다는 것이다. 이에 대해서 이러면 안된다고 하면, 각종 보안 프로그램들이 철벽으로 작동하고 있고 ISMS 심사를 받았으니 됐다고 말한다.

Spring Boot 1.5 – 지원 끝

지원이 끝난 프레임워크다. 일단 지원이 끝났다면 될수 있는대로 빠르게 업데이트를 해야 한다. 하지만, 작동하는데 아무런 문제가 없으니까 계속 사용한다. 뭔가 문제가 생기면 그 라이브러리만 업데이트 하는 식이다.

Spring boot 1.5 전체를 패키지는 방대한데, 그중에서 자주 사용하는 일부가 문제가 생겼다는 뉴스가 나오면 그것만 바꾸는 식이다.

이렇다보니 Spring boot 2.x 에 대한 기술 습득은 꿈도 못꾼다. Reactive 가 뭐고 그래서 그게 뭐가 좋은지도 모르고 그것을 하면 뭐가 이득이 된다는 것은 둘째고 기술지원도 끝난 프레임워크를 그대로 고수하면서 DB 구조를 파악하고 데이터를 넣다 뺏다 하는 CRUD 프로그래밍으로 화면에 원하는 것을 이루게 되면 그것이 프로그래밍이 된 것이라는 사고방식에 젖어 있다.

고민따위는 안중에도 없고, 구글에서 검색해서 읽어본 기사에 뭘 좀 안다고 지식자랑하기에만 바쁘고 정작 책상에 앉아서 하는 일이라곤 지식자랑과는 한참 뒤처진 기술들을 다루고 있을 뿐이다.

지원이 끝났다는 것은 더 이상 보안관련 업데이트를 해주지 않는다는 것을 뜻한다. 물론 전 세계적으로 문제가 될 경우에 End Of Life 가 된 경우라도 업데이트를 제공해 주긴 한다. 하지만 지원이 끝난 것들은 더 이상 보안에 대해서 신경을 놔버리는 경우고 거들떠도 안보다는 사실을 알아야 한다.

아무도 신경을 안쓰는 거들떠보지도 않는 프레임워크로 일을 하는 것이 뭐 그리 대단한지, 단가 900 은 받아야겠다고 하는데 괜히 프리 경력이 정규직 경력을 인정받지 못하는게 아니다.

이렇게 글을 적어놓으면

프로젝트 오퍼를 낸 조직이 가이드를 내놓고 어떻게 하라고 해야지 그걸 왜 프리보고 뭐라 하냐?

이렇게 말할게 뻔하다. 그러니까 정규직 경력이 안되는 거다. 정규직에서는 적극적인 사람을 원하지 그렇게 수동적으로 뭐 가이드 안주면 못하겠다는 식의 사고를 가진 사람을 뽑으려 하지 않는다. 그것도 적어도 경력이 10년 이상이라면 이렇게 해서는 안된다, 이런 것쯤은 좀 바꿔야 한다는 정도도 있어야 하고 적극적으로 문제에 대해서 문제가 있으며 바꿔야 한다는 것을 적어도 어필 정도는 해줘야 하는데, 그런게 안되잖아..

더 큰 문제는 대충한다는데 있다. Spring Framework 3.x 처럼 오래된 것이긴 하지만 나름대로 프로그래밍 방법들이 존재한다. 과거에는 쓸만했던 것이여서 자료를 찾아보면 그때당시에 자료들도 상당하다. 그러면 Spring Framework 3.x 에 대한 그 때 당시에 쓸만했던 것을 지금에 쓰고 있느냐… 그냥 다 대충한다. 어짜피 딴데가면 안쓸거… 구형인데 뭐… 이거 열심해서 뭐하냐…. 아무도 열심히 안한다.

어짜피 구형이라 열심히 안하고 그렇다고 뭐 바꾸자는 제안조차 못하고… 그냥 돌아가게만 만들자식이란게 문제다.

Wildfly 13 – End Of Life

Wildfly 는 예전에 Jboss AS 라는 오픈소스 엔터프라이즈 자바 서버였다. 그러던 것이 Redhat 에서 프로젝트를 인수(?) 하고 자사의 상용 엔터프라이즈 자바 서버를 Jboss EAP 라는 이름을 붙이자 네이밍에 차이를 두기 위해서 Wildfly 로 이름을 바꾼다.

Wildfly 는 이름을 바꾼것만이 아니라 릴리즈 주기를 바꾸게 되는데, 분기마다 메이저 업데이트를 하는 정책으로 바꾼 것이다. (분기마다 인지 1년마다 인지… 정확하지는 않음. 몇달 주기로 메이저 업데이트를 하겠다는 정책임) 이러한 정책은 Wildfly 의 Long Term Support 버전이 없다는 것을 말한다.

업데이트를 못 쫓아가는건 당연하고 무엇보다 JEE 스펙도 버전이 올라감에 따라 달라지다 보니 Servlet 엔진을 필요로하는 경에 버전 호환성이 맞지 않는 경우가 생긴다. 그것조차도 그렇다 치더라도 Wildfly 13 도 지원이 끝나기는 마찬가지다.

이렇게 주기적인 메이저 업데이트를 하는 자바 서버에 경우에 주기적으로 업데이트를 할 수 없는 경우에는 선택을 하지 말았어야 했다. RHEL 도 한번 설치하면 업데이트를 안하고 버티는 경우가 부지기수인데, 주기적인 업데이트를 고려해서 이것을 선택햇을리는 만무하다.

금융권은 더 심각

현재 내가 하고 있는 프로젝트의 경우에 한정해서 말을 하다보면, “그건 니가 있는 곳이 ㅈ같은 경우지… 똥 밟은 너를 탓해라” 하는 인간들이 대다수다.

그렇다면 과거, 그것도 긴 과거가 아닌 올해 있었던 금융권 프로젝트를 예를들어보자.

  • RedHat Enterprise Linux 7.7
  • gcc, g++ 컴파이러, automake, autoconf
  • WebLogic 12.1.x
  • Spring Framework 3.x

이런 환경에서 마이데이터니 뭐니,, 모바일이니 뭐니 서비스하고 앉아 있는게 현실이다.

더 웃긴건, WebLogic 서버를 사용하는데 환경은 AWS 클라우드에 올렸다는데 있다. 이게 뭐가 문제가 되느냐 하겠지만 WebLogic 는 Admin 서버와 Managed 서버로 나뉘며 Admin 서버가 반드시 있어야 하고 Managed 서버가 Admin 서버에 등록이 되어 있어야 한다.

이렇게 되면 AWS 클라우드에서 반드시 사용해야만 하는 AutoScaling 을 이용할 수가 없다. 물론 아예 이용할 수 없는 것은 아니지만, 별도의 노력이 조금 더 들어간다. 하지만, 그 별도의 노력조차도 안해서 그런지 AutoScaling 은 아예 접었고 닭 한마리 이벤트를 날리면 접속자가 폭주해 장애가 나는 곳이 금융 시스템이다.

KISA 도 결국 문제

이래저래 문제 해결을 위해서 노력한다고 하지만 한개는 있다. 그중에 다음과 같은 말들을 많이 한다. 적어도 금융권에서는..

Redhat Enterprise Linux 8.x 는 아직 보안 인증이 안됐다.

누가 어떻게 RHEL 8.x 에 대해서 보안인증을 해주나? KISA 가? KISA 가 발행하는 취약점 가이드에 Linux 에 대해서 RHEL 7 기준으로 설명되어 있어서 RHEL 8.x 는 아직 인증 없는 거다?

KISA 보안 가이드 유의사항

KISA 가 발행하는 취약점 가이드에는 위와같이 유의사항이 나와 있다. 하지만 현장에 있다보면 ‘인증이 아직 안됐다’ 라는 말을 많이 듣는다. 공공금융 프로젝트에서 인증을 안해준건지 뭔지 모르겠지만, KISA 라는 ISMS 주관사에서 저렇게 하고 있음에도 불구하고 다른 권위도 없고 보안에 관련해 국가적 위임도 받지 않는 곳에서 ‘인증’ 이란걸 하고 있다면 KISA 가 적극적으로 나서서 못하게 해야 한다.

더군다나 KISA 의 경우에 보안 취약점 가이드에는 절대로 하지 말아야 하는 항목들은 없다. 예를들어 ‘gcc 컴파일러 설치 금지’ 와 같은 것들도 없고 End of Life 된 운영체제나 프레임워크에 대한 금지 항목도 없다. ISMS 라는게 결국에는 가이드에 맞춰서 그걸 했냐하는 정도에 그치다보니 ‘그것만으로 됐다’ 하는 것으로 끝이다.

국가기관이 저렇게 나약하게, ‘절대적 기준 아니다’, ‘다양한 점검 방업을 사용하여 취약점 분석평가를 수행’ 등.. 말이 좋아서 저런 표현이지 그냥 대놓고 “니들이 알아서 잘 해봐라” 식과 뭐가 다르냐?

Log4j2 보안 취약점 패치

현재 프로젝트에서 log4j 보안 취약점 패치를 위해서 살펴보니 log4j 1.x 버전이였다. 지금의 log4j2 의 보안취약점은 JNDI 관련되어 있는데, log4j 1.x 에는 JNDI 기능 자체가 없다. 따라서 본 취약점에는 아무런 문제가 없다.

이런식의 결론에 도달하게 되어서 결국에는 아무런 조치도 하지 않았다. log4j 1.x 는 더 이상 유지보수가 안되는 버전이다. 하지만 이번에 터진 취약점과는 관계가 없기 때문에 그런대로 넘어가는 거다.

라고 윗분들의 결정했다. 이게 대한민국의 현주소다.

그래서 log4j 를 그냥 놔둘수도 없고 해서 logback 으로 바꿔 놓을려고 생각중이다. 테스트를 해보니 잘 될거 같다. 보고 따위는 안한다.. 그냥 바꿔놓는 거지.. 그냥 바꿔놔도 모른다. 아무도.. ㅋㅋ

스트림(Stream)

자바(Java) 세계에서 언제부터인지 스트림(Stream) 이라는 단어를 목격하게 되었다. 내 기억으로는 Java 8 에서부터 시작된 것 같은데 난데없는 이 단어가 왜 그렇게 핵심이 되었는지가 의문이였다. 도대체 왜 스트림(Stream) 이냐 하는 질문에 대한 대답을 듣기도 어려웠던 시절이기도 하다. 그져 사용하는 방법을 익히는데에 몰두하는 모습만 목격됐을 뿐이다.

java.util.stream

스트림(Stream) 에 대한 정의는 다양하다.

데이터 소스(Array, List) 로부터 흐름을 가지는 데이터의 집합체이며 통합연산을(bulk processing) 통해 데이터를 변형시키고 최종적으로 소비자가 그 데이터를 소비하도록 한다.

스트림을 다루게 되면 항상 다음과 같은 데이터 소스들을 만나게 된다. 모두 데이터의 집합체들이다.

  • Array
  • List

하필 왜 데이터 집합체들일까

컴퓨터 알고리즘 필요성과 유사한 스트림(Stream)

난데 없이 컴퓨터 알고리즘을 꺼내온 이유가 있다. 컴퓨터 알고리즘을 공부할때에 가장 먼저 만나는 것이 정렬(sort)문제이다. 그런데, 이런 질문을 하게된다.

왜 하필 정렬부터 인가?

이에 대한 대답은 간단다.

Compute 연산과 Memory 공간을 절약하기 위해서..

컴퓨터가 중복된 데이터를 어떻게 찾아낼까? 정렬을하면 쉽게 해결된다. 정렬된 데이터가 아니라면 모든 데이터를 비교해야 하지만 정렬할 경우에 같은 위상을 같은 데이터 값이 나오게 되는데 이를 하나만 남기고 지우면 간단해 진다.

이렇게 함으로써 Memory 공간도 절약하게 되고 이렇게 중복되지 않은 데이터를 가지고 Compute 연산을 할 경우에 당연히 그에 들어가는 비용도 줄게 된다.

자바에서 스트림도 이와 유사하다.

자바에서 데이터를 다루는 방법은 다양한다. 이는 데이터 소스를 통해서 다루어지는데, 이 데이터 소스를 간단하게 타입(Type) 이라고 생각해보자. 정수형, 문자열 등은 가장 단순한 타입이다.

이런 타입들은 단 하나의 데이터만 저장하고 있을 뿐 “데이터들” 을 가지고 있지 않다. Compute 연산 알고리즘에서는 여러 데이터들의 집합을 다룬다. 컴퓨터가 가지고 있는 데이터들이란 집합을 이야기 한다. 따라서 데이터 소스라고하면 “데이터들” 을 지칭하며 자바에서 이런형태의 데이터 타입은 Array, List 가 대표적이다.

그럼 이런 생각을 하게된다. 데이터 집합체들을 어떻게 하면 빠르게 중복을 제거하고 연산을 하게 만들 것인가? 과거에 For loop 문과 같은 것을 이용해서 조건식을 붙이면서 사용을 할 수도 있다.

람다(Lambda)

연속된 데이터들을 다루기만 할 거라면 단순하게 For loop 문을 이용하면 된다. 만일 이런 생각을 하게 된다.

연속된 데이터를 처리할때에 병렬을 이용해서 처리보자.

For loop 문에서 병렬처리는 쉬운게 아니다. Thread 를 이용할 수도 있지만 이건 동시성 프로그래밍이지 병렬은 아니다.

이를 위해서 자바 8 에서는 람다(Lambda) 를 도입했다. 이것에 대한 정의를 보면 함수형 프로그래밍(Funtional Programming) 이라는 말을 자주 접하게 되는데 병렬연산을 가능하게 하는 부분이다.

자바 8 스트림은 이 람다를 기반으로 한다. 결국에 스트림은 벌크 프로세싱(Bulk Processing) 을 람다를 사용해 구현하여 빠른 고속 데이터 처리가 가능하다.

스트림 – 흐른다.

스트림의 중요한 특징은 흐름이다. 프로그래밍에서 데이터를 다룰때 흐름 없이 다루는 경우도 많다. 앞에서 컴퓨터가 다루는 데이터는 “데이터들” 이라고 했는데, 이것들을 흐름을 가지고 연산을 수행하는게 스트림이다.

“흐른다” 라는 말을 수도관을 떠올리게 한다. 왼쪽에 물을 흘려보내면 오른쪽으로 물이 나온다. 데이터를 왼쪽에서 흘려보내면 오른쪽으로 물이 나온다. 만일 이 물이 설탕물로 만들고 싶다면 중간에 설탕을 뿌리면된다. 이물질을 제거하고 싶다면 이물질 제거기를 설치하면 된다.

이렇게 보면 누군가 데이터를 흘려보내는 놈이 필요하고 데이터를 받아 마시는 놈이 필요하게 된다. 이것을 Producer 와 Comsumer 관계라고 부른다.

리액티브 와 무슨 관계?

자바 스트림과 Reactive 관계보다 차이가 존재한다.

스트림(Stream) 은 데이터를 생산하면 즉각 소비가 발생한다. 하지만 리액티브 는 그렇지 않다. 리액티브 은 시간이 지남에 따라서 생산과 소비가 발생한다. 생산과 소비가 즉각적이지 않다.

이말을 잘 생각해 볼 필요가 있다. 스트림은 데이터를 다루는 영역에서 매우 유용할 수 있다. 프로그래밍 연산을 할 경우에 적합하게 사용되어질 수 있다. 하지만 Reactive 는 프로그래밍 연산보다 네트워크를 통한 데이터 요청과 리턴에 접합한 모델이라고 할 수 있다.

차이는 또 있다. 리액티브 에서 생산자는 반드시 흐름 데이터만 만들지 않는다. 대표적으로 웹에서 클릭(Click) 조차도 리액티브 에서 생산자가 될 수 있다. 그래서 연속된 데이터 흐름이 없다보니 뭔가 생산하는 개념이 아닌것이여서 생산자(Producer) 라는 말을 쓰지 않는다.

“즉각적으로 소비가 발생하지 않는다” 라는 말도 중요하다. 비동기적으로 데이터 리턴이 발생한다는 것을 의미 한다. 하지만 리턴 값을 받기 위한 준비는 항상하고 있다는것도 중요하다.

리액티브 는 네트워크를 통한 데이터 요청, 리턴 모델에 적합하다. 리액티브 요청한 것에 대한 데이터들을 다룰때에는 스트림을 이용할 수도 있다.

GC 알고리즘

reference counting algorithm

count를 관리하여 reference count가 0이되면 그때그때 garbage collection을 수행하는것이다.object에 reference 가되면 reference count는 1이 증가하고 reference가 사라지면 1이 감소하는 식으로 동작한다. 그런데 이 reference 관계가 간접적이라 하더라도 참조하고 있는 모든 object에 대해 연쇄적으로 reference count가 변경된다 이reference count가 0 이될때마다 garbage collection이 발생하기 때문에 자연스럽게 Pause Time이 분산되어 실시간 작업에도 거의 영향을 주지 않는 장점이 있다. 그러나 reference의 변경이나 garbage collection의 결과에 따라 각 object마다 reference count를 변경해 주어야하기 때문에 이에 대한 관리 비용이 상당하다 또한 Garbage collecition이 연쇄적으로 일어 날수 있다는 것도 문제가 될 수 있다. 하지만 무엇보다 이 reference counting algorithm 문제는 memory leak의 가능성이 농후하다는데 있다 reference count가 0이 되어야 garbage collection이 될것인데 순환 참조의 구조에서는 regerence count가 0이 될 가능성이 희박하다 그렇기 때문에 garbage 가 되어도 heap 에서 사라지지 않게 되어 memory leak 을 유발할 가능성이 커지게 된다

mark-and-sweep algorithm

reference에 따라 object마다 count를 하는 방식대신, root set에서 시작하는 reference의 관계를 추적하는 방식을 사용한다. 이러한 방식은 garbage를 detection하는데 있어 상당히 효과적이기 때문에 이후의 garbage collection에서 detection은 대부분 이 algorithm을 사용하고 있다.

mark-and-sweep algorithm은 mark phase와 sweep phase로 나뉘어 진다. mark phase는 gabage object를 구별핸재는 단계로 root set 에서 reference 관계가 있는 object에 marking하는 방식으로 수행된다.marking 을 위해서는 여러가지 방법이 있는데 주로 object header에 flag 나 별도의 btimap table등을 사용한다

이 mark-and-sweep algorithm은 reference관계까 정확하게 파악될 뿐만 아니라 reference의 관계의 변경 시에 부가적인 작업을 하지 않기 때문에 속도가 향상된다는 장점이 있다. 그러나 garbage collection 과정 중에는 heap의 사용이 제한된다. 그것은 작업의 정확성과 meomry corruption을 방지 하기 위함인데 이 경우 프로그램이 잠깐 멈추는 현상이 발생한다 또하나의 문제는 바로 fragementation이 발생 할수 있다는 것이다.

mark-and-compaction algorithm

make-and-sweep algorithm의 fragmentation이라는 약점을 국복하고자 sweep 대신 compaction을 포함한 것이다.

이 algorithm은 mark phase와 compaction phase로 구성되어 있다. compaction이란 작업은 말 그대로 live object를 연속된 메모리 공간에 차곡차곡 적재하는 것을 의미하며 보통 하위 address로 compaction을 수행한다 compaction에는 여러가지 방식이 있는데 arbitrary 방식, linear방식, sliding 방식 등이 있다. 이중 arbitrary방식은 순서가 보장되지 않는 방식이다. linear방식은 reference의 순서대로 정렬된 compaction이다 그리고 sliding 방식은 할당된 순서대로 정렬되는 방식이다.

compaction을 원활하게 하기 위해 handle과 같은 자료구조를 사용할 수도 있다. handle에는 객체의 논리적인 주소와 실제 주소가 들어가 있다. mark phase에서는 live object의 handel에 marking한다. compaction phase에서는 일단 marking 된 정보를 바탕으로 garbage object를 sweep 한 후 heap의 한 쪽 방향으로 live object를 이동시킨다. 이동하는 방법은 sliding compaction의 방법을 사용한다. 그 이후 이동한 새로운 주소로 모든 reference를 변경하는 작업을 수행한다

fragmentation의 방지에 초점이 맞추어져 있기 때문에 메모리 공간의 효율성이 가장 큰 장점이다.그러나 compaction 작업 이후에 모든 reference를 업데이트 하는 작업은 경우에 따라서 모든 object를 access하는등의 부가적인 overhead가 수반 될 수도 있다. 또한 mark phase와 compaction phase는 모두 suspend 현상이 발생한다는 단점이 있다

copying algorithm

heap 을 active영역과 inactive 영역으로 나누어서 사요한느 특징을 가지고 있다. 이중 active영역에만 object를 할당 받을 수 있고 active영역이 꽉 차게 되어 더이상 allocation이 불가능하게 되면 garbage collection이 수행된다.

garbage collection이 수행되면 모든 프로그램은 일단 suspend 상태가 된다. 그리고 live object를 inactive영역으로 copy하는 작업을 수행한다. object를 copy할때 각각의 reference 정보도 같이 변경된다.

이렇게 작업이 끝나면 active영역에는 garbage object만 남아 있게 되고 inactive 영역에는 live object만이 남아 있게 된더. 그런데 inactive영역에 object를 copy 할 때는 한쪽 방향에서부터 차곡차곡 적재를 하기 때문에 마치 compaction을 수행한 것처럼 정렬이 된 상태가 된다 이렇게 garbage collection이 완료되는 시점에서 active 영역은 모두 freememory가 되고 active 영역과 inactive 영역은 서로 바뀌게 된다. 이를 scavenge라고 한다.

fragmantation의 방지에는 효과적이기는 하지만 전첼 heap의 절반 정도를 사용하지 못한다는 단점이 존재한다. 또한 suspend현상과 copy에 대한 overhead는 필요악이라 할 수 있을 것이다.

generational algorithm

copy algorithm의 연장선상에 있다. heap을 active, inactive로 나누는 것이 아니라 age 별로 몇개의 sub heap으로 나눈다. object는 youngest generation sub heap 에 할당되고 그 곳에서 성숙하게 되면 그 다음 age에 해당하는 sub heap으로 올라가 결국 oldest generation sub heap 까지 promotion하는 방식으로 동작한. age가 임계값을 넘엇 다른 generation으로 copy되는 것을 promotino이라고 한다.

무엇보다 이 algorithm의 장점은 각 sub heap에서 각각 적절한 algorithm을 적용 할수 있다는 것이다 이러한 장점으로 인해 hosspotjvm이나 ibm jvm에서도 generational algorithm을 사용하고 있다

train algorithm

Heap을 작은 Memory Block으로 나누어 Single Block 단위로, Mar Phase와 Copy Phase로 구성된 Garbage Collection을 수행한다.이러한 특징때문에 이 algorithm은 incremental algorithm이라고도 한다.

garbage collection을 수행하는 단위가 single block 인 만큼 전체 heap의 suspend가 아닌 collection 중인 memtory block만 suspend가 발생한다 다시 말해 suspend를 분산시켜 전체적인 pause time을 줄이자는 아이디어인 것이다.

heap을 tran으로 구성한다 이 train은 car라고 불리는 fixed size의 memory block을 묶어 놓은 체인이다. train은 필요에 따라 car를 추가 할수도 있고 하나의 train이 추가 될 수도 있다. 그리고 각 memory block은 car외부에서의 참조를 기억하는 remember set이라는 장치가 있다. 이러한 구조 때문에 train algorithm이라는 명칭을 사용하게 되었다.

remember set은 train algorithme이 single block 단위로 garbage collection을 한다는 의미에서 상당히 중요하다 copy나 compaction을 통해 objectr가 이동을 하게 되면 당연히 그 주소가 변경될 것이고 이를 참조하는 object들도 따라서 reference가 변겨이 되어야한다. 그런데 obejct가 이동을 하게 되면 당연히 그 주소가 변경될 것이고 이를 참조하는 object들도 따라서 reference가 변경이 되어야 한다. 그런데 object는 자신이 참조하는 refence는 가지고 있지만, 누가 나를 참조하고 있는지에 대한 정보는 가지고 있지 못하다. 그렇기 때문에 이동한 object를 누가 참조하고 있는지를 알기 위해서는 , 최악의 경우 모든 root set과 object의 reference를 모두찾아 다녀야 하는 경우가 발생한다. 이경우 suspend의 분산 효과는 현저히 떨어진다.

remember set은 이러한 일을 방지하는 효과가 있다. remember set을 구성하기 위해서는 write barrier라는 장치를 동반하게 된다. wite barrierㅇ는 간단한 코드로 되어 있는 instruction의 set, 즉 작은 이벤트 프로그램 또는 트리거 정도로 이해할 수 있다. 보통 write barrier는 reference 관계를 맺을때 대상 object가 같은 car에 있지 않을 경우 대상 object의 car에 있는 remember set에 기록하는 식으로 구성되어 있다

adaptive algorithm

지금까지 설명한 것과는 달리 특정 collection방법을 지칭하는 것은 아니다. heap의 현재 상황을 모니터링하여 적절한 algorithm을 선택 적용하는 것 또는 heap sizing을 자동화하는 일련의 방법을 의미한다. 이것은 hotspot jvm의 ergonomics 기능 또는 adaptive optino이나 ibm jvm의 tilting 기능 등으로 구현이 되고 있다.

유용한 JVM 플래그들 – Part 8 (GC Logging)

이 씨리즈의 마지막은 가비지 컬렉션 로깅과 연관된 플래그들이다. GC 로그는 힙의 잠재적인 개선, GC설정 이나 애플리케이션의 객체할당 패턴을 들어내주는 아주 중요한 툴이다. 각 GC가 발생하면, GC 로그는 과정과 결과에 대한 정밀한 데이터를 제공한다.

-XX:+PrintGC

-XX:+PrintGC 나 혹은 별명인 -verbose:gc 는 모든 young generation GC와 모든 풀GC에 대해 라인으로 출력되는 단순히 GC 로깅 모드를 활성화한다. 예를들면 다음과 같다.

라인의 시작은, ‘GC’ 혹은 ‘Full GC’, GC 타입이다. 그 다음이 GC 전과 후에(화살표로 구분되는) 점유 힙 메모리고 괄호안에 용량은 현재 힙의 용량이다. 라인의 끝은 GC가 수행된 시간이다(초단위 시간).

따라서, 첫째라인에서, 246656K→243120K(376320K) 는 GC가 점유 힙 메로리를 246656K 에서 243120K 로 줄였다는 뜻이다. GC 당시에 힙 용량은 376320K 이고 GC 는 0.0929090 시간을 소비했다.

단순한 gC 로깅 포캣은 사용되는 GC 알고리즘과 아무런 상관이 없고 더이상 상세함을 제공하지 않는다. 위의 예제에서 우리는 GC가 young 에서 old generation 으로 객체가 이동될때에 로그에 기록할 수 없다. 그러한 이유로, 상세한 GC 로깅은 단순한 로깅보다 훨씬 유용하다.

-XX:+PrintGCDetails

만약 -XX:+PrintGC 보다 -XX:+PrintGCDetails 를 사용한다면, 우리는 사용되는 GC 알고리즘에따라 달라지는 “상세한” GC 로깅 모드를 활성화 한다. 처리량 컬렉터를 사용하는 young generation GC 에 의해서 생성된 출력을 살펴보는 것으로 시작한다. 좀 더 읽기 편하게, 나는 출력물을 몇개의 라인으로 나누고 그들을 들여쓰기를 했다. 실제 로그에서, 이것은 한줄이며 사람이 읽기는 불편하다.

우리는 단순한 GC 로그로부터 몇가지 요소를 알수 있었다. 점유 힙 메모리를 246648K 에서 243136K 감소시키고 0.0935090 초를 소비한 young generation GC가 있다. 추가로, young generation 자체에 대한 정보를 얻는데, 이것의 용량과 점유률뿐만아니라 사용된 컬렉터가 무엇인지를 알 수 있다. 위 예제에서, “PSYoungGen” 컬렉터는 점유된 young generation 힙 메모리가 142816K 에서 10752K 로 줄일 수 있었다.

우리는 young generation 용량을 알고 있기 때문에, young generation 이 다른 객체할당을 수용할 수 없기때문에 GC를 발생시켰다라고 쉽게 이야기할 수 있다. 활용가능한 142848K 중에 142816K 이 사용되었다. 게다가, 우리는 우리는 young generation에서 제거 된 개체의 대부분이 아직 살아 있고 old generation 으로 이동되었다고 결론 내릴 수 있다: 142816K→10752K(142848K) 와 246648K→243136K(375296K) 를 비교해 young generation 이 거의 완벽하게 비였음에도 불구하고, 전체 힙 점유률은 거의 같게 남았다.

상세 로그 섹션의 “Times”은 GC에 의해서 사용된 운영체제의 커널 공간(“sys”), 사용자 공간(“user”)으로 나뉜 CPU 시간에 대한 정보를 포함한다. 또, GC가 동작중에 소비한 real time(“real”) 가 보인다. (0.09 sms 로그에서 보여준 0.0935090 시간의 반올림일 뿐이다.) 만약 위 예제처럼 CPU time 이 소비된 real time 보다 상당히 높다면 GC가 다중 쓰레드를 사용해 동작했다고 결론내릴 수 있다. 이러한 경우, 기록된 CPU time 은 모든 GC 쓰레드들의 CPU time의 총 합이다. 그리고 추가로, 나는 위 예제에서 컬렉터가 8개의 쓰레드를 사용했다고 말할 수 있다.

이제 풀GC 출력에 대해서 생각해보자.

young generation 에 대한 추가된 상세함으로 인해서 old 와 permanert generation 에 대해 상세함을 우리에게 제공해준다. 전체 세가지 세대들에 대해, 우리는 사용된 컬렉터, GC 전후에 점유율, GC 시에 용량을 알 수 있다. 주목할 것은 총 힙에 대해 보여준 각 숫자는 ( 243136K→241951K(628736K) ) 는 young 과 old generation 의 숫자 합과 같다. 위 예제에서, 241951K 의 총 힙을 점유했고 9707K 는 young generation, 232244K 는 old generation 을 점유했다. 풀GC는 1.53초 소비됐고 user space에서 CPU time 10.96 초는 GC가 다중 쓰레드로 상되었음을 보여준다.(위와같이 8개 쓰레드)

다른 세대들에대한 상대한 출력은 GC 발생에대한 추론을 가능하게 한다. 만약에, 모든 세대에 대해, GC전에 점유한 로그 상태가 거의 현재 용량과 같다면 이 세대는 GC가 발생가능성이 높다. 하지만, 위 예제에서, 이것은 세가지 세대에대해 해당되지 않는다. 그러면 무엇이 이 경우에 GC를 발생시켰을까? 처리율 컬렉터에서 GC 인체공학이 세가지 세대중에 하나가 고갈되기전에 GC가 이미 실행되어야 한다고 결정했다면 실제로 이러한 일이 발생될 수 있다.

풀GC 또한 애플리케이션이나 외부 JVM 인터페이스중에 하나를 통해서 명시적으로 요청이 있을때에 발생될 수도 있다. “system GC” 경우에는 GC 로그에서 “Full GC” 대신에 “Full GC(System)”으로 시작되는 라인을 가지기 때문에 쉽게 파악할 수 있다.

시리얼 컬렉터(Serial Collector) 에서, 상세 GC 로그는 처리량 컬렉터와 매우 유사하다. 한가지 다른 점이라면 다른 GC 알고리즘을 사용하고 있기 때문에 다양한 섹션들에 이름들이 다르다.(예를들어, old generation 섹션은 “ParOldGen” 대신에 “Tenured” 라 부른다). 정확한 이름의 컬렉션을 사용하는 것이 좋은데, JVM의 의해서 사용되어지는 특정 가비지 컬렉션의 로그로부터 추론을 할 수 있기 때문이다.

CMS 컬렉터(CMS Collector)에서, young generation GC에 상세 로그는 역시 처리량 컬렉션과 매우 유사하지만 old generation GC에 대해서는 같지 않다. CMS 컬렉터에서 old generation GC들은 다른 단계를 사용해 애플리케이션에 동시적으로 작동한다. 그렇기 때문에, 그것의 출력은 풀GC 출력과 다르다. 추가적으로 다른 단계에대한 라인들은 일반적으로 동시적 컬렉션이 동작중에 발생된 young generation GC 에대한 라인에 의해서 로그와 분리된다. 다른 컬렉터들을 이미 봐왔기 때문에 GC 로그의 모든 요소들이 익숙하고 그래서 다른 단계에 대한 로그를 이해하기란 어렵지 않다. 다만 처리중일때에 특별히 조심하고 애플리케이션과 동시적으로 모든 단계가 실행된다는 것을 상기해야만 한다. 따라서, 반대되는 stop-the-world 컬렉션들처럼, 개별적인 단계 혹은 전체 GC 주기가 오랫동안 실행되는 시간이 항상 문제가 있다는 것은 아니다.

이 시리즈의 part 7에서 알아봤듯이, 풀GC들은 CMS 컬렉터가 재시간에 CMS 주기가 완료되지 않으면 발생 될 수 있다. 만약 그게 발생하면, GC 로그는 추가적으로 풀GC 발생 원인이 무엇인지와 같은 힌트를 포함한다. 예를들어, “concurrent mode failure” 와 같은.

이 기사를 짧게 유지하기 위해서, 나는 CMS 컬렉터 GC 로그의 상세한 설명을 자제할 것이다. 또, 실제 컬렉터 작성자중에 한분이 여기에 이미 훌륭한 설명을 발표했다. 나는 이것을 읽기를 강력히 추천한다.

-XX:+PrintGCTimeStamps and -XX:+PrintGCDateStamps

이것은 (단순하게 혹은 상세하게) GC 로그에 시간과 날짜 정보를 추가하는게 가능하다. -XX:+PrintGCTimeStamps 에서 timestamp 은 JVM이 시작된 이후에 초단위로 흐른 실제시간을 반영하도록 매 라인마다 추가된다. 예를들어

그리고 -XX:+PrintGCDateStamps 를 지정하면 로그가 쓰여질때에 각 시작 라인이 절대 날짜와 시간을가지고 시작된다.

두가지 출력이 좋아보이면, 두 플래그 조합이 가능하다. 나는 항상 두가지 플래그를 지정하도록 권하는데, 그러한 정보는 다른 소스들로부터 데이터를 가지는 GC 로그 데이터를 연관시키기위해 매우 유용하다.

-Xloggc

기본적으로 GC 로그는 stdout 에 쓰여진다. 대신 -Xloggc:<file> 로 우리는 출력 파일을 지정할 수 있다. 주목할 것은 이 플래그는 -XX:+PrintGC 과 -XX:+PrintGCTimeStamps 처럼 묵시적으로 지정한다. 역시, 나는 필요하다면 이러한 플래그들을 명시적으로 지정하길 권하는데, 새로운 JVM 버전들에 예기치않은 변화로부터 여러분들로부터 보호해준다.

“Manageable” Flags

자주 논의되는 것중에 프로덕트 시스템에 JVM에 GC 로깅을 활성화해야하느냐 마느냐 하는 것이다. GC 로깅의 오버헤드는 보통 매우 적다라는데 나는 분명히 “yes”다. 하지만, 이것은 JVM이 시작할때에 우리는 GC 로깅에 대한 고민을 할 필요가 없다건 좋은 것이다.

HotSpot JVM 은 “manageable”이라 불리우는 특별한(하지만 아주 작은) 카테고리 플래그들을 가진다. 관리 플래그들에 대해 런타임으로 그들의 값을 변경하는 것이 가능하다. 여기서 논의한 모든 플래그들과 “PringGC” 로 시작하는것들이 “manageable” 카테고리에 속한다. 따라서, 우리는 실행중인 JVM에서 우리가 원할때마다 언제든지 GC 로깅을 활성화하거나 비활성화할 수 있다. 관리 플래그들을 지정하기 위해서 우리는 JDK에 포함된 jinfo 를 사용하거나 JMX 클라이언트와 HotSpotDiagnostic MXBean 의 연산인 setVMOption를 호출해 가능하다.

 

유용한 JVM 플래그들 – Part 7 (CMS Collector)

HotSpot JVM의 CMS 컬렉터(The Concurrent Mark Sweep Collector)는 주요한 목표중에 하나를 가진다: 낮은 애플리케이션 일시정지 시간. 이 목표는 웹 애플리케이션과 같은 대부분의 상호작용 애플리케이션들에게 중요하다. 관련된 JVM 플래그들을 살펴보기전에, 짧막하게 CMS 컬렉터 운영과 이것을 사용할때에 부닥치게될 주요 이슈들에대한 요점을 다룰것이다.

처리량 컬렉터와 같이, CMS 컬렉터는 old generation 에서 객체들을 다루지만 그 운영방식은 훨씬더 복잡하다. 처리량 컬렉터는 늘 애플리케이션 쓰레드들을 잠시 멈추게하지만, 아마도 적지않은 시간, 그 시간을 애플리케이션이 안전하게 무시할수 있도록 한다. 그와 대조적으로, CMS 컬렉터는 대부분 애플리케이션 쓰레드들과 동시적으로 실행되도록 디자인되었고 아주 적은(혹은 짧은) 잠시 정지시간만 발생시킨다. 응용프로그램과 동시에 GC를 실행하는 단점은 다양한 동기화와 데이터 불일치 이슈를 발생시킨다. 안전성과 현재 동시성 실행을 당성하기 위해서 CMS 컬렉터의 GC 사이클은 많은 연속된 단계들로 나뉜다.

Phases of the CMS Collector

CMS 컬렉터의 GC 주기는 6단계로 구성된다. 네가지단계는(“Concurrent” 로시작하는 이름을 가진) 실제 애플리케이션에 동시에 실행되는 반면에 두단계는 애플리케이션 쓰레드들이 정지한다.

  1. 표기 초기화(Initial Mark): 애플리케이션들은 그들의 객체 참조들을 수집하기 위해 잠시 멈춘다. 이것이 끝났을때, 애플리케이션 쓰레드들은 다시 시작된다.
  2. 동시적 표기(Concurrent Mark): 1단계에서 수집된 객체 참조들로부터 시작해서, 모든 다른 참조되는 객체들까지 하게된다.
  3. 동시적 전세정(Concurrent Preclean): 두번째 단계가 실행되는 동안 애플리케이션 쓰레드에 의해서 만들어진 객체 참조에서 변경사항은 두번째 단계로부터 결과를 갱신하는데 사용되어진다.
  4. 다시표기(Remark): 세번째 단계처럼 역시 동시에 발생한다. 게다가 객체 참조에 변화가 발생한다. 따라서, 애플리케이션 쓰레드는 어카운트를 업데이트하는 동안 한번 이상 정지되고 실제 청소작업이 발생되기전에 참조된 객체의 정확한 뷰를 보장한다. 이 단계는 필수적인데, 여전히 참조되는 객체를 수집하는걸 피해야 하기 때문이다.
  5. 동시적 청소(Concurrent Sweep): 더 이상 참조되지 않는 모든 객체들은 힙에서 제거된다.
  6. 동시적 리셋(Concurrent Reset): 컬렉터는 다음 GC를 시작할때 클린 상태를 위해서 어떤 집청소 작업(housekeeping work)를 한다.

흔히 잘못 생각하는 것이 CMS 컬렉션은 애플리케이션에 대해서 전체적으로 동시적으로 실행된다고 여기는 것이다. 동시적 단계와 비교했을때 stop-to-world 단계조차도 일반적으로 매우 짧기때문에 그렇지 않다는것을 봤다.

또 주목해야 하는 것이, CMS 컬렉터가 old generation GC에 대해서 대부분 동시적 솔루션을 제공한다 하더라도, young generation GC들은 여전히 stop-to-world 접근법을 사용해 다루어진다. 이렇게하는 이론적 근거는 young generation GC들은 일시 정지 시간이라는게 상호작용 애플리케이션에서조차 만족할만큼 전통적으로 충분히 짧다는 것이다.

Challenges

실제 세계의 애플리케이션에서 CMS 컬렉터를 사용할때, 우리는 튜닝을 위한 요구사항을 만들때에 두가지 주요한 도전에 직면한다.

  1. 힙 파편화(Heap fragmentation)
  2. 높은 객체 할당 비율(High object allocation rate)

힙 파편화는 발생 가능한데, 처리율 컬렉터와 다르게, CMS 컬렉터는 단편화(defragmentation)를 위한 어떤 메커니즘도 포함하지 않는다. 결과적으로 총 힙 공간이 꽉찬것과 상관없이 객체할당을 할 수 없는 상황을 애플리케이션 스스로 찾아야할지 모른다. – 이는 객체를 수용하기 위해 이용 가능한 연속 메모리 영역이 없기 때문이다. 이러한 일이 발생하면, 동시성 알고리즘은 더이상 도움이 안되며 따라서, 최후의 수단으로, JVM은 전체 GC(Full Gc)를 발생시킨다. 전체 GC는 처리량 수집기에 의해 사용 된 알고리즘을 실행하는 것을 상기하면 파편화 이슈는 해결된다. – 그것은 또한 애플리케이션을 정지시킨다. 따라서, CMS 컬렉터가 가지고온 모든 동시성 이면에는 여전히 긴 stop-to-world 일시정지가 발생되는 위험이 있다. 이것은 디자인 부분으로(JVM 디자인) 따라서 그것을 바꿀수는 없다. – 우리는 컬렉터를 튜닝함으로써 그 가능성을 줄일 수 있다. 눈에 띄는 stop-to-world 일시정지로부터 100% 안전성을 보장해야만하는 상호작용 애플리케이션에서는 문제가 된다.

두번째 도전은 애플리케이션의 높은 객체 할당 비율이다. 만약 객체 인스턴스화하는 비율이 컬렉터가 힙에서 죽은 객체를 제거하는 비율보다 아주 높다면 동시적 알고리즘은 여러번 실패한다. 어떤점에서, old generation은 young generation 에서 승격된 객체를 수용할 충분히 활용가능한 공간이 없을 것이다. 이러한 상황은 “concurrent mode failure” 라고하며 JVM은 힙 파편화때와 마찬가지로 움직인다. 바로 전체 GC.

이러한 시나로중에 하나가 실제로 일어날때를보면 (공교롭게도 실제 프로덕트 시스템에서도 일반적으로 발생한다), old generation 에 불필요한 엄청난 양의 객체들이 그곳에 존재한다는 것이 판명된다. 한가지 가능한 대책은 old generation 으로 짧게 사는 객체들의 조기 승격을 방지하기 위해 young generation 을 증가시키는 것이다. 다른 해결책은 프로파일러(profiler)을 사용하거나 운영중인 시스템의 힙 덤프를 얻어 과도한 객체할당에 대해서 객체들을 규명하고 애플리케이션을 분석해서 최종적으로는 많은 양의 객체할당을 줄이는 것이다.

다음으로 우리는 CMS 컬렉터 튜닝을 위해 활용가능한 적절한 JVM 플래그들에 대해서 살펴볼 것이다.

-XX:+UseConcMarkSweepGC

이 플래그는 애초에 CMS 컬렉터를 활성화하기 위해 필요하다. 기본적으로, HotSpot 은 처리율 컬렉터를 대신 사용한다.

-XX:+UseParNewGC

CMS 컬렉터를 사용할때, 이 플래그는 다중 쓰레드를 사용해 young generation GC의 parallel 실행을 활성화한다. 아마도 컨셉상 young generation GC 알고리즘에서 사용되는 것과 같기때문에 처리량 컬렉터에서 봤던 -XX:+UseParallelGC 플래그를 다시 사용하지 않는다는데 놀랄 것이다. 하지만, young generation GC 알고리즘과 old generation GC 알고리즘의 상호작용이 CMS 컬렉터에서 다르며 young generation 에서의 구현은 서로 달라 결국 이 둘 플래그는 다른 것이다.

주목할 것은 현재 JVM 버전에서 -XX:+UseConcMarkSweepGC를 지정하면 자동적으로 -XX:+UseParNewGC가 활성화 된다. 결과적으로, 만일 parallel young generation GC가 매력적이지 않다면 -XX:-UseParNewGC 세팅함으로써 비활성화할 필요가 있다.

-XX:+CMSConcurrentMTEnabled

이 플래그를 지정하면, 동시적 CMS 단계는 다중 쓰레드로 동작한다. 따라서 다중 GC 쓰레드는 모든 애플리케이션 쓰레들에서 parallel에서 작동한다. 이 플래그는 이미 기본값으로 활성화된다. 만약 serial 실행이 더 바람직하다면, 하드웨어 사양을 고려했을때, 다중 쓰레드 실행은 -XX:-CMSConcurrentMTEnabled 를 통해서 비활성화될 수 있다.

-XX:ConcGCThreads

-XX:ConcGCThreads=<value> 플래그는 (이전 버전에서 -XX:ParallelCMSThreads 으로 알려진) 동시적 CMS 단계가 동작할때에 사용할 쓰레드 개수를 정의한다. 예를들어, 값이 4면 CMS 주기의 모든 동시적 단계는 4개의 쓰레드를 사용해 동작한다는 뜻이다. 비록 높은 쓰레드의 개수가 동시적 CMS 단계의 속도를 높여줄수 있지만 추가적으로 동기화 오버헤드를 발생시킨다. 따라서 특정 애플리케이션을 다룰때, CMS 쓰레드의 수의 증가가 실제로 성능향상을 가지고 오는지 그렇지 않는지를 측정해야만 한다.

만일 이 플래그를 명시적으로 지정해주지 않으면 JVM은 처리량 컬렉터에서 알려진 -XX: ParallelGCThreads 플래그 값에 기반에 기본값 parallel CMS 쓰레드 수를 계산한다. 사용된 계산 공식은 ConcGCThreads = (ParallelGCThreads + 3)/4 이다. 따라서 CMS 컬렉터에서, -XX:ParallelGCThreads 플래그는 stop-to-world GC 단계에서만 영향을 주는게 아니라 동시성 단계에서도 영향을 준다.

요약을 하자면, CMS 컬렉터 실생에 다중쓰레드 설정을 위한 몇가지 방법이 있다. 이렇게 말하는 이유는, 먼저 CMS 컬렉터의 기본 세팅값을 사용해보고 튜닝이 필요하면 먼저 측정을하도록 권고하기 때문이다. 프로덕트 시스템에서(혹은 프로덕트와 유사한 테스트 시스템에서) 측정이 애플리케이션의 목표로하는 일시 정지 시간에 도달하지 않았다면 이러한 플래그를 통한 GC 튜닝은 고려해볼만 하다.

-XX:CMSInitiatingOccupancyFraction

처리량 컬렉터는 힙이 꽉 찼을때만 GC 주기를 시작한다. 예를들어, 새로운 객체를 할당할 공간이 없거나 객체를 old generation으로 승격시킬 공간이 없을때. CMS 컬렉터에서는 동시적 GC 동안에도 애플리케이션이 동작중여야하기 때문에 오랜시간을 기다리면 안된다. 따라서, 애플리케이션이 out of memory 되기전에 GC 주기를 끝내기 위해서 CMS 컬렉터는 처리량 컬렉터보다 일찍 GC 주기를 시작할 필요가 있다.

서로 다른 애플리케이션이면 객체 할당 패턴도 서로 다르며, JVM은 실제 객체 할당 및 비할당에 대한 런타임 통계를 수집하고 CMS GC 주기를 언제 시작할지 결정할때 사용한다. 이 과정을 부트스랩기, JVM은 첫번째 CMS 실행을 시작할때 힌트를 획득한다. 그 힌트는 -XX:CMSInitiatingOccupancyFraction=<value> 통해서 퍼센트로 old generation 힙 공간의 사용량으로 표시되어 지정될 수 있다. 예를들어 값이 75면 old generation의 75%를 획득했을때에 첫번째 CMS 주기를 시작하라는 의미다. 전통적으로 CMSInitiatingOccupancyFraction 의 기본값은 68 이다. (오랫동안 경험을 통해 얻은 수치다)

-XX+UseCMSInitiatingOccupancyOnly

우리는 런타임 통계에 기반해 CMS 주기를 시작할지 결정하지 않도록 JVM 에게 지시하기 위해 -XX+UseCMSInitiatingOccupancyOnly 를 사용할 수 있다. 대신, 이 플래그가 활성화된 때에, JVM은 첫음 한번만이 아닌 매번 CMS주기에서 CMSInitiatingOccupancyFraction 값을 사용한다. 그러나, 대부분의 경우 JVM이 우리 인간보다 GC 의사결정을 좀 더 잘한다는 것을 염두에 둬야 한다. 따라서, 이것은 합당한 이유가(ex, 측정을 통해 데이터를 얻었을때에) 있을때 뿐만아니라 애플리케이션에 의해서 생성된 객체의 생명주기에대해서 실제로 좋은 지식을 가지고 있는 경우에 이 플래그를 사용해야 한다.

-XX:+CMSClassUnloadingEnabled

처리량 컬렉터와 비교해, CMS 컬렉터는 기본적으로 permanent generation 에 GC를 수행하지 않는다. 만약 permanent generation GC가 필요하다면, -XX:+CMSClassUnloadingEnabled 통해서 활성화될 수 있다. 이전버전의 JVM에서는 추가적으로 -XX:+CMSPermGenSweepingEnabled 플래그 지정이 필요했었다. 주의할 것은, 이 플래그를 지정하지 않는다하더라도, 공간이 부족하게 되면 permanent generation 의 가비지 컬렉트를 시도할 것이지만 컬렉션은 동시적으로 수행되지 않을 것이다. – 대신, 다시 한번, 전체 GC가 동작할 것이다.

-XX:+CMSIncrementalMode

이 플래그는 CMS 컬렉터의 점진적 모드(incremental mode)를 활성화 한다. 점진적 모드는 애플리케이션 쓰레드에 완전히 양도(yield)되도록 동시적 단계를 주기적으로 잠시 멈추게 한다. 결론적으로, 컬렉터는 전체 CMS 주기를 완료시키기 위해서 아주 오랜시간을 가질 것이다. 따라서, 점진적 모드 사용은 일반적인 CMS 사이클에서 동작중인 컬렉터 쓰레드가 애플리케이션 쓰레드와 아주 많이 간섭이 발생되고 있다고 측정되어질경우에 유효하다. 이것은 동시적 GC 수용을 위해 충분한 프로세스를 활용하는 현대 서버 하드웨어에서 아주 드물게 발생된다.

-XX:+ExplicitGCInvokesConcurrent 와 -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses

오늘날, 가장 폭 널게 받아들여지는 최고의 기술은 애플리케이션에서 System.gc() 호출에 의해서 명시적으로 호출되는 GC를 차단하는 것이다. 이러한 조언이 사용되어지는 GC 알고리즘과 관련이 없다고 생각하고 있는데, CMS 컬렉터를 사용하고 있을때에 시스템 GC는 기본적으로 전체 GC를 발생시키는 아주 않좋은 이벤트이기 때문에 언급하고 넘어간다. 운좋게도, 기본 행동을 바꿀수 있는 방법이 있다. -XX:+ExplicitGCInvokesConcurrent 플래그는 JVM이 시스템 GC 요청이 있을때마다 전체GC 대신 CMS GC를 실행하도록 지시한다. 두번째 플래그인 -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses 는 시스템 GC가 요청될 경우에 추가로 permanent generation 을 CMSGC에 포함하도록 해준다. 따라서, 이러한 플래그의 사용으로, 우리는 예기치못한 시스템GC의 stop-to-world에 대해서 우리 스스로 보호할 수 있다.

-XX:+DisableExplicitGC

CMS 컬렉터 주제를 다루는 동안, 어떤 타입의 컬렉터를 사용하던지간에 시스템 GC 요청을 완벽하게 무시하도록 JVM에게 지시하게하는 -XX:+DisableExplicitGC 플래그를 언급할 좋은 기회다. 필자에게, 이 플래그는 더 이상 생각할 필요도 없이 모든 JVM 운영에서 안전하게 지정되어질수 있도록 기본 플래그 세트에 포함된다.

유용한 JVM 플래그들 – Part 6 (Throughput Collector)

실제로 우리가 찾은 대부분의 애플리케이션 영역에서, 가비지 컬렉션(GC) 알고리즘은 두가지 기준에 따라 평가되어져 왔다.

  1. 보다 높은 처리율을(throughput) 달성하기 위한 좀 더 나은 알고리즘
  2. 결과적으로 좀 더 적은 일시 정지시간을(pause times) 가지는 좀 더 나은 알고리즘

먼저, 우리는 GC 맥락에서 “일시 정지시간” 과 “처리율” 말을 명확하게 할 필요가 있다. JVM은 항상 전용의 쓰레드에서, “GC 쓰레드”라 부르는, GC를 수행한다. GC 쓰레드가 활성화될때마다, 그들은 활용할 프로세서와 CPU 시간을 가지고 활동적인 “application 쓰레드”들과 경쟁한다. 조금 단순화하면, 우리는 애플리케이션 쓰레드들이 실행중일때에 전체 프로그램 실행 시간의 일부분으로 “처리율”을 생각한다. 예를들어, 99/100 이라는 처리율은 평균 애플리케이션 쓰레드들이 100분의 99초 프로그램 실행 시간을 가지고 작동했고 반면에 GC 쓰레드들은 오직 동일한 시간범위동안 1초만 가지고 동작했을 뿐이다.

“일시정지시간(pause time)“이라는 용어는 GC 쓰레드로 인해서 애플리케이션 쓰레드가 완변하게 일시정지되어지는 시간범위를 말한다. 예를들어, GC 동안 100ms 의 일시정지 시간은 100ms 마다 애플리케이션 쓰레드가 활성화되지 않았다는 것이다. 만약 우리가 동작중인 애플리케이션에대해 100ms의 “평균 일시 정지시간(average pause time)“으로 용어를 말하면 관측되는 그러한 애플리케이션에 모든 정지 시간이 평균 100ms 길이를 가진다는 뜻이다. 비슷하게, 100ms 의 “최대 일시 정지시간(maximum pause time)“은 100ms 보다 큰 일시 정지시간이 관측되지 않았다는 뜻이다.

Throughput vs. pause times

높은 처리율은 애플리케이션의 최종 사용자가 느끼기에 애플리케이션 쓰레드들이 상용제품처럼 작동하는 것이기 때문에 매력적이다. 직관적으로, 만약 처리율이 아주 높다면 애플리케이션이 좀 더 빠르게 동작한다. 낮은 일시 정지시간들 또한 매력적이다. 왜냐하면 최종 사용자 관점에서 행(hanging)이 걸린 애플리케이션은 GC 나 다른 이유에 의해서 발생되어진 것과 상관없이 항상 원하지 않는다. 애플리케이션 타입에 따라서, 200ms의 짧은 일시 정지시간조차도 최종 사용자의 경험을 방해할 수 있다. 따라서, 특히나 대화형 애플리케이션에서 낮은 최대 정지시간을 가지는 것은 중요하다.

불행하게도, “높은 처리율(high throughput)” 과 “낮은 일시 정지시간(low pause times)“은 같은 목적을 가지는 경쟁관계에 있다. 다시 명확하게하기 위해서 아주 단순화한 방법으로 생각해보자. GC는 안전하게 실행되기 위해서 어떤 전제조건이 필요하다. 예를들어, 애플리케이션 쓰레드들은 객체들의 상태를 변경하지 않도록 보장되어야 하는 반면에 GC 쓰레드들은 어떤 객체들이 아직도 참조되어 있는지 아닌지를 결정하기위해서 노력한다. 이러한 이유 때문에, 애플리케이션 쓰레드들은 반드시 GC 동안 정지된다.(혹은 사용되어지는 알고리즘에 따라, 특정 GC 단계중에서만 애플리케이션 쓰레드는 정지된다.) 하지만 이것은 쓰레드 스케줄링에 있어서 추가적인 비용이 발생한다. 컨텍스 스위치(context switches)를 통한 직접 비용과 캐쉬 영향으로 발생되는 간접비용. 이것은 각 GC가 어떤 무시할수 없는 오버헤드를 포함하는 추가적인 JVM 내부 안전에 대한 비용을 포함해 그들이 실제 작업을 수행하는 GC 쓰레드가 소비하는 시간을 추가할지를 측정한다. 따라서, 우리는 가능한한 GC를 적게 실행시킴으로써 최대 처리율을 달성할 수 있지다. i.e., only when it is unavoidable, to save all the overhead associated with it.

그러나, GC의 빠르게만 실행하기는 GC가 동작할때마다, 그동안 힙에 축적된 객체 수가 더 높아질때마다, 더많은 작업을 해야한다는 뜻이다. 단일 GC는 완료될때까지 아주 높은 평균과 최대 일시 정지시간을 야기시켜 좀 더 많은 시간이 소요된다. 따라서, 낮은 일시 정지 시간에서, 각 단일의 동작이 더 빨리 완료되도록 자주 GC를 실행하는 것이 바람직 할 것이다. 이것은 추가적인 오버헤드를 동반하고 처리율을 떨어트리고 우리는 다시 원점으로 되돌아 오게된다.

요약하면, GC 알고리즘을 사용하거나 디자인할때에 우리는 목적이 무엇인지를 결정해야 한다: GC 알고리즘 두 목적중에 하나만(최대 처리율이나 최소 일시 정지시간에 맞춰) 타켓으로하거나 그들사이에 타협점을 찾기위해 노력해야 한다.

Garbage collection on the HotSpot JVM

우리는 이미 young generation GC 에 대해서 part 5 에서 논의했었다. old generation 에 대해서, HotSpot JVM은 주요하게 두가지 클래스의 GC 알고리즘을 제공한다. (지금은 새로운 G1 GC 알고리즘을 따로 두고) 첫번째 클래스는 최대 처리율을 위해 노력해는 알고리즘이고 두번째는 최소 일시정지 시간을 위해 노력하는 알고리즘이다. 오늘은 첫번째에 포커스를 맞춘다. “처리율 지향(throughput-oriented)” 클래스.

우리는 설정 플래그들에 초점을 맞추길 원하는대로, 나는 HotSpot 에서 제공되어지는 처리율 지향 GC 알고리즘의 짧은 개요을 제공할 것이다. Old generation 에서 메모리 공간이 부족해 객체할당이 실패할때 GC 알고리즘은 작동된다.(일반적으로, “메모리 할당”은 young generation 에서 객체 승격(Old generation 으로)이다.) “GC roots” 로 부르는 것부터 시작하면, GC는 힙에서 도달한 객체를 찾고 살아있다고 표시한다. 나중에 GC는 그들이 싱글, 파편화되지 않는 메모리 블록을 회득하기위해(역주, 이게 뭔소린지 모르겠다. 싱글을 획득한다…), 살아있는 객체를 Old generation 으로 이동시키고 나면 남아있는 메모리 영역은 해제된다. 거기다 young generation GC 알고리즘 동작처럼 다른 힙 영역에서는 복사 전략을 따르지 않는다. 반면에 같은 힙 영역에서 모든 객체는 비파편화된채 유지된다. 컬랙터들은 GC 성능을 위해서 하나 혹은 그 이상의 스레드들을 사용한다. 하나 이상의 쓰레드를 사용할때, 알고리즘의 다른 단계들은 다른 GC를 방해하지 않도록 각각의 GC 쓰레드는 주로 자체 영역에서 작동하도록 세분화된다. GC 중에, 모든 애플리케이션 쓰레드들은 일시정지되어지고, GC가 끝났을때에만 재시작되어진다. 이제 우리는 처리율 지향의 GC 알고리즘과 관련있는 아주 중요한 플래그들을 살펴보자.

-XX:+UseSerialGC

우리는 단일 쓰레드버전의 처리율 지향 가비지 컬렉터인 serial 를 활성화기위해 이 플래그를 사용한다. young generation 과 old generation GC 모두 단일 GC 쓰레드만으로 실행되어질 것이다. 이 플래그는 오직 단일 프로세서 코어만 사용하는 JVM에서만 권장되어져 왔다. 이러한 상황에서, 다중 GC 쓰레드를 사용하는것은 역효과를 가지고 오는데 그 이유는 이러한 쓰레드들은 CPU 자원을가지고 경쟁을 하고 동기화 오버헤드를 유발시키기 때문에 실전에서는 절대로 동작하지 않는다.

-XX:+UseParallelGC

이 플래그에서, 우리는 다중GC 쓰레드를 사용하기위해서 다중으로 young generation GC를 실행하도록 JVM에게 요청한다. java 6 에서, 내 의견으로는, -XX:+UseParallelOldGC를 사용하는게 더 낫기때문에 이 플래그는 사용해서는 안된다. java 7 에서는 조금 바뀌었는데 -XX:+UseParallelGC 가 -XX:+UseParallelOldGC 와 같은 효과로 사용되어질 수 있기 때문이다.

-XX:+UseParallelOldGC

이 플래그의 이름은 “old” 가 마치 “deprecated” 처럼 들려서 조금 안스럽다. 그러나, “old”는 실제로 old generation 을 가르기 때문에 -XX:+UseParallelOldGC가 왜 -XX:+UseParallelGC보다 더 바람직한지를 설명해준다: 추가로 parallel young generation GC는 parallel old generation GC를 또한 활성화한다. 나는 사용이 높은 처리율이 필요하고 JVM이 두개 이상의 다중 코어를 사용할때라면 이 플래그 사용을 권고한다.

참고로, 처리율 지향 HotSpot GC 알고리즘의 다중(parallel) 버전들은 다중 실행(parallel execution)을 통해서 처리율을 증가시키는데 목적이 있기 때문에 자주 “throughput collectors” 이라고 부른다.

-XX:ParallelGCThreads

-XX:ParallelGCThreads=<value> 에서 우리는 다중 GC를 위해 사용되어질 GC 스레드의 수를 지정할 수 있다. 예를들어, -XX:ParallelGCThreads=6 에서 각 다중GC는 6의 쓰레드를가지고 실행되어질 것이다. 만약 이 플래그를 명시적으로 지정하지 않으면, JVM은 활용가능한 프로세서들의 수를 기반으로 계산한 기본값을 사용한다. 이것을(기본 값) 결정하는 요인은 Runtime.availableProcessors() 자바 메소드에 의해서 리턴된 N 값이다. N ⇐ 8 다중 GC는 그것을 다 사용한다. 그래서 N GC 쓰레드라고 한다. 그런데 N>8 활용가능한 프로세서들이 있다면 GC 쓰레드의 수는 3+5N/8 로 계산되어진다.

기본값을 사용하는것은 JVM이 프로세서나 시스템을 독점적으로 사용할때에 대부분 적합하다. 하지만, 만약 같은 머신에서 하나 이상의 JVM나 부족한 CPU에서 모두 돌리고 있다면, GC 쓰레드의 수를 줄이기 위해서 적절한 값으로 -XX:ParallelGCThreads 를 사용해야 한다. 예를들어, 4개 서버 JVM이 16개 프로세서 코어를 가진같은 머신에서 동작중이라면, -XX:ParallelGCThreads=4 는 다른 JVM 의 GC와 상호간섭이 일어나지 않기 위해서 적절한 선택이다.

-XX:-UseAdaptiveSizePolicy

처리율 컬렉션들은 GC설정에서 사용자 친화성을 향상시키기 위한 흥미로운(하지만 일반적인, 적어도 현대 JVM에서는) 메커니즘을 제공한다. 이 메커니즘은 java 5에서 HotSpot 를 위해서 소개된 “인체공학”이라고 알려진 일부분이다. 인체공학에서, 가비지 컬렉터는 GC세팅들과 마찬가지로 변경들이 GC성능을 향상시킬수 있다는 증거가 있다면 동적으로 다른 힙 영역의 크기에 변경을 가할 수도 있다. “GC 성능 향상”의 엄밀한 의미는 -XX:GCTimeRatio 와 -XX:MaxGCPauseMillis 를 통해 사용자에 의해서 지정되어질 수 있다.

인체공학이 기본적으로 활성화된다는 것을 알고 있는게 중요하다. 적응적인 행동(역주, 아마도 JVM이 시스템을 감지해 그것에 걸맞게 플래그 세팅값을 자동으로 세팅하는 것을 말하는것 같다) 처럼 인체공학은 JVM의 가장 강력한 기능중의 하나이다. 여전히, 가끔은 특정 애플리케이션에 최적인 세팅이 무엇인지를 아주 명확한 생각을 가져야 하고, JVM이 우리의 세팅을 나잡하게 하는걸 원하지 않는다. 이런 상황에서, 우리는 -XX:-UseAdaptiveSizePolicy 세팅을 통해서 몇몇의 인체공학의 비활성화를 고려해야 한다.

-XX:GCTimeRatio

-XX:GCTimeRatio=<value> 은 처리율 달성을 위해서 목표값을 JVM에 설정할 수 있다. 더 정확하게, -XX:GCTimeRatio=N 값은 애플리케이션 쓰레드의 실행시간에(총 프로그램 실행 시간과 연관된) 대해 N/(N+1)의 목표비율을 지정한다. 예를들어 -XX:GCTimeRatio=9 우리는 애플리케이션 쓰레드들이 적어도 총 실행시간의 9/10 을 활성화하게 요구할 수 있다. (그리고, 따라서, 나머지 1/10 이 GC 쓰레드들이다.) 실행시간에 측정을 바탕으로, JVM은 목표 처리량에 도달되도록 힙 및 GC 설정을 수정하려고 할 것이다. -XX:GCTimeRatio 기본값은 99인데, 애플리케이션 쓰레드들은 적어도 총 실행시간에 99퍼센트에 대해 실행한다.

-XX:MaxGCPauseMillis

플래그 -XX:MaxGCPauseMillis=<value>는 JVM에게 최대 일시정지 시간을 목표값(밀리초)으로 설정하도록 한다. 런타임에서, 처리율 컬렉터는 일시 중지 시간을 통해 통계를(가중 평균 및 표준 편차) 계산한다. 만일 통계가 목표값을 초가해 일시 정지시간을 경험할 위험이 있다고 제안하면, JVM은 그들을 줄이기 위해서 힙과 GC 세팅을 수정한다. 한가지 유의할 점은 통계는 young 과 old generation GC 별로 계산되어진다. 또 기본적으로 최대 정지 시간을 설정하는 목표값은 없다.

만일 최대 정지시간과 최소 처리율에 대한 목표값을 지정하면, 최대 일시 정지 시간 목표를 달성하는 것에 우선순위를 더 높게 가진다. 물론, JVM이 열심히 노력한다고 하더라도 양쪽 모두 목표 달성을 보증하지는 않는다. 결국, everything depends on the behavior of the application at hand

최대 일시 정지 시간 목표를 설정했을때, 우리는 너무 작은 값을 선택하지 않도록 주의해야 한다. 우리는 지금까지 알고 있듯이, 일시정지 시간을 낮게 유지하기 위해, JVM은 달성할수있는 처리량에 심각하게 영향을 주는 총 GC 갯수를 증가시켰다. 그것은 애플리케이션이 주요 목표처럼, 마치 웹 애플리케이션의 경우처럼, 낮은 일시정시간을 요구하기 때문에 나는 처리율 컬렉터만 사용하길 추천하지 않고 대신 CMS 컬렉터로 바꾸길 권한다. CMS 컬렉터는 이 시리즈의 다음번 주제다.

유용한 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 에서 보여주는 상응한 갑보다 약간 작다는 것에 의문을 가지고 있다. 왜 이렇게 차이가 나는지 알고 있는 사람이 있다면 나에게 좀 알려달라.