2018년 9월 15일 토요일

[Java] Ergonomics - Hotspot VM 문서를 보며 이해하기

Ergonomics의 사전적 의미는 "인체 공학의"라는 뜻입니다만, 이걸 정확히 어떻게 번역해야할지 몰라 그냥 영어를 썼습니다. 사람에 따라 다르고 변수가 많아 이를 다루는 인체 공학의 성격과, JVM의 GC 설정 역시 machine에 따라 다르고 애플리케이션에 따라 변수가 생길 수 있는 상황이 비슷하기 때문에 Ergonomics라는 단어를 쓴 걸로 이해하면 되겠습니다.

Ergonomics는 JVM과 Garbage collection이 애플리케이션의 성능을 높이기 위해 행위 기반의 휴리스틱 조절과 같은 처리들을 일컫습니다.

JVM은 platform에 따라서 garbage collector와 heap 크기, 그리고 런타임 compiler의 기본 선택이 달라집니다. 다양한 애플리케이션 타입에 따라 다양한 설정이 있을 수 있는데 이에 따른 command line 튜닝을 더 적게 하기 위해서죠. 게다가 행위 기반의 튜닝은 애플리케이션의 동작에 따라 heap 의 크기를 동적으로 최적화하기도 합니다. 이번 포스팅에서는 기본 설정과 행위 기반의 튜닝에 대해 살펴볼 것입니다. 여기서 기본 설정이 어떤가를 이해하고, 상세한 설정에 대해서는 다른 섹션에서 다루도록 하겠습니다.

Garbage Collector, Heap, and Runtime Compiler 기본 설정

아래와 같은 물리 스펙의 서버가 기준입니다.
  • 2개 이상의 물리 프로세서
  • 2GB이상의 물리 메모리
위 경우엔 아래가 기본입니다.
  • Garbage-First (G1) collector
  • 물리 메모리의 1/64의 초기 heap 크기(16GB machine일 경우 256MB입니다)
  • 물리 메모리의 1/4의 최대 heap 크기(16GB machine일 경우 4GB입니다)
  • Tiered compiler, using both C1 and C2

행위 기반의 튜닝

HotSpot VM garbage collectors는 정지 시간 최소화 혹은 애플리케이션 throughput 향상, 두 목표 중 하나에 최적화 된 설정을 할 수 있습니다. 만약 둘 중 하나를 만족시켰다면, 나머지 하나를 최대한 만족시키려고합니다. 물론 둘을 항상 충족시킬 수 있는 건 아닙니다. 둘 중의 하나도 만족 못 할 수도 있는거죠. 애플리케이션은 최소 살아있는 모든 객체를 위한 공간이 있어야하는데, 다른 설정이 목표 달성이 불가능하도록 될 수도 있는 거죠

최대 정지시간 목표

 정지 시간은 Garbage collector가 애플리케이션을 멈추고 더이상 사용하지 않는 객체들을 정리하여 공간을 확보하는 것입니다. 최대 정지 시간의 목표는 이 정지되는 시간 중 가장 오래 정지되는 시간을 제한하는 것입니다. 평균 정지시간과 이 평균 정지시간의 분산값은 Garbage collector가 관리합니다. 평균은 Garbage collection 시작부터 걸리는 시간이지만 최근에 멈춘 횟수가 많을수록 더 무겁도록 가중될 수 있습니다. 만약 정지 시간의 분산을 더한 평균이 최대 목표 정지 시간보다 클 경우, Garbage collector는 그 목표는 충족되지 않았다고 판단하게 됩니다.
 최대 정지 시간 목표는 command-line option으로 -XXMaxGCPauseMillis=<nnn>으로 줄 수 있습니다. 이 옵션은 Garbage collector에게 <nnn>만큼보다 더 적게 정지하도록 하라는 힌트로 해석됩니다. Garbage collector는 Java Heap 크기와 <nnn> milli초 보다 더 짧도록 다른 파라미터를 조절합니다. 그러나 이렇게 조절함으로써 Garbage collection이 더 자주 일어날 수도 있고, 애플리케이션 전체 Throughput이 감소할 수 있습니다. 그럼에도 불구하고 설정된 정지 최대 정지 시간을 충족 못 시킬 수도 있습니다. 정리하면 -XXMaxGCPauseMillis 옵션은 권고일 뿐입니다. Garbage collector가 최선을 다하겠지만, 보장되는 값이 아니라는 점에 주의하셔야합니다.

