首页 » Java程序员修炼之道 » Java程序员修炼之道全文在线阅读

《Java程序员修炼之道》6.5 垃圾收集

关灯直达底部

内存自动管理是Java平台最重要的组成部分之一。在出现Java和.NET这样的托管平台之前,开发人员把大部分时间都用在追踪不完善的内存处理引发的bug上了。

然而近年来,内存自动分配技术发展的如此先进可靠,已经变得让人无法察觉了,因此大部分Java开发人员不知道Java平台的内存管理是如何完成的,不知道可以使用哪些选项,也不知道如何在框架限定内进行优化。

这说明Java的做法取得了成功。大多数开发者不知道内存和GC系统的细节是因为他们没必要知道。虚拟机在这方面做得非常棒,在处理大多数应用时都不用特别调整,所有大多数应用从没调整过。

本节我们将讨论在确实需要做些调整的情况下你能做什么。我们会给出基本原理,解释为了运行Java进程该如何处理内存,并探索标记和清除集合的基础,再讨论两个工具——jmapVisualVM。最后介绍两个收集器——并发标记清除(Concurrent Mark-Sweep,简称CMS)和新的垃圾优先(Garbage First,简称G1)收集器。

也许你有个服务器端程序耗光了内存,或者承受着长时间中断的痛苦。在6.5.3节讨论jmap时,我们将会告诉你一个查看类是否占用大量内存的简单办法。我们还会教你使用控制虚拟机内存配置的选项开关。

先从基本算法开始吧。

6.5.1 基本算法

标准的Java进程既有栈又有堆。栈保存原始型局部变量(引用型局部变量会指向以堆方式分配的内存)。堆保存要创建的对象。图6-4展示了各种类型变量存储的位置。

图6-4 堆和栈中的变量

注意,对象的原始型域仍然分配在堆内的地址上。Java平台对堆内存回收和再利用的基本算法被称为标记和清除,应用程序中代码已经不再使用它了。

6.5.2 标记和清除

标记和清除是最简单、也是出现最早的垃圾收集算法。业内还有其他内存自动管理技术,比如Perl和PHP等语言采用的引用计数1,有人说它更简单,但它是不需做垃圾收集的方案。

1 引用计数就是为每个内存对象维护一个引用数值,当有新的引用指向该对象时则将其引用计数加一,销毁时则减一。当引用计数为零时就收回该对象占用的内存资源。这种方式虽然简单,但存在两个问题:每次内存对象被引用或引用被销毁时必须修改引用计数,造成整体性能消耗;出现循环引用时难以处理。——译者注

最简单的标记和清除算法会暂停所有正在运行的线程,并从一组“活”对象——在任何用户线程的任何堆栈帧中存在引用(不管是局部变量、方法参数、临时变量,还是某些非常少见的情况)的对象——开始遍历其引用树,标记出遍历路径上的所有活对象。遍历完成后,所有没被标记的都被当做垃圾,可以回收(清除)。注意,被清除的内存不会还给操作系统,而是还给JVM。

Java平台对基本的标记清除方法进行了改进,采用“分代式垃圾收集”。在这种方法中,会根据Java对象的生命周期将堆内存划分为不同的区域。在对象的生存期内,对它的引用可能指向内存中几个不同区域(如图6-5所示)。在垃圾收集过程中,可能会将对象移动到不同区域。

图6-5 内存中的伊甸园、幸存者乐园、终身颐养园和PermGen区

这样做是因为根据对系统运行时期的研究,发现对象的生存期或者较短,或者很长。Java平台把堆内存划分为不同区域可以充分利用对象生命周期的这种特点。

出现时长不确定的暂停怎么办?

Java和.NET经常受到这样的批评:标记和清除式的垃圾收集不可避免地会导致世界停转(所有用户线程都必须停止),而且这种暂停的时长是不确定的。

其实这个问题被夸大了。对于服务器端软件来说,应用程序不会在意垃圾收集引起的暂停。为了避免暂停或完全收集而精心制作解决方案完全是凭空想象——除非经过认真分析,发现全内存收集时间真的存在问题,才应该避免。

1. 内存区域

