閱讀853 返回首頁    go 阿裏雲 go 技術社區[雲棲]


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;
02  
03 public class PigInThePython {
04   static volatile List pigs = new ArrayList();
05   static volatile int pigsEaten = 0;
06   static final int ENOUGH_PIGS = 5000;
07  
08   public static void main(String[] args) throws InterruptedException {
09     new PigEater().start();
10     new PigDigester().start();
11   }
12  
13   static class PigEater extends Thread {
14  
15     @Override
16     public void run() {
17       while (true) {
18         pigs.add(new byte[32 1024 1024]); //32MB per pig
19         if (pigsEaten > ENOUGH_PIGS) return;
20         takeANap(100);
21       }
22     }
23   }
24  
25   static class PigDigester extends Thread {
26     @Override
27     public void run() {
28       long start = System.currentTimeMillis();
29  
30       while (true) {
31         takeANap(2000);
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);
36           return;
37         }
38       }
39     }
40   }
41  
42   static void takeANap(int ms) {
43     try {
44       Thread.sleep(ms);
45     catch (Exception e) {
46       e.printStackTrace();
47     }
48   }
49 }

現在我們將這個係統的吞吐量定義為“每秒可以消化的豬的頭數”。考慮到每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

  上一篇:go  躋身數據科學領域的五條職業規劃道路
  下一篇:go  除了4K非編NAS、VPN網關 阿裏雲在成都還發布了哪些產品?