Java SE 6 Hotspot [TM] 虚拟机垃圾回收调优

译者注:这段时间由于项目性能不佳需要对Java的垃圾回收进行调优,在网上找到了该文的翻译版http://wangxu.me/blog/p/209,阅读过程发现该翻译版中错别字较多,并且很多地方翻译得不够准确,于是我就阅读了英文原版。考虑到有很多人不习惯读英文版,因此我在之前翻译版的基础上进行了重新翻译,并对照原文审校了多次,以期在符合中文阅读习惯的同时能够更加忠实于原文。翻译这篇文章花费了我将近7个工作日,如果大家发现还有翻译得不够准确的地方,请留言告诉我,祝大家学习愉快。

1      概述

Java 平台标准版(Java SE™)被广泛应用于各种应用,从桌面上小小的 applet 到大型服务器上的 Web Service。为了支持各种不同的部署场景,Java HotSpot™虚拟机提供了多种垃圾回收器,每种都为满足不同的需求而设定。这也是为了满足大大小小不同应用需求的重要部分。不过,那些需要高性能的用户、开发者和管理员们也被选择适合他们应用的垃圾回收器所困扰着。在J2SE™ 5.0 中,虚拟机基于应用运行的主机类型自动选择垃圾回收器去掉了这个负担。

这个垃圾回收器的“更好的选择”总的说是一种进步,但这并不意味着对所有应用来说都是最好的选择。对于有极端性能或其他需求的用户,仍需要显式地指定垃圾回收器,并调优某些参数,以达到满意的性能。本文为这些需求提供了一些相关信息。首先,本文会基于串行的stop-the-world垃圾回收器来介绍垃圾回收器的基本特征和基本调优选项。接下来会介绍其他垃圾回收器的特定特性和如何选择一个垃圾回收器。

何时选择垃圾回收器?对于一些应用,这个答案可能是“永远不”,也就是说,低频率、短停顿的垃圾回收可以让这些应用程序运行良好。对于一大类应用程序,特别是那些需要处理大量数据(若干GB)、很多线程和很多事务的应用程序来说,情况就不一样了。

Amdahl 观察到,大部分工作并不能被很好地并行化;一部分工作总是被顺序执行,无法从并行化中获益。对 Java™ 平台来说情况也是如此。特别的,在 J2SE 1.4 以前,Sun Java平台的虚拟机并不支持并行垃圾回收,这样,在多处理器系统中,垃圾回收会对并行应用产生严重影响。

下图显示了一个除垃圾回收以外其他方面均具有完全可伸缩性的理想系统的性能曲线。红色曲线是一个在单处理器系统中花费1%的时间用于垃圾回收的应用程序。在 32核系统中,应用将损失 20% 的吞吐量。而一个花费10%时间用于垃圾回收的应用(不考虑单处理器系统中额外的垃圾回收时间)扩展到32 核系统时,会损失超过75%的吞吐量。



这说明小型系统中微不足道的速度问题会成为大型系统中的严重性能瓶颈。但是,减少这些性能瓶颈的小改动能够获得很大的性能收益。对足够大型的系统来说,选择合适的垃圾回收器并进行必要的调优是绝对值得的。

对于大多数“小”应用(运行于现代处理器上,需要大约100MB堆的应用)来说,串行垃圾回收器通常是足够的。其他垃圾回收器会带来额外的开销和/或复杂性,这是其他垃圾回收器专业行为的代价。如果一个应用不需要其他垃圾回收器的专业行为,那么就使用串行垃圾回收器。一个不应该使用串行垃圾回收器的场景是运行在一个拥有大内存和两个或多个处理器的系统上的具有超多线程的大型应用。当应用运行在这些服务器级的主机上时,虚拟机会默认选择并行垃圾回收器(参见下面的工效学)。

本文以Solaris™操作系统(平台版本)中的Java SE 6作为参考。不过,文中所述的概念和建议也适用于所有支持的平台,包括Linux, Microsoft Windows和Solaris 操作系统(x86平台版本)。此外,文中提到的命令行选项在所有平台均可用,虽然它们的缺省值在各个平台可能有所不同。

2      工效学(Ergonomics)

工效学是一个J2SE 5.0引入的概念。引入工效学的目的是当JVM启动时在不设置或设置很少几个命令行选项情况下通过选择:

  • 垃圾回收器,
  • 堆尺寸,
  • 和运行时编译器

来提供更好的性能,而不是使用它们(上面提到的三项)的默认值。这个选择假定应用所运行的主机类型和应用的类型一致(也就是说,大型应用运行在大型机器上)。这些选项简化了垃圾回收的调优。对于并行垃圾回收器,用户可以指定应用的最大中断时间和希望的吞吐量。这和指定堆大小来进行性能调优是相对应的。这可以提升使用大尺寸堆的大型应用的性能。最常用的工效学相关内容可以参考“Ergonomics in the 5.0 Java Virtual Machine”这篇文章。建议在使用本文提到的细节配置之前尝试该文章中介绍的工效学方法。

本文中的工效学特性被作为并行垃圾回收器的自适应尺寸策略的一部分。这包括指定垃圾回收性能的目标和性能调优的一些附加选项。

3      代

J2SE平台的优势之一是它将开发人员从复杂的内存分配和垃圾回收中解放出来。然而,一旦垃圾回收成为主要的性能瓶颈,就有必要理解一下这些隐藏的实现细节。垃圾回收器对应用程序使用对象的方式进行假定,并将这些假定反映在可调优参数中,通过调整这些参数就可以提高性能而不牺牲掉抽象性。

当一个对象从运行程序中的任何点都不再可达时,它就被认为是垃圾。最直接的垃圾回收算法只是简单地迭代所有可达对象。任何没有被迭代到的对象都可以被认为是垃圾。这个方法的用时和存活对象数量成正比,这对于那些维护着大量存活数据的大型应用来说是不可接受的。

从J2SE 1.2开始,虚拟机就引入了多种使用分代回收generational collection)的不同垃圾回收算法。与朴素的垃圾回收会检查堆中的每个存活对象不同,分代垃圾回收采用很多观测到经验特征,来最小化回收无用对象(垃圾)的工作量。最重要的经验特征是弱代假设weak generational hypothesis),该假设认为大部分对象都只存活一小段时间。

下图中的蓝色区域是对象生存期的典型分布。横轴是以分配的字节数度量度量的对象生存期。纵轴方向计算的字节数是相应生存期对象的总字节数。左侧的峰值表示分配之后不久就被回收的对象。例如,迭代器对象常常只会在一个循环中被用到。



