首頁  >  文章  >  Java  >  詳解為任務關鍵型Java應用優化垃圾回收(上)

詳解為任務關鍵型Java應用優化垃圾回收(上)

黄舟
黄舟原創
2017-03-23 11:01:011053瀏覽

最近,我有機會去測試並優化幾個基於Java購物和門戶網站程序,它們運行在Sun/Oracle的JVM上。訪問量最高的幾個應用在德國。在很多情況下,垃圾回收是Java伺服器效能的關鍵因素。本文中,我們將研究一些先進垃圾回收演算法想法以及一些重要的調節參數。我們會在各種真實場景中比較這些參數。

從垃圾回收的角度來看,Java伺服器程式可以有廣泛多樣的需求:

  1. 一些高流量應用程式需要回應大量請求並創建非常多的對象。有時候,一些使用了高資源消耗框架的中等流量應用也會遇到相同的問題。總之,對垃圾回收來說,如何有效清理這些產生的物件是一個很大的挑戰。

  2. 另外,有些應用程式需要長時間運作並且在運行過程提供穩定的服務,要求效能不會隨著時間而慢慢變差或突然惡化。

  3. 某些場景需要嚴格限制使用者回應時間(例如網路遊戲或投注應用程式等),幾乎不允許額外的GC暫停。

在許多場景中,你可以透過不同的優先順序將幾個需求結合起來。我的幾個範例程式對第一點要求比第二點高很多,但是絕大部分程式不會同時對這三方面要求都高。這給你留下了足夠權衡的空間。

預設配置下JVM GC的效能

JVM有很多改進,但在程式執行時仍無法對任務做最佳化。除了上面提到的三點,預設的JVM設定還有一個優先權僅次於它們的需求:減少記憶體佔用。考慮到成千上萬的用戶並不是在記憶體充足的伺服器上運行。對許多電子商務產品也很重要,因為這些應用程式大部分時間都配置在開發筆記本上運行,而不是在商用伺服器上。因此,如果你的伺服器配置最小的堆空間和GC參數,例如下面這樣配置,

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

這樣肯定會導致系統運作不夠有效率。首先,好的做法不僅配置記憶體最大限制,還需要配置初始記憶體大小,以避免伺服器在啟動過程中逐步增加記憶體。否則代價會很大。當知道伺服器需要多少記憶體時(你應該及時查明),最好將初始記憶體大小與最大記憶體設定相等。可以透過以下JVM參數來設定:

-Xms1024m -XX:PermSize=256m

最後一個經常在JVM配置的基本選項是配置新生代堆記憶體大小,與上面設定的方式類似:

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

下面的章節會對上面的配置以及更複雜的配置給出解釋。首先,讓我們來看一個入口網站的應用,它運行在一台相當慢的測試主機上。當進行負載測試時,它的垃圾回收是怎麼工作的:

圖1 堆大小稍微優化後的JVM在25小時左右的GC行為(-Xms1024m - Xmx1024m -XX:NewSize=200m -XX:MaxNewSize=200m)

其中,藍色的曲線表示總的堆內存佔用量隨時間的變化,垂直的灰色線條表示GC暫停的間隔。

除了曲線圖,GC操作的關鍵指標和效能顯示在最右邊。首先我們來看看在這次測試中,垃圾被創造(和回收)的平均量。 30.5MB/s的數值被標示為黃色,因為這是一個相當大但還可以的垃圾產生速率,對一個引導性的GC調優例子而言還算可以。其他數值表示JVM在清理這些垃圾時的表現:99.55%的垃圾是在新生代中被清理的,老年代的只佔0.45%。這個結果相當不錯,因此標示為綠色。

之所以有這樣的結果,可以從GC引入的暫停間隔看出來(以及處理用戶請求的工作線程):有很多但很短暫的新生代GC間隔,平均每6s一次,持續時間不會超過50ms。這些暫停使JVM停止運作的時間佔總時間的0.77%,但每次暫停對等待伺服器回應的使用者來說完全感覺不到。

另一方面,老年代GC的暫停只佔總時間的0.19%。但是,在這段期間老年代GC只清理了0.45%的垃圾,而新生代GC用佔0.77%的時間清理了99.55%的垃圾。可見,與新生代GC相比,老年代GC是多麼低效。另外,老年代GC的暫停平均觸發速率不到一小時一次,但平均持續時間可達到8s,最大異常值甚至達到19s。由於這些暫停會真正停止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應用優化垃圾回收(上)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn