这篇文章的灵感来自于在内存管理术语中的“ Pig in the Python ”定义。 显然,该术语用于解释GC反复促进大对象世代相传的情况。 据推测,这样做的效果类似于Python吞下整个猎物,只是在消化过程中被固定住了。
在接下来的24小时里,我简直无法理解令人窒息的python的图片。 就像精神科医生所说的那样,消除恐惧的最好方法就是谈论它们。 所以我们开始。 但是,除了Python,其余的故事都是关于垃圾收集优化的。 我承诺。
垃圾收集暂停是众所周知的性能瓶颈。 现代JVM确实带有高级垃圾收集器,但是据我所知,为特定应用程序找到最佳配置仍然很困难。 为了有机会手动解决该问题,需要了解垃圾收集算法的确切机制。 这篇文章可能会在这方面为您提供帮助,因为我将使用一个示例来演示JVM配置中的微小更改如何影响应用程序的吞吐量。
例
我们用来演示GC对吞吐量影响的应用程序很简单。 它仅包含两个线程:
- PigEater –模拟一种情况,Python不断吃掉另一头猪。 该代码通过在java.util.List中添加32MB字节并在每次尝试后休眠100ms来实现此目的。
- PigDigester –模拟异步摘要过程。 该代码仅通过使该猪列表无效来实现消化。 由于这是一个相当累的过程,因此在每次参考清洁后,该线程将休眠2000ms。
两个线程都会在while循环中运行,继续吃和消化,直到蛇吃饱为止。 大约有5,000头猪被吃掉。
package eu.plumbr.demo;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 {@Overridepublic void run() {while (true) {pigs.add(new byte[32 * 1024 * 1024]); //32MB per pigif (pigsEaten > ENOUGH_PIGS) return;takeANap(100);}}}static class PigDigester extends Thread {@Overridepublic 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();}}
}
现在让我们将该系统的吞吐量定义为“每秒消化的猪的数量”。 考虑到每100毫秒后将猪塞入python,我们看到该系统的理论最大吞吐量因此可以达到10头/秒。
配置GC示例
让我们来看一下使用两种不同配置的系统行为。 在所有情况下,该应用程序都是使用具有8G物理内存的双核Mac(OS X 10.9.3)运行的。
第一种配置:
- 4G堆( -Xms4g –Xmx4g )
- 使用CMS清理旧的(-XX:+ UseConcMarkSweepGC ),并并行清理年轻的-XX:+ UseParNewGC )
- 已将堆的12,5%(- Xmn512m )分配给年轻一代,从而进一步将Eden和Survivor空间的大小限制为相同大小。
第二种配置有些不同:
- 2G堆( -Xms2g –Xmx2g )
- 使用并行GC在年轻一代和终身一代中进行垃圾收集( -XX:+ UseParallelGC )
- 已将堆的75%分配给年轻一代( -Xmn1536m )
现在该下赌注了,哪种配置在吞吐量方面表现更好(每秒吃掉的猪,还记得吗?)。 你们那些花钱买第一种配置的人,我一定会让您失望的。 结果完全相反:
- 第一种配置(大堆,较大的旧空间,CMS GC)每秒可以吃掉8.2头猪
- 第二种配置(较小的堆2倍,较大的幼小空间,并行GC)每秒可吞噬9.2头猪
现在,让我对结果进行透视。 分配的资源减少了2倍(在内存方面),我们的吞吐量提高了12% 。 这与常识相反,可能需要进一步澄清实际发生的情况。
解释GC结果
您所面对的原因并不是太复杂,而当您更仔细地观察测试运行期间GC的操作时,答案就直盯着您。 为此,您可以使用自己选择的工具,我在jstat的帮助下进行了深入研究,类似于以下内容:
jstat -gc -t -h20 PID 1秒
查看数据,我注意到第一个配置经历了1,129个垃圾回收周期(YGCT + FGCT),总共花费了63.723秒:
Timestamp S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT
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
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
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
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秒。
Timestamp S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT
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
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
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
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倍。
因此,这个故事实现了两个目的。 首先,最重要的是,我希望我能从头上看到一条令人窒息的Python的照片。 另一个更重要的收获是–调整GC充其量是一项棘手的工作,需要深刻理解几个基本概念。 即使使用本博客文章中使用的真正琐碎的应用程序,您将要面对的结果也会对吞吐量和容量规划产生重大影响。 在实际应用中,差异甚至更加惊人。 因此,选择就是您的选择,您既可以掌握概念,也可以专注于日常工作,并让Plumbr根据您的需求找到合适的GC配置 。
翻译自: https://www.javacodegeeks.com/2014/09/garbage-collection-increasing-the-throughput.html