时间:2020-11-28 14:14:45 | 栏目:JAVA代码 | 点击:次
在看内存管理术语表的时候偶然发现了”Pig in the Python(注:有点像中文里的贪心不足蛇吞象)”的定义,于是便有了这篇文章。表面上看,这个术语说的是GC不停地将大对象从一个分代提升到另一个分代的情景。这么做就好比巨蟒整个吞食掉它的猎物,以至于它在消化的时候都没办法移动了。
在接下来的这24个小时里我的头脑中充斥着这个令人窒息的巨蟒的画面,挥之不去。正如精神病医生所说的,消除恐惧最好的方法就是说出来。于是便有了这篇文章。不过接下的故事我们要讲的不是蟒蛇,而是GC的调优。我对天发誓。
大家都知道GC暂停很容易造成性能瓶颈。现代JVM在发布的时候都自带了高级的垃圾回收器,不过从我的使用经验来看,要找出某个应用最优的配置真是难上加难。手动调优或许仍有一线希望,但是你得了解GC算法的确切机制才行。关于这点,本文倒是会对你有所帮助,下面我会通过一个例子来讲解JVM配置的一个小的改动是如何影响到你的应用程序的吞吐量的。
示例
我们用来演示GC对吞吐量产生影响的应用只是一个简单的程序。它包含两个线程:
PigEater ?C 它会模仿巨蟒不停吞食大肥猪的过程。代码是通过往java.util.List中添加 32MB字节来实现这点的,每次吞食完后会睡眠100ms。
PigDigester ?C 它模拟异步消化的过程。实现消化的代码只是将猪的列表置为空。由于这是个很累的过程,因此每次清除完引用后这个线程都会睡眠2000ms。
两个线程都会在一个while循环中运行,不停地吃了消化直到蛇吃饱为止。这大概得吃掉5000头猪。
public class PigInThePython {
static volatile List pigs = new ArrayList();
static volatile int pigsEaten = 0;
static final int ENOUGH_PIGS = 5000;
public static void main(String[] args) throws InterruptedException {
new PigEater().start();
new PigDigester().start();
}
static class PigEater extends Thread {
@Override
public void run() {
while (true) {
pigs.add(new byte[32 * 1024 * 1024]); //32MB per pig
if (pigsEaten > ENOUGH_PIGS) return;
takeANap(100);
}
}
}
static class PigDigester extends Thread {
@Override
public void run() {
long start = System.currentTimeMillis();
while (true) {
takeANap(2000);
pigsEaten+=pigs.size();
pigs = new ArrayList();
if (pigsEaten > ENOUGH_PIGS) {
System.out.format("Digested %d pigs in %d ms.%n",pigsEaten, System.currentTimeMillis()-start);
return;
}
}
}
}
static void takeANap(int ms) {
try {
Thread.sleep(ms);
} catch (Exception e) {
e.printStackTrace();
}
}
}
现在我们将这个系统的吞吐量定义为“每秒可以消化的猪的头数”。考虑到每100ms就会有猪被塞到这条蟒蛇里,我们可以看到这个系统理论上的最大吞吐量可以达到10头/秒。
GC配置示例
我们来看下使用两个不同的配置系统的表现分别是什么样的。不管是哪个配置,应用都运行在一台拥有双核,8GB内存的Mac(OS X10.9.3)上。
第一个配置:
1.4G的堆(-Xms4g -Xmx4g)
2.使用CMS来清理老年代(-XX:+UseConcMarkSweepGC)使用并行回收器清理新生代(-XX:+UseParNewGC)
3.将堆的12.5%(-Xmn512m)分配给新生代,并将Eden区和Survivor区的大小限制为一样的。
第二个配置则略有不同:
1.2G的堆(-Xms2g -Xms2g)
2.新生代和老年代都使用Parellel GC(-XX:+UseParallelGC)
3.将堆的75%分配给新生代(-Xmn 1536m)
4.现在是该下注的时候了,哪个配置的表现会更好一些(就是每秒能吃多少猪,还记得吧)?那些把筹码放到第一个配置上的家伙,你们一定会失望的。结果正好相反:
1.第一个配置(大堆,大的老年代,CMS GC)每秒能吞食8.2头猪
2.第二个配置(小堆,大的新生代,Parellel GC)每秒可以吞食9.2头猪
现在我们来客观地看待一下这个结果。分配的资源少了2倍但吞吐量提升了12%。这和常识正好相反,因此有必要进一步分析下到底发生了什么。
分析GC的结果
原因其实并不复杂,你只要仔细看一下运行测试的时候GC在干什么就能发现答案了。这个你可以自己选择要使用的工具。在jstat的帮助下我发现了背后的秘密,命令大概是这样的:
通过分析数据,我注意到配置1经历了1129次GC周期(YGCT_FGCT),总共花了63.723秒:
第二个配置一共暂停了168次(YGCT+FGCT),只花了11.409秒。
考虑到两种情况下的工作量是等同的,因此――在这个吃猪的实验中当GC没有发现长期存活的对象时,它能更快地清理掉垃圾对象。而采用第一个配置的话,GC运行的频率大概会是6到7倍之多,而总的暂停时间则是5至6倍。
说这个故事有两个目的。第一个也是最主要的一个,我希望把这条抽风的蟒蛇赶紧从我的脑海里赶出去。另一个更明显的收获就是――GC调优是个很需要技巧的经验活,它需要你对底层的这些概念了如指掌。尽管本文中用到的这个只是很平常的一个应用,但选择的不同结果也会对你的吞吐量和容量规划产生很大的影响。在现实生活中的应用里面,这里的区别则会更为巨大。因此,就看你如何抉择了,你可以去掌握这些概念,或者,只关注你日常的工作就好了,让Plumbr来找出你所需要的最合适的GC配置吧。