分享

HotSpot JVM的内存管理

 Baruch 2017-10-03

1 引言(Introduction)

在强大的Java™ 2 平台下,J2SE可以自动的管理内存,所以将开发者从复杂的显性管理内存的工作中解放出来。

本片文档是对Sun J2SE 5.0 release版本的Java HotSpot JVM内存管理机制的概述。描述了可用于内存管理的几种内存收集器,以及给出了一些建议,例如,如何选择和配置内存收集器、如何配置收集器内存区域的尺寸。也给出了一些资料,列出了一些影响内存收集器行为的常用选项并且提供了大量有关于细节的文档链接。

Section 2 是写给那些刚刚接触自动内存管理的读者。其中有一个简短的讨论是与要求程序员手动释放对象空间对比,自动内存管理机制可以获得的好处。

Section 3 概述了一些内存回收的基本概念、设计方案和性能指标,并且介绍了“代”,“代”是根据对象寿命将内存组织成了不同的区域。已经证明了这种组织方式可以在内存回收中减少暂停时间和整理大区域的成本。

接下来的内容集中在了HotSpot JVM上。Section 4描述了4种可以使用的内存收集器,其中一种是在J2SE 5.0 update 6中引入的,还记录了他们使用的常见的内存组织形式。另外Section 4简单介绍了每种收集器的运行原理以及适合于哪种场景。

Section 5 描述一种在J2SE 5.0 release中的新技术,它可以根据应用运行的平台和操作系统自动的选择内存收集器类型和堆尺寸以及工作模式(client or server);还可以根据用户期望的目标自动调节垃圾收集的参数,这个技术叫做工效学(ergonomics)。

Section 6 给出了一些关于如何选择和配置内存收集器的建议以及如何处理OutOfMemoryError异常的建议。Section 7 简单描述了一些可以用来评估内存收集表现的工具,并且在Section 8 中列出了与内存收集器的选项和行为相关的最常用的命令行参数。最后Section9 给出了本文档提到的一些议题的更细节文档的链接。


2 手动 VS 自动内存管理(Explicitvs. Automatic Memory Management)

内存管理是下面的描述的过程。当已经分配过内存的对象不在被使用的时候需要释放它所占用的内存并可以分配给接下来的申请者。这个过程在一些程序语言中是由程序员控制的,然而在这种复杂的工作中经常会发生一些错误,这些错误会导致不可预计的行为或者直接导致程序的崩溃,所以开发者的大量时间花费在调试和纠正这种错误上。

在手动内存管理的语言中经常遇到的问题就是悬挂引用(dangling references)。含义是一个对象被其他对象引用着,但是它占用的内存却已经被释放掉了,那么如果引用者访问被引用对象并且那个空间已经分配给了新的对象时,结果就是不可预知的。

手动管理内存的另一个常见问题是空间泄漏(space leaks)。这种泄漏发生在不能释放掉已经分配但不再使用的内存。例如,当你释放链表的空间时,恰巧释放在第一个元素占用的空间的时候发生了错误,那么链表中其他元素就不再被引用了,程序也无法引用他们,所以它们既不能被使用也不能被覆盖。如果发生了很多这样的泄漏,那么所有的可用内存都可能被消耗掉。

 

另一种管理内存的方法是自动管理,这种方法很常见,尤其是在现代的面向对象的语言中。自动管理使用了一种叫做内存垃圾收集器(garbage collector)的程序。自动的内存管理可以提高代码的抽象度和可靠性。

垃圾收集器避免了悬挂指针(dangling reference),原因是一个仍然被引用的对象永远不会内存回收并且也不会被认为已经被释放掉了。垃圾收集器解决了空间泄漏(space leaks)问题,原因是它可以自动释放不再被引用的空间。


3 内存收集的概念(Garbage Collection Concepts)

一个内存收集器具有下面的功能:

l  分配内存

l  保证所有被引用的对象还在内存中

l  可以释放在运行的代码中不再引用的对象的内存

如果对象被引用着,那我们说它活着(live);如果对象不再被引用了,那我们说他死了(dead),术语称作垃圾(garbage)。寻找并释放这些对象的空间的过程就做垃圾收集(garbagecollection)。

垃圾收集解决了内存分配的很多问题但不是所有问题。例如,你可以无限期的创建对象并保持对他们的引用,直到内存耗尽。垃圾收集本身也是一个复杂的工作,需要消耗时间和资源。

垃圾收集器使用了一种精确的算法用于组织内存、分配与释放空间,并且这种算法对于编程人员来说是透明的。被分配的空间来自于被称为堆(heap)的大块的内存池。

那么在什么时候会出发垃圾收集动作呢?一般来说整个堆或一部分被填满或者达到某一百分比数值时将被收集。

为了满足一个内存分配的请求,必须要在堆中找到一块特定尺寸的没有被使用的内存,然而这是一个艰难的任务。大多数动态内存分配算法的主要问题是避免碎片(fragmentation),这样可以使内存的分配与释放更有效率。


理想的垃圾收集器的特点(Desirable Garbage Collector Characteristics)

一个垃圾收集器必须是即安全又足够聪明的。也就是说,存活的数据一定不能被释放掉,并且垃圾对象应该在尽量少的收集周期中被释放掉。垃圾对象即使出现了一定规模的循环引用也一定要释放掉。

