Java HotSpot VM内存管理白皮书

本文由博主自行翻译,未经博主授权,不得随意转发。后续博主会陆续发布一系列关于JVM内存管理的文章,敬请期待。

1、介绍

Java2平台标准版本(J2SE)的一个优点就是它的自动内存管理功能,使得开发人员与显式的内存管理的复杂性分隔开。

本文对Sun J2SE5.0版本中Java HotSpot虚拟机(JVM)内存管理提供了一份概述。它描述了用于执行内存管理的垃圾回收器,并给出了一些关于选择和配置垃圾回收器以及为垃圾回收器操作的内存区域设置大小的建议。它还是一份参考,列出了一些最常用的影响垃圾回收器行为的选项,并且提供了大量更详细文档的链接。

第二节针对那些对自动内存管理还是新手的读者。它对于如此管理内存较之需要程序员显式为数据分配空间的好处有一个简短的讨论。

第三节介绍了一般垃圾回收器的概念、设计选择以及性能指标。它还介绍了一种常用的基于对象期望存活时间将内存分为不同区域的组织形式,称为“代”。这种分离为“代”的方式已被证明在减小广泛范围内应用的垃圾回收暂定时间以及总体成本方面是有效的。

文章的剩余部分提供了针对HotSpot JVM的信息。第四节描述了四个有效的垃圾回收器,包括一个在J2SE5.0 update 6中新引入的,记录了它们使用的“代”内存组织。对于每个垃圾回收器,第四节概述了使用的回收算法类型,并具体说明什么时候适合选择这个回收器。

第五节描述了一项在J2SE5.0新引入的技术,结合(1)基于应用运行的平台和操作系统,自动选择垃圾回收器、堆大小以及HotSpot JVM(客户端或者服务器),以及(2)基于用户指定的期望行为动态垃圾回收调优。这项技术成为人类工程学。

第六节提供了选择和配置垃圾回收器的建议。它还提供了一些建议如如何应对OutOfMemoryError异常。第七节简略的描述了一些工具,可以用于评估垃圾回收性能,第八节列出了大多数常用的与垃圾回收器选择和行为相关的命令行选项。最后,第九节提供了本文章包括的各种主题更详细文档的链接。

2、显式VS自动内存管理

内存管理是识别什么时候已分配对象不再需要,并释放这些对象使用的内存,使其可用于后续分配的过程。在一些程序语言中,内存管理是程序员的职责。该任务的复杂性导致许多常见的错误,这些错误可能导致意外的或者错误的程序行为和崩溃。因此,开发人员的大部分时间通常用于调试和尝试纠正此类错误。

在显示内存管理的程序中经常发生的一个问题是悬空引用(dangling references)。它可能重新分配一个对象使用的空间,而这个对象仍被一些其它对象引用。如果拥有这样引用(dangling)的对象尝试访问原始对象,但是空间已经被重新分配给了一个新的对象,结果是不可预知的而不是预期的。

显示内存管理的另一个常见问题是内存泄露。这些泄露在内存已被分配而且不再被引用但未被释放的时候发生。例如,如果你打算释放一个链表使用的空间但是你错误的只释放了列表的第一个元素,剩余的列表元素不再被引用但是它们已经脱离了程序的范围,既不再被使用也不会被恢复。如果发生足够的泄露,它们可能持续消耗内存,直到用尽所有有效内存为止。

现在常用的一种内存管理的替代方法,特别是大多数现代面向对象语言使用的,是由一个称为“垃圾回收器”的程序自动管理。自动内存管理可以增强接口和更可靠的代码抽象。

垃圾回收避免了悬空引用的问题,因为一个仍然引用的对象永远不会被垃圾回收,因此将不会被视为释放。垃圾回收还解决了上面描述的内存泄露的问题,由于它自动释放所有不再引用的内存。

3、垃圾回收概念

一个垃圾回收器负责:

  • 分配内存
  • 确保任何引用对象保持在内存中
  • 并且回收执行代码中不再引用的对象使用的内存

依然被引用的对象被认为是活的。不再被引用的对象被认为是死的,被称为垃圾。发现并释放(也称为回收)这些对象所使用的内存的过程称为垃圾回收。

垃圾回收解决了许多,但不是全部的内存分配问题。例如,你可以无限期的创建对象并持续引用它们,直到没有更多可用内存为止。垃圾回收也是一项复杂的任务,需要耗费它自身的时间和资源。

用于组织内存、分配和释放空间的精确算法由垃圾回收器处理,对程序员隐藏。空间通常是从一个称为“堆”的内存池中分配的。

什么时间执行垃圾回收取决于垃圾回收器。通常,当堆被填满或者到达一个占用的阈值百分比时,整个堆或它的一部分被回收。

执行一个分配请求的任务,即在堆中找到一定大小的未使用的内存块,是一项困难的任务。对于大多数动态内存分配算法来说,主要问题是在保持分配和释放效率的同时避免碎片(见下文)。

令人满意的垃圾回收器特征

一个垃圾回收器必须既安全又全面。也就是说,活着的数据必须永远不会被错误的释放,垃圾不应该被排除在几个回收周期之外仍未被回收。