当然,有些对象确实存活时间更长,因此分布曲线延伸到了右边。例如,典型情况下,有些对象在初始化的时候被创建,并一直存活到进程结束。在这两种极端情况之间,是那些存活时间中等的对象,在图中表现为峰值右边的块状区域。有些应用可能会有看起来十分不同的分布曲线,不过绝大多数的应用都是这个常见的形状。大部分对象都会“英年早逝”这个事实让高效的垃圾回收变得具有可能性。

为了优化这样的应用(即大部分对象都会“英年早逝”的应用),内存被按照generation)进行管理,或者说,按照存放不同年龄对象的内存池进行管理。当一个代被填满后,虚拟机就对该代执行垃圾回收。大部分对象都被分配在用于存储年轻对象的内存池中(年轻代),并且大部分对象也在这里死去(当作垃圾被回收)。当年轻代被填满时,虚拟机会执行一次只针对年轻代的小回收minor collection);其他代的垃圾不会被回收。基于弱代假设成立以及年轻代中的大部分对象都是垃圾并可以被回收利用的假定,我们可以对小回收进优化。该回收算法的成本一阶正比于被收集的存活对象数量;回收一个满是死亡对象的年轻代会非常迅速。在每次小回收过程中,一部分来自于年轻代的幸存对象会被转移至年老代tenured generation)。最终,当年老代被填满而需要回收的时候,虚拟机就会执行一次主回收major collection),这个过程中整个堆都会被回收。主回收通常会比小回运行更长时间,因为这个过程会涉及大量的对象。

如前文所述,对不同的应用,工效学会动态选择垃圾回收器来提供较好的性能。串行垃圾回收器用于那些数据量比较小的程序,而且它的缺省参数也让大多数小程序能够高效工作。而大吞吐量垃圾回收器用于那些有中到大数据量的应用。工效学选择的堆尺寸参数和自适应尺寸策略的特性用于为服务器应用提供更好的性能。这些选择在大多数情况下工作得很好。这就引出了本文的核心宗旨:

如果垃圾回收器成为了瓶颈,你可能需要调整整个堆的大小和每个代的大小。检查垃圾回收器的详细输出,然后检查垃圾回收器参数对你关注的各个性能指标的影响。

(除并行垃圾回收器之外的所有垃圾回收器)缺省的代布局大概是这样的。



初始化的时候,JVM会虚拟保留一个最大的地址空间,只有真正需要时,才会分配物理内存。整个保留的对象地址空间被划分为年轻代和年老代。

年轻代包括eden和两个幸存者空间(survivor space)。大部分对象最初在eden里被分配出来。一个幸存者空间在任意时刻都是空的并作为eden中存活对象的目的地,另一个幸存者空间则用于下一次回收。对象按照这种方式在幸存者空间之间进行拷贝,直到足够老之后被复制到年老代中。

第三个代是永久代permanent generation),其与年老代密切相关,这里保存的数据被虚拟机用来描述那些Java语言层面没有等价物的对象。例如,描述类和方法的对象就存放在永久代中。

3.1    性能考虑因素

对于垃圾回收的性能,主要有两种度量方法:

1.        吞吐量Throughput )。吞吐量是在一段足够长的时间中,没有花费在垃圾回收上的时间占总时间的百分比。吞吐量包含内存空间分配的时间(不过内存空间分配速度调优一般是没有必要的)。

2.        停顿Pauses)。停顿是由于等待垃圾回收而导致程序没有响应的时间。

不同的用户对垃圾回收有不同的需求。例如,对于一个web server而言,吞吐量是合理的性能度量标准,因为垃圾回收期间的停顿是可以容忍的,或者说很容易被网络延迟所掩盖。不过,对于交互式图形界面程序而言,极短的停顿都会极大地影响用户体验。

有些用户对其他的因素很敏感。Footprint是一个进程的工作集,由pages(内存页)和cachelines(缓存行)来度量。在内存受限或者进程数量很多的系统上,footprint决定着应用的可伸缩性。Promptness是对象死掉和该对象所占内存重新可用之间的时间间隔,这是分布式系统(包括远程方法调用(RMI))的一个重要考虑因素。

总之,一个特定的代尺寸选择是上述这些因素进行权衡的结果。例如,一个非常大的年轻代可以最大化吞吐量,但会以footprint、Promptness和停顿时间作为代价。通过使用一个较小的年轻代可以最小化年轻代停顿,但是这会以吞吐量为代价。近似地,调整一个代的大小不会影响到其他代的垃圾回收频率和停顿时间。

没有一个合适的方法用于设置代的大小。最好的选择由程序使用内存的方式和用户的需求来决定。这样,虚拟机给出的垃圾回收器选项并不总是最优的,我们可以使用后面介绍的命令行选项替代虚拟机的默认选项。

3.2   测量

使用特定于应用的指标,可以很准确地得到吞吐量和footprint。例如,web服务器的吞吐量可以使用一个客户端负载生成器来测量,而该服务器的footprint则可以在Solaris操作系统中使用pmap命令来测量。另一方面,通过查看虚拟机的诊断报告,可以很容易地估算出垃圾回收导致的停顿。

命令行选项-verbos:gc可以使JVM在每一次垃圾回收时输出关于堆和垃圾回收的信息。例如,下面是一个大型服务器应用的输出:

[GC325407K->83000K(776768K), 0.2300771 secs]

[GC325816K->83372K(776768K), 0.2454258 secs]

[Full GC267628K->83769K(776768K), 1.8479984 secs]

在这里我们看到两次小回收和紧跟其后的一次主回收。箭头前后的数字(例如第一行的325407K->83000K)分别表示垃圾回收前后的所有存活对象占用的空间。小回收执行完后,仍然还存在一些不能被回收的垃圾对象(死掉的对象),这些对象要么存放在年老代中,要么被年老代或永久代中的对象所引用。

后面括号中的数字(例如第一行中的 (776768K))是堆的提交大小(committed size):虚拟机不向操作系统申请内存的情况下,用于存储java对象的内存空间。注意,这个数字只包括一个幸存者空间(共有两个),因为在任一时间,只有一个幸存者空间可用,同时这个数字也不包括永久代的空间,因为永久代保存虚拟机使用的元数据。

最后一个数字(例如 0.2300771 secs)是垃圾回收所花费的时间;在这个例子里大约是四分之一秒。