理想的垃圾收集器的操作是非常高效的,不会引起长暂停(在这段时间里应用程序是无法运行的)。尽管如此,像大多数与计算相关的系统一样,需要在时间、空间和频率保持平衡。例如,如果堆比较小,那么垃圾收集会很快但是堆也会很快被填满,所以收集的频率会更高。相反,一个大体积的堆需要花费更长的时间才能填满,所以会大大降低回收频率,但是每次回收的时间会更长。

理想的垃圾收集器的另一个特点是对存储碎片的限制。当垃圾对象被释放时,释放的空间将以小的字节形式存在于各个领域,因此,当为大的目标分配空间时,任何一个邻近领域中的已释放空间都不够大。一种清除碎片的方法是压缩法(Compaction),下节讲述设计选择时将会详细介绍压缩法。

可收缩性(Scalability)也很重要。在多处理器系统中多线程应用上,分配空间不应该成为可收缩性的瓶颈,并且收集也不应该成为一个瓶颈。


设计选择(Design Choices)

当设计或者选择垃圾收集器的算法时,需要做许多选择。


串行VS.并行(Serial versus Parallel)


对于串行收集,一次只会有一件事会发生。例如,即使当多CPU体系可以应用时,只有一个会被用来执行收集任务。当使用并行收集时,垃圾收集工作被分成几部分,这些子部将会在不同的CPU上被同时执行。同时执行会使垃圾收集得更快,但是代价是会增加复杂性和潜在碎片。


并发VS. 完全停顿(Concurrent versusStop-the-world)

当执行停顿垃圾收集(stop-the-worldgarbage collection)时,应用程序会被完全挂起。相反,一个或者多个垃圾收集任务也可以并发的与应用程序同时执行。通常,一个并发收集器可以并发的执行垃圾收集的大部分工作,但是也会不可避免的引发一个小的停顿。停顿垃圾收集要比并行的垃圾收集器简单,因为收集期间的堆是冻结的,其中的对象不会发生变化;它的缺点是应用程序一定会发生停顿。相应的,如果并发的执行垃圾收集则暂停时间会更短,但是内存收集器必须更加小心,因为在其工作期间一个对象可能已经被应用程序修改过了;这种额外的负担影响了并发收集器的性能并且要求一个大尺寸的堆。


压缩VS.非压缩VS.拷贝

在垃圾收集器判断完存储器中有用的数据和垃圾数据后,垃圾收集器会压缩存储器,将有用的数据移到一起,而剩下的存储空间被全部回收。压缩结束后,在释放的地址上分配一个新的目标是非常简单快速的。可以用一个简单的指针指向下一个未分配的空间。与压缩收集器相比,非压缩收集器用占位(in-place)垃圾目标来释放空间,与压缩收集器不同,非压缩收集器不通过移动所有有用的数据来创造一个大的再回收空间。非压缩收集器的优点是可以快速进行垃圾收集,缺点是存在潜在碎片。通常,用在适当位置(in-place)再分配一个堆比用压缩法更为昂贵。在存储器邻近区域寻找一个有足够空间来分配新目标的堆是很有意义的。第三个选择是拷贝收集器,它拷贝有用的数据到不同的存储地址。这样它的资源空间就可以被认为是可分配的,并且可以快速容易的分配,但是拷贝收集器的缺点是需要拷贝数据的额外的时间和空间。


性能指标

垃圾收集器的性能指标如下:

1.        吞吐量(Throughput):没有花费在垃圾回收的时间占总执行时间的百分比。

2.        逆吞吐量(Garbage collection overhead):垃圾收集的时间所占总执行时间的百分比。

3.        暂停时间(Pause time):当垃圾收集器执行时,应用程序停止运行的时间。

4.        收集频率(Frequency of collection):收集器工作的频度,与应用程序运行情况有关。

5.        印迹(Footprint):一种度量方式,就像堆的大小。

6.        及时性(Promptness):当一个目标变成垃圾后,多久这个存储空间会变成有用的。

一个交互式请求可能会要求低的暂停次数,然而总共的执行时间对于非交互式请求来说更重要;一个实时的应用可能要求不能出现长时间的停顿又要平衡花费在垃圾收集的各个阶段的时间;在小型个人微机或者嵌入系统中可能更加在意小的印迹。


世代收集器(Generational Collection)

使用世代收集器技术,存储器被分成几个代,就是用独立的池容纳不同年龄的目标。例如,最广泛应用的方案有两个代:一个为新对象,一个为就对象。

不同的代可以用不同的算法来执行垃圾收集,每个算法根据不同代的不同测试结果进行优化。世代收集器会开发如下的测试,被称作弱世代假说(Weak generational hypothesis),相关的应用采用几种程序语言编写,包括Java程序语言:

1.        大多数分配目标长时间不被引用,也就是,它们夭折了。

2.        从老一代到新生代目标的引用很少存在。

新生代收集器的发生频率与效率都很高,因为新生代空间通常较小并且可能容纳许多不再被引用的目标。

新生代收集器中存活的对象最终会进入旧生代,见图片1。旧生代比新生代明显大,并且内存的消耗速度更慢。结果,旧生代收集器很少执行,并且需要更长的时间来完成。


       因为新生代发生垃圾收集的频度分厂高,所以在考虑为它选择收集算法的时候需要特别考虑收集速度;另外,由于旧生代占用的堆空间是最大的并且必须在低垃圾密度下很好的工作,所以它的算法更侧重与空间的使用效率。