垃圾回收器还应该高效的运行,不会引入长时间的暂停(在这期间应用程序不能运行)。然而,与大多数计算相关的系统一样,这常常存在时间上、空间上以及频率上的权衡。例如,如果一个堆比较小,回收会很快,但是堆也会更快的填满,因此需要更频繁的回收。相反,一个大的堆将耗费更长时间填满,因此回收会不那么频繁,但是它们可能需要更长的时间。

另一个可取的垃圾回收器的特征是碎片的限制。当释放垃圾对象的内存时,空闲空间可能在各个区域中以小块的形式出现,导致在任何一个连续区域中都没有足够的空间用于分配给一个大对象。消除碎片的一种方法称为“压缩”,它将在接下来的各种垃圾回收器设计选中讨论。

伸缩性也很重要。分配不应成为多处理器系统上多线程应用的可伸缩性瓶颈,回收也不应该称为瓶颈。

设计选择

在设计或选择垃圾回收算法时,必须做出许多选择:

  • 串行 VS 并行

对于串行回收,在一个时刻只发生一件事情。例如,即使有多个CPU可用,也只使用一个来执行回收。当使用并行回收时,垃圾回收的任务被拆分成几部分,这些部分在不同的CPU上同时执行。同时操作可以更快的完成回收,以一些额外的复杂性和潜在的碎片为代价。

  • 并发 VS Stop-the-world

当stop-the-world垃圾回收执行时,在回收期间,应用程序的执行完全暂停。或者,可以同时执行一个或者多个垃圾回收任务,即与应用程序同时执行。通常,一个并发垃圾回收器并发执行大部分工作,但是偶尔也可能需要做一些短暂的stop-the-world暂停。stop-the-world垃圾回收比并发回收更简单,因为回收期间,堆被冻结,对象没有改变。它的缺点是对某些应用程序来说暂停可能是不可取的。相对的,当垃圾回收并发执行时,暂停时间会更短,但回收器必须格外小心,因为它操作的对象可能同时在被应用程序更新。这增加了并发回收器的开销,会影响性能,需要更大的堆。

  • 压缩 VS 非压缩 VS 复制

垃圾回收器确定内存中哪些对象是存活的以及哪些是垃圾之后,它可以压缩内存,将所有存活的对象移到一起并且完全回收剩余内存。压缩后,很容易快速在第一个空闲的位置分配一个新对象。一个简单的指针可以用来跟踪下一个可用于对象分配的位置。相比一个压缩回收器,一个非压缩回收器就地释放垃圾对象使用的空间,换言之,它不会像压缩回收器一样移动所有存活的对象以创建一个大的再生区。它的好处是更快的完成垃圾回收,缺点是潜在的碎片。总得来说,从一个就地回收的堆中分配内存比从一个压缩堆要更昂贵。它可能需要在堆中查找一个足够大的连续的内存区域来容纳新对象。第三选择是复制回收器,它拷贝(或搬出)存活对象到一个不同的内存区域。它的好处是源区域接下来可以被认为是空的,可用于后续快速和容易的分配,但是缺点是需要额外的拷贝时间,以及需要额外的空间。

性能指标

  • 吞吐量——考虑在长时间内,没有花费在垃圾回收上的总时间的百分比。
  • 垃圾回收的开销——与吞吐量想反,即花费在垃圾回收上的总时间的百分比。
  • 暂停时间——当垃圾回收发生时,应用程序执行停止的时间长度。
  • 回收频率——回收发生的频率,相对于应用程序执行。
  • 空间量(Footprint)——一个尺寸的度量,如堆大小
  • 及时性(Promptness)——一个对象变为垃圾到内存变为可用的时间

一个交互应用可能需要较低的暂停时间,而总的执行时间对一个非交互应用则更重要。一个实时(real-time)应用要求在任何时期内垃圾回收暂停时间以及回收器花费的时间比都有很小的上界。一个小的空间量(footprint)可能是在小型的个人计算机或者嵌入式系统中运行的应用程序的主要关注点。

分代回收

当使用一项称为“分代回收”的技术时,内存被分为若干“代”,即容纳不同年龄对象的独立的池。例如,最广泛使用的配置有两代:一个用于年轻对象,一个用于年老对象。

可以使用不同的算法在不同的代执行垃圾回收,每个算法优化基于该特定代的常见的特征。就使用几种编程语言包括Java语言编写的应用程序而言,分代垃圾回收利用以下观测结果,称为弱代假设:

  • 大多数分配的对象不会被长期引用(被认为是存活的),也就是说,它们年轻就死了。
  • 存在很少的从较老对象对较年轻对象的引用。

年轻代回收发生相对频繁,效率高、速度快,因为年轻代空间通常很小并且可能包含大量不再被引用的对象。

经历多次年轻代垃圾回收幸存的对象最终提升到年老代。图1所示,年老代通常比年轻代更大,其占用率增长较慢。因此,年老代回收较少,但需要花费更长的时间才能完成。

图1.代垃圾回收

对年轻代垃圾回收算法的选择通常强调速度,由于年轻代回收频繁。另一方面,年老代通常由一个空间利用率更高的算法来管理,因为年老代占据了堆的大部分,年老代算法必须在垃圾密度比较低的情况下很好的工作。

4、J2SE5.0 HotSpot JVM中的垃圾回收器

截止J2SE 5.0 update 6,Java HotSpot虚拟机包含四个垃圾回收器。所有回收器都是分代的。本节描述了回收器的代和类型,并讨论了为什么对象分配通常快速并高效。然后提供了每个回收器的详细信息。