JVM为存储不同生命周期阶段的对象将内存分成了几个不同区域。

  • 伊甸园——伊甸园是对象最初降生的堆区域,并且对大多数对象来说,这里是它们唯一存在过的区域。

  • 幸存者乐园——这里通常有两个空间(或者也可以认为是被分成两半的一个空间)。从伊甸园幸存下来的对象会被挪到这里。它们有时候被称为从何而来和到哪里去。除非正在执行垃圾收集,否则总有一个幸存者空间是空的,原因会在后面给出。

  • 终身颐养园——终身制空间(即老一代)是那些“足够老”的幸存对象的归宿(从幸存者空间挪过来的)。在年轻代收集过程中是不会碰终身制内存的。

  • PermGen——这是为内部结构分配的内存,比如类定义。PermGen不是严格的堆内存,并且普通的对象最后不会在这里结束。

就像前面提到的,这些内存区域的垃圾收集方式也不尽相同。具体来说有两种方式:年轻代收集和完全收集。

2.年轻代收集

年轻代收集只会清理“年轻的”空间(伊甸园和幸存者乐园)。其过程相当简单。

  • 在标记阶段发现的所有仍然存活的年轻对象都会被挪走:
    • 那些足够老的对象(从次数足够多的GC中幸存下来的)进入终身颐养园;
    • 剩下那些年轻的存活对象进入幸存者乐园里空着的空间。
  • 最后,伊甸园和最近腾空的幸存者乐园就可以重用了,因为它们里面已经全是垃圾了。

当伊甸园满了的时候就会触发一次年轻代收集。注意,标记阶段必须遍历整个生存对象图。也就是说如果有个年轻对象被一个终身对象引用了,终身对象所持有的引用也必须被扫描到并标记上。否则只被终身对象引用的伊甸园对象可能会出问题。如果标记阶段不是全遍历,这个伊甸园对象就再也看不到了,而且不可能对它做出正确处理。

3.完全收集

当年轻代收集不能把对象放进终身颐养园时(空间不够了),就会触发一次完全收集。根据老年代所用的收集器,这可能会牵涉到老年代对象的内部迁移。这样做是为了确保必要时能从老年代对象所占的内存中给大的对象腾出足够的空间。这被称为压缩。

4.安全点

要想做垃圾收集,至少得让所有应用线程暂停一会儿。但是线程不可能为了GC说停就停。所以它们给执行GC留出了特定的位置——安全点。常见的安全点是方法被调用的地方(“调用点”),不过也有其他安全点。为了执行垃圾收集,所有应用程序线程都必须停在安全点上。

我们暂停一下,先介绍一个简单的工具——jmap,它能帮你弄清楚程序运行时的内存使用情况,以及所有内存都用在哪里。我们后续还会介绍一个更先进的GUI工具,但既然很多问题都可以用非常简单的命令解决,你最好应该知道如何使用,而不是直接就使用GUI工具。

6.5.3 jmap

Oracle JVM自带了一些简单的工具,可以帮你了解运行中的进程。jmap是其中最简单的一个,用来显示Java进程的内存映射(它也能分析Java核心文件1,甚至能连到远程调试服务器上)。让我们回到电子商务服务器端应用程序的例子上,用jmap对它进行一番探索。

1 Java核心文件(Java core file)主要保存各应用线程在某一时刻的运行位置,即JVM执行到哪个类、哪个方法及哪一行上。它是一个文本文件,打开后可以看到每一个线程的执行栈,以及stack trace的显示。一般Java程序遇到致命问题,在JVM死掉之前会产生两个文件,其中就有Java核心文件,另一个是HeapDump文件。有时为了调试或查找性能问题也会手工生成这两个文件。——译者注

1.默认视图

jmap最简单的用法是查看连接到进程里的本地类库。除非你的应用程序里有很多JNI代码,否则这种用法通常没什么用,但我们还是会演示一下,以免你忘了指定jmap选项时被它搞糊涂:

 $ jmap 19306Attaching to process ID 19306, please wait...Debugger attached successfully.Server compiler detected.JVM version is 20.0-b110x08048000     46K           /usr/local/java/sunjdk/1.6.0_25/bin/java0x55555000     108K          /lib/ld-2.3.4.so... some entries omitted0x563e8000     535K          /lib/libnss_db.so.2.0.00x7ed18000     94K           /usr/local/java/sunjdk/1.6.0_25/jre/lib/i386/libnet.so0x80cf3000     2102K         /usr/local/kerberos/mitkrb5/1.4.4/lib/         libgss_all.so.3.10x80dcf000     1440K         /usr/local/kerberos/mitkrb5/1.4.4/lib/libkrb5.so.3.2  