Throughput 목표

이 목표는 Garbage를 수집하는데 걸린 시간이라는 측면과, Garbage collection과 별개의 애플리케이션 시간에 관련된 값으로 측정된 것입니다.
 command-line 옵션으로는 -XX:GCTimeRatio=<nnn>으로 지정할 수 있습니다. 이 비율은 Garbage collection시간과 애플리케이션이 동작하는 시간의 비율로 1/(1+nnn) 값입니다. 예를 들면 -XX:GCTimeRatio=19라면, 1/(1+19) = 5%이며 이는 전체 실행시간에서 Garbage collection 시간이 5%이하로 차지하도록 권고하는 값입니다.

Garbage collection의 소요시간은 실행 정지를 유발하는 모든 Garbage collection의 총 시간입니다. 만약 Throughput 목표가 충족되지 않는다면, Garbage collector가 가능한 행동은 수집 정지 사이 애플리케이션이 정상 동작하는 시간을 늘릴 수 있도록 heap size를 늘리는 것입니다.

만약 Garbage collector가 위 두 목표를 모두 충족시켰다면, 목표 중 하나가 충족되지 않도록 heap 크기를 줄입니다. Garbage collector가 사용할 수 있는 최대 최소 힙크기는 -Xms=<nnn>과 -Xmx=<mmm>으로 각각을 설정할 수 있습니다.

튜닝 전략


만약 기본 최대 heap size보다 더 크게 할 필요성을 알지 못한다면 heap을 최대값으로 설정하지 마세요. 딱 애플리케이션에서 충분한 만큼만 Throughput 목표로 설정하세요.
애플리케이션의 행동 변화는 heap이 늘었다 줄었다하는 크기 변화를 일으킬 수 있습니다. 예를 들면 애플리케이션이 어떠한 이유로든 메모리에 많은 할당을 한다고 합시다. 그러면 같은 throughput을 유지하기 위해 heap은 늘어나게 됩니다.
만약 힙 크기가 최대 크기로 늘어나고 throughput 목표가 충족되지 않는다면, 그건 throughput 목표 대비 heap 최대 크기가 너무 작은 것이라고 할 수 있습니다. 최대 heap 사이즈를 플랫폼의 최대 물리 메모리와 가까운 값으로 설정하되 메모리 swap이 일어나지 않도록 조정하세요. 그리고 다시 애플리케이션을 실행하시면 됩니다. 만약 그래도 throughput 목표가 여전히 충족되지 않았다면, 애플리케이션 실행시간에 대한 목표가 사용 가능한 메모리량에 비해 너무 높은 것입니다.
만약 Throughput 목표가 충족될 수 있지만, 정지시간이 너무 길다면, 최대 정지시간 목표를 설정하세요. 이 목표를 설정한다는 것은 throughput 목표가 충족되지 않을 수 있다는 것을 의미하며, 애플리케이션 특성에 따라 적절한 값으로 타협하여 설정하세요.


Garbage collector가 목표를 충족을 위해 heap사이즈를 위아래로 조정하는 것은 자연스러운 일입니다. 애플리케이션이 안정 상태에 도달했더라도 그럴 수 있습니다. Throughput 목표는 heap크기가 커져야 유리하며, 최대 정지시간은 작아져야 달성하기 쉽기때문에 이 두 목표는 상충될 수 있습니다.

[Java] GC introduction - Hotspot VM 문서를 보며 이해하기

이 문서는 Oracle Java10의 문서를 기반으로 이해한 내용을 거의 그대로 정리한 것입니다.

1. GC란 무엇인가?