4 在J2SE 5.0 HotSpot虚拟机中的垃圾收集器(Garbage Collectors in the J2SE 5.0 HotSpot JVM)

The Java HotSpot virtual

J2SE 5.0 update6 中JVM包含四种垃圾收集器,都是基于代的,本节描述了代与其他类型的组合。并针对每个收集器为什么可以快速和有效的分配对象而给出了详细的资料。


HotSpot Generations

在JVM中内存被分为三代:新生代(YoungGeneration)、旧生代(OldGeneration)和持久代(PermanentGeneration)。其中新生代一般用于放置新创建的对象,而旧生代则将一些经过几次垃圾回收仍然没有被回收的对象转移存放进来。持久代用于放置一些为JVM本身的方便性而使用的对象,比如类定义、方法定义、常量、代码段等。

新生代是由一个Eden区和两个survivor区组成。如图2。大多数的对象是在Eden区生成的,(正如上文所说,少数对象可能会直接分配到旧生代里)。当一个survivor区满时,有些对象经过至少一个回收周期仍然未被回收将要进入旧生代之前,将被存放到另一个标记为空的survivor区内,等待下一次的垃圾回收。

 

Garbage Collection Types(垃圾收集的类型)

当新生代满时,自身范围内会做一个代内收集(有时称为轻微收集)。当旧生代或者持久代满时,将会做一次完整的收集(有时称为大规模收集),也就是说将会收集所有的代。首先收集新生代,有专门针对新生代的算法,它在新生代里可以高效的回收垃圾。然后是执行可以收集旧生代和持久代的算法。如果需要压缩,那么将会分开压缩每一个代。

当旧生代满时,如果新生代先收集,有些对象可能将要从新生代转入到旧生代,在这种情况下,在CMS收集器中新生代里的算法是不会执行的,而是旧生代的算法针对整个堆(CMS旧生代算法比较特殊,它无法收集新生代)。


Fast Allocation(快速分配)

你将会在下面看到垃圾收集器的说明。在许多情况下,内存会有大块的空间用来分配对象,使用了bump-the-pointer技术使得这些变得更有效率,也就是说,要保存之前分配的对象的末地址,在余下代中检查是否有符合新分配的要求的对象,如果有将更新指针和初始化对象。

为多线程应用分配业务时,必须注意多线程安全。如果使用全局锁将会成为瓶颈和降低性能,所以JVM使用了一种Thread-Local Allocation Buffers(TLABs)技术,为每个线程提供自己的缓冲区(即代的一小部分)用来内存分配,从而提高多线程分配的吞吐量。因为,只有一个线程分配于每个TLAB,分配可以利用高效的bump-the-pointer技术,而不需要任何锁定。极少情况下,当一个线程完全被TLAB占用并且必须得到一个新的,可以同步执行,一些操作TLABs技巧可以减少空间的浪费。比如,TLABs是平均分配尺寸耗费小于1%的Eden,在每个分配中结合使用TLABs与线性的使用bump-the-pointer技术的方式是高效的,只需要10个本地指令。


Serial Collector(串行收集器)

在串行收集器中只会使用一颗CPU进行垃圾回收,所以新生代和旧生代的回收一定是串行发生的,并且在垃圾回收期间应用程序是被挂起的。


使用串行收集器的新生代

Figure 3展示了一次使用串行收集器回收新生代内存的过程。存活在Eden中的对象被复制到了初始化为空的survivor space(在图中被标记为To)中,除了那些由于太大而直接复制到了旧生代的对象;存活在非空的survivorspace(在图中被标记为From)中的对象有两个出路,如果相对来说仍然是新对象那么也被复制到了初始化为空的survivor space中,否则相对来说就是旧对象了,那么被复制到旧生代中。注意:如果To区域被填充满了,那么来自Eden和From区域的没有进入To区域的对象直接进入旧生代,无论这个数量有多大。经过这次复制之后那些原本存活在Eden和From区域的对象就可以认为是死亡(dead)的了,它的空间可以被用来重分配(虽然回收器并没有检查或标记这些对象,但是他们确实已经是垃圾对象了,在图中被X标记)。


在一次新生代垃圾回收之后,原来的Eden和From区域都被清空了,只有To区域有对象,此时From与To区域完成了角色转换,见Figure 4。



使用串行收集器的旧生代

在串行收集器中旧生代和持久代使用的是标记-清除-压缩(mark-sweep-compact)算法。在标记阶段,收集器检查哪些对象是存活的;在清除阶段,可以识别出哪些是垃圾对象;然后进入压缩阶段,收集器将存活对象向旧生代区域开始的方向滑动(旧生代也是如此),那么另一端就会出现一个连续的大块的自由空间,见Figure 5。压缩后允许在旧生代和持久代中使用快速的bump-the-point技术分配内存。


什么时候使用串行收集器

串行收集器为那些以客户端(client-style)方式运行的和不要求低暂停时间(low pause times)的应用提供了选择。根据当今的硬件条件,串行收集器可以高效率的管理那些使用了64M堆空间的应用并且可以0.5s内完成完全收集,降低有害暂停(worst-case pauses)。


选择串行收集器

在release版本的J2SE 5.0中,串行收集器在非服务器模式(server-class)的机器上是默认的垃圾回收策略;当然也可以使用-XX:UseSerialGC的命令行参数指明使用串行收集器。


Parallel Collector(并行收集器)