第三行中主垃圾回收的格式也是类似的。

-verbos:gc产生的输出格式可能在将来的版本里有所改变。

使用-XX:+PrintGCDetail选项可以查看更多垃圾回收相关的信息。下面是串行垃圾回收器使用该选项打印出来的信息。

[GC [DefNew: 64575K->959K(64576K),0.0457646 secs] 196016K->133633K(261184K), 0.0459067 secs]

该信息显示,这次小回收收回了98%的年轻代空间,DefNew: 64575K->959K(64576K),花费0.0457646秒(大约45毫秒)。

整个堆的使用率下降到大约51% 196016K->133633K(261184K),而且通过最终的花费时间 0.0459067秒显示在垃圾回收中有轻微的例外开销(在年轻代之外的时间)。

选项-XX:+PrintGCTimeStamps会提供每次回收开始的时间戳。这对于查看垃圾回收频率非常有用。

111.042: [GC 111.042: [DefNew:8128K->8128K(8128K), 0.0000505 secs] 111.042: [Tenured:18154K->2311K(24576K), 0.1290354 secs]  26282K->2311K(32704K),0.1293306 secs]

垃圾回收在程序运行后111秒开始。小回收几乎在相同时间启动。输出中还显示了由Tenured描述的主回收信息。年老代的空间使用率下降到大约 10% 18154K->2311K(24576K) ,用时 0.1290354秒(大约130毫秒)。

和 -verbose:gc 一样,-XX:+PrintGCDetails产生的输出格式也可能在将来的版本里有所变动。

4      设定代的大小

很多参数会影响到代的大小。下图阐明了堆的提交空间和虚拟空间的区别。虚拟机初始化的时候,整个堆空间都是保留的。堆保留空间大小可以通过选项-Xmx指定。如果-Xms参数小于-Xmx参数,那么不是所有的保留空间都被立刻提交到虚拟机之中。未提交的空间在该图中标记为 virtual。堆的不同部分(永久段、年老段和年轻段)可以按需扩展到虚拟空间的上限。

一些参数是堆的一部分和另一部分的比率,例如参数NewRatio表示年老代对年轻代的比例。这些参数将在下面讨论。


4.1   整个堆

注意,接下来关于堆的生长、收缩和缺省堆大小的讨论均不适用于并行垃圾回收器。(关于并行垃圾回收器的堆大小调整和缺省堆大小的细节信息,请参考相关章节)不过,用于控制整个堆大小和代尺寸的参数也适用于并行垃圾回收器。

因为在代被填满时才进行垃圾回收,所以吞吐量反比于可用内存数量。总可用内存数是影响垃圾回收性能的最重要因素。

缺省情况下,虚拟机在每次垃圾回收后通过增加或减少堆尺寸来尽量保持空闲空间与存活对象所占空间之间的比例在一个区间之内。这个目标区间由参数-XX:MinHeapFreeRatio=<minimum>和-XX:MaxHeapFreeRatio=<maximum>设置,而总的堆大小的下限和上限分别由-Xms<min>和-Xmx<max>来指定。这些参数在 32 位 Solaris 系统(SPARC 平台版本)中的缺省值如下表所示:

Parameter

Default Value

MinHeapFreeRatio

40

MaxHeapFreeRatio

70

-Xms

3670k

-Xmx

64m

因为64位系统上的对象比32位系统上的对象更大,因此堆尺寸参数的缺省值按比例增加了将近 30%。

应用上表给定的这些参数后,如果一个代的空闲空间所占比例低于 40%,虚拟机就会扩展该代的内存空间,以保持空闲内存所占比例为 40%,扩展之后代所占空间不得超过该代的最大允许尺寸。同样地,如果空闲空间所占比例超过 70%,虚拟机就会缩小该代以保持空闲空间所占比例为70%,收缩之后代所占空间不得小于该代的最小尺寸。

使用这些缺省参数时,大型服务器应用经常遇到两种问题。其一是启动慢,原因是初始的堆尺寸过小,经常需要经历多次主回收才能达到稳定值。另一个更紧迫的问题是,对于大多数服务器应用来说,缺省的最大堆尺寸太小了。对于服务器应用程序而言,设置的一般原则是:

  • 除非遇到了停顿问题,否则给虚拟机尽量多的内存。因为缺省尺寸(64MB)通常都太小了。
  • 将-Xms和-Xmx设置成相同的值,虚拟机就不需要进行最重要的尺寸决定,这样就增强了可预见性。
  • 一般地,增加处理器数量的同时增加内存,因为内存分配可以被并行化。

作为参考,有一个单独的页面会介绍一些可用的命令行参数

4.2   年轻代

影响位居次席的是用于年轻代的堆比例。年轻代越大,进行小回收的次数也就越少。不过,在堆大小给定的情况下,年轻代越大,就意味着年老代越小,这会增加主回收的频率。所以最佳选择依赖于应用中所分配对象的生命期分布。

年轻代的尺寸默认由NewRatio控制。例如,设置-XX:NewRatio=3意味着年轻代和年老代的比例是1:3。换句话说,eden和幸存者空间的总和是整个堆大小的四分之一。

参数NewSize和MaxNewSize分别指定年轻代大小的下限和上限。就像给-Xms和-Xmx设置相同的值可以固定堆的尺寸一样,将这两个参数设成相同的值也会固定年轻代的大小。这样可以比使用NewRatio允许的整数倍更细粒度地调整年轻代的大小。

4.2.1  幸存者空间

如果需要,SurvivorRatio可以用来调整幸存者空间的大小,不过这对性能影响一般不大。例如,-XX:SurvivorRatio=6 会将幸存者空间和eden的比例设置为1:6。换句话说,每个幸存者空间大小将是eden大小的六分之一,是整个年轻代大小的八分之一(不是七分之一,因为一共有两个幸存者空间)。

如果幸存者空间过小的话,拷贝回收会直接溢出到年老代的空间中。如果幸存者空间太大的话,他们会空着浪费掉。每次垃圾回收,虚拟机会选择一个对象在进入年老代之前被复制的次数阈值。通过选择这个阈值可以保证幸存者空间是半满的。命令行选项-XX:+PrintTenuringDistribution可以显示这个阈值和年轻代中对象的年龄。这个选项对于观测应用中的对象生存期分布也是有用的。

下表是32 位 Solaris操作系统(SPARC平台版)上各个选项的缺省值;这些缺省值在其他平台上可能有所差异。

 

Default Value

Parameter

Client JVM

Server JVM

