本文系翻译: 原文地址:mechanical-sympathy.blogspot.com/2013/07/java-garbage-collection-distilled.html
java垃圾回收精华
串行(Serial),并行(Parallel),并发(Concurrent),CMS,G1,年轻代(Young Gen),新生代(New Gen),老生代(Old Gen),永久代(Perm Gen),伊甸区(Eden), 年老区(Tenured), 幸存区(Survivor Spaces),安全点(Safepoints),和数百种JVM启动标志。当你试图调优垃圾回收器,使你的java应用能获得所需要的吞吐量和延迟,这些概念难到你了吗?如果它们使你困惑,我相信很多人也正和你一样。阅读垃圾回收的文档感觉就像是阅读飞机的帮助文档一样。每一个旋钮和仪表盘都有详细的解释。但是没有地方指导你怎么让它能飞起来。本文将试图解释在特定的工作中选择和调优垃圾回收算法的一些权衡点。
我们主要关注通常使用的Oracle的Hotspot JVM 和OpenJDK的收集器。在最后我们会讨论其他商业的JVMS来说明其他的方案。
权衡点(The Tradeoffs)
俗话说:“从来没有不劳而获”。当我们得到某些事物的时候,通常不得不需要放弃另外一些事物,当谈论垃圾收集的时候,我们主要考虑三个收集器的指标:
1、吞吐量:花费在GC上的时间占整个应用程序工作的比例。通过‑XX:GCTimeRatio=99设置目标吞吐量,99表示1%的时间用于GC。
2、延迟:因为垃圾回收,而引起的响应暂停的时间。通过‑XX:MaxGCPauseMillis=<n>设置目标GC暂停的延迟。
3、内存:我们的系统使用内存来存储状态,在管理的时候它们常常需要复制和移动。在任意一个时间点系统中剩余的存活对象称之为存活集( Live Set)。通过–Xmx<n> 设置最大堆的大小,从而调节在应用程序中可用堆的大小。
注:通常Hotspot并不能达到这些目标,并且即使已经大大的偏离了目标,任然会没有任何警告继续运行。
延迟的影响会穿插在整个运行过程当中。可能我们能够接受增加一些平均短的延迟,来减少最坏情况下的延迟,或则使其较不频繁。术语“实时”并不是我们认为的尽可能最低的延迟。而是在不考虑吞吐量的时候,有一个明确的短的延迟。
对于某些大任务的应用来说,吞吐量是最重要的指标。比如一个长期运行的批处理作业,如果偶尔暂停几秒来垃圾回收并不要紧,只要整体的工作可以尽早的完成即可。
而对于几乎所有其他的应用,从直面人类用户的应用程序到金融交易系统,如果出现系统在几秒甚至有时候几毫秒无响应,将导致灾难性的后果,在金融交易系统中,往往需要牺牲一些吞吐量来换取一致的延迟。我们也有可能需要应用程序限制物理内存,必须控制它占用的空间,在这种场景下,我们必须放弃延迟和吞吐量方面的性能考量。
权衡的通用结论如下:
作为均摊的成本,垃圾回收在很大程度上可以通过使用更大的内存和相应的垃圾回收算法来消减成本。
可以观察到在最坏情况下,由延迟引发的响应暂停。可以通过限制存活集(live set),保持堆的大小在小的范围来减少。
暂停发生的频率可以通过管理堆和代的大小,并且控制应用程序的对象分配率来减少。
长时间暂停的频率可以通过并行运行GC和应用程序来减少,但有时会影响吞吐量。
对象生命周期
垃圾回收算法的优化通常都是期望大部分对象只有很短的生命周期,只有少部分对象有较长的生命周期。在大部分应用中,大部分对象的生命周期限制在一个明确的时间段里,小部分对象的生命周期贯穿整个JVM生命周期。在垃圾收集理论中,这种现象通常被称为“infant mortality(婴儿死亡率,大量对象生存时间很短)” 或则 “weak generational hypothesis(弱年代假设)”。例如:循环迭代内的变量大多生命周期短暂,而静态字符串则在JVM整个生命周期中都有效。
实验表明,分代垃圾收集器的吞吐量通常比非分代垃圾回收器有一个数量级的提升,因而几乎在所有的服务器的JVM中,通常把对象分代。我们发现新分配的对象所在区域能存活的对象是非常稀疏的。因此使用一个收集器清理这个新生代里面少数活着的对象,并且将它们拷贝到老生代里是非常有效的。Hotspot垃圾回收器使用在GC周期中幸存的次数来作为一个对象的年纪。
注:如果你的应用程序不断的产生大量的对象,并且存活相当长的时间,可以预见你的应用程序将会花费一段长的时间去回收垃圾,同样可以预计到你也将花费一段时间来调优Hotspot的垃圾回收器。这是由于这种情况下分代的“过滤器”不太有效。并且结果还会导致存活代的收集更频繁,时间更长。老生代是紧密的,所以老生代的收集算法的效率会更低。分代垃圾回收器往往分为两个不同回收周期:针对短时间存活对象的回收的新生代回收(Minor collections)和对年老区回收的更低频率的老年代回收(Major collections)
世界为之暂停(Stop-The-World Events)
在垃圾回收过程中的应用程序暂停被称之为“世界暂停事件(stop-the-world events)”。在实际工程中由于内存管理的需要,定期暂停正在运行的程序,对于垃圾回收器来说是必须的。根据不同的算法,不同的回收器在不同的时间,在不同的执行点上暂停应用程序(stop-the-world)。为了暂停整个应用程序,首先要暂停所有正在运行的线程。当系统在一个“安全点”的时候,垃圾回收器通过发送一个信号让线程暂停,并开始垃圾回收,“安全点”是指在程序执行中,所有的GC根对象是已知的,并且所有的堆对象的内容是一致的时间点。依赖于线程正在做的事情,它将花费一些时间达到“安全点”。“安全点”的检查通常是执行方法的返回,或则循环边界结束,但是可以进行优化,在某些时候可以更加动态的判断。比如:一个线程正在复制一个大的数组,克隆一个大的对象,或者执行一个有限次的单纯计数的循环。它可能需要几毫秒才能到达下一个“安全点”。对于低延迟的应用,到达安全点的时间(TTSP)是非常重要的。除了其他的GC标志之外,启用‑XX:+PrintGCApplicationStoppedTime 标志可以输出这个时间。
注:对于有大量正在运行的线程的应用程序来说,当暂停应用程序(stop-the-world)发生时,系统将会发生明显的调度压力。并在结束后恢复。因此较少的依赖暂停应用程序(stop-the-world)的算法将会更加有效。
Hotspot中的堆结构
去理解不同的收集器的方式,是探讨java堆结构如何支持分代机制的最好的方式。
伊甸区(Eden)的大部分对象都是刚刚被分配的。幸存区(survivor)是临时存储那些从伊甸区(Eden)里幸存下来的对象。当我们讨论新生代回收(minor collections)的时候将描述幸存区(survivor)的用途。伊甸区(Eden)和幸存区(survivor)常常统称为“年轻代(young)”或则“新生代(new)”
存活足够久的对象,将最终移到年老(tenured )区里。
永久代也是运行时存放对象的区域,它存储像类(Classes)和静态字符串(static Strings)一样不被销毁的对象。不幸的是在许多应用程序中,在持续运行的前提下,类加载的通常有一个激进的假设:即类是不会销毁的。在java 7中的本地化的String会从永久(permgen)代移动到年老(tenured)区。并且java 8从HotSpot虚拟机中删除了“永久代(Permanent Generation),这不再本文的讨论范围里。大部分其他的商业收集器不使用一个单独的永久代,而是往往把所有长期存活的对象放到老生代里面。
注:虚拟空间(Virtual spaces)允许收集器调整区的大小,以满足延迟和吞吐量的要求。收集器对每一次的收集做统计,并调整相应区的大小,来达到目标。
对象的分配
为了避免竞争,每一个线程都分配一个线程本地分配缓冲区(Thread Local Allocation Buffer (TLAB)),线程在其中分配对象。使用TLABs允许对象分配的规模等于线程的数量,避免了单个内存资源的竞争问题。凭借TLAB对象分配是一个廉价的操作。它简单的为对象的大小分配一个指针,大部分平台上大约需要10个指令。java堆内存的分配比C在运行时使用malloc 函数分配内存更加廉价。
注:鉴于个别对象分配是很廉价的,小集合分配的速率与对象分配的速度是成正比的。
当一个TLAB被耗尽率,线程可以简单从伊甸区(Eden)请求一个新的。当伊甸区(Eden)用完后,开始一次新生代回收(minor collection)。
大对象(-XX:PretenureSizeThreshold=<n>)在年轻代(young generation)的分配可能失败,因此必须分配在老年代(old generation),比如:大数组。
如果阈值的设置低于TLAB大小,适合在TLAB的对象将不会创建在老生代(old generation)。新的G1收集器在处理大对象的时候有所不同,在后面单独的部分讨论。
新生代的回收(Minor Collections)
当伊甸区(Eden)填满之后,触发一次新生代回收(Minor Collections)。通过将所有在新生代里存活的对象适当的复制到幸存区(survivor space)和年老区(tenured space)来完成。复制到年老区(tenured space)通常称为晋升(promotion)或则老年化(tenuring)。晋升针对那些足够老的对象(– XX:MaxTenuringThreshold=<n>),或者幸存空间(survivor space)溢出。
存活的对象是指那些应用程序可以访问到的对象,不能访问的其他任何对象,可以被认为是死的。在新生代的收集(minor collection)中,存活对象的复制是通过从GC根对象(GC Roots)开始,反复地复制任何从GC根对象可到达的对象到幸存区(survivor space)来完成的。
GC根对象(GC Roots)通常包括应用程序、JVM内部的静态字段和线程堆栈帧的引用,所有的这些有效的引用,构成了应用程序可到达对象的图谱。
在分代收集中,新生代可到达对象图谱的GC根(GC Roots)还包括老生代对新生代的任何引用。这些引用也必须进行处理,以确保在新生代里面所有可到达对象在新生代的回收(minor collection)后任然是存活的。通过使用了“卡表(card table)”识别这些跨代引用。Hotspot 的卡表是一个bytes数组,其中每个字节(byte)用于跟踪的在相应的老生代的512字节区域里可能存在跨代引用,引用被存储在堆里,“store屏障(store barrier)”代码将标记卡表(card table)的卡片来表明在相关的512字节的堆里面从老生代到新生代可能存在的一个潜在引用。 在收集时卡片表(card table)被用于扫描跨代引用,结果作为在新生代中有效的GC根(GC Roots)。因此在新生代收集(minor collections)中一个重要的固定成本是与老生代的大小成正比的。
在Hotspot里面新生代有两个幸存区(survivor spaces),交替的扮演“to-space”和“from-space”的角色。在新生代垃圾回收开始时,作为一个新生代回收中复制的目标区域,to-space的幸存区(survivor spaces)通常是空的。from-space的幸存区(survivor spaces)的一个组成部分是上一次新生代回收的目标幸存区(survivor space),和伊甸(Eden)区一样,里面的存活对象都需要复制到目标幸存区。
新生代回收的主要消耗就是复制对象到幸存区和年老区(tenured spaces)。在新生代回收中不存在对死亡对象的处理消耗。新生代回收的
工作量直接与存活对象的数量相关,与新生代的大小无关。伊甸(Eden)区的大小每增加一倍,新生代回收的总时间几乎会减少一半。因此,可以在内存和吞吐量中获得平衡。伊甸(Eden)的大小翻倍,每一次收集周期里的收集时间会增加,但是如果需要晋升(promoted)的对象数量和老生代的大小是固定的,那么增加的时间是很少的。
注:在Hotspot中新生代是收集会导致暂停应用(stop-the-world events),这在我们的堆越来越大和存活对象越来越多的情况下会是一个很大的问题。我们已经开始看到新生代中使用并发收集来达到减少暂停时间目标的需要。
老生代的收集(Major Collections):
老生代的收集(Major Collections)是指在老生代(old generation)上的垃圾收集,收集的对象是从年轻代晋升上来的对象。在大多数应用中,绝大部分的程序状态都会在老年代里结束生命周期。在老年代上存在的GC算法也是最多的。有一些是整个空间填满时开始压缩,另一些是回收与应用程序并行,提起防止整个空间填满。
老年代的收集器会预测什么时候需要收集,以避免年轻代的晋升失败。收集器跟踪设置在老年代上的阈值,一旦阈值被超过,则开始一次回收。如果这个阈值不能满足晋升需求,则触发一次“FullGC”。一次FullGC将涉及从年轻代上晋升中的所有对象,并且压缩老年代。晋升失败是非常昂贵的操作,因为所有这个周期里的状态和晋升对象都必须回到原来的地方,然后触发FullGC。
注:为了避免晋升失败,你需要调整你的填充空间(为晋升失败保留的buffer)),让老年代可以容纳晋升后的对象(‑XX:PromotedPadding=<n>)
注:当一次FullGC后堆需要增长 。可以通过将–Xms 和 –Xmx设置为一样的值,来避免在FullGC时的堆调整大小。
与FullGC相比,一次对老生代的压缩(compaction)可能是应用程序会经历的最长的暂停应用(stop-the-world)。压缩的时间和在年老区(tenured space)中存活对象的数量成线性增长关系。
年老区(tenured space)的填充速率可以通过增加幸存区(survivor spaces)的大小和延长晋升到老年区(tenured space)前的存活时间来减少。但是,由于在新生代收集(Minor collections)中,在幸存区之间的复制成本增加,幸存区(survivor spaces)大小的增加和在延长在晋升之前在新生代收集(Minor collections)(–XX:MaxTenuringThreshold=<n>)的存活时间,也会增加新生代收集(Minor collections)的成本和暂停时间,
串行收集(Serial Collector)
串行收集(Serial Collector)是最简单的收集器,并且对于单处理器的系统也是最好的选择。也是所有收集器里面使用最少的。对于新生代的收集和老生代的收集均使用一个单独的线程。在年老区的对象使用简单的空闲指针(bump-the-pointer)算法(译者:按照这种技术,JVM内部维护一个指针(allocatedTail),它始终指向先前已分配对象的尾部,当新的对象分配请求到来时,只需检查代中剩余空间(从allocatedTail到代尾geneTail)是否足以容纳该对象,并在“是”的情况下更新allocatedTail指针并初始化对象。下面的伪代码具体展示了从连续内存块中分配对象时分配操作的简洁性和高效性)即可。当老年代填满后会触发老年代收集。
并行收集(Parallel Collector)
并行收集器有两种形式。一种是并行收集器(-XX:+ UseParallelGC),它在新生代的收集中使用多线程来执行,在老生代的收集中使用单线程执行。另一种是从java 7U4开始默认使用并行老生代收集器(Parallel Old collector )(‑XX:+UseParallelOldGC),它在新生代的收集和老生代的收集均使用多线程。在年老区的对象使用简单的空闲指针(bump-the-pointer)算法即可。当老生代填满后会触发老生代收集。
在多处理器系统上并行老生代收集器(Parallel Old collector )在所有收集器中有最大吞吐量。只有收集开始时它才会影响到正在运行的程序,然后使用的最有效的算法并行的多个线程的收集。这使得并行老生代收集器(Parallel Old collector )非常适合批处理应用。
剩余存活的对象的数量比堆的大小对收集老生代的成本影响更大。因此可以通过使用更大的内存和接受暂停的时间更长但是次数更少来提高并行老生代收集器(Parallel Old collector)的效率,以提供更大的吞吐量。
因为对象晋升到老年区是一个简单的空闲指针(bump-the-pointer)和复制操作,可以预期这个对新生代的收集是最快的。
对于服务性应用程序来说,并行老生代收集器(Parallel Old collector )必须首先保持对端口的调用。如果老年代的收集暂停超过了你应用程序的容忍,你需要考虑使用可以与应用程序并发执行的并发收集器来收集老生代的对象,
注:基于现代的硬件,对老生代的压缩每GB的存活对象预计需要暂停一到五秒。
注:在多插槽CPU的服务器应用程序中使用-XX:+ UseNUMA 并行收集器有时能获得更好的性能,它的伊甸区(Eden)的分配是在线程本地的CPU插槽上,可惜的这个功能是不提供给其他收集器。
并发标记清理收集器( Concurrent Mark Sweep (CMS) )
CMS(-XX:+ UseConcMarkSweepGC)收集器在老生代中使用,收集那些在老生代收集中不可能再到达的年老对象。它与应用程序并发的运行,在老生代中保持一直有足够的空间以保证不会发生晋升失败。
晋升失败将会触发一次FullGC,CMS按照下面多个步骤处理:
1、初始标记:寻找GC根对象;
2、并发标记:标记所有从GC根开始可到达的对象;
3、并发预清理:检查被更新过的对象引用和在并发标记阶段晋升的对象。
4、重新标记:捕捉预清洁阶段以来已更新的对象引用。
5、并发清理:通过回收被死对象占用的内存更新可用空间列表。
6、并发重置:重置数据结构为下一次运行做准备。
当年老对象变成不可到达,占用空间被CMS回收并且放入到空闲空间列表中。当晋升发生的时候,会查询空闲空间列表,为晋升对象找到适合的空间。这增加了晋升的成本,从而相比并行收集器也增加了新生代收集的成本。
注:CMS 不是压缩收集器,随着时间的推移在老生代中会导致碎片。对象晋升可能失败,因为一个大的对象可能在老生代在找不到一个可用空间。当发生这样事件后,会记录一条“晋升失败”的消息,并且触发一次FullGC来压缩存活的年老对象。对于这种压缩驱动的FullGCs,可以预计相比在老生代中使用并行老生代收集器(Parallel Old collector )暂停的时间为更长,因为CMS使用单线程压缩。
CMS尽可能的与应用程序并发运行,它具有许多含义。首先,由于收集器会占用CPU的时间,因此CPU可用于应用程序的时间减少。CMS消耗的时间量与晋升到老年区的对象数量呈线性关系。第二、对于并发GC周期中的某些阶段,所有的应用线程必须到达一个安全点,比如标记GC根和执行并行的重新标记检查更新。
注:如果一个应用程序年老区的对象发生非常明显的变化,重新标记阶段将是非常耗时的,在极端情况下,它可能比一个完整的并行老生代收集器(Parallel Old collector)的压缩时间还要长。
CMS通过降低吞吐量、更费时的新生代的收集,更大的空间占用,来降低FullGC的频率。 根据不同的晋升率,与并行收集(Parallel Collector)相比吞吐量减少10%-40%。CMS也要求多于20%的空间来存放额外的数据结构和“漂浮垃圾(floating garbage)”,漂浮垃圾是值在并发标记阶段丢掉的,到下一个收集周期处理的对象。
高晋升率和由此产生的碎片,可以通过增加新生代和老生代空间的大小来降低。
注:当CMS收集的空间不能满足晋升的时候,它可能遇到“并发模式失败”,在日志中可以找到记录。产生这种情况的一个原因是收集的太迟了,这样可以通过调整策略来解决。另外的原因是收集的空间空闲率跟不上高的晋升率或则某些应用高的对象更新率。如果你的应用的晋升率和更新率太高,你可能需要改变你的应用程序来减少晋升的压力。使用更多的内存有时候可能会使得情况更糟,因为CMS需要扫描更多的内存。
Garbage First (G1) 收集器
G1 (-XX:+UseG1GC)收集器是一个在java 6中使用新的收集器,现在从java 7U4开始正式支持。它是一个部分并发的收集算法,它会尝试通过小步增量暂停世界的方式压缩老年区,来努力最小化FullGC,而因为碎片引起的FullGC正是CMS的一个噩梦。G1也是分代收集器,但是它与其他收集器器使用不同的堆组织方式,它根据不同的用途,它将堆分为大量((~2000))固定大小的区(regions),相同用途的堆也是不连续的(译者:Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合)。
G1采用并发的标记区域的方式来跟踪区域之间的引用,并且只关注收集能收集到最大空闲区的区域(译者:G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回价值最大的Region(这也就是Garbage-First名称的来由))。这些区域的收集是暂停程序的方式,增量的将存活的对象复制到一个空的区域里面,从而收集的过程是压缩的。在同一个周期里收集的区域叫做收集组(Collection Set)
如果一个对象大小超过了区域大小的50%,那么它会被分配到一个大区域里面,可能是当前区域大小的几倍。在G1下,收集和分配大对象是非常昂贵的操作,目前还没有任何优化措施。
任何压缩收集器所面临的挑战不是移动对象,而是对这些对象的引用更新。如果一个对象被许多区域引用,那么更新这些引用会比移动对象更加耗时。G1通过“记忆集(Remembered Sets)” 跟踪区域中的那些有来自其他区域引用的对象。记忆集(Remembered Sets)是一些卡片的集合,这些卡片上标记着更新信息。如果记忆集(Remembered Sets)变大,那么G1久明显变慢了。当从一个区域转移对象到另外区域的时候,那么对应暂停时间的长度与需要扫面和更新引用的区域的数量成正比。
维护记忆集(Remembered Sets)会增加新生代收集的成本,导致比并行老生代收集器(Parallel Old collector)和CMS收集器对新生代的收集时暂停更长的时间。
G1是目标驱动性,通过–XX:MaxGCPauseMillis=<n>设置延迟时间,默认是200ms,该目标将影响在每个周期做的工作量,也是竭尽所能要保证的唯一依据。设置目标在几十毫秒大多是徒劳的,并且几十毫秒的目标也不是G1的关注点。
当一个应用程序可以容忍0.5-1.0秒的暂停来增量压缩,G1是对于拥有一个大堆,并且会逐渐碎片化的场景来说是很好的通用的收集器。G1 倾向于降低在最环情况下暂停的频率,而正是CMS的问题,为了处理产生碎片而扩展了新生代收集和对老生代增量压缩。大部分的暂停被限制在一个区域而不是整个堆的压缩。
与CMS一样,G1也会因为无法保证晋升率而失败,最终回到暂停程序的FullGC上。就像CMS“并发模式失败”一样,G1也可能遭受转移失败,在日志中能看到“目标空间溢出(to-space overflow)”。这种情况发生在对象转移的区域没有足够的空闲空间的时候,与晋升失败类似。如果发生这种情况,请尝试使用更大的堆,更多标记线程,但在某些情况下,需要应用程序作出改变,以减少分配比率。
对G1来说一个具有挑战性的问题是处理高关注率的对象和区域。 当区域里存活的对象没有被其他区域大量引用。增量停止世界的压缩方法效果很好。如果一个对象或者区域是被大量引用的,记忆集(Remembered Sets)将会相应变大。并且G1将会避免收集这些对象。最终,不得不导致频繁的中等长度的暂停时间来压缩堆。
其他并发收集器(Alternative Concurrent Collectors)
CMS 和 G1通常认为是最并发的收集器,但是当你观察整个工作过程,很显然新生代,晋升、甚至许多老生代的工作都不是并发的。对于老生代来说CMS是最并发的算法,G1更像是暂停程序的增量收集器。CMS和G1都会有明显的和有规律的暂停应用的事件发生,并且最坏情况下往往使他们不适合严格的低延迟应用,如金融交易或交互型的用户界面。
其他的收集器如:Oracle的JRockit Real Time,IBM WebSphere的Real Time的,和Azul 的Zing。 JRockit和Websphere的收集器在延迟上比CMS和G1更加有优势,但是在大多数情况下它们有吞吐量的限制,并且仍然遭受明显的暂停应用的事件。Zing是本作者知道的唯一一款Java收集器,能对所有代都真正并发收集和压缩,同时保持了高吞吐率。Zing确实有一些亚毫秒级的暂停程序的事件,但这些是在收集周期的相移,并且与存活对象集的大小无关。
JRockit的RT在控制堆的大小,有高的对象分配率的时候可以实现暂停时间在几十毫秒,但是偶尔会失败而回到完全压缩暂停。WEBSPHERE RT通过约束的分配比率和存活集的大小,可以实现毫秒级别的暂停时间。Zing在高分配率时通过在所有阶段并发,能达到亚毫秒级的暂停。无论堆大小,Zing是能够保持一致的行为,并且允许用户按照需要使用更大的堆,来保证应用程序的吞吐量,或则对象模型状态的需求,而不用担心增加暂停时间。
对于所有的并发收集器来说关注延迟目标,你就必须放弃一些吞吐量和空间。根据并发收集器的效率,你可能放弃一点点的吞吐量,但是通常你总是需要显著增加空间。如果真正的并发,暂停程序的事件将很少发生,那么需要更多的CPU内核来支持并发操作和维持吞吐量。
注:所有的并发收集器当有足够的空间时候,往往能更有效地分配对象。根据经验,为了能高效的操作,你应该预算至少两到三倍于存活集的大小。然而,维持并发操作所需的空间随着应用程序的吞吐量,以及相关的对象分配和晋升率的增长而增长。因此,对于高吞吐量的应用,维持较高的堆大小堆存活对象的比例非常有必要。鉴于目前系统拥有的巨大的内存空间,这对于服务器并不是什么问题。
垃圾收集监控和调整(Garbage Collection Monitoring & Tuning)
为了理解你的应用程序和垃圾收集是如何工作的,启动JVM的时间至少需要添加如下参数:
-verbose:gc
-Xloggc:
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution
-XX:+PrintGCApplicationConcurrentTime
-XX:+PrintGCApplicationStoppedTime
然后加载日志到像Chewiebug的工具进行分析。
为了看到动态的GC过程,可以使用JVisualVM并且安装Visual GC插件。这将使你能看到你的应用程序的GC行为。
为了能获得一个适合你应用的GC需要,你需要一个有代表性的可以重复执行的负载测试。当你掌握每个收集器是如何工作的,根据不同的配置运行负载测试,直到达到你理想的吞吐量和延迟目标。从最终用户的角度来看,重要的是要测量延迟。可以通过捕获每个测试请求的响应时间,并且使用直方图来记录结果, 如HDR直方图(HdrHistogram)或干扰物直方图(Disruptor Histogram)。如果有延迟尖峰超出可接受范围,然后尝试关联GC日志来判断是否是GC问题。它是可能是其他问题导致的延迟高峰。另一种有用的工具是
jHiccup,它可以用来跟踪在JVM中暂停,并且可以整合多个系系统到一个整体。使用jHiccup测量你的空闲系统几个小时,通常情况你会得到一个令人惊讶的结果。
如果延迟尖峰是由于GC导致,那么可以关注在调整CMS或G1看是否可满足的延迟目标。有时这是不可能的,因为高分配和晋升率与低时延的要求是冲突的。 GC优化是一个需要高度技巧的工作,往往需要修改应用程序,以减少对象分配或对象生存期。如果需要在时间、GC优化和应用程序的修改,精通方面权衡,那么购买商业并发压缩的JVMs,比如JRockit Real Time 和 Azul Zing可能也是必需的。