一般用得比较多的是-heap-histo选项,下面我们就来讨论这两个选项。

2.堆视图

使用-heap选项时,jmap会抓取进程当前的堆快照。在输出结果中能看到构成Java进程堆内存的基本参数。

堆的大小是年轻代、老年代加上PermGen区的总和。但在年轻代内部有伊甸园和幸存者乐园,并且我们还没告诉你这些区域的大小之间有什么关系。这些区域的相对大小是由一个叫做幸存比例的数值决定的。

我们来看一些输出样例。你能在其中看到伊甸园、幸存者乐园(标签为FromTo)、终身颐养园(Old Generation)以及一些相关信息:

$ jmap -heap 22186Attaching to process ID 22186, please wait...Debugger attached successfully.Server compiler detected.JVM version is 20.0-b11using thread-local object allocation.Parallel GC with 13 thread(s)Heap Configuration:   MinHeapFreeRatio = 40   MaxHeapFreeRatio = 70   MaxHeapSize = 536870912 (512.0MB)   NewSize = 1048576 (1.0MB)   MaxNewSize = 4294901760 (4095.9375MB)   OldSize = 4194304 (4.0MB)   NewRatio = 2   SurvivorRatio = 8  //伊甸园=(From+To)*幸存比例   PermSize = 16777216 (16.0MB)   MaxPermSize = 67108864 (64.0MB)Heap Usage:  PS Young Generation  Eden Space:    capacity = 163774464 (156.1875MB)  //伊甸园=(From+To)*幸存比例    used = 58652576 (55.935455322265625MB)    free = 105121888 (100.25204467773438MB)    35.81301661289516% used  From Space:    capacity = 7012352 (6.6875MB)   //伊甸园=(From+To)*幸存比例    used = 4144688 (3.9526824951171875MB)    free = 2867664 (2.7348175048828125MB)    59.10553263726636% used  To Space:    capacity = 7274496 (6.9375MB)   //伊甸园=(From+To)*幸存比例    used = 0 (0.0MB)    free = 7274496 (6.9375MB)    0.0% used   //To空间当前为空  PS Old Generation    capacity = 89522176 (85.375MB)    used = 6158272 (5.87298583984375MB)    free = 83363904 (79.50201416015625MB)    6.87904637170571% used  PS Perm Generation    capacity = 30146560 (28.75MB)    used = 30086280 (28.69251251220703MB)    free = 60280 (0.05748748779296875MB)    99.80004352072011% used  

尽管空间的基本构成可能会非常有用,但在这副图里看不到堆里面有什么。如果能看到是哪些对象占用了内存中的空间,你就知道内存都到哪里去了。jmap恰好提供了一个柱状图模式,可以让你看到这些数据的简单统计结果。

3.柱状视图

柱状视图显示了系统中每个类型的实例(还有一些内部实体)占用的内存量。各个类型按使用内存多少排列,这样就比较容易看到最大的内存猪。

当然,如果所有内存都交给了框架和平台类,这里可能就没你什么事了。但如果真有一个你的类,有了这些信息便能更好地干预它的内存占用。

