在看内存管理术语表的时候偶然发现了”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头/秒。
我们来看下使用两个不同的配置系统的表现分别是什么样的。不管是哪个配置,应用都运行在一台拥有双核,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在干什么就能发现答案了。这个你可以自己选择要使用的工具。在jstat的帮助下我发现了背后的秘密,命令大概是这样的:
jstat -gc -t -h20 PID 1s
通过分析数据,我注意到配置1经历了1129次GC周期(YGCT_FGCT),总共花了63.723秒:
1Timestamp 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秒。
1Timestamp 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译站
转载自 并发编程网 - ifeve.com 相关资源:Java中垃圾回收器GC对吞吐量的影响测试