853
阿裏雲
技術社區[雲棲]
GC對吞吐量的影響
在看內存管理術語表的時候偶然發現了”Pig in the Python(注:有點像中文裏的貪心不足蛇吞象)”的定義,於是便有了這篇文章。表麵上看,這個術語說的是GC不停地將大對象從一個分代提升到另一個分代的情景。這麼做就好比巨蟒整個吞食掉它的獵物,以至於它在消化的時候都沒辦法移動了。

在接下來的這24個小時裏我的頭腦中充斥著這個令人窒息的巨蟒的畫麵,揮之不去。正如精神病醫生所說的,消除恐懼最好的方法就是說出來。於是便有了這篇文章。不過接下的故事我們要講的不是蟒蛇,而是GC的調優。我對天發誓。
大家都知道GC暫停很容易造成性能瓶頸。現代JVM在發布的時候都自帶了高級的垃圾回收器,不過從我的使用經驗來看,要找出某個應用最優的配置真是難上加難。手動調優或許仍有一線希望,但是你得了解GC算法的確切機製才行。關於這點,本文倒是會對你有所幫助,下麵我會通過一個例子來講解JVM配置的一個小的改動是如何影響到你的應用程序的吞吐量的。
示例
我們用來演示GC對吞吐量產生影響的應用隻是一個簡單的程序。它包含兩個線程:
- PigEater – 它會模仿巨蟒不停吞食大肥豬的過程。代碼是通過往java.util.List中添加 32MB字節來實現這點的,每次吞食完後會睡眠100ms。
- PigDigester – 它模擬異步消化的過程。實現消化的代碼隻是將豬的列表置為空。由於這是個很累的過程,因此每次清除完引用後這個線程都會睡眠2000ms。
兩個線程都會在一個while循環中運行,不停地吃了消化直到蛇吃飽為止。這大概得吃掉5000頭豬。
01 |
package eu.plumbr.demo;
|
03 |
public class PigInThePython {
|
04 |
static volatile List pigs = new ArrayList();
|
05 |
static volatile int pigsEaten = 0 ;
|
06 |
static final int ENOUGH_PIGS = 5000 ;
|
08 |
public static void main(String[] args) throws InterruptedException {
|
09 |
new PigEater().start();
|
10 |
new PigDigester().start();
|
13 |
static class PigEater extends Thread {
|
18 |
pigs.add( new byte [ 32 * 1024 * 1024 ]); //32MB per pig
|
19 |
if (pigsEaten > ENOUGH_PIGS) return ;
|
25 |
static class PigDigester extends Thread {
|
28 |
long start = System.currentTimeMillis();
|
32 |
pigsEaten+=pigs.size();
|
33 |
pigs = new ArrayList();
|
34 |
if (pigsEaten > ENOUGH_PIGS) {
|
35 |
System.out.format( "Digested %d pigs in %d ms.%n" ,pigsEaten, System.currentTimeMillis()-start);
|
42 |
static void takeANap( int ms) {
|
45 |
} catch (Exception e) {
|
現在我們將這個係統的吞吐量定義為“每秒可以消化的豬的頭數”。考慮到每100ms就會有豬被塞到這條蟒蛇裏,我們可以看到這個係統理論上的最大吞吐量可以達到10頭/秒。
GC配置示例
我們來看下使用兩個不同的配置係統的表現分別是什麼樣的。不管是哪個配置,應用都運行在一台擁有雙核,8GB內存的Mac(OS X10.9.3)上。
第一個配置:
- 4G的堆(-Xms4g -Xmx4g)
- 使用CMS來清理老年代(-XX:+UseConcMarkSweepGC)使用並行回收器清理新生代(-XX:+UseParNewGC)
- 將堆的12.5%(-Xmn512m)分配給新生代,並將Eden區和Survivor區的大小限製為一樣的。
第二個配置則略有不同:
- 2G的堆(-Xms2g -Xmx2g)
- 新生代和老年代都使用Parellel GC(-XX:+UseParallelGC)
- 將堆的75%分配給新生代(-Xmn 1536m)
現在是該下注的時候了,哪個配置的表現會更好一些(就是每秒能吃多少豬,還記得吧)?那些把籌碼放到第一個配置上的家夥,你們一定會失望的。結果正好相反:
- 第一個配置(大堆,大的老年代,CMS GC)每秒能吞食8.2頭豬
- 第二個配置(小堆,大的新生代,Parellel GC)每秒可以吞食9.2頭豬
現在我們來客觀地看待一下這個結果。分配的資源少了2倍但吞吐量提升了12%。這和常識正好相反,因此有必要進一步分析下到底發生了什麼。
分析GC的結果
原因其實並不複雜,你隻要仔細看一下運行測試的時候GC在幹什麼就能發現答案了。這個你可以自己選擇要使用的工具。在jstat的幫助下我發現了背後的秘密,命令大概是這樣的:
jstat -gc -t -h20 PID 1s
通過分析數據,我注意到配置1經曆了1129次GC周期(YGCT_FGCT),總共花了63.723秒:
1 |
Timestamp S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT |
2 |
594.0 174720.0 174720.0 163844.1 0.0 174848.0 131074.1 3670016.0 2621693.5 21248.0 2580.9 1006 63.182 116 0.236 63.419
|
3 |
595.0 174720.0 174720.0 163842.1 0.0 174848.0 65538.0 3670016.0 3047677.9 21248.0 2580.9 1008 63.310 117 0.236 63.546
|
4 |
596.1 174720.0 174720.0 98308.0 163842.1 174848.0 163844.2 3670016.0 491772.9 21248.0 2580.9 1010 63.354 118 0.240 63.595
|
5 |
597.0 174720.0 174720.0 0.0 163840.1 174848.0 131074.1 3670016.0 688380.1 21248.0 2580.9 1011 63.482 118 0.240 63.723
|
第二個配置一共暫停了168次(YGCT+FGCT),隻花了11.409秒。
1 |
Timestamp S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT |
2 |
539.3 164352.0 164352.0 0.0 0.0 1211904.0 98306.0 524288.0 164352.2 21504.0 2579.2 27 2.969 141 8.441 11.409
|
3 |
540.3 164352.0 164352.0 0.0 0.0 1211904.0 425986.2 524288.0 164352.2 21504.0 2579.2 27 2.969 141 8.441 11.409
|
4 |
541.4 164352.0 164352.0 0.0 0.0 1211904.0 720900.4 524288.0 164352.2 21504.0 2579.2 27 2.969 141 8.441 11.409
|
5 |
542.3 164352.0 164352.0 0.0 0.0 1211904.0 1015812.6 524288.0 164352.2 21504.0 2579.2 27 2.969 141 8.441 11.409
|
考慮到兩種情況下的工作量是等同的,因此——在這個吃豬的實驗中當GC沒有發現長期存活的對象時,它能更快地清理掉垃圾對象。而采用第一個配置的話,GC運行的頻率大概會是6到7倍之多,而總的暫停時間則是5至6倍。
說這個故事有兩個目的。第一個也是最主要的一個,我希望把這條抽風的蟒蛇趕緊從我的腦海裏趕出去。另一個更明顯的收獲就是——GC調優是個很需要技巧的經驗活,它需要你對底層的這些概念了如指掌。盡管本文中用到的這個隻是很平常的一個應用,但選擇的不同結果也會對你的吞吐量和容量規劃產生很大的影響。在現實生活中的應用裏麵,這裏的區別則會更為巨大。因此,就看你如何抉擇了,你可以去掌握這些概念,或者,隻關注你日常的工作就好了,讓Plumbr來找出你所需要的最合適的GC配置吧。
本文最早發表於我的個人博客Java譯站
最後更新:2017-05-23 16:03:57