HotSpot的代

Java HotSpot虚拟机中的内存分为三代:年轻代、年老代和一个永久代(译者注:永久代在J2SE 8.0中已经废弃,引入了元空间)。大多数对象最初在年轻代被分配。年老代容纳了经过几次年轻代回收幸存下来的对象,以及一些大对象可能直接在年老代被分配。永久代持有JVM发现便于让垃圾回收器管理的对象,诸如描述类和方法的对象,以及类和方法本身。

年轻代由一个称为伊甸(Eden)的区域加上两个较小的幸存(Survivor)空间组成,如图2所示。大多数对象最初在伊甸区分配(如前所述,一些大的对象可能直接在年老代分配)。幸存空间持有至少一次年轻代回收之后幸存的对象,因此在被认为“足够老”以致提升到年老代之前被赋予了额外的机会死亡。在任何给定的时间,幸存空间(图中标为From)中的一个持有这些对象,而另一个是空的,保持未使用直到下一次回收。

图2.年轻代内存区域

垃圾回收类型

当年轻代被填满后,一次只包含该代的年轻代回收(有时称为Minor GC)会被执行。当年老代或者永久代被填满,通常称为完全垃圾回收(有时称为Major GC)的操作被执行。也就是说,所有代会被回收。通常,年轻代先回收,使用专门为该代设计的回收算法,因为它通常是识别年轻代垃圾的最高效的算法。然后接下来提到的年老代回收算法是在年老代和永久代上运行。如果发生压缩,则每一代分别压缩。

如果年轻代先回收,有时年老代太满以致不能接受所有应该从年轻代提升到年老代的对象。在这种情况下,对于除CMS之外的回收器,年轻代回收算法不会运行。相反,年老代回收算法在整个堆被使用(CMS年老代算法是一个特例,因为它不能回收年轻代)。

快速分配

正如你将在下面的垃圾回收器描述中看到的,在许多情况下,有大量的连续内存块可以用来分配对象。这样的块的分配是高效的,使用一项简单的bump-the-pointer技术。即总是保持对上一次分配对象的结束位置的追踪。当需要满足一个新的分配请求时,所有需要做的是检查对象是否适合代的剩余部分,如果是,更新指针并初始化对象。

对于多线程应用,分配操作需要是多线程安全的。如果使用全局锁来确保这一点,那么分配到一个代将称为瓶颈并降低性能。相反,HotSpot JVM采用一项称为线程本地分配缓冲区(Thread-Local Allocation Buffers:TLABs)的技术。通过给每个线程自己的缓冲区(即代的一小部分)来分配,从而提高了多线程分配的吞吐量。由于只有一个线程可以被分配到每个TLAB,分配可以使用bump-the-pointer技术快速完成,无需任何锁定。只有偶尔当一个线程填满它的TLAB并需要获得一个新的时,必须同步进行。由于TLAB的使用,几项减小空间浪费的技术被使用。例如,TLAB由分配器分配尺寸平均浪费少于伊甸区的1%。TLAB与利用bump-the-pointer技术的线性分配的使用组合使每次分配都是高效的,只需要10个左右的本地指令。

串行回收器

使用串行回收器,年轻代和年老代回收都是串行完成的(使用单个CPU),以stop-the-world的方式。即,应用程序执行在回收发生时被停止。

使用串行收集器的年轻代回收

图3演示了一次使用串行回收器的年轻代回收的操作。伊甸区的存活对象被拷贝到最初为空的幸存空间,图中表为To,除了那些太大而不适合于放到To中的对象。这些对象直接被拷贝到年老代。在已使用的幸存空间(标为From)中的存活对象中,仍旧相对年轻的也被拷贝到另一个幸存空间(即To),而相对较老的对象被拷贝到年老代。注意:如果To空间已满,未被拷贝它里面的来自伊甸区或From空间的存活对象是长期持有的(拷贝到年老代),不管它们经历了多少次年轻代垃圾回收而存活下来。在存活对象已被拷贝后,仍存在于Eden和From空间中的任何对象,按照定义,是非存活的,它们不需要检查(这些垃圾对象在图中标记了一个X,虽然事实上回收器不会检查或标记这些对象)。

图3.串行年轻代回收

在一次年轻代回收完成之后,伊甸区和原先被占用的幸存空间是空的,只有原先空的幸存空间包含存活对象。此时,存活空间互换角色。见图4。

图4.一次年轻代回收之后

使用串行回收器的年老代回收

使用串行回收器,年老代和永久代通过一个标记-清除-压缩(mark-sweep-compact)的回收算法回收。在标记阶段,回收器标识哪些对象依旧存活。清除阶段“扫视”整个代,识别垃圾。回收器然后执行平移压缩,将存活对象移到年老代空间的开始(永久代类似),在另一端使得任何空闲空间成为一个单独的连续块。见图5。该压缩允许任何将来到年老代或者永久代的分配可以使用快速bump-the-pointer技术。

图5.年老代压缩

什么时候使用串行回收器

串行回收器是那些运行于客户机上的、不需要较低暂停时间的大多数应用的选择回收器。在今天的硬件,串行回收器可以有效的管理大量的非平凡的应用,使用64MB堆和相对短的完全回收低于半秒的最坏情况的暂停。

串行回收器选择

