GC对吞吐量的影响

    xiaoxiao2024-03-06  128

    在看内存管理术语表的时候偶然发现了”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秒:

    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对吞吐量的影响测试
    最新回复(0)