NewRatio

8

2

NewSize

2228K

2228K

MaxNewSize

not limited

not limited

SurvivorRatio

32

32


服务应用的设置准则是:年轻代的最大尺寸通过最大堆尺寸和 NewRatio 计算而得。MaxNewSize的缺省值“not limited”表示表示:除非在命令行中为MaxNewSize指定一个值,否则这个计算得到的值不会受到MaxNewSize的限制。

  • 首先确定可以提供给虚拟机的最大堆尺寸。然后在不同年轻代尺寸条件下,得到性能度量值,然后从中找到最优设置。

注意:为了防止过多的缺页错误和换页,最大堆尺寸应该总是小于机器上安装的内存数量。

  • 如果整个堆的尺寸是确定的,增加年轻代的尺寸就会减少年老代的尺寸。一定要保证年老代的尺寸足够大,使之可以在容纳所有应用全程中都要用到的存活对象的同时,还能留有一定的闲置空间(10-20%或更多)。
  • 依照上述年老代的约束:

给年轻代分配大量内存。

因为内存分配可以并行化,所以增加处理器数量的同时增加年轻代的尺寸。

5      可用的垃圾回收器

到目前为止,我们讨论的还都是串行垃圾回收器。不过Java HotSpot虚拟机一共包含三种不同的回收器,每种都有不同的性能特性。

1.        串行垃圾回收器使用单线程执行所有垃圾回收工作,因为没有线程间通信的开销,串行垃圾回收器相当高效。因为串行垃圾回收器不能从多处理器硬件中获益,所以其最适合于单处理器系统,当然在小数据量的应用中(不大于100MB),它对于多处理器系统也是有用的。在某些硬件和操作系统配置下JVM会缺省使用串行垃圾回收器,你也可以使用-XX:+UseSerialGC选项显式指定使用串行垃圾回收器。

2.        并行垃圾回收器(或吞吐量垃圾回收器)并行地执行小回收,这会显著减少垃圾回收的开销。并行垃圾回收器适用于运行在多处理器或多线程硬件上拥有中等或大尺寸数据的应用。在某些硬件和操作系统配置下,JVM会缺省使用并行垃圾回收器,你也可以使用-XX:+UseParallelGC选项显式指定使用并行垃圾回收器。

更新:并行压缩parallel compaction)是 J2SE 5.0update 6 版本引入的新特性,并在 Java SE 6 之中得到加强,该特性允许并行垃圾回收器并行地执行主回收。如果不使用并行压缩,主回收只能单线程执行,这会严重限制系统的可伸缩性。你可以使用命令行选项-XX:+UseParallelOldGC启用并行压缩。

3.        并发垃圾回收器通过并发地执行大部分垃圾回收工作(也就是当应用运行时)来尽可能缩短垃圾回收带来的停顿。并发垃圾回收器适用于那些拥有中到大量数据、对响应时间要求高于吞吐量要求的应用,因为最小化停顿的技术会降低应用性能。你可以使用-XX:+UseConcMarkSweepGC选项启用并发垃圾回收器。

5.1   选择垃圾回收器

除非你的应用有非常严格的停顿时间需求,否则首先运行你的应用,让虚拟机选择垃圾回收器。如果有必要,调整堆的大小来改善性能。如果性能仍然无法达到你的目标,那么使用如下指导方针作为选择一个垃圾回收器的出发点。

  1. 如果应用的数据集很小(大约不超过100MB),那么使用-XX:+UseSerialGC选项选择串行垃圾回收器。
  2.  如果应用运行在单处理器系统中,并且没有什么停顿时间要求,那么让虚拟机选择垃圾回收器,或者使用-XX:+UseSerialGC选择串行垃圾回收器。
  3. 如果(a)应用的峰值性能是第一位的,并且(b)没有停顿时间要求或停顿一两秒或更长是可接受的,那么让虚拟机选择垃圾回收器,或者使用-XX:+UseParallelGC选择并行垃圾回收器,并(可选)通过-XX:+UseParallelOldGC启用并行压缩。
  4. 如果响应时间比总体吞吐量更为重要,并且垃圾回收停顿必须控制在1秒以内,那么通过-XX:+UseConcMarkSweepGC参数启用并发垃圾回收器。如果仅有一个或两个处理器可用时,考虑使用下文将要介绍的“增量模式”。

因为性能依赖于堆的尺寸、应用维持的存活数据量,以及可用处理器的数量和速度,所以这些指导方针仅仅是选择垃圾回收器的出发点。停顿时间对这些因素尤为敏感,所以上面提到的1秒阈值只是个大致数值:在许多硬件和数据量的组合情况下,并行垃圾回收器可能会导致停顿时间超过1秒;反过来说即在某些组合下,并发垃圾回收器不能保证停顿时间小于1秒。

如果推荐的垃圾回收器没有达到期望的性能,首先应该尝试调整堆和代的尺寸来达到期望的目标。如果仍然不成功的话,尝试更换一个垃圾回收器:使用并发垃圾回收器来减少停顿时间,使用并行垃圾回收器来增加多处理器系统中的吞吐量。

6      并行垃圾回收器

并行垃圾回收器(也被称为吞吐量回收器)类似于串行回收器,也是一种分代垃圾回收器;其主要区别在于并行垃圾回收器使用多个线程来加快垃圾回收过程。你可以使用选项 -XX:+UseParallelGC指定使用并行垃圾回收器。缺省情况下,只有小回收会并行执行,主回收仍然单线程运行。不过,使用选项-XX:+UseParallelOldGC启用并行压缩可以让主回收和小回收都并行执行,从而进一步减少垃圾回收开销。

在一个有N个处理器的计算机上,并行垃圾回收器使用N个垃圾回收器线程;不过这个数量可以在命令行选项里指定(参见下文)。在一台单处理器主机上,由于并行执行存在开销(例如同步),并行垃圾回收器的性能或许不如串行垃圾回收器。然而,当在一个双处理器的机器上运行拥有中等或大尺寸堆的应用程序时,并行垃圾回收器略优于串行垃圾回收器,如果有多于两个处理器可用时,它就能远胜于串行垃圾回收器。