J2SE5.0发行版中,在非服务器类型的机器上,串行收集器自动被选择作为默认的垃圾回收器,如第5节所述。在其它机器上,串行回收器可以通过使用命令行选项-XX:+UseSerialGC来显式请求。

并行回收器

现在,许多Java应用运行在拥有大量物理内存和多个CPU的机器上。并行回收器,也称为吞吐量回收器,被开发用来充分利用有效的CPU,而不是让它们大多数空闲而只有一个执行垃圾回收工作。

使用并行垃圾回收器的年轻代回收

并行回收器使用了串行回收器年轻代回收算法的一个并行版本。它仍然是一个stop-the-world以及拷贝回收器,但是以并行的方式执行年轻代回收,使用多个CPU,减少垃圾收集开销,从而增加应用程序吞吐量。图6演示了对于年轻代,串行回收器和并行回收器的不同。

图6.串行和并行年轻代回收对比

使用并行回收器的年老代回收

对于并行回收器,年老代垃圾回收使用与串行回收器相同的串行标记-清除-压缩的回收算法。

什么时候使用并行回收器

可以从并行回收器获益的应用程序是那些运行在多个CPU的机器上并且没有暂停时间约束的应用,因为不经常的但是可能很长的年老代回收将仍然会出现。并行回收器通常合适的应用程序示例包括批处理、计费、工资、科学计算等等。

你可能需要考虑在并行回收器之上选择并行压缩回收器(下面描述),因为前者执行所有代的并行回收,而不仅仅是年轻代。

并行回收器选择

J2SE5.0发行版中,并行回收器在自动被选择作为服务器类型(在第5节定义)机器上的默认垃圾回收器。在其它机器上,并行回收器可以被显式的请求,通过使用-XX:+UseParallelGC命令行参数。

并行压缩回收器

并行压缩回收器在J2SE 5.0 update 6中被引入。它与并行回收器的不同是它使用一个新的算法进行年老代垃圾回收。注意:最终并行压缩回收器将取代并行回收器。

使用并行压缩回收器的年轻代回收

对于并行压缩回收器,年轻代垃圾回收使用与并行回收器年轻代垃圾回收相同的算法完成。

使用并行压缩回收器的年老代回收

使用并行压缩回收器,年老代和永久代在一个stop-the-world中被回收,平移压缩大多是并行的。回收器使用三个阶段。首先,每个代在逻辑上划分为固定大小的区域。在标记阶段,直接从应用程序代码可访问的存活对象初始集合被划分到垃圾回收线程中,然后所有存活对象被并行标记。当一个对象识别为存活,它所在区域的数据随这个对象的大小和位置信息更新。

摘要(summary)阶段操作区域,而不是对象。由于先前回收的压缩,通常每个代的左侧部分将是密集的,包含绝大多数存活对象。可以从这些密集区域恢复的空间量不值得压缩它们的成本。因此摘要阶段做的第一件事情是检查区域的密度,从最左边的开始,直到它到达一个点,可以从一个区域以及那些它右侧的区域恢复的空间值得压缩这些区域的成本。这个点左侧的区域被称为dense prefix,没有对象被移动到这些区域。这个点右侧的区域将被压缩,消除所有死空间。摘要阶段计算并存储每个压缩区域的存活数据的首字节的新位置。注意:摘要阶段目前作为一个串行阶段实现;并行化是可能的,但是对性能不像标记和压缩阶段并行化一样重要。

在压缩阶段,垃圾回收线程使用摘要数据识别需要填充的区域,并且各线程可以独立的将数据拷贝到区域中。这会在一端产生一个密集填充的堆,而在另一端有一个大的空闲块。

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

与并行回收器一样,并行压缩回收器对于运行在多个CPU的机器上的应用是有益的。另外,年老代回收的并发操作降低了暂停时间,使得并行压缩回收器比并行回收器更适合于有暂停时间约束的应用。并行压缩回收器可能不适合于运行在大型共享设备(如SunRays)上的应用,这类设备上没有单一应用可能长期独占几个CPU,可以考虑减少用于垃圾回收的线程数(通过-XX:ParallelGCThreads=n命令行参数)或者选择一个不同的回收器。

并行压缩回收器选择

如果你想要使用并行压缩回收器,你必须通过指定命令行选项-XX:+UseParallelOldGC来选择它。

并发标记清除回收器(CMS)

对于大多数应用程序来说,端到端吞吐量不如快速响应时间重要。年轻代回收器通常不会引起长时间暂停。然而,年老代回收,虽然比较少,但可能会造成长时间暂停,尤其当涉及大的堆时。为了解决这个问题,HotSpot JVM包含了一个称为并发标记清除(CMS)的回收器,也称为低延迟回收器。

使用CMS回收器的年轻代回收

CMS回收器回收年轻代的方式与并行回收器相同。

使用CMS回收器的年老代回收

使用CMS回收器的年老代回收的大部分与应用程序执行并发执行。

CMS回收器的一个回收周期从一个短暂的暂停开始,称为初始标记,它识别从应用程序代码直接可达的存活对象的初始集合。然后,在并发标记阶段,回收器标记那些从初始集合间接可达的所有存活对象。由于标记阶段执行时,应用程序也正在运行和更新引用字段,所以并非所有存活对象都能保证在并发标记阶段结束时被标记。为了解决这个问题,应用程序再次暂停,称为再次标记(remark),它通过再次访问在并发标记阶段被修改的任何对象来完成标记。由于再次标记(remark)暂停比初始标记更大,因此并行运行多个线程以提高其效率。