现在很多Java应用运行在大内存和多CPU的机器上。并行收集器又称为吞吐量收集器,是为了高效使用可用CPU的策略,避免出现其他CPU空闲只有一颗CPU进行垃圾回收时的情景。


使用并行收集器的新生代

串行收集器对新生代的算法是串行收集器的并行版本。也具有停顿(stop-the-world)和复制(copying)特征,但是由于使用了多CPU并行的收集新生代,所以降低了垃圾回收的耗时提高了应用的吞吐量。Figure 6反映了这种差别。


使用并行收集器的旧生代

此时的旧生代使用了与串行收集器相同的算法,即标记-清理-压缩(mark-sweep-compact)算法。


什么时候使用并行收集器

因为仍然会发生低频率的长耗时的旧生代垃圾回收动作,所以那些运行在多CPU机器上的并且对暂停时间(pausetime)不敏感的应用可以获得益处。例如那些进行批量处理的应用,如账单计算、工资计算、科学计算等等。

你可能在考虑选择并行压缩的收集器(下面会讲到的内容)而不仅仅是并行收集器,因为前者可以在任何的代中使用并行收集而不仅是新生代。


选择并行收集器

在release版本的J2SE 5.0中,并行收集器在服务器模式(server-class)的机器上是默认的垃圾回收策略;当然也可以使用-XX:UseParallelGC的命令行参数指明使用并行收集器。


Parallel Compacting Collector(并行压缩收集器)

并行压缩收集器是在J2SE 5.0 update 6版本中被引入的,与并行收集器相比在对旧生代的收集上使用了新的算法。注意:最终,并行压缩收集器会取代并行收集器。


使用并行压缩收集器的新生代

与使用并行收集器的新生代相同。


使用并行压缩收集器的旧生代

在并行压缩收集器中,通常利用带有滑动压缩功能的并行方式(parallel fashion with sliding compaction)在同一个停顿(stop-the-world)中回收旧生代和持久代,共分三个阶段。首先,在逻辑上将每个代分割成固定尺寸的区域。在标记阶段(marking phase),可以通过应用代码直接到达的存活的对象集合(the initial set of live objects)被划分在垃圾收集器所有的线程(garbage collection threads)中,然后所有的存活对象被并行的做了标记;当一个对象被确定为存活的时候,该对象的尺寸和位置会被用来更新它所在的区域的数据。

在总结摘要阶段(summary phase)对区域的操作,经过了对前面的对象集合的压缩以后的典型结果是每个代的左边包含了很多的存活对象,所以密度变大了;对这些密集的区域进行压缩来释放空间代价较高不划算;所以在摘要阶段的第一件事情是检查每个区域的密度,从区域的最左边开始直到某个点(point),这个点是区域可分配的(couldbe recovered from a region)并且它的右边区域是值得压缩的;这个点的所有的左边区域被使用密集前缀(dense prefix)标记起来,其中的对象不会发生移动;这个点的右边所有区域会被压缩释放空间;摘要阶段会计算和保存每个压缩区域中每个存货对象的第一个字节所在的新位置。注意:目前摘要阶段是以串行的方式实现的,虽然也可以采取并行的方式,但并不像在并行标记和并行压缩阶段(parallelization of the marking and compaction phases)那么重要。

在压缩阶段,收集器的线程们使用摘要信息判断某个区域是否应该被填充(压缩),并且可以独立的向某个区域复制数据;经过了这个阶段,堆的一端密度非常大,另一端是一个大块的空白区域。


增量模式(IncrementalMode)

并发标记-清除收集器可以运行在增量的模式下运行,意思是说可以增量的完成并发阶段(concurrent phases)的工作;这意味着可以降低长并发阶段后处理应用的消耗。这项工作被分割成小的时间片分散在新生代的垃圾回收中。这个功能对于那些CPU数量较少(例如1颗或2颗)并且需要在并发收集器中获得低暂停时间的应用。关于这个模式的更多信息可以见《Tuning Garbage Collection with the 5.0 Java™ Virtual Machine》的第九章节。


什么时候使用并行压缩收集器

和并行收集器一样,多CPU的应用可以从中受益。另外,对旧生代的并行压缩可以减少暂停时间并且使并行压缩收集器比并行收集器更合理,而并行收集器的暂停时间对于应用来说是个瓶颈。可能并行压缩收集器不适合运行在大的共享的机器上(例如SunRays),因为其上的每个应用都不应该独占多个CPU以便获得跟多的处理时间;在这样的机器上我们可以减少垃圾回收的线程数(通过配置-XX:ParalleGCThreads=n的命令行参数)或者选择其他的收集器方案。


选择并行压缩收集器

使用-XX:UseParallelOldGC参数可以启用并行压缩收集器。


Concurrent Mark-Sweep (CMS)Collector(并发标记-清除收集器)

对于很多应用来说,快速的响应速度比端到端的吞吐量(end-to-end throughput)更重要。通常新生代的收集时间的不会引起长的暂停时间,但是旧生代的收集(虽然发生的频率不高)可能引起长暂停,尤其是对大尺寸堆栈的收集。为了解决这个问题,HotSpot JVM引入了一种叫做并发标记-清除收集器(Concurrentmark-sweep简拼CMS),也叫做低反应时间收集器(low-latency collector)。


使用并发标记-清除收集器的新生代

与并行收集器相同。


使用并发标记-清除收集器的旧生代

并发标记-清除收集器对于旧生代的收集的大部分时间是与应用程序并发执行的。

