>  기사  >  Java  >  미션 크리티컬 Java 애플리케이션을 위한 가비지 수집 최적화에 대한 자세한 설명(1부)

미션 크리티컬 Java 애플리케이션을 위한 가비지 수집 최적화에 대한 자세한 설명(1부)

黄舟
黄舟원래의
2017-03-23 11:01:011023검색

최근에는 Sun/Oracle JVM에서 실행되는 여러 Java 기반 쇼핑 및 포털 애플리케이션을 테스트하고 최적화할 기회가 있었습니다. 가장 많이 방문한 앱은 독일에 있습니다. 많은 경우 가비지 수집은 Java 서버 성능에 중요한 요소입니다. 이 기사에서는 몇 가지 고급 가비지 수집 알고리즘 아이디어와 몇 가지 중요한 조정 매개변수를 연구합니다. 다양한 실제 시나리오에서 이러한 매개변수를 비교합니다.

가비지 수집 관점에서 볼 때 Java 서버 프로그램에는 다양한 요구 사항이 있을 수 있습니다.

  1. 일부 트래픽이 많은 애플리케이션은 많은 양의 데이터에 응답해야 합니다. 요청을 처리하고 매우 많은 개체를 생성합니다. 때로는 높은 리소스 소비 프레임워크를 사용하는 일부 중간 트래픽 애플리케이션에서 동일한 문제가 발생합니다. 즉, 이렇게 생성된 개체를 효과적으로 정리하는 방법은 가비지 수집에 있어서 큰 과제입니다.

  2. 또한 일부 애플리케이션은 장시간 실행되어야 하며, 운영 중에도 안정적인 서비스를 제공해야 하므로 시간이 지남에 따라 성능이 서서히 저하되거나 갑자기 저하되지 않아야 합니다.

  3. 일부 시나리오에서는 사용자 응답 시간에 대한 엄격한 제한이 필요하며(예: 온라인 게임 또는 베팅 애플리케이션) 추가 GC 일시 중지가 거의 허용되지 않습니다.

많은 시나리오에서 여러 요구사항을 서로 다른 우선순위와 결합할 수 있습니다. 내 샘플 프로그램 중 일부는 두 번째 지점보다 첫 번째 지점에 대한 요구 사항이 훨씬 높지만 대부분의 프로그램은 세 가지 측면 모두에 대해 동시에 높은 요구 사항을 갖고 있지 않습니다. 이로 인해 절충의 여지가 많이 남게 됩니다.

기본 구성에서 JVM GC의 성능

JVM은 많은 개선이 있었지만 여전히 프로그램이 실행되는 동안 작업을 최적화할 수 없습니다. 위에서 언급한 세 가지 사항 외에도 기본 JVM 설정에는 메모리 사용량 감소라는 두 번째 우선 순위가 있습니다. 메모리가 충분한 서버에서 수천 명의 사용자가 실행되고 있지 않다는 점을 고려하십시오. 또한 이러한 애플리케이션은 상용 서버가 아닌 대부분의 경우 개발 랩톱에서 실행되도록 구성되어 있기 때문에 많은 전자 상거래 제품에 있어서도 중요합니다. 따라서 서버가

java -Xmx1024m -XX:MaxPermSize=256m -cp Portal.jar my.portal.Portal

와 같이 최소 힙 공간과 GC 매개변수로 구성된 경우 시스템이 비효율적으로 실행될 수 있습니다. 첫째, 서버가 시작하는 동안 점차적으로 메모리가 늘어나는 것을 방지하기 위해 최대 메모리 제한뿐만 아니라 초기 메모리 크기도 구성하는 것이 좋습니다. 그렇지 않으면 비용이 매우 많이 들 것입니다. 서버에 필요한 메모리 양을 알게 되면(그리고 시간이 지나면 알아내야 함) 초기 메모리 크기를 최대 메모리 설정과 동일하게 만드는 것이 가장 좋습니다. 다음 JVM 매개변수를 통해 설정할 수 있습니다.

-Xms1024m -XX:PermSize=256m

JVM에서 자주 구성되는 마지막 기본 옵션은 위에서 설정한 방식과 유사하게 차세대 힙 메모리 크기를 구성하는 것입니다.

-XX:NewSize=200m -XX:MaxNewSize=200m