在再次标记(remark)阶段结束时,堆中的所有存活对象都保证已被标记,因此后续并发清除阶段回收所有那些已识别的垃圾。图7演示了年老代回收使用串行标记清除压缩(mark-sweep-compact)回收器与CMS回收器的不同。

图7.串行和CMS年老代回收对比

由于某些任务,如再次标记(remark)阶段的对象重新访问,增加了回收器必须做的工作量,它的开销也会随之增加。这对于大多数试图减少暂停时间的回收器来说是一种典型的这种方案。

CMS回收器是唯一的非压缩回收器。也就是说,它释放了死对象占用的空间之后,不会移动存活对象到年老代的一端。见图8。

图8.年老代的CMS清除(但是不压缩)

这节省了时间,但是由于空闲空间不是连续的,回收器不能再使用一个简单的指针来指示可以用于下一个对象分配的空闲位置。相反,现在它需要使用空闲列表。更确切的说,它创建了一些列表将内存未分配区域链接到一起,每次一个对象需要分配时,必须查找合适的列表(基于需要的内存数量),找到一个足够容纳这个对象的区域。因此,年老代中的分配比简单的bump-the-pointer技术更昂贵。这也给年轻代回收带来额外开销,因为年老代中的大多数分配是在年轻代回收期间对象提升到年老代时发生的。

CMS回收器另一个缺点是需要比其它回收器更大的堆。考虑到在标记阶段运行应用程序运行,它可能继续分配内存,从而可能持续的增加年老代。此外,虽然在标记阶段回收器确保识别所有存活对象,但某些对象在该阶段期间可能会变为垃圾,它们将不能被回收,直到下一次年老代回收。这些对象被称为漂浮垃圾。

最后,由于缺少压缩,可能会产生碎片。为了处理碎片,CMS回收器跟踪热门对象大小,评估将来的需求,可以拆分或合并空闲块来满足需求。

与其它回收器不同,CMS回收器在年老代被填满时不会启动一个年老代回收。相反,它试图尽早启动回收,以便它可以在这发生之前完成。否则,CMS回收器就会转变成比并行和串行回收器使用的stop-the-world标记清除更耗时。为了避免这种情况,CMS回收器基于以前回收时间统计数据和年老代被占用速度的一个时间启动。如果年老代的占有率已超过启动占有率,CMS回收器也将启动一次回收。启动占用率的值通过命令行选项-XX:CMSInitiatingOccupancyFraction=n设置,其中n是年老代大小的一个百分比。默认为68。

总之,与并行回收器相比,CMS回收器降低了年老代暂停——有时明显——以牺牲年轻代较长的暂停、一些吞吐量的减少以及额外的堆大小需求为代价。

增量模式

CMS回收器可以在一种模式中使用,其中并发阶段是增量完成的。这种模式旨在通过周期性停止并发阶段并回到应用程序处理的方式减轻长并发阶段的影响。回收器所做的工作被拆分为在年轻代回收之间调度的小的时间块。当需要并发回收器提供的低暂停时间的应用程序运行在少量处理器(如1个或2个)的机器上时,这个特征非常有用。使用这种模式的更多信息,参见第9节提到的“在JVM 5.0中调整垃圾回收”文章。

什么时候使用CMS回收器

如果你的应用程序需要较短的垃圾回收暂停并且能够在应用程序运行时与垃圾回收器共享处理器资源,可以使用CMS回收器。(由于它的并发性,CMS回收器在回收周期内,将CPU周期与应用程序分开)通常,具有一份相对较大长久存活数据(一个大的年老代)的应用程序,并运行在两个或者更多处理器的机器上,往往受益于此收集器的使用。Web服务器就是一个例子。对于任何具有低暂停时间要求的应用程序都应该考虑CMS回收器。它也可以在单个处理器上以中等大小年老代为交互式应用提供良好的结果。

CMS回收器选择

如果你想使用CMS回收器,你必须显示的通过指定命令行选项-XX:+UseConcMarkSweepGC来选择。如果你想要它运行在增量模式下,也需要通过–XX:+CMSIncrementalMode选项启用。

5、人类工程学——自动选择和行为调整

在J2SE5.0版本中,基于应用程序运行的平台和操作系统自动选择垃圾回收器、堆大小以及HotSpot虚拟机(客户端或服务器)的默认值。这些自动选择更好地满足不同类型应用程序的需求,同时与以前版本相比,命令行选项更少 。

此外,为并行垃圾回收器增加了动态调整回收的新方法。通过这种方法,用户指定期望的行为,垃圾回收器动态地调整堆区域的大小,以达到所要求的行为。平台依赖的默认选择与使用期望行为的垃圾回收调整的组合称为人类工程学。人类工程学的目标是使用最少的命令行调整从JVM提供良好的性能。

回收器、堆大小以及虚拟机的自动选择

服务器类型机器被定义为:

  • 两个或者多个物理处理器以及
  • 2GB或更多GB物理内存

这个服务器类型机器的定义适用于所有平台,除了运行Windows操作系统的32位平台。

在非服务器类机器上,JVM、垃圾回收器和堆大小默认值为:

  • 客户端JVM
  • 串行垃圾回收器
  • 初始堆大小4MB
  • 最大堆大小64MB