Garbage collector는 문서에 다음과 같이 정의되어 있습니다.

Garbage collector는 다음과 같이 자동으로 동적 메모리 관리를 수행하는 것이다.

  • 운영체제로부터 메모리를 할당하고, 또 할당 받은 메모리를 돌려준다.(응?)
  • 애플리케이션에게 요청받은 메모리를 나눠줍니다(응?)
  • 애플리케이션에서 메모리의 어느 부분이 여전히 사용하는지 판단한다.
  • 사용하지 않는 메모리를 애플리케이션에서 다시 사용할 수 있도록 정리한다.

아니, Garbage collector는 메모리를 청소하는 놈 아닙니까?

네 저도 그렇게 알고 있었습니다만, 문서를 보니 할당과 해제를 모두 관리하는 놈이었군요(흠.. 인터레스팅)

계속해서 HotSpot Garbage collector는 그러한 것들의 동작을 효과적으로 하기 위해 다음과 같은 다양한 기술을 사용한다고 합니다.(별거 없음 주의)
  • 객체의 수명에 따른 세대 분리로 가장 많이 정리할 가능성이 있는 메모리 영역을 포함한 heap 영역에 집중함.
  • 여러 thread를 사용하여 병렬로 동작하거나 애플리케이션 백그라운더에서 동시에 오래동안 동작을 수행한다.
  • 살아남은 객체를 밀집 시킴으로써 여유 있는 공간들이 연속적으로 이어진 더 큰 공간이 되도록 함

자 그럼, 왜 적절한 Garbage Collector 선택이 중요할까요?

Garbage collector의 목적은 동적 메모리 관리를 애플리케이션 개발자가 하지 않도록 하기 위함입니다. C의 포인터를 써보신 분은 알 것입니다. alloc - free를 개발자가 명시적으로 함으로써 받게되는 스트레스를. 물론 Java 개발자들은 "개발은 편하지만 관리는 아니다"라는 점을 깨달으실겁니다. 세상의 공짜는 없으니까요. 어찌되었든, 개발자는 동적 메모리의 할당과 해제를 매우 세심하게 객체의 생명주기를 관리하면서 개발할 필요는 없어졌다 이 말입니다. 추가적인 런타임 오버헤드(GC 작업으로 인한)라는 비용을 떠안은 대신, 메모리 관리와 관련된 에러를 완전히 제거한 Trade-off의 결과죠.

자 그렇다면, Garbage collector는 언제 문제가 될까요? 몇몇 애플리케이션에서는 절대 문제가 되지 않습니다. 즉, 몇몇 애플리케이션에선 Garbage collection으로 인해 치명적이지 않은 횟수와 시간동안 잠시 멈출 뿐이기 때문에, 잘 동작할 수 있습니다. 그러나, 많은 클래스를 가지고 있는 애플리케이션, 특히 아주 많은 데이터(수 GB급), 많은 Thread와 많은 트랜잭션이 반복되는 그러한 애플리케이션에서는 그렇지 않습니다. 경험상, 지금 이 글을 보고 있을 현업자분들은 거의 모두 어느 정도는 신경을 쓰며 개발해야하며, 이 GC의 튜닝 경험이 경력의 척도가 되기도 합니다.

Amdahl('암달'이라고 읽음)의 법칙을 아십니까? 뜬금없이 웬 Amdahl이냐고요? Amdahl의 법칙에 따르면, 주어진 문제에 있어 병렬 처리 속도의 향상은 문제의 순차적인 부분에 의해서 제한된다고 합니다. 무슨 의미냐구요? 대부분의 부하는 완벽하게 병렬처리화 될 수는 없다는 겁니다. 일부는 항상 순차적으로 해야하기때문에 병렬처리로 항상 해결할 수 있는게 아니라는 말이죠. Java 플랫폼에서는 현재 네 가지의 Garbage collection이 지원되며, Serial GC만 빼고 나머지는 병렬처리로 성능을 향상 시킬수 있으나, Garbage collection작업을 하는 동안의 오버헤드를 가능한 낮게 하는게 정말 중요합니다.