垃圾回收器线程的数量可以用-XX:ParallelGCThreads=<N>选项来控制。如果使用命令行选项显式地调优堆,并行垃圾回收器获得较好性能使用的堆尺寸一阶相等于串行垃圾回收器需要的堆尺寸。启用并行垃圾回收器仅仅是让小回收造成的停顿更短一些。因为有多个垃圾回收器线程参与小回收的过程,在将年轻代中对象移动到年老代中的过程中有极少的可能性造成一些碎片。每个垃圾回收线程都保留一块专属的年老代空间,用于年轻代对象向年老代移动,将年老代的可用空间划分为“移动缓冲区”(promotion buffer)的过程会导致碎片效应。减少垃圾回收器线程的数量可以减小碎片效应、增加专属年老代的空间。

6.1   代

正如上面提到的,并行垃圾回收器的代的布局和串行垃圾回收器不同。其布局如下图所示。


6.2   工效学

自J2SE 5.0开始,并行垃圾回收器成为了server级机器的缺省垃圾回收器,详细资料可以参考“Garbage Collector Ergonomics”。此外,并行垃圾回收器使用一种自动调优方法,该方法允许用户指定期望的行为而不是指定代的大小和其他底层调优细节。这些可以指定的行为包括:

  • 最大垃圾回收停顿时间
  • 吞吐量
  • Footprint (也就是堆尺寸)

最大停顿时间目标由选项-XX:MaxGCPauseMillis=<N>来指定。这个选项仅仅是提示(hint)垃圾回收器停顿时间不得大于<N>毫秒;缺省情况下没有最大停顿时间目标。如果指定了停顿时间目标,JVM就会相应调整堆尺寸和其他垃圾回收相关参数,以保持垃圾回收停顿时间小于该指定值。注意,这些调整可能会导致总体吞吐量的降低,而且在某些情况下,可能无法达到要求的停顿时间目标。

吞吐量目标由垃圾回收时间和非垃圾回收时间(也就是应用程序时间)的比例度量。这个目标可以用命令行选项-XX:GCTimeRatio=<N>来指定,这样垃圾回收时间和应用程序时间的比例将是1 / (1 + <N>)。

例如,-XX:GCTimeRatio=19设置了1/20或5%的时间用于垃圾回收的目标。该选项的缺省值是99,表示1%的时间用于垃圾回收。

最大堆footprint使用之前介绍的-Xmx<N>选项指定。此外,只要其他优化目标得到满足,垃圾回收器会隐式地最小化堆尺寸。

6.2.1  目标的优先级

这些目标的优先级顺序如下:

1.      最大停顿时间目标

2.      吞吐量目标

3.      最小堆尺寸目标

最大停顿时间目标会被首先满足。仅当最大停顿时间目标被满足的情况下,才会去满足吞吐量目标。类似的,仅当前两个目标都被满足的情况下,才会考虑去满足footprint目标。

6.2.2  代尺寸调整

每次垃圾回收结束的时候,垃圾回收器都会更新其保存的平均停顿时间之类的统计信息。同时它会检查各个目标是否被满足了,是否有调整代尺寸的需要。这之中的例外情况就是显式的垃圾回收(例如调用 System.gc())会被忽略掉(执行显式垃圾回收不会更新这些统计信息)。

增加和缩小一个代的大小是通过增量(固定百分比的代大小)实现的,这样一个代就能分步达到其最佳尺寸。JVM按照不同的比率增加和收缩代尺寸,缺省情况下,增加按照20%的增量进行,收缩按照5%的增量进行。年轻代和年老代增量百分比分别通过命令行标志-XX:YoungGenerationSizeIncrement=<Y>和-XX:TenuredGenerationSizeIncrement=<T>来设定。而缩小比例则通过-XX:AdaptiveSizeDecrementScaleFactor=<D>命令行标志来调整。如果增量是X%,那么减量就是(X/D)%。

如果垃圾回收器在启动时决定增加一个代的大小,会有一个额外的百分比追加到增量上。这个附加的百分比随着回收的次数增加而减少,并且它的影响不是长期的。这个额外增量意在提高启动性能。收缩增量则没有这个额外的百分比。

如果最大停顿时间目标没有达到,一次只会缩小一个代的尺寸。如果两个代的停顿时间都没有达到目标,停顿时间较大的那个代会首先被缩小。

如果总体吞吐量目标没有达到,那么两个代的大小都会增加。每个代都按照各自对垃圾回收时间的贡献比例分别增加。例如,如果年轻代的垃圾回收时间占去了25%的总垃圾回收时间,并且年轻代的全部增量应该是20%,那么这时它的增量就是5%(25%×20%)。

6.2.3  缺省堆尺寸

如果没有在命令行中进行设置,那么初始堆尺寸的和最大堆尺寸会基于主机内存计算得到。如下表所示,堆所占用的内存比例由命令行选项 DefaultInitialRAMFraction和DefaultMaxRAMFraction来控制。(表中的memory表示计算机的系统内存数量。)

 

Formula

Default

Initial heap size

memory/ DefaultInitialRAMFraction

memory/4

Maximum heap size

MIN(memory / DefaultMaxRAMFraction, 1GB)

Min(memory/4, 1GB)


6.3   过多的GC时间和OutOfMemory错误注意,不论系统中有多少内存,缺省的最大堆尺寸不得超过1GB。

当有过多的时间用于垃圾回收时,并行垃圾回收器会抛出一个OutOfMemoryError 错误,即如果超过98%的时间用于垃圾回收并且只有少于2%的堆被回收的话,垃圾回收器就会抛出一个OutOfMemoryError错误。这个特性用于防止堆太小而导致程序长时间无法正常工作。如果有必要,你可以使用命令行选项-XX:-UseGCOverheadLimit来关闭这个特性。

6.4   测量

并行垃圾回收器的详细输出和串行垃圾回收器的输出基本相同。

7      并发垃圾回收器

并发垃圾回收器适用于那些需要较短停顿时间,且在运行期可以和垃圾回收器共享处理器资源的应用。典型情况下,那些拥有大量长期存活数据(年老代比较大),并且运行在拥有两个或更多处理器上的应用易于从使用并发垃圾回收器上获益。无论如何,任何要求较短停顿时间的应用都应该考虑使用并发垃圾回收器;例如,观察发现:运行在单处理器主机上并且拥有适中年老代的交互式应用程序使用并发垃圾回收器取得了比较好的性能,特别是使用增量模式时,效果更加明显。你可以使用命令行选项-XX:+UseConcMarkSweepGC来启用并发垃圾回收器。