在服务器类机器上,JVM总是服务器JVM,除非你显式指定-client命令行参数来请求客户端JVM。在服务器类机器上运行服务器JVM,默认垃圾回收器是并行垃圾回收器。否则,默认是串行垃圾回收器。

在运行使用并行垃圾回收器的JVM(客户端或服务器)的服务器类机器上,默认初始和最大堆大小是:

  • 初始堆大小是物理内存的1/64,最多1GB(注意,最小初始堆大小是32MB,因为服务器类型机器定义为至少2GB内存,2GB的1/64是32MB)。
  • 最大堆大小(Maximum)是物理内存的1/4,最多1GB。

除此以外,使用与非服务器类型机器相同的默认大小(4MB初始堆大小和64MB最大堆大小)。默认值可以被命令行选项覆盖。相关选项如第8节所示。

基于行为的并行回收调整

在J2SE5.0版本中,一个新的调整方法被添加到并行垃圾回收器,基于应用程序对垃圾回收的期望行为。命令行选项用于根据最大暂停时间和应用程序吞吐量目标指定期望行为。

最大暂停时间目标

最大暂停时间目标通过以下命令行选项指定:

-XX:MaxGCPauseMillis=n

它被解释为对并行回收器的一个提示,即期望n毫秒或者更少的暂停时间。并行回收器将调整堆大小和其它垃圾回收相关的参数,以求保持垃圾回收暂停时间小于n毫秒。这些调整可能导致垃圾回收器降低应用程序总体吞吐量,在某些情况下,无法满足期望暂停时间目标。

最大暂停时间目标分别应用到每一代。通常,如果没有达到目标,该代会变得更小以求满足目标。默认情况下没有设置最大暂停时间目标。

吞吐量目标

吞吐量目标根据垃圾回收所花费时间和垃圾回收以外的时间(称为应用程序时间)来衡量的。目标由命令行选项指定。

垃圾收集时间与应用程序时间的比率为

1 / (1 + n)

例如-XX:GCTimeRatio=19设置垃圾回收占总时间的5%的目标。默认目标是1%(即n=99)。花费在垃圾回收上的时间是所有代的总时间。如果没有达到吞吐量目标,代的大小会增加,为了增加应用程序在回收之间运行的时间。更大的代需要更多的时间来填充。

占用空间的目标

如果吞吐量和最大暂停时间的目标已满足,垃圾回收器将减小堆大小,直到无法满足其中的一个目标(总是吞吐量目标)。未满足的目标接下来解决。

目标优先级

并行垃圾回收器首先尝试满足最大暂停时间目标。只有满足它后,它们才处理吞吐量目标。同样,只有前面两个目标满足后才考虑占用空间目标。

6、建议

前一节中描述人类工程学带来垃圾回收器、虚拟机和堆大小的自动选择,这对于大多数应用程序来说是合理的。因此,选择和配置一个垃圾回收器的最初建议是什么也不做!也就是说,不要指定使用某个特定的垃圾回收器等。让系统根据你的应用程序运行的平台和操作系统做出自动选择。然后测试你的应用。如果它的性能是可接收的,具有足够高的吞吐量和足够低的暂停时间,那么你就完成了。你不需要对垃圾回收器选项进行诊断和修改。

另一方面,如果你的应用程序似乎有与垃圾回收相关的性能问题,那么你可以做的最简单的事情首先是鉴于你的应用程序和平台特性,考虑默认选择的垃圾回收器是否合适。如果否,显式选择你认为合适的回收器,并查看性能是否变得可接受。

你可以使用如第7节描述的那些工具来度量和分析性能。基于结果,你可以考虑修改选项,诸如控制堆大小或者垃圾回收行为的选项。一些最常用的选项参见第8节。请注意:性能调优最好的方法是先测量,然后调整。使用与你的代码实际使用有关的测试来测量。此外,谨防过度优化,因为应用程序数据集、硬件等等——甚至垃圾回器收实现!——可能随时间而改变。

本节提供有关选择垃圾回收器和指定堆大小的信息。然后提供调整并行垃圾回收器的建议,并给出了一些关于OutOfMemoryError如何处理的建议。

什么时候选择一个不同的垃圾回收器

第4节讲了对于每个回收器,推荐使用该回收器的情况。第5节描述了默认自动选择串行或者并行回收器的平台。如果你的应用程序或环境特性不同于默认回收器,那么通过以下命令行选项中的一个显示的请求回收器:

  • –XX:+UseSerialGC
  • –XX:+UseParallelGC
  • –XX:+UseParallelOldGC
  • –XX:+UseConcMarkSweepGC

堆大小

第5节讲了默认初始堆及最大堆的大小是多少。这些尺寸可能适合许多应用,但是如果你的性能问题分析(见第7节)或者一个OutOfMemoryError(在后面小节讨论)表明特定代或者整个堆的大小的问题,你可以通过第8节中指定的命令行选项修改尺寸。例如,非服务器类机器上的默认64MB的默认最大堆尺寸往往太小,因此你可以通过-Xmx选项指定一个较大的尺寸。除非你遇到长时间暂停的问题,尝试向堆授权尽可能多的内存。吞吐量与可用内存量成正比。有足够的可用内存是影响垃圾回收性能的最重要因素。