并发标记-清除收集器的生命周期是以一个叫做初始化标记(initial mark)的短暂停开始的;这个过程标示出一组可以通过程序代码直接到达的存活的对象。然后在并发标记(concurrent mark phase)阶段,收集器将这些对象中的间接可到达(transitively reachable)的存活对象标记出来;但是这个阶段应用程序也在运行并更新着引用对象,所以在这个阶段结束时并不是所有的存货对象都被发现并标记了;为了解决这个问题,应用程序进入了第二次暂停,叫做重新标记(remark),期间通过重新访问所有在并发标记阶段修改过的对象完成标记工作,多线程的并行运行提高了效率。

在重新标记阶段的后期,堆中所有的存货对象都被检查到了并被做了标记,所以随后的并发清除阶段(subsequent concurrent sweep phase)回收了所有的垃圾空间。Figure 7显示了使用串行标记-清除-压缩(serial mark-sweep-compact)收集器和并发标记-清除收集器的旧生代的差别。


由于一些不得不做的任务,比如在重新标记阶段的重新访问对象(remark objects),增加了收集器的消耗,所以这是一种典型的为了缩短暂停时间做的牺牲。

并发标记-清除收集器是唯一一种无压缩的收集器;也就是说在回收了垃圾对象的空间后并没有将旧生代的存货对象移动到某一端,见Figure 8。


这样做虽然节省了时间,但是由于空闲空间(free space)不是连续的,所以不能使用一个简单的指针就标示出为下一个需要分配内存的对象的空闲位置(free location)。结果,它现在需要使用空闲列表;也就是说它创建了一些将所有未非配的内存区域关联起来的链表;当为一个对象分配空间时它必须根据对象的大小搜索这些列表找到一个可以容纳这个变量的空间;所以此时在旧生代分配空间的代价要高于使用简单的bump-the-pointer方案,并且当新生代向旧生代移动对象时增加了新生代的代价。

并发标记-清除收集器另一个弊端是需要比其他方案使用更大的堆。鉴于应用可以在标记阶段并发运行,它可能继续占用申请新的空间,所以可能需要占用更多的旧生代空间;另外虽然在标记阶段(mark phase)收集器检查到了所有的存活对象,但是由于应用程序的运行这些对象可能在该阶段结束时变成了垃圾对象,并且只有在旧生代下一次垃圾回收时才能被释放掉,所以这些对象称为漂浮垃圾(floating garbage)。

最后,由于缺少空间压缩所以可能出现碎片(fragmentation)。为了解决碎片,并发标记-清除收集器跟踪主流对象的尺寸预测将来的需要,并且可能分割或合并空闲块来满足需求。

与其它的收集器不同,并发标记-清除收集器并不是在旧生代满了以后才进行垃圾收集,而是试图更早的开始垃圾收集;这样旧生代就不会满了。因此并行和串行收集器是在标记-清理-压缩(mark-sweep-compact)算法上,而并发标记-清除收集器最耗时的阶段是停顿(stop-the-world)。为了避免这个,并发标记-清除收集器会基于这样的策略开始一次收集,包括有代表性(regarding)的前面的收集次数和旧生代被填充的速度;当旧生代的分配率超过初始占有率(initiatingoccupancy)时也会发生一次收集,可以通过-XX:CMSInitiatingOccupancyFraction=n的参数分配初始占有率(initiating occupancy)的值,其中n是旧生代的百分比,默认值为68。

总结,与并行收集器相比较并发标记-清除收集器出色的降低了旧生代的暂停时间,代价是稍微增加了新生代的暂停时间、降低了一些吞吐率并且需要更大的堆。


增量模式(IncrementalMode)

并发标记-清除收集器可以运行在增量的模式下运行,意思是说可以增量的完成并发阶段(concurrent phases)的工作;这意味着可以降低长并发阶段后处理应用的消耗。这项工作被分割成小的时间片分散在新生代的垃圾回收中。这个功能对于那些CPU数量较少(例如1颗或2颗)并且需要在并发收集器中获得低暂停时间的应用。关于这个模式的更多信息可以见《Tuning Garbage Collection with the 5.0 Java™ Virtual Machine》的第九章节。

 

什么时候使用并发标记-清除收集器

当你的应用需要更短的垃圾收集的暂停时间时并且可以在应用运行时拿出一部分CPU资源用来垃圾回收时可以使用并发标记-清除收集器。(由于是并发的所以并发标记-清除收集器与应用一起竞争CPU资源。)典型场景为,具有很多长生命的大数据(也就是说由一个大的旧生代),运行在多个CPU的机器上,使用这种收集器可以获得益处的应用。网络服务(web servers)就是一个例子。并发标记-清除收集器可以使绝大多数应用满足低暂停时间的要求。它可以在旧生代具有中等尺寸的、运行在单CPU的应用上具有不错的表现。


选择并发标记-清除收集器

可以通过命令行参数-XX:UseConcMarkSweepGC启用并发标记-清除收集器。


5 工效学-自动选择与行为调节(Ergonomics -- Automatic Selections and BehaviorTuning)

在J2se 5.0 release版本中,内存收集器和堆大小和HotSpot VM(client or server)都可以根据应用运行的操作系统与平台打开默认的选项。这项功能可以更好的适应各种应用的不同需求,同时与之前的版本相比可以减少命令行参数的设置。