和其他垃圾回收器类似,并发垃圾回收器也是分代垃圾回收器;所以也有小回收和主回收。并发垃圾回收器通过在应用线程运行时并发地使用独立的垃圾回收线程跟踪所有可达对象,来缩短主回收导致的停顿时间。在每个主回收周期中,并发垃圾回收器会在垃圾回收开始和垃圾回收中期让所有的应用线程停顿一小段时间。第二次停顿相对而言时间更长,在此期间会有多个线程来进行回收工作。剩下的回收工作(包括大部分的存活对象跟踪和清除不可达对象的工作)由一个或多个和应用线程并发运行的垃圾回收器线程来执行。小回收会在进行的主回收周期中穿插进行,其工作方式和并行垃圾回收器类似(特别需要说明的就是,在小回收期间,应用线程都会被停止)。

并发垃圾回收器所采用的基本算法在技术报告A Generational Mostly-concurrent Garbage Collector里有介绍。注意,具体实现细节在不同版本里会有细微的变化。

7.1   并发的开销

对并发垃圾回收器来说,较短的主回收停顿时间是以处理器资源作为代价的(这些资源如果不用在垃圾回收器上肯定就用在应用上了)。最明显的开销就是并发回收阶段会使用一个或多个处理器资源。在N处理器系统中,垃圾回收的并发部分会使用K个可用的处理器,其中1<=K<=上限{N/4}。(注意,K值的选择和上限将来可能会发生变化。)除了并发阶段处理器的使用,启用并发也会引起额外的开销。所以,尽管并发垃圾回收器显著减少了停顿时间,但和其他垃圾回收器相比,应用的总体吞吐量会受到轻微的影响。

在一个多核计算机上,垃圾回收并发部分执行的时候,应用程序仍然有可用的CPU,所以并发垃圾回收器并没有让程序停顿。这通常意味着更短的停顿,但也意味着应用可用的处理器资源更少,并且可以预料到应用的运行速度减慢,对于CPU密集型应用来说更是如此。在不超过上限的情况下,随着N的上升,垃圾回收器导致的处理器资源减少会相对变小,而从并发垃圾回收的获益则相对提高。下一节“并发模式失败”会讨论这种处理器规模扩张的潜在限制 。

因为在并发阶段至少有一个处理器用于了垃圾回收,所以在单处理器(单核)系统中,并发垃圾回收器一般不会带来什么好处。不过,并发垃圾回收有一个分离模式可以在单处理器或双处理器系统中显著减少停顿时间;后面的增量模式中将会进一步介绍其细节。

7.2   并发模式失败

并发垃圾回收器使用一个或多个与应用线程同时运行的垃圾回收线程,其目标是在年老代和永久代被填满之前完成垃圾回收。如前文所述,在一般的操作中,并发垃圾回收器的大部分跟踪与清理工作是在程序运行的同时并发进行的,所以程序线程只有极短的停顿。但是,如果在年老代被填满之前并发垃圾回收器不能完成回收不可达对象,或是年老代中的可用闲置空间无法满足一次分配操作的时候,应用就不得不被暂停下来以等待回收过程完成。这种不能并发地完成垃圾回收的情况被称为并发模式失败,它表明需要对并发垃圾回收器的参数进行调整了。

7.3   过多的GC时间和OutOfMemory错误

如果用于垃圾回收的时间过多,并发垃圾回收器会抛出OutOfMemoryError错误,即如果多于98%的时间用于垃圾回收并且只有少于2%的堆被回收的话,并发垃圾回收器就会抛出OutOfMemoryError错误。这个特性用来防止堆太小而导致程序长时间无法正常工作。如果有必要,你可以使用命令行选项-XX:-UseGCOverheadLimit来关闭这个特性。

这个策略和并行垃圾回收器基本是一致的,惟一的区别就是执行并发垃圾回收的时间并未计算在98%的时间限制之内。也就是说,只有那些在程序停顿时进行垃圾回收的时间才被计算在内。这些垃圾回收(即使程序停顿下来的垃圾回收)常常是由并发模式失败或显式垃圾回收请求(如调用 System.gc())导致的。

7.4   浮动垃圾

与HotSpot中的其他垃圾回收器一样,并发垃圾回收器是一种至少会识别堆中可达对象的跟踪回收器。按照Jones and Lins的说法,并发垃圾回收器是一种增量更新(Incremental Update)垃圾回收器。因为应用线程和垃圾回收器线程在主回收过程中并发执行,那么那些垃圾回收器跟踪到的对象就可能在垃圾回收完成之后变得不可达。这些没有被回收的不可达对象被称为浮动垃圾floating garbage)。浮动垃圾的数量取决于垃圾回收周期的长短和程序中引用更新(也被称为mutation)的频率。另一个原因是年轻代和年老代的垃圾回收是独立的,彼此都是对方的根。粗略的经验规则表明,增加20%的年老代空间就会产生浮动垃圾。一个垃圾回收周期中的浮动垃圾会在下一个垃圾回收周期中被回收。

7.5   停顿

一个并发回收周期中,并发垃圾回收器会两次暂停应用。第一次将从根(例如,来自应用线程栈、寄存器和静态对象等的对象引用)和堆的其他地方(如年轻代)直接可达的对象标记为存活的。第一次停顿被称为初始标记停顿initial mark pause)。第二次停顿发生在并发跟踪阶段末尾,用来发现在垃圾回收线程跟踪完一个对象之后其引用又被应用线程更新而没有被并发跟踪到的对象。第二次停顿被称为重标记停顿remark pause)。

7.6   并发阶段

可达对象图的并发跟踪发生在初始标记停顿和重标记停顿之间。在这个并发跟踪阶段,一个或多个并发垃圾回收器线程会使用那些对应用程序可用的处理器资源,所以尽管应用不会停顿,计算密集型应用的吞吐量可能会在此阶段和其他并发阶段受到相当大的损失。重标记停顿之后,还有一个并发清理阶段,该阶段会回收所有标记为不可达的对象。一旦回收周期结束,并发垃圾回收器就会一直等待到下一个主回收周期开始,在这期间并发垃圾回收器基本不消耗任何计算资源。

7.7   启动并发回收周期

在串行回收器中,每当年老代被填满时,都会引发一次主回收,并且所有应用线程都会在主回收期间暂停运行。并发垃圾回收器与之不同,它需要在足够早的时间开始垃圾回收,以便能在年老代被填满之前完成垃圾回收;否则应用程序就会因为发生并发模式失败而导致较长的停顿。有好几种会启动并发垃圾回收的情况。