다음 장에서는 위 구성과 더 복잡한 구성에 대해 설명합니다. 먼저 상당히 느린 테스트 호스트에서 실행되는 포털 애플리케이션을 살펴보겠습니다. 로드 테스트를 수행할 때 가비지 수집은 어떻게 작동합니까?

그림 1 약 25시간 동안 약간 최적화된 힙 크기를 사용한 JVM의 GC 동작(-Xms1024m - Xmx1024m -XX: NewSize=200m -XX:MaxNewSize=200m)

이 중 파란색 곡선은 시간에 따른 총 힙 메모리 사용량의 변화를 나타내고, 회색 세로선은 GC 일시정지 간격을 나타냅니다.

그래프 외에 가장 오른쪽에는 GC 운영의 주요 지표와 성과가 표시됩니다. 먼저 이 테스트 중에 생성된(및 수집된) 가비지의 평균 양을 살펴보겠습니다. 30.5MB/s 값은 노란색으로 표시됩니다. 이는 규모가 크지만 허용 가능한 가비지 생성 속도이며 입문 GC 튜닝 예제에 적합하기 때문입니다. 다른 값은 이 쓰레기를 정리하는 JVM의 성능을 나타냅니다. 새 세대에서는 쓰레기의 99.55%가 정리되고 이전 세대에서는 0.45%만 정리됩니다. 이 결과는 꽤 좋으므로 녹색으로 표시됩니다.

이러한 결과의 이유는 GC(및 사용자 요청을 처리하는 작업자 스레드)에서 도입한 일시 중지 간격에서 볼 수 있습니다. 차세대 GC 간격은 많지만 매우 짧습니다. 평균적으로 6초에 한 번입니다. 지속 시간은 50ms를 넘지 않습니다. 이러한 일시 중지로 인해 전체 시간의 0.77% 동안 JVM이 중지되었지만 각 일시 중지는 서버의 응답을 기다리는 사용자가 전혀 인지할 수 없었습니다.

반면, Old Generation GC Pause는 전체 시간의 0.19%만을 차지합니다. 하지만 이 기간 동안 구세대 GC는 0.45%의 쓰레기만 청소한 반면, 신세대 GC는 0.77%의 시간을 들여 99.55%의 쓰레기를 청소했다. 신세대 GC에 비해 구세대 GC가 얼마나 비효율적인지 알 수 있다. 또한, 구세대 GC 일시중지의 평균 트리거 속도는 시간당 1회 미만이지만 평균 지속 시간은 8초에 도달할 수 있고 최대 이상치는 19초에 달합니다. 이러한 일시 중지는 실제로 사용자 요청을 처리하는 JVM 스레드를 중지하므로 일시 중지는 가능한 한 드물고 지속 시간이 짧아야 합니다.

위의 관찰을 통해 세대별 가비지 수집의 기본 튜닝 목표를 도출할 수 있습니다.

  • 신세대 GC는 빈번한 가비지 수집을 피하기 위해 최대한 많은 가비지를 수집해야 합니다. 이전 세대에서는 GC가 자주 발생합니다.

分代垃圾回收的基本思想与堆内存大小调整

先从下图开始。这个图可以通过JDK工具得到,比如jstat或者jvisualvm以及它的visualgc插件:

图2 JVM的堆内存结构,包括新生代的子分区(最左列)

Java的堆内存由永久代(Perm),老年代(Old)和新生代(New or Young)组成。新生代进一步划分为一个Eden空间和两个Survivor空间S0、S1。Eden空间是对象被创建时的地方,经过几轮新生代GC后,他们有可能被存放在Survivor空间。如果想了解更多,可以读一下Sun/Oracle的白皮书Memory Management in the Java HotSpot Virtual Machine

默认情况下,作为整体的新生代特别是Survivor空间太小,导致在GC清理大部分内存之前就无法保存更多对象。因此,这些对象被过早地保存在老年代中,这会导致老年代被迅速填满,必须频繁地清理垃圾。这也是图1中产生较多的Full GC暂停的原因。

(译者注:一般新生代的垃圾回收也称为Minor GC,老年代的垃圾回收称为Major GC或Full GC)

优化新生代内存大小

优化分代垃圾回收意味着让新生代,特别是Survivor空间,比默认情形大。但是同时也要考虑虚拟机使用的具体GC算法。