另外,为提供了动态调节并行内存收集器行为的方法。根据这种方法用户仅需要配置期望的表现,内存收集器会动态的调节不同区域的大小来满足要求。不同的操作系统的默认值和内存收集器的选项组合就像是工效学一样,而它的目的就是通过更少的命令行参数达到好的表现。


内存收集器和堆大小和虚拟机的默认行为(Automatic Selection of Collector, Heap Sizes, andVirtual Machine)

作为服务器级(server-class)机器需要满足下面的两个条件:

l  具有两个或两个以上的CPU

l  具有2G或2G以上的内存

除了32-bit的某个window操作系统外适合于所有平台的服务器级(server-class)机器的定义。

对于非服务器级(sever-class)机器的一个默认值如下:

l  The client JVM

l  串行收集器

l  堆的初始化大小为4MB

l  最大堆为64MB

对于服务器级(server-class)的机器除非手动添加-client参数否则使用server的JVM策略,并且使用并行收集器为默认的内存收集策略,否则默认为串行收集器。

在一个服务器级(server-class)的机器上如果使用了并行收集器则无论使用了哪种JVM策略(client or server)默认的堆的初始化和最大尺寸如下:

l  初始化堆的尺寸为物理内存的1/64,上限是1GB。(注意:初始化堆的最小尺寸为32MB,因为服务器级(server-class)机器的最小内存为2GB,它的1/64就是32MB。)

l  最大的堆尺寸为物理内存的1/4,上限为1GB。

否则使用非服务器级(non-server-class)机器的默认值(堆的初始化为4MB最大值64MB)。可以通过命令行参数覆盖默认值,相关的选项见section 8。


调节并行收集器的行为(Behavior-based Parallel Collector Tuning)

在J2SE 5.0 release版本中,增加了一种通过设定应用的内存收集器表现的方式调整并行的内存收集器。命令含参数体现了最大暂停时间和应用吞吐率的目标。


最大的暂停时间目标

最大的暂停时间目标是没有默认值的,可以通过下面的参数设定最大的暂停时间目标:-XX:MaxGCPauseMillis=n,这代表了期望并行收集器的暂停时间控制在n或n毫秒以下。这些调整可能会降低应用的全局吞吐量,并且在一些情况下是无法满足期望的暂停时间的。

最大的暂停时间目标可以分别应用于每个代。通常,如果没有达到这个目标可以尝试减少代尺寸以便达标;


吞吐量目标

吞吐量目标是用来衡量花费在内存收集和非内存收集上(应用的)的时间的。可以通过下面的的命令行参数来设定:-XX:GCTimeRatio=n,那么内存收集与应用耗时的比例是1/(1+n),例如设定-XX:GCTimeRatio=19时内存收集目标为占总耗时的5%。默认情况n=99即1%的时间用于垃圾回收。花费在内存收集上的时间就是每个代的总时间。如果没有达到吞吐量的目标,自动尝试增大代的尺寸,因为需要花费更多的时间才能填充大尺寸的代,所以应用程序可以运行更长的时间。


内存量目标

如果吞吐量和最大暂停时间都达标了,那么内存收集器会尝试使用最少的内存。


目标的优先级

并行收集器(parallel garbage collectors)以满足最大暂停时间为第一目标;然后会努力满足吞吐量的目标;内存量的目标在这两个之后。


6 建议(Recommendations)

前面介绍的工效学(自动配置)功能使得内存收集器、虚拟机和堆大小的选择对于大多数的应用程序来说都是合理的。因此,第一条对于内存收集策略的选择和配置的第一条建议就是什么都不要做,(也就是说不要明确指定使用哪个内存收集器,等等。)让系统根据应用运行的操作系统和平台自动选择;如果此时的表现是可以接受的,具有充足的吞吐量和足够低的暂停时间,那么你的工作就结束了,不需要你去调整内存收集器的选项。

另外,如果应用在内存收集环节表现不佳的话,那么对你来说最简单的事情是根据应用和平台的情况思考一下默认的内存收集器是否是合适的,如果不合适,可以手动选择一个,看看应用的表现是否已经可以达标。

你可以使用section 7中提到的工具来衡量和分析内存的表现。基于前面的结果你可以考虑修改选项了,例如那些控制对尺寸和内存回收器表现的参数。一些常见的选项可以在section 8中见到。请注意:先衡量后调整参数是最好的方法,而这种衡量通常需要使用与代码相关的测试。并且,不要过度优化,因为应用的数据集、硬件等等,甚至内存收集器的实现都可能随着时间而发生变化。

本章节提供了一些关于选择内存收集器和配置堆尺寸的一些信息。然后提供关于配置并行收集器的建议和当我们遇到OutOfMemoryError异常时应该怎样做的忠告。


什么时候选择另一种内存收集器

Section 4告诉我们在什么情况下适合使用哪种收集器;Section 5描述了在什么平台上什么作为默认的收集器。如果你的应用或环境与默认的情况差别很大,可以使用命令行参数指定使用哪种收集器:

l  -XX:+UseSerialGC

l  -XX:+UseParallelGC

l  -XX:+UseParallelOldGC

l  -XX:+UseConcMarkSweepGC


堆的尺寸

Section 5叙述了默认的初始化和最大的堆尺寸,这些尺寸对于大多数的应用,但是如果你遇到了与某个代或者这个堆尺寸有关的分析性能问题或者OutOfMemoryError异常,那么你可以使用Section 8中的命令行参数手动的修改。例如在非服务器级的(non-server-class)的机器上的默认最大堆的尺寸为64M,通常可能太小了,那么你可以通过-Xmx选项指定一个更大的值。一般来说可以为堆分配尽量大的尺寸,除非很长的暂停时间为你带来了麻烦。吞吐量是与内存大小成比例的,充足的内存量是影响内存收集器性能的最重要的因素。