基于最近的历史记录,并发垃圾回收器维护了一个年老代变满的预期剩余时间和一个垃圾回收周期的预期时间。基于这些动态估计,并发垃圾回收器会及时地启动垃圾回收周期以让垃圾回收周期在年老代变满之前完成。因为并发模式失败的代价非常高,出于安全考虑,这些估算值都被适当地放大。

如果年老代的占用率超出一个年老代初始占用率initiating occupancy,年老代的百分比),垃圾回收器也会启动一个并发垃圾回收。这个初始占用率的缺省值大约是92%,不过这个值在不同版本中可能略有不同。你可以通过命令行选项-XX:CMSInitiatingOccupancyFraction=<N>手动调整,其中N是一个0-100的整数,代表年老代的占用百分比。

7.8   调度停顿

年轻代和年老代的垃圾回收停顿是独立发生的。他们不会重合,但可能会快速连续发生,即一个垃圾回收的停顿紧跟着下一个垃圾回收的停顿,这样从外界来看就是一个长停顿。为了避免这种情况,并发垃圾回收器会调度重标记停顿的时间,使之发生在前后两个年轻代停顿之间。这个调度目前还没有应用于初始标记停顿,因为它通常会比重标记停顿短很多。

7.9   增量模式

并发垃圾回收器可以在这样一种模式下工作:并发阶段以增量的方式进行。回忆一下,在并发阶段,垃圾回收线程会使用一个或多个处理器。所谓增量模式是指通过周期性地停止并发阶段,将处理器资源还给应用程序来减少长并发阶段的影响。这种模式(在这称为“i-cms”)将垃圾回收器的并发工作划分为小块时间,并在年轻代垃圾回收之间调度执行。这个特性对于那些运行在较少处理器的机器上(1或2个处理器的)并且需要较短并发垃圾回收停顿时间的应用来说非常有用。

并发垃圾回收周期通常包括如下几步:

  • 停止所有的应用线程,识别从根可达的对象集,然后继续所有的应用线程
  • 在应用线程执行的同时,使用一个或多个处理器并发地跟踪可达对象图
  • 使用一个处理器,并发重新跟踪上一步跟踪之后对象图发生变化的部分
  • 停止所有的应用线程,重新跟踪上次检查之后根和对象图发生变化的部分,然后继续运行所有应用线程
  • 使用一个处理器,并发地清理不可达对象并将回收的内存添加到用于分配空间的闲置内存列表上。
  • 使用一个处理器并发地调整堆的大小,并准备下一个回收周期所需的数据结构

正常情况下,在并发跟踪阶段,并发垃圾回收器使用一个或多个处理器,而不会主动让出它们。类似的,在清理阶段,并发垃圾回收器也会始终独占地使用一个处理器,而不主动让出它。对于一个有响应时间限制的应用(特别是该应用运行在只有一两个CPU的主机上时)来说,其代价太高了。增量模式通过将并发阶段分解为一系列的短时间活动,并在两个小停顿之间调度它们解决了这个问题。

i-cms使用占空比duty cycle)来控制并发回收器自发放弃处理器之前的工作量。占空比是年轻代回收之间允许并发垃圾回收器运行的时间百分比。i-cms可以根据应用的行为自动计算占空比(这也是推荐的方法,称为automatic pacing),当然,你也可以通过命令行给占空比设定一个固定值。

7.9.1  命令行选项

下面是控制 i-cms的命令行选项(下文给出了初始选项集的推荐值):

 

 

Default Value

选项

描述

J2SE 5.0和之前的版本

Java SE 6及之后的版本

 

 

 

 

-XX:+CMSIncrementalMode

启用增量模式。注意并行垃圾回收器也必须被启用(使用-XX:+UseConcMarkSweepGC选项),否则该选项无效。

不启用

不启用

-XX:+CMSIncrementalPacing

启用automatic pacing。增量模式占空比基于JVM运行时回收的统计信息自动调整。

不启用

不启用

-XX:CMSIncrementalDutyCycle=<N>

两次小回收之间允许并发回收器运行的时间百分比(0-100)。如果启用automatic pacing,那么这个值就是初始值。

50

10

-XX:CMSIncrementalDutyCycleMin=<N>

CMSIncrementalPacing选项启用后,占空比的下限(0-100)。

10

0

XX:CMSIncrementalSafetyFactor=<N>

计算占空比时,需要额外增加的一个百分比(0-100)。

10

10

-XX:CMSIncrementalOffset=<N>

小回收之间,增量模式占空比的偏移量(0-100)。

0

0

-XX:CMSExpAvgFactor=<N>

计算并发回收统计信息的指数平均值时,当前样本的权值(0-100)。

25

25


7.9.2  推荐选项

要在Java SE 6里使用 i-cms,需要使用如下命令行选项:

-XX:+UseConcMarkSweepGC-XX:+CMSIncrementalMode -XX:+PrintGCDetails -XX:+PrintGCTimeStamps

前两个选项分别启用并发垃圾回收器和 i-cms。后两个参数不是必须的,它们只是要求垃圾回收器将诊断信息打印到标准输出,这样,垃圾回收器的行为就可以被看到并用于后续分析。

注意,对于J2SE 5.0和之前的版本,我们建议 i-cms 使用如下的初始命令行选项集:

-XX:+UseConcMarkSweepGC-XX:+CMSIncrementalMode -XX:+PrintGCDetails -XX:+PrintGCTimeStamps-XX:+CMSIncrementalPacing -XX:CMSIncrementalDutyCycleMin=0 -XX:CMSIncrementalDutyCycle=10

除了三个用于控制icms automatic pacing的选项外,其他都跟Java SE 6的推荐选项一样。这些额外的选项只是简单指定了在Java SE 6中变成缺省的值。

7.9.3  基本疑难解答

i-cms automatic pacing特性使用程序运行时收集到的统计信息计算占空比,以使并发垃圾回收在堆被占满之前完成。不过,由于过去的行为并不能完美地预测将来的行为,因此这些估计值可能并不总是足够准确到阻止堆被填满。如果发生太多次堆被填满后的垃圾回收,可以尝试下面这些步骤,一次使用一个:

步骤

选项

增加安全系数

-XX:CMSIncrementalSafetyFactor=<N>

增加最小占空比

-XX:CMSIncrementalDutyCycleMin=<N>

关闭automatic pacing并使用一个固定的占空比

-XX:-CMSIncrementalPacing -XX:CMSIncrementalDutyCycle=<N>


7.10   测量