在决定了你可以给整个堆的内存总量之后,你然后可以考虑调整不同代的大小。影响垃圾回收性能的第二大因素是年轻代在堆中所占的比重。除非你发现过多的年老代垃圾回收或者暂停时间的问题,授权年轻代充足的内存。然而,当你正在使用串行回收器时,给年轻代的内存不要超过堆的总大小的一半。

当你正使用一个并行垃圾回收器,最好指定期望行为,而不是精确的堆大小值。让回收器自动并动态的修改堆大小,以满足该期望行为,如下所述。

并行回收器的调整策略

如果选择的垃圾回收器(自动或者显式)是并行垃圾回收器或并行压缩垃圾回收器,那么继续指定一个对你的应用程序来说足够的吞吐量目标(见第5节)。不要为堆选择一个最大值,除非你知道你需要堆大于默认的最大堆大小。堆将增长或者缩小到一个尺寸来支撑选择的吞吐量目标。在初始化以及应用程序行为变化期间堆大小的一些振荡是可以预料的。

如果堆增长到最大值,在大多数情况下,这意味着在这个最大值内,吞吐量目标不能被满足。将最大值设置为接近平台总体物理内存大小但是不会导致应用程序交换的值。再次执行应用程序。如果吞吐量目标仍未达到,那么应用程序时间的目标对于平台上的可用物理内存来说便太高了。

如果吞吐量目标可以被满足,但是有太长的暂停,选择一个最大暂停时间目标。选择一个最大暂停时间目标可能意味着你的吞吐量目标将无法满足,因此选择一个对应用程序可接收的折中值。

当垃圾回收器视图满足相互竞争的目标时,堆的大小会发生震荡,即使应用程序已经达到稳定状态。实现吞吐量目标(可能需要更大的堆)的压力与最大暂停时间和最小占用量目标(两者都可能需要较小的堆)竞争。

如何处理OutOfMemoryError

大多开发者必须解决的一个常见问题是由java.lang.OutOfMemoryError导致终止的应用程序。当没有足够的空间用于分配一个对象时,会抛出该异常。也就是说,垃圾回收不能提供更多可用的空间来容纳新对象,并且堆不能进一步扩展。一个OutOfMemoryError错误并不一定意味着内存泄漏。这个问题可能只是一个配置问题,例如,如果指定堆大小(或者默认堆大小,如果没指定)对应用程序来说是不够的。

诊断OutOfMemoryError的第一步是检查完整的错误信息。在异常信息中,更多信息是在“java.lang.OutOfMemoryError”之后提供的。下面是一些常见例子,关于额外信息可能是什么、它可能意味着什么以及如何处理:

  • Java heap space

这表明不能在堆中分配。这个问题可能只是一个配置问题。你可能得到该错误,例如,如果由-Xmx命令行参数指定(或者默认选择)的最大堆大小对应用程序来说是不足的。它还可能表示不再需要的对象不能被垃圾回收,因为应用程序无意的持有对它们的引用。HAT工具(见第7节)可以用来查看所有可访问对象,并了解哪些引用使每个对象都存活。这个错误另一个潜在的来源可能是应用程序对finalizer的过度使用,诸如调用finalizer的线程不能跟上finalizer队列的增加速度。JConsole管理工具可用于检测即将终结的对象数目。

  • PermGen space

这表明永久代已满。如前所属,这是JVM存储它的元数据的堆区域。如果一个应用加载了大量的类,那么永久代可能需要增加。你可以通过指定命令行选项-XX:MaxPermSize=n来完成,其中n指分配大小。

这表明应用程序试图分配一个大于堆内存大小的数组。例如,应用程序尝试分配一个512MB的数组,但是堆的最大值为256MB,那么将抛出这个错误。在大多数情况下,这个问题很可能是堆太小或者BUG导致应用程序尝试创建一个数组,它的大小因计算错误而异常巨大。

第7节描述的一些工具可以用来诊断OutOfMemoryError问题。对于这项任务,一些最有用的工具是HAT(Heap Analysis Tool)、JConsole管理工具,以及jmap工具与-histo选项。

7、评估垃圾回收性能的工具

可以使用各种诊断和监控工具评估垃圾回收性能。本节提供了它们中一些的简要介绍。有关更多信息,请参见第9节中的“工具和故障排除”链接。

–XX:+PrintGCDetails 命令行选项

获得垃圾回收初始信息的最简单方法其中之一是指定–XX:+PrintGCDetails命令行选项。对于每一次回收,这将导致信息的输出,诸如各代垃圾回收前后存活对象的大小、各代总的可用空间、回收花费的时间长度。

–XX:+PrintGCTimeStamps 命令行选项

如果使用了命令行选项-XX:+PrintGCDetails,除了输出的信息之外,在每次回收开始时输出一个时间戳。这个时间戳可以帮助你将垃圾回收日志与其他日志事件关联起来。

jmap

jmap是一个包含在JDK Solaris和Linux(不包括Windows)版本中的命令行工具。它为正在运行的JVM或者核心文件打印内存相关的统计信息。如果使用它时不添加任何命令行选项,那么它打印加载的共享对象列表,类似于Solaris pmap工具的输出。更具体的信息,可以使用-heap、-histo或者-permstat选项。

-heap选项用于获取包括垃圾回收器名称、特定算法的细节(诸如用于并行垃圾回收的线程数目)、堆配置信息和堆使用摘要信息。