当你知道了能够整个堆分配的尺寸,就可以考虑如何分配给每个代了。第二重要的影响内存收集器的性能的因素是新生代的比例。为新生代分配大的内存量,除非你的旧生代不够用了或者暂停时间有问题。尽管如此,当使用串行收集器时不要让新生代的尺寸超过堆尺寸的一半。

当使用一种并行收集器时最好的方法并不是设置堆尺寸,而是设置期望的表现值,然后让收集器自动的动态的去修改堆尺寸,以便达到期望的表现。


并行收集器的配置策略(Tuning Strategy for the ParallelCollector)

如果使用了并行收集器或并行压缩收集器,那么可以为它分配吞吐量的目标,当然这个目标对于应用来说应该是够用的。除非你知道默认的堆尺寸不够用,否则不要设置最大尺寸。堆会自动的膨胀与收缩到满足吞吐量目标的尺寸。在初始化和应用程序行为放生变化的时候,需要这种膨胀与收缩。

如果堆长到它的最大尺寸,大多数情况下说明在这个尺寸下是不能满足设定的吞吐量目标的。尝试将堆最大尺寸设置为与接近平台物理内存的一个值,但是不要引起应用的页面交换。重新运行应用程序,如果还是达不到吞吐量的目标,说明在这个目标对于当前平台与内存量来说太高了。

如果达到了最大吞吐量但是暂停时间过长,那么可以尝试设置最大暂停时间为目标。但是这样又有可能因为无法达到吞吐量的目标,所以最大暂停时间应该设置为应用可以接受的一个折中值。

即使应用已经达到了良好的状态,为了满足竞争性的目标堆尺寸还是会变化。当满足了最大吞吐量目标的时候,收集器的状态可能对最大暂停时间和最小内存使用量目标是不利的。


怎样应对OutOfMemoryError异常(What to Do about OutOfMemoryError)

许多开发者都遇到过的一个情景是应用遇到OutOfMemoryError异常后终止了。这个异常发生在没有足够的空间分配给对象时,也就是说此时内存收集器再也找不出可用的空间容纳新的对象了,并且对空间也不能再扩张了。OutOfMemoryError并不意味着一定发生了内存泄漏,很有可能仅是一个简单的配置问题,例如当前配置的堆尺寸(或默认尺寸)对于应用来说就是不够用的。

诊断OutOfMemoryError的第一步是检查完整的出错信息(the full errormessage)。在这个也常信息中,“java.lang.OutOfMemoryError”是进一步的信息。下面是这些附加信息的常见的例子,以及这些信息的含义和我们应该采取的措施:

l  Java heap space
这意味着不能在堆中为某个对象分配空间了。这可能是个配置问题,可能是-Xmx参数值或默认值太小了;也可能意味着发生了内存泄漏,我们不再使用的对象还被应用无意间引用着,导致内存收集器无法回收给对象。HAT(见Section 7)可以查看所有可到达对象的引用关系;另一个可能的原因是应用程序过多使用析构函数引起的,例如析构函数调用的线程跟不上析构函数在队列中增长的速度。Jconsole可以查看等待被析构的对象的数量。

l  PerGen space
这意味着持久代的空间已经满了。通过前面的描述我们知道这里是JVM存储内部数据的空间。如果应用加载了大量的类,那么应该增加持久代的尺寸。参数为-XX:MaxPermSize=n,n就是空间尺寸。

l  Requested array size exceeds VM limit
这意味着应用尝试为一个比堆尺寸更大数组分配空间。例如:应用打算为一个数组分配512MB但是堆最大只有256MB,那么就会发生这个异常。大多数情况下这可能是要么堆尺寸太小要么是应用计算数组空间时出现了巨大偏差。

一些在Section 7中描述的工具可以诊断OutOfMemoryError问题。用处最大的应该是Heap Analysis Tool(HAT)、jconsole和使用-histo参数的jmap。


7 评估内存收集性能的工具(Tools to Evaluate Garbage Collection Performance)

很多的诊断与监控的工具可以用来评估内存收集的性能。本章节提供了这些工具的概述信息,更多的信息可以见Section 9的“Tools and Troubleshooting”部分。


-XX:+PrintGCDetails命令行参数

获得关于内存收集器的第一手资料的最简单方法就是在命令行上使用-XX:PrintGCDetails参数。每次内存回收都会输出每个代在回收前后的活动对象占用的空间大小、可用总空间大小和花费的时间。


-XX:+PrintGCTimeStamps命令行参数

如果使用了-XX:_PrintGCDetails,则在每次内存收集前打印一个时间戳信息。这个时间戳信息可以帮助你将内存回收的日志与其他日志事件区分开。


jmap

jmap是发布在Solaris与linux操作系统的JDK中的命令行工具。它可以打印一个正在运行的JVM或核心文件(corefile)的统计信息。如果没有添加任何参数,那么jmap将已加载的共享对象以列表的形式打印出来,这有点像Solaris的pmap工具的输出。为了获得特定的信息可以使用-heap、-histo或-permstat选项。

-heap选项包含了下面的信息:内存收集器的名字、特定运算法则的细节(例如用于并行内存收集的形成编号)、堆的配置信息和堆使用情况的描述信息。