下面是使用-verbose:gc和-XX:+PrintGCDetails选项时,并发垃圾回收器的输出,其中一些小细节已经被去掉了。注意,并发垃圾回收器的输出中掺杂着小回收的输出;典型情况下,一个并发回收周期期间会发生很多小回收。CMS-initial-mark表明一个并发垃圾回收周期的开始。CMS-concurrent-mark: 表明并发标记阶段的完成,CMS-concurrent-sweep表明并发清除阶段的完成。之前没有提到过的预清除阶段以CMS-concurrent-preclean为标志。预清除是重标记阶段CMS-remark的准备工作,其可以并发执行。最后一个阶段由CMS-concurrent-reset标志:这是下一个并发回收周期的准备工作。

[GC [1 CMS-initial-mark: 13991K(20288K)] 14103K(22400K), 0.0023781 secs][GC [DefNew: 2112K->64K(2112K), 0.0837052 secs] 16103K->15476K(22400K), 0.0838519 secs]...[GC [DefNew: 2077K->63K(2112K), 0.0126205 secs] 17552K->15855K(22400K), 0.0127482 secs][CMS-concurrent-mark: 0.267/0.374 secs][GC [DefNew: 2111K->64K(2112K), 0.0190851 secs] 17903K->16154K(22400K), 0.0191903 secs][CMS-concurrent-preclean: 0.044/0.064 secs][GC [1 CMS-remark: 16090K(20288K)] 17242K(22400K), 0.0210460 secs][GC [DefNew: 2112K->63K(2112K), 0.0716116 secs] 18177K->17382K(22400K), 0.0718204 secs][GC [DefNew: 2111K->63K(2112K), 0.0830392 secs] 19363K->18757K(22400K), 0.0832943 secs]...[GC [DefNew: 2111K->0K(2112K), 0.0035190 secs] 17527K->15479K(22400K), 0.0036052 secs][CMS-concurrent-sweep: 0.291/0.662 secs][GC [DefNew: 2048K->0K(2112K), 0.0013347 secs] 17527K->15479K(27912K), 0.0014231 secs][CMS-concurrent-reset: 0.016/0.016 secs][GC [DefNew: 2048K->1K(2112K), 0.0013936 secs] 17527K->15479K(27912K), 0.0014814 secs]

与小回收停顿时间相比,初始标记停顿时间更短。如上输出所示,并发阶段(并发标记、并发预清除和并发清除)通常会比小回收停顿持续更长时间。不过注意,应用并没有在这些并发阶段中停顿下来。重标记停顿时间通常和一个小回收所花费的时间相当。重标记停顿受某些应用特征(如较高的对象修改率会增加这个停顿)和上一次小回收之后的时间(即,更多的年轻代对象可能会增加这个停顿)的影响。

8      其他因素

8.1    永久代尺寸

对大部分应用来说,永久代对于垃圾回收的性能没有显著的影响。不过,一些应用会动态地生成和加载很多类;例如,一些Java Server Pages(JSP)页面的实现。这些应用可能需要一个较大的永久代去存放这些额外的类。如果出现这样的情况,你可以用命令行选项-XX:MaxPermSize=<N>来增大最大永久代尺寸。

8.2   Finalization; Weak, Soft and Phantom References

一些应用使用 finalization 和 weak, soft, phantom 引用与垃圾回收进行交互。这些特性在Java语言层面对性能造成影响。一个这方面的例子是通过finalization来关闭文件描述符,但这会导致一个外部资源依赖于垃圾回收的及时性(即如果不执行垃圾回收的话,该外部资源会一直处于打开状态)。所以依赖垃圾回收来管理内存之外的资源不是个好主意。

相关资源章节中有一篇文章深入讨论了一些finalization的常见错误和避免这些错误的技术。

8.3   显式垃圾回收

应用程序和垃圾回收器的另一个交互途径是显式调用System.gc()进行完整的垃圾回收。即使没有必要(也就是说一次小回收可能就足够了),这也会强制进行一次主回收,所以通常应该避免这种情况。通过使用-XX:+DisableExplicitGC标志(使虚拟机忽略System.gc()调用)禁用显式垃圾回收可以得出显式垃圾回收对性能的影响。

最常见的显式垃圾回收场景是 RMI 的分布式垃圾回收(DGC)。使用 RMI 的应用会引用在其他虚拟机中的对象。如果不间或地回收本地堆,这些应用中的垃圾就不会被回收,所以RMI会周期性地强制进行完整的垃圾回收。这些回收的频率可以使用属性进行控制。例如

java-Dsun.rmi.dgc.client.gcInterval=3600000  -Dsun.rmi.dgc.server.gcInterval=3600000…

指定显式垃圾回收的频率是每小时运行一次,而不是缺省的每分钟一次。不过,这可能会导致某些对象在被回收之前存活太长时间。如果DGC活动时间没有上限要求的话,这些参数可以被设置到高达Long.MAX_VALUE来让显式垃圾回收的时间间隔无限长。

8.4   Soft References

服务器虚拟机中的soft references比客户机中soft references存活时间更长。其清理频率可以用命令行选项-XX:SoftRefLRUPolicyMSPerMB=<N>来控制,该选项指定对于堆中的每兆闲置空间softreference保持存活(一旦它不强可达了)的毫秒数。该选项的默认值是每兆1000毫秒,这意味着对于堆中的每兆闲置空间,soft reference会(在最后一个强引用被回收之后)存活1秒钟。注意,这只是一个近似值,因为soft reference只有在垃圾回收时才会被清除,而垃圾回收并不会定时进行。

8.5   Solaris 8Alternate libthread

Solaris 8操作系统提供了另一个版本的线程库libthread,它直接将线程绑定到轻量级进程(LWP)。使用libthread能使一些应用获益极大,并潜在地对所有多线程应用都有好处。下面的命令会为java载入libthread(BASH 格式):

LD_PRELOAD=/usr/lib/lwp/libthread.so.1

export LD_PRELOAD

java ...

在Solaris 8上才需要该命令,因为在Solaris 9操作系统中,这是缺省的线程库,而在Solaris 10中,这是惟一的线程库。

9      资源

1.     HotSpot VM Frequently Asked Questions (FAQ)

2.     GC output examples介绍了如何解释不同垃圾回收器的输出。

3.     How to Handle Java Finalization’s Memory-Retention Issues介绍了一些常见的finalization错误和避免他们的方法。

4.        Richard Jones and Rafael Lins,Garbage Collection: Algorithms for Automated Dynamic Memory Management, Wileyand Sons (1996), ISBN 0-471-94148-4

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值