-histo选项可以用于获得一个堆的与类相关的图。对于每个类,它打印了堆中实例的数量、这些对象消耗的以字节为单位的内存总量、完全限定类名。当试图了解堆的使用情况时,这个图非常有用。

配置永久代的大小对于动态生成并加载大量类的应用程序(例如,JSP和Web容器)是非常重要的。如果一个应用加载了“太多”的类,那么会抛出一个OutOfMemoryError异常。jmap的-permstat选项可以用于获取永久代中对象的统计信息。

jstat

jstat工具使用了HotSpot JVM内置的工具来提供应用程序运行相关的性能和资源消耗信息。该工具可以用于诊断性能问题,特别是与堆大小和垃圾回收相关的问题。它的许多选项可以打印关于垃圾回收行为、各代容量和使用情况的统计数据。

HPROF: Heap分析器

HPROF是JDK5.0提供的一个简单的分析器代理。它是一个动态链接库,使用JVM TI与JVM相互联系。它以ASCII或者二进制格式输出剖析信息到文件或者套接字。这个信息可以由一个分析器前端工具进一步处理。

HPROF能够呈现CPU使用率、堆分配统计以及监控竞争情况。除此之外,它可以输出完整的堆dump并报告JVM中所有监控和线程的状态。HPROF在分析性能、锁竞争、内存泄露以及其它问题时非常有用。HPROF文档的链接见第9节。

HAT: 堆分析工具

堆分析工具(HAT)帮助调试无意的对象保持。这个术语用于描述一个对象不再被需要,但是由于来自一个存活对象的通过某个路径的引用,它却仍保持存活。HAT提供了一个方便的方式来浏览通过HPROF生成的堆快照中的对象拓扑。该工具运行允许一些查询,包括“显示从rootset到这个对象的全部引用路径”。HAT文档的链接见第9节。

8、垃圾回收相关的关键选项

许多命令行选项可以用来选择垃圾回收器、指定堆或代的大小、修改垃圾回收行为、获取垃圾回收统计信息。本节展示了一些最常用的选项。有关可用选项的更完整的列表和详细信息,请参阅第9节。注意:你指定的数字可以以“m”或“M”结束表示MB、“k”或“K”结束表示KB、“g”或“G”结束表示GB。

垃圾回收器选择

选项 垃圾回收选择
–XX:+UseSerialGC 串行
–XX:+UseParallelGC 并行
–XX:+UseParallelOldGC 并行压缩
–XX:+UseConcMarkSweepGC 并发标记清除(CMS)

垃圾回收器统计信息

选项 描述
–XX:+PrintGC 在每次垃圾回收时输出基本信息
–XX:+PrintGCDetails 在每次垃圾回收时,输出更详细的信息
–XX:+PrintGCTimeStamps 在每次垃圾回收事件开始时输出一个时间戳。当每次垃圾回收开始时,与–XX:+PrintGC或–XX:+PrintGCDetails配合使用来显示。

堆和代的大小

选项 默认值 描述
–Xmsn 见第5节 堆的初始字节大小
–Xmxn 见第5节 堆的最大字节大小
–XX:MinHeapFreeRatio=minimum和–XX:MaxHeapFreeRatio=maximum 最小值为40,最大值为70 空闲空间占总堆大小比例的目标范围。它们被应用到每个代。例如,如果最小值为30,一个代的空闲空间百分比低于30%,该代的大小被扩展以获得30%的空闲空间。类似的,如果最大值为60,并且空闲空间的百分比超过60%,该代的大小被缩小到只有60%的空闲空间。
–XX:NewSize=n 依赖于平台 年轻代默认的初始字节大小
–XX:NewRatio=n 在客户端JVM上为2,在服务器JVM上为8 年轻代和年老代之间的比例。例如,如果n是3,则比例为1:3,伊甸区和幸存空间的组合大小是年轻代和年老代总大小的1/4。
–XX:SurvivorRatio=n 32 每个幸存空间与伊甸区的比例。例如,如果n是7,每个幸存空间是年轻代的1/9(不是1/8,因为有两个幸存空间)。
–XX:MaxPermSize=n 依赖于平台 永久代的最大值

并行和并行压缩回收器的选项

选项 默认值 描述
–XX:ParallelGCThreads=n CPU数量 垃圾回收线程数量
–XX:MaxGCPauseMillis=n 指示回收器期望的暂停时间为n毫秒或者更少。
–XX:GCTimeRatio=n 99 该数值设置了一个目标,总时间的1/(1+n)用于垃圾回收。

CMS回收器选项

选项 默认值 描述
–XX:+CMSIncrementalMode 不启用 启用并发阶段增量执行的模式,该模式下周期性的停止并发阶段以将处理器返回到应用程序。
–XX:+CMSIncrementalPacing 不启用 允许基于应用程序行为自动控制CMS回收器在放弃处理器之前所做的工作。
–XX:ParallelGCThreads=n CPU数量 用于并行年轻代回收和年老代回收并行部分的垃圾回收线程数量。

9、更多信息

HotSpot垃圾回收和性能调优

人类工程学

选项

工具和故障排除

终结(Finalization)

其它


如果你愿意支持博主的工作,可以通过微信进行打赏

猜你喜欢

转载自blog.csdn.net/elinespace/article/details/78822114