当前硬件上运行的Sun/Oracle虚拟机使用了ParallelGC作为默认GC算法。如果使用的不是默认算法,可以通过显式配置JVM参数来实现:

-XX:+UseParallelGC

默认情况下,这个算法并不在固定大小的Eden和Survivor空间中运行。它使用了一种自适应调整大小的策略,称为“AdaptiveSizePolicy”策略。正如描述的那样,它可以适应很多场景,包括服务器以外的机器的使用。但在服务器上运行时,这并不是最优策略。为了可以显式地设置固定的Survivor空间大小,可以通过以下JVM参数关闭它:

-XX:-UseAdaptiveSizePolicy

一旦这么设置后,就不能进一步增加新生代空间的大小,但我们可以有效地为Survivor空间设置合适的大小:

-XX:NewSize=400m -XX:MaxNewSize=400m -XX:SurvivorRatio=6

“SurvivorRatio=6”表示Survivor空间是Eden空间的1/6或者是整个新生代空间的1/8,在这个例子中就是50MB,而自适应大小策略经常运行在非常小的空间上,大约只有几MB。使用现在的配置,重复上面的负载测试,我们得到了下面的结果:

图3 堆内存优化后的JVM在50小时内的GC行为(-Xms1024m -Xmx1024m -XX:NewSize=400m -XX:MaxNewSize=400m -XX:-UseAdapativeSizePolicy -XX:SurvivorRatio=6)

这次的测试时间是上次的两倍,而垃圾的平均创建速率和之前基本一致(30.2MB/s,之前是30.5MB/s)。然而,整个过程只有两次老年代(Full)GC暂停,25小时左右才发生一次。这是因为老年代垃圾死亡速率(所谓的promation rate)从137kB/s减小到了6kB/s,老年代的垃圾回收只占整体的0.02%。同时新生代GC的暂停持续时间仅仅从平均48ms增加到57ms,两次暂停的间隔从6s增长到10s。总之,关闭了自适应大小调整,合理地优化堆内存大小,使GC暂停占总时间的比例从0.95%减小到0.59%,这是一个非常棒的结果。

优化后,使用ParNew算法作为默认ParallelGC的替代,也能得到相似的结果。这个算法是为了与CMS算法兼容而开发的,可以通过JVM参数来配置-XX:+UseParNewGC。关于CMS下面会提到。这个算法不使用自适应大小策略,可以运行在固定Survivor大小的空间上。因此,即使使用默认的配置SurvivorRatio=8,也比ParallelGC拥有更高的服务器利用率。

避免老年代GC的长时间暂停

上述结果的最后一个问题就是,老年代GC的长时间暂停平均为8s左右。通过适当的优化,老年代GC暂停已经很少了,但是一旦触发,对用户来说还是很烦人的。因为在暂停期间,JVM不能执行工作线程。在我们的例子中,8s的长度是由低速老旧的测试机导致的,在现代硬件上速度能快3倍左右。另一方面,现在的应用一般使用1G以上的堆内存,可以容纳更多的对象。当前的网络应用使用的堆内存能达到64GB,(至少)需要一半的内存来保存存活的对象。在这种情况下,8s对老年代暂停来说是很短的。这些应用中的老年代GC可以很随意地就接近1分钟,对于交互式网络应用来说是绝对不能接受的。

缓解这个问题的一个选择就是采用并行的方式处理老年代GC。默认情况下,在Java 6中,ParallelGC和ParNew GC算法使用多个GC线程来处理新生代GC,而老年代GC是单线程的。以ParallelGC回收器为例,可以在使用时添加以下参数:

-XX:+UseParallelOldGC

从Java 7开始,这个选项和-XX:+UseParallelGC默认被激活。但是,即使你的系统是4核或8核,也不要期望性能可以提高2倍以上。通常的结果会比2被小一些。在某些例子中,比如上述例子中的8s,这种提高还是比较有效的。但在很多极端的例子中,这还远远不够。解决方法是使用低延迟GC算法。

下篇中会讨论CMS(The Concurrent Mark and Sweep Collector)、幽灵般的碎片、G1(Garbage First)垃圾收集器和垃圾收集器的量化比较,最后给出总结。

위 내용은 미션 크리티컬 Java 애플리케이션을 위한 가비지 수집 최적화에 대한 자세한 설명(1부)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.