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