-histo选项可以显示堆的一个class-wise柱状图。每个种类都可以显示在堆中的实例数量、这些实例以byte为单位的内存消耗量、完整的类名。这个柱状图让你了解堆的使用情况很有帮助。

对于动态产生和加载大量类的应用(例如JSP和web容器),设置持久代的尺寸是很重要的。如果应用程序加载了太多的类就会抛出OutOfMemoryError异常。-permstat选项可以显示持久代的统计信息。


jstat

jstat工具使用了HotSpot JVM的内建文书(built-in instrumentation)提供了正在运行的应用程序的性能和资源占用情况的信息。当诊断性能或堆尺寸和垃圾收集时可以使用该工具。它的一些选项可以显示垃圾收集行为和每个代的容量与使用量的柱状图。


HPROF:Heap Profiler

HPROF是一个内嵌在JDB 5.0中的简单的剖析代理。他是一个动态链接库文件(dll文件)提供了使用JVM TI(Java Virtual Machine ToolsInterface)访问JVM的接口。它可以将剖析信息写到一个文件或以ASCII或二进制的方式发送个socket,这些信息可以被其他的前端剖析工具使用。

HPROF可以显示CPU的使用情况、堆的分配情况并监视竞争情况(contentionprofiles)。另外它可以输出完整的heapdumps和报告在JVM中的所有监视器和线程的状态。HPROF对性能分析、锁的竞争、内存泄露等都很有帮助。在section 9中可以找到管理HPROF文档的链接。


HAT:Heap Analysis Tool

HAT可以帮助调试无意间引用的对象(unintentional objectretention),也就是永远不再使用的对象却别某个存货对象通过某个路径引用着。这个工具可以方便的在HPROF产生的堆快照中查看对象的拓扑图(topology),并且允许大量查询,包括“显示从根集合可以到达这个对象的所有路径”。在section 9有关于HAT文档的链接。


8 与内存回收有关的主要选项(Key Options Related to Garbage Collection)

很多命令行参数可以用来选择内存收集器、设置堆或代的尺寸、修改内存收集的行为,并且获得内存收集的统计信息。本章节介绍一些最常用的选项。在Section 9中可以看到完整列表和详细信息。注意:你填写的数组可以以m或M、k或K、g或G结束。

内存回收器的选项(Garbage Collector Selection)

选项

内存回收器类型

–XX:+UseSerialGC

串行(Serial)

–XX:+UseParallelGC

并行(Parallel)

–XX:+UseParallelOldGC

并行压缩(Parallel compacting)

–XX:+UseConcMarkSweepGC

并发标记-清除(Concurrent mark–sweep (CMS))


内存收集器的统计信息(Garbage Collector Statistics)

选项

描述

-XX:+PrintGC

每次内存回收时输出基本信息

-XX:+PrintGCDetails

每次内存回收时输出更多的详细信息

-XX:+PrintGCTimeStamps

当使用了-XX:+PrintGC或-XX:+PrintGCDetails时每次内存回收前输出时间戳

 

堆和代尺寸(Heap and Generation Sizes)

选项

默认值

描述

–Xmsn

见section 5

堆的初始化尺寸,以字节为单位

–Xmxn

见section 5

堆的初始化尺寸,以字节为单位

–XX:MinHeapFreeRatio=minimum

and

–XX:MaxHeapFreeRatio=maximum

40(最小)

70(最大)

堆总空间的空闲比例区间,被应用在每个代上。例如,如果最小值为30并且某个代的空闲空间不足30%,那么这个代会扩张以便保留30%的空闲空间;如果最大值为60并且空闲空间超过了60%,那么代会收缩以便仅保留60%的空闲空间;

-XX:NewSize=n

平台相关

新生代默认的初始化空间大小,以字节为单位

-XX:NewRatio=n

2在client JVM

8在server JVM

新生代与旧生代的比率。例如,如果n是3,则比率为1:3并且那么Eden和两个survivor区域占了新生代和旧生代总空间的1/4

-XX:SurvivorRatio=n

32

每个survivor空间与Eden的比率。例如,如果n是7,那么每个survivor空间占新生代的1/9(因为共有两个新生代)

-XX:MaxPermSize=n

平台相关

持久代的最大空间


并行与并行压缩收集器的选项(Options for the Parallel andParallel Compacting Collectors)

选项

默认值

描述

-XX:ParallelGCThreads=n

CPU的数量

并行收集器使用的线程数量

-XX:MaxGCPauseMillis=n

无默认值

最长的目标暂停时间,实际的暂停时间应该不大于这个数值,以毫秒为单位

-XX:GCTimeRatio=n

99

吞吐量目标,在默认值的情况下吞吐量为99%,内存回收耗时比率1%


并发标记-清除收集器(Options for the CMS Collector)

选项

默认值

描述

-XX:+CMSIncrementalMode

Disabled

是否激活增量模式。在增量模式下并发阶段是以增量的方式运行的,周期性的停止并发阶段释放处理器去执行应用程序

-XX:+CMSIncrementalPacing

Disabled

是否激活这种功能。根据应用行为在释放CPU之后自动分配CMS收集器的工作量

-XX:ParallelGCThreads=n

CPU的数量

平行回收新生代的内存时的线程数量以及并行处理旧生代时线程数量


来自

http://java./performance/reference/whitepapers/6_performance.html

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多