小小的警告:jmap使用类型内部名称。比如字符数组会写成[C,类对象的数组会显示[Ljava.lang.Class;

$ jmap -histo 22186 | head -30num #instances #bytes class name----------------------------------------------1:    452779   31712472      [C2:    76877    14924304      [B3:    20817    12188728      [Ljava.lang.Object;4:    2520     10547976      com.company.cache.Cache$AccountInfo5:    439499   9145560       java.lang.String6:    64466    7519800       [I7:    64466    5677912       <constMethodKlass>   //VM内部对象和类型信息8:    96840    4333424       <methodKlass>9:    6990     3384504       <symbolKlass>10:   6990     2944272       <constantPoolKlass>11:   4991     1855272       <instanceKlassKlass>12:   25980    1247040       <constantPoolCacheKlass>13:   17250    1209984       java.nio.HeapCharBuffer14:   13515    1173568       [Ljava.util.HashMap$Entry;15:   9733     778640        java.lang.reflect.Method16:   17842    713680        java.nio.HeapByteBuffer17:   7433     713568        java.lang.Class18:   10771    678664        [S19:   1543     489368        <methodDataKlass>   //VM内部对象和类型信息20:   10620    456136        [[I21:   18285    438840        java.util.HashMap$Entry22:   9985     399400        java.util.HashMap23:   13725    329400        java.util.Hashtable$Entry24:   9839     314848        java.util.LinkedHashMap$Entry25:   9793     249272        [Ljava.lang.String;26:   11927    241192        [Ljava.lang.Class;27:   6903     220896        java.lang.ref.SoftReference  

因为在柱状图模式下输出的数据很多,所以上面只显示了输出内容的一部分。你可能要用grep或其他工具来查看柱状图视图,找到感兴趣的细节。

输出中有很多占用内存的[C实体。字符数组数据经常出现在String对象里(字符串的内容就存在那里),所以这不奇怪——大多数Java程序里都有很多字符串。但从柱状图中还能看出其他有趣的事情。先来看看下面两个。

前几个实体里唯一一个应用类是Cache$AccountInfo——其他全是平台或框架类型——所以它们是开发人员可以完整控制的最重要的类型。AccountInfo对象占了很多空间——大概2 500个实体占了10.5MB(或者每个账号占4KB)。对于账号细节来说这实在是很多。

这个信息真的非常有用。你已经知道代码里什么占内存最多了。假如老板现在过来告诉你,因为大规模促销,一个月内系统客户数可能要暴增10倍。你知道这可能会给系统增加很多压力——AccountInfo对象可是个凶猛的大家伙。虽然你有点担心,但至少你已经开始分析这个问题了。

jmap输出的信息可以作为潜在问题处理决策流程的辅助输入。你是不是应该把账号缓存分开,减少该类型保存的信息项,或者买更多的内存给服务器装上。在做出任何决定之前,你还要做很多分析工作,但已经有个起点了。

柱状图模式下还能看到其他有意思的事情,这次指定-histo:live选项。这是告诉jmap只处理存活对象,而不是整个堆(jmap默认会处理所有对象,也包括还没被收集的垃圾)。让我们看看这次输出什么:

$ jmap -histo:live 22186 | head -7num   #instances     #bytes         class name----------------------------------------------1:        2520            10547976      com.company.cache.Cache$AccountInfo2:        32796           4919800       [I3:        5392            4237628       [Ljava.lang.Object;4:        141491          2187368       [C  

注意输出的变化——字符数据已经从31MB降到了2MB左右了,证明你第一次看到的String对象里有将近三分之二都是等待回收的垃圾。然而账号对象全是活的,进一步证明了它们是消耗内存的主要力量。

使用jmap时应该稍微谨慎点。进行该操作时JVM还在运行(如果你不走运,还有可能在读取快照期间做了垃圾回收),所以你应该多运行几次,特别是在你看到任何奇怪或太好的结果时。

4.产生离线导出文件

jmap能创建导出文件,像这样:

jmap -dump:live,format=b,file=heap.hprof 19306  

导出结果可以用来做离线分析,可以留给jmap以后自己用,也可以留给Oracle的jhat(Java堆分析工具)做高级分析。可惜我们没办法在这里全面讨论。

使用jmap可以看到一些基本设置和程序的内存占用。然而要做性能调优,一般需要对GC子系统有更多控制,其标准方式是通过命令行参数,我们来看一些控制JVM的参数,用它们使JVM的行为更适用于你的应用程序。

6.5.4 与GC相关的JVM参数

JVM的参数非常多(最少上百个),用来定制JVM运行时的行为。本节我们会讨论一些跟垃圾收集有关的选项,后续章节中还会讨论其他选项。

非标准的JVM选项

-X:开头的选项不是标准选项,在其他JVM上可能不可用。

-XX:开头的是扩展选项,不要随便使用。很多与性能相关的选项都是扩展选项。

有些选项相当于布尔型的参数,并且前面有+作为它的开关。还有带参数的选项,比如-XX:CompileThreshold=1000(这个方法会在调用次数达到1000之后才被JIT编译)。还有一些参数(包括很多标准参数)既没有开关也不能带参数。

表6-2中是基本的GC选项,还有这些选项的默认值(如果存在)。

表6-2 基本垃圾收集选项

选项效果-Xms<几MB>m 堆的初始大小(默认2MB)-Xmx<几MB>m堆的最大大小(默认64MB)-Xmn<几MB>m 堆中年轻代的大小-XX:-DisableExplicitGC让调用System.gc不产生任何作用

一个常用的小技巧是把-Xms-Xmx的大小设成一样的。这样进程就会用恰当的堆尺寸运行,没必要在执行过程中调整大小(可能会引发意想不到的降速)。

表中最后一个选项输出GC的标准信息到日志中,我们在下一节会讨论如何解释这些信息。

6.5.5 读懂GC日志

为了充分利用垃圾收集,你需要经常看看子系统在做什么。除了基本的verbose:gc标记,还有很多可以控制输出信息的选项。

别拿GC日志不当回事儿,你可能时不时地就会发现自己被输出信息淹没了。下一节讨论VisualVM时你会发现,有一个可视化工具可以帮你看到VM的行为,这个工具非常有用。不管怎样,会读日志以及了解影响GC的基本选项非常重要,因为有时候你可能没法用GUI工具。最常用的GC日志选项如表6-3所示。

表6-3 用于扩展日志的额外选项

选项效果-XX:+PrintGCDetails 关于GC更详细的细节-XX:+PrintGCDateStampsGC操作的时间戳-XX:+PrintGCApplicationConcurrentTime 在应用线程仍然运行的情况下用在GC上的时间

这些选项组合在一起时,会产生下面这种日志:

6.580: [GC [PSYoungGen: 486784K->7667K(499648K)]1292752K->813636K(1400768K), 0.0244970 secs]  

我们把它分解,看看每一部分是什么意思:

<time>: [GC [<collector name>: <occupancy at start>➥ -> <occupancy at end>(<total size>)] <full heap occupancy at start>➥ -> <full heap occupancy at end>(<total heap size>), <pause time> secs  

第一块是GC的发生时间,从JVM启动开始算,到发生时的秒数。然后是用来收集年轻代的收集器名称(PSYoungGen)。接着是年轻代收集前后占用的内存,以及年轻代的总大小。接着是反映完全收集情况的相同部分。

除了GC日志选项,还有一个选项如果不经解释可能会引起误解。用选项-XX:+PrintGCApplicationStoppedTime产生的日志是这样的:

Application time: 0.9279047 secondsTotal time for which application threads were stopped: 0.0007529 secondsApplication time: 0.0085059 secondsTotal time for which application threads were stopped: 0.0002074 secondsApplication time: 0.0021318 seconds  

这些并不一定指GC用了多长时间,而是指在一个从安全点开始的操作中,线程停了多长时间。这包括GC操作,但也包括其他安全点操作(比如Java 6中的偏向锁操作1),所以没有十足把握说这是指GC时长。

1 在Java 6之前,加锁会导致一次原子CAS(Compare-And-Set)操作。对于没有争用的资源,该操作会造成无谓的开销。为解决这一问题,Java 6中引入了偏向锁技术,即偏向于第一个加锁的线程,该线程后续加锁操作不需要同步。其基本实现方式为:锁最初为NEUTRAL状态,当第一个线程加锁时,将该锁的状态修改为BIASED,并记录线程ID,这一线程在后续加锁时若发现状态是BIASED并且线程ID是当前线程ID,则只设置一下加锁标志,不需要进行CAS操作。其他线程若要加这个锁,需要使用CAS操作将状态替换为REVOKE,并等待加锁标志清零,以后该锁的状态就变成DEFAULT。这一功能可用-XX:-UseBiasedLocking命令禁止。——译者注

所有这些信息对记录日志和事后分析都有用,但不容易做可视化处理。而很多开发人员在做初始分析时都喜欢使用GUI工具。好在HotSpot VM(标准的Oracle VM,稍后讨论)自带了一个非常实用的工具。

6.5.6 用VisualVM查看内存使用情况

VisualVM是Oracle JVM自带的可视化工具。它是插件架构,采用标准配置,比JConsole用起来更方便。

图6-6是标准的VisualVM汇总界面。启动VisualVM并把它连接到本地运行的程序上,就能看到这样的界面。(VisualVM也能连接到远程应用上,但有些功能通过网络不可用。)这个界面中VisualVM连接的是MacBook Pro上运行的Eclipse,你可以看到我们用来编写本书代码的Eclipse的设置。图6-6如下所示。

图6-6 VisualVM汇总界面

右侧面板顶部有很多标签。其中有扩展(Extension)、样例(Sampler)、JConsole、MBeans和VisualVM插件。VisualVM插件为掌握Java运行时的动态情况提供了非常棒的工具。建议你在用VisualVM做任何实际工作前把这些插件都装上。

图6-7展示了内存占用的“锯齿”模式。这绝对是Java平台中内存占用情况的经典表现。它表示对象被分配在伊甸园中,使用,然后在年轻代中被回收。

图6-7 VisualVM总览界面

每次年轻代收集之后,被占用的内存量回落到基线水平。这个水平是终身制对象和幸存者对象合起来的用量,可以用它来确定Java进程的健康状况。如果基线在进程工作时保持稳定或者逐渐递减,则表明内存的使用情况非常健康。

如果基线水平上升,也不一定就是出错了,可能只是有些对象的生存期很长,长到足够转入终身颐养园中。在这种情况下,最终会进行一次完全收集。完全收集会导致锯齿模式再次出现,从而使内存占用回落到基线水平。如果完全收集基线持续保持稳定,进程不会耗光内存。

锯齿上斜坡的陡度是进程使用年轻代内存(通常是伊甸园)的频率,这个概念很重要。降低年轻代收集的频率基本上就是降低锯齿的陡度。

内存使用情况的另外一种可视化方式如图6-8所示。你能看到伊甸园、幸存者乐园(S0和S1)、终身颐养园及PermGen区。在程序运行时,你能看到各个空间的大小变化。特别是在年轻代收集之后,可以看到伊甸园变小,幸存者乐园中两个空间的角色也互相转换了。

图6-8 VisualVM的可视化GC插件

探索内存系统和运行时环境有助于你理解代码如何运行。相应地,这也表明VM提供的服务对性能影响很大,所以你绝对应该花时间研究一下VisualVM,尤其要结合XmxXms这些选项试一下。

下一节中,我们将要讨论JVM中的一项新技术,这项技术会在执行过程中自动降低堆内存的占用量。

6.5.7 逸出分析

本节介绍了JVM最近的一项修改,内容仅供参考。程序员不能直接控制或影响这项修改,并且在最近发布的Java中,这项优化是默认的。因此本节中没有太多关于这项修改的信息或例子。所以如果你想了解一下JVM提升自身性能的技巧,请继续。如果没兴趣,可以跳到6.5.8节去研究并发的垃圾收集。

逸出分析乍一看是个相当出人意料的想法。其基本思路是分析方法并确认其中哪个局部变量(的引用类型)只用在方法内部,以及哪些变量不会传入其他方法或从当前方法中返回。

这样JVM就可以在当前方法的栈框架内部创建这个对象,而不再使用堆内存。这会减少程序年轻代收集的次数,从而提高性能。请参见图6-9。

图6-9 逸出分析避免了对象的堆分配

这就是说可以避免堆分配,因为在当前方法返回时,被局部变量占用的内存就自动释放了。用这种不牵扯堆分配的方式分配变量空间不会产生垃圾,当然就不需要收集垃圾。

逸出分析是减少JVM垃圾收集的新办法。它能对线程的年轻代收集次数产生显著影响。经实践证明,它通常能对总体性能产生百分之几的影响。虽然影响不是特别大,但也很有价值,特别是在进程的垃圾收集次数比较多的时候。

从Java6u23往后,逸出分析是默认打开的,所以新版Java的速度免费提升了。

现在我们去看另外一个对代码有巨大影响的环节——收集策略的选择。我们从一个经典的高性能选择(并发标记清除)开始,然后看一看最新的收集器——垃圾优先。

选择高性能收集器有很多原因。应用程序可能会从较短的GC暂停中受益,并且也愿意运行更多线程(占用CPU资源)来加快速度。或者你想控制GC暂停的频度。除了基本的收集器,你还可以用选项迫使平台采用不同的收集策略。在接下来的两节中,我们会介绍两个把这种可能性变成现实的收集器。

6.5.8 并发标记清除

并发标记清除(CMS)收集器是Java 5推荐的高性能收集器,在Java 6中仍然保持了旺盛的生命力。可以通过下面几个选项激活它,如表6-4所示。

表6-4 用于CMS收集器的选项

选项效果-XX:+UseConcMarkSweepGC打开CMS收集-XX:+CMSIncrementalMode增量模式(一般都需要)-XX:+CMSIncrementalPacing配合增量模式,根据应用程序的行为自动调整每次执行的垃圾回收任务的幅度(一般都需要)-XX:+UseParNewGC并发收集年轻代-XX:ParallelGCThreads=<N>GC使用的线程数

这些选项会覆盖垃圾收集的默认设置,为GC配置有N个并行线程的CMS垃圾收集器。这些线程会尽可能地在并发模式下完成GC工作。

这种并发方式是如何工作的呢?下面是与标记清除相关的三个重要事实:

  • 某种世界停转(简称STW)的暂停是不可避免的;
  • GC子系统绝对不能漏掉存活对象,这样做会导致JVM垮掉(或者更糟);
  • 只有所有应用线程都为整体收集暂停下来,才能保证收集所有的垃圾。

CMS利用了最后一点。它制造两个非常短暂的STW暂停,并且在GC周期的剩余时间和应用程序的线程一起运行。这表明它愿意跟“伪阴性”妥协,由于竞争危害而无法标识某些垃圾(被漏掉的垃圾会在下一个GC周期中得到收集)。

CMS还要在运行时做复杂的记账工作,记录哪些是垃圾,哪些不是。这些额外的开销是为了在不停止应用线程的情况下运行GC所付出的代价。CMS在有更多CPU核心的机器上会表现得更好,并且会制造更频繁的短暂暂停。它的日志输出如下所示:

2010-11-17T15:47:45.692+0000: 90434.570: [GC 90434.570:[ParNew: 14777K->14777K(14784K), 0.0000595 secs]90434.570:[CMS: 114688K->114688K(114688K), 0.9083496 secs] 129465K->117349K(129472K),[CMS Perm : 49636K->49634K(65536K)] icms_dc=100 , 0.9086004 secs][Times: user=0.91 sys=0.00, real=0.91 secs]  

这些日志和6.4.4节中基本的GC日志差不多,但增加了CMSCMS Perm收集器部分。

最近几年,CMS作为最佳高性能收集器的地位受到了挑战,挑战者是垃圾优先(G1)收集器。我们来看看这颗冉冉升起的新星,了解一下它的新颖方法,以及它能够突破所有现存的Java收集器的原因。

6.5.9 新的收集器:G1

G1是Java平台中崭新的收集器。本来想把它和Java 7一起发布,但后来作为预发布版本跟Java 6一起发布了,到Java 7时就是成品了。它在Java 6中并没有得到广泛的应用,但随着Java 7逐渐普及,有望让G1成为高性能应用(也可能是所有应用)的默认选择。

G1的核心思想是暂停目标(pause goal),也就是程序在执行时能为GC暂停多长时间(比如每5分钟20ms)。G1会竭尽所能达成暂停目标。它和我们原来遇到的收集器完全不同,并且开发人员对GC如何执行有更多控制权。

G1不是真正的分代式垃圾收集器(尽管它仍然使用标记清除法)。相反,G1把堆分成大小相同的区域(比如每个1MB),不区分年轻区和年老区。暂停时,对象被撤到其他区域(就像伊甸园对象被挪到幸存者乐园一样),清空的区域被放回到(空白区的)自由列表上。这种将堆划分为大小相同区域的做法如图6-10所示。

图6-10 G1如何划分堆空间

这个新的收集策略让Java平台可以统计收集单个区域需用的平均时长。这样你就可以在合理范围内指定一个暂停目标。G1只会在有限的时间内收集尽可能多的区域(尽管在收集最后一个区域时所用的时间可能比预期的长)。

要打开G1,需要用到表6-5中的选项。

表6-5 G1收集器的选项

选项效果-XX:+UseG1GC打开G1收集-XX:MaxGCPauseMillis=50告诉G1它在一次收集中暂停的时间应该尽量保持在50ms以内-XX:GCPauseIntervalMillis=200告诉G1它将两次收集的时间间隔尽量保持在200ms以上

这些选项可以组合,比如设置最大暂停目标是50ms,暂停间隔不能少于200ms。当然,GC系统所能承受的压力是有限的。必须有充足的暂停时间把垃圾取出来。每隔100年1ms的暂停目标肯定是不现实的。

G1可以支持的负载和应用类型范围很广。如果你的应用程序已经到了需要对GC调优的地步,G1会是一个不错的选择。

在下一节中,我们会介绍JIT编译。对于很多或大多数程序来说,这是唯一一个可以为产生高性能代码做出最大贡献的因素。我们会学习JIT编译的基础知识,最后解释一下如何打开JIT编译的日志,让你能够判断正在编译哪个方法。