Figure 1-1은 Garbage collection의 overhead와 프로세서 수에 따른 Throughput의 저하를 보여줍니다.

Description of Figure 1-1 follows
Figure 1-1
위 도표를 보았을 때 저처럼 의문을 가지는 사람이 있을겁니다. 프로세서가 많은데 왜 Throughput이 더 떨어지지? 왜냐면 그게 Amdahl의 망할 법칙때문이거든요. Throughput은 처리"율"을  의미합니다. 아무리 병렬처리를 하더라도, 순차적으로 처리해야하는 부분은 병렬처리로도 답이 없기때문에 무한정 processor를 늘리더라도 오히려 효율은 떨어지는 셈인거죠. GC 시간이 1%대 정도의 어플리케이션이라면 processor가 2개일때보다 32개일 때 효율이 약 20%가 손실되는 셈입니다. GC시간이 30%를 차지하는 (이미 망한) 애플리케이션은 더 처참하죠. Throughput의 90%가 손실됩니다.

이 도표는 작은 시스템을 개발할 때의 throughput 문제는 무시할 정도였지만 큰 시스템으로 scale-up될 때 주요 병목이 될 수도 있다는 것을 보여줍니다. 그러나 병목을 제거하는 작은 개선이 큰 성능 향상을 가져올 수 있다고 볼 수도 있습니다. 충분히 큰 시스템은 올바른 Garbage collector를 선택하고 필요하다면 적절히 튜닝하는 것이 가치가 있다는 것을 말하는 것이죠.

Serial collector는 보통 매우 작은 애플리케이션에 적합합니다. 특히 현대의 프로세서 수준에서 약 100MB 까지의 heap을 요구하는 시스템말이죠. (엔터프라이즈 애플리케이션에서는 과연 이 정도만를 요구하는 시스템이 있을까 모르겠습니다만...) 나머지 collector들은 추가적인 오버헤드와 복잡성을 가지고 있는데 이는 특화된 동작 방식에 대한 비용입니다. 만약 특화된 동작 방식이 필요없는 애플리케이션이라면 그냥 serial collector를 쓰시면됩니다. Serial collector가 적합하지 않을 때가 언제냐면, 많은 양의 메모리와 2개 이상의 프로세서 환경에서 크고 무거운 쓰레드들이 있는 애플리케이션입니다. 애플리케이션이 서버일 때는 G1 collector가 기본 collector입니다(Java9부터 적용되는 이야기입니다. Java9이전은 parallel collector가 기본 collector입니다)

2018년 9월 1일 토요일

[자료구조] 왜 Java는 heap을 사용하는가(heap memory와 heap data structure의 연관성)

Computer science를 공부하다보면, 필수적으로 공부하는 것 중 대표적인 것은 Data structure(자료구조)와 Programming language일 것이다.

일반적으로 Data structure의 후반부를 공부하게 되면 Tree형 자료구조를 공부하며 Heap data structure를 공부해보았을 것이다.

그리고 Programming language에서, Java, C, C++등 동적할당에 해당하는 new키워드로 instance 생성시에 heap memory에 해당하는 영역에 memory가 할당된다는 것도 알고 있을 것이다.

그렇다면 아래와 같은 의문을 가질 수 있다.

왜 동적할당이 되는 영역의 memory는 Heap일까? List나 다른 자료구조일 수는 없는걸까?

정답을 말하면, Heap Memory와 Heap Data structure는 전혀 별개다. 이름때문에 헷갈릴 수 있지만 Heap memory에서 말하는 Heap은 Pool에 가깝다. 링크에 따르면 1975년 경부터 가용 메모리 pool의 개념으로 heap이란 단어를 썼다고 한다.

그러니 헷갈리지 말자.

요약 : Heap Memory와 Heap data structure는 전혀 관계가 없다.

[TroubleShooting] sbt could not find or load main class file

Have you gotten this error message, when you execute sbt in git bash? Error: Could not find or load main class file Caused by: java.lang....