java 内存管理 —— 《Hotspot内存管理白皮书》

时间:2023-01-16 10:54:09

说明

  要学习Java或者任意一门技术,我觉得最好的是从官网的资料开始学习。官网所给出的资料总是最权威最知道来龙去脉的。而Java中间,垃圾回收与内存管理是Java中非常重要的一部分。《Hotspot内存管理白皮书》是了解Java垃圾收集器最权威的文档。相比于其他的一些所谓翻译文章,本文的翻译更加准确,通顺和全面。在翻译的过程中如果出现一些问题,如果出现问题或者表述不清楚的地方,可以直接在评论区评论。

1.简介

   JavaTM 2 Platform, Standard Edition (J2SETM) 其中一个强大特性便是自动内存管理,从而对开发者屏蔽了复杂的显式内存管理。

  这篇论文针对在J2SE 5.0版本发布的JAVA Hotspot虚拟机,对它做出了简要概述。本论文描述了垃圾收集器如何执行内存管理,并且在对垃圾收集器的选择和配置、可操作的内存区域的大小设置给出了建议。本文也提供了一些链接,其中一部分链接列举了可以影响垃圾收集器行为的选项,另一部分列举了更多详细的文档。

  文章的第二节为那些对于自动内存管理还是新事物的用户而设立。它简明的讨论了与需要程序员显示的回收内存相比,自动内存管理的有何益处。文章第三节紧接着描述分代垃圾回收的概念,方案选择,性能指标。本节也介绍了一个叫作“分代”的通用内存区域划分的方法,它是基于对象生存时间的内存区域划分方法。对于大部分应用来说,这种基于代的划分有效地减少了垃圾回收时的暂停时间和整体性能开销。

  本论文的剩余部分提供了Hotspot VM的 细节信息。第四部分描述了可用的四个垃圾回收器,包括在J2SE 5.0 update 6新出现的回收器,以及这些垃圾回收器都在使用的分代的内存划分方法。对任何一个收集器,第4部分对适合于他们各自的垃圾回收算法的类型和要求做出了概述。

  第五部分描述了在J2SE 5.0中发布的可以在根据运行中的应用程序的平台和操作系统中来系统地自动选择垃圾收集器(1),堆内存大小,HotSpot虚拟机(客户端模式或者服务器模式),以及动态垃圾回收(2)以便自适应用户自定义的行为的技术,这项技术被称为人体工程学。

  第六部分提供了选择和配置垃圾回收器的推荐规范。它也提供了关于OutOfMemoryErrors时该如何做的一些建议。第七部分简明的描述了评估垃圾收集性能时可以用到的一些工具。第八部分例举了有关垃圾收集器相关选项和行为的一些通用命令行。最终,第九部分提供了本论文所覆盖的观点的详细文档的链接。

2.显式VS自动内存管理

  内存管理是一个识别不再使用的被分配的对象,重新分配(释放)被这些对象占用的内存,并且使这一系列的分配可用的过程。在一些程序语言中,内存管理是程序员的责任。这个复杂的任务造成了许多通用的能够造成异常和错误的程序执行行为以及崩溃。结果,大量的开发者把时间花费在调试和力图纠正这些错误上面。

  一个在显式内存管理编程上面经常出现的问题就是悬空引用。他是一个对象使用空间已经被重新分配但是其他对象仍在引用。如果一个对象试图去引用这个对象最初的引用,但是这个对象已经被分配到新的对象,这个结果是未知的并且不是被期望的。

  另外一个显式内存管理的公共问题是内存空间泄露。泄露造成的原因是对象不再被引用但是并没有被释放。举个例子,如果你打算释放一个链表占用的空间,但是你犯了一个只释放了这个链表头结点的错误,链表剩余的元素不再引用但他们在程序中不可达,并且不能被重新使用或者重新覆盖。如果足够的泄露发生,他们能不断的消耗内存直到所有内存被耗尽。

  使用叫做垃圾回收器的程序来自动管理内存被认为是内存管理的一种通用替代方法,尤其是在现在大多数面向对象的语言中。自动内存管理增加了接口的抽象和代码的可靠性。

  垃圾收集器避免了悬空引用的问题。因为仍然被引用的对象永远不会被垃圾会收取回收所以肯定不会被释放。垃圾收集器也解决了上面描述的内存空间泄漏的问题,因为它会自动释放所以不再引用的内存。

3.垃圾收集器的理念

  垃圾收集器有如下职责:

  1.分配内存

  2.确保任何被引用的对象仍然在内存中间

  3.重新覆盖那些在执行代码中引用不可达的内存。

  仍然被引用的对象叫做存活的对象。不再引用的对象被认为是死亡的对象,术语上叫做垃圾。发现和释放(或者被称为再生)这些对象所占用对象的过程叫做垃圾回收。

  垃圾回收器解决了大量的但并不是所有的内存分配问题。比如你可以无限期的创建对象和引用它们直到没有内存可以使用。垃圾收回收器自身所占用的时间和资源也非常复杂。

  垃圾回收器使用精确的算法来组织内存的分配和再分配,并且对程序员屏蔽细节。内存空间通常从叫作“堆”的内存池中分配。

  垃圾收集的时机取决于垃圾收集器。通常是垃圾占满了整个堆或者一部分的某个临界值的时候开始回收垃圾。

  比较令人满意的内存分配请求包括了在堆中发现一块大小确定的未被使用的内存,但这却是一个复杂的任务。主要是问题是大多数动态内存分配算法都要避免碎片化来保持对象的分配和释放都很高效。

令人满意的垃圾收集器特质

  一个垃圾回收器必须安全和全面。也就是说,活着的对象不能被错误的释放,并且垃圾对象在经历超过数个较小数字的回收周期以后不能仍然无人认领。
高效的垃圾收集操作也是比较令人满意的,它不会在应用运行期间造成长时间的停顿。在与计算机有关的系统中,需要在时间,空间,和回收频率上寻求平衡。比如,如果堆比较小,收集会非常快但是却容易被用满,因此需要更频繁的回收。相反的,一个比较大的堆填满需要更长的时间,回收频率也会变少,但是回收的时间会变长。

  另外一个令人满意指标的垃圾回收器地碎片化的限度。当一个垃圾对象的内存被释放时,释放的空间可能会出现一系列连续的小块,但是这些连续的小块任何一块都不足以分配一个较大的对象。一个消除碎片化的方法叫做压缩,就是下面一系列垃圾回收器所讨论的设计思想。

  可扩展性也很重要。内存的分配不应该成为可扩展性的瓶颈。回收也不应该成为瓶颈。

设计思想

  在设计或者选择垃圾回收算法的时候有几个思想是必须考虑的:

  • 串行还是并行

      在串行收集器中,同一个时间只有一件事发生。举个例子,即使是多个CPU的系统,也只有一个CPU用来执行垃圾回收。当并行的垃圾回收器用的时候,垃圾回收任务被同时地分配在不同的CPU,这些同时地操作使得垃圾回收更快地完成,但是增加了复杂性和潜在碎片的风险。

  • 并发 VS Stop-the-world

      当“Stop-the-world”类型收集器执行的时候,整个应用程序会被完全挂起。相对的,一个或者多个垃圾回收器能够并发地执行,也就是说与应用程序同时执行。通常,一个并发的垃圾收集器自己通常自身是并发的工作,但是偶尔也会造成造成一个短暂的“Stop-the-world”停顿。 “Stop-the-world”要比并发的收集器简单,因为在整个收集期间,堆被冻结并且对象不再改变。由于某些应用程序的暂停会造成不良的影响,所以这可能是一个劣势。相应的,并行的垃圾收集暂停的时间更短,但是垃圾收集器却需要更多小心,因为垃圾收集正在操作的对象可能会同时被应用程序更新。对于并发收集器来说这些额外的开销会影响性能并且需要更大的堆。

  • 压缩 VS 不压缩 VS 拷贝

      当一个垃圾收集器决定内存中的哪些对象存活哪些对象需要回收的时候,它可以压缩内存,把活着的对象移动到一起然后完整的恢复剩余的内存。压缩之后,非常容易快速的释放和回收内存。使用一个简单的指针来跟踪下一个可以分配对象的内存位置。与压缩收集器相对应的是,不压缩的收集器原地释放被垃圾对象占用的空间,也就是说,它不会采用与压缩的垃圾回收器移动所有活着的对象的方式来创建内存区域。这样做的优势是垃圾回收会非常快,但是缺点就是会有潜在的碎片。通常来说,原地释放的内存上重新分配对象也比在压缩的堆上分配内存代价要更昂贵。不压缩的垃圾回收器必须搜索一个连续的足够大的区域来容纳新对象。第三个可以选择的垃圾收集器就是吧或者的对象拷贝或者疏散到不同的内存区域。这样的好处是源区域可以很快的被清空并且容易的连续分配。缺点是增加了拷贝需要的时间并且可能需要额外的空间。

性能指标

  有诸多的指标来计算垃圾收集器的性能,包括:

  • 吞吐量:未被垃圾收集的时间占总时间的百分比,应该被认为在一个长时间内。
  • 垃圾回收的天花板:吞吐量的倒数,也就是说,垃圾回收占总时间的百分比。
  • 暂停时间: 当应用程序暂停以执行垃圾回收的时间。
  • 回收频率:多久垃圾回收发生一次,相对与整个应用程序的执行时间。
  • 内存需求:堆大小的测量,比如堆大小
  • 实时性:在对象变成垃圾到内存变得可用的时间。
      一个交互系统可能需要低暂停时间,然而对于一个非交互系统应用程序而言整个可执行的时间却更加重要。一个实时系统在任何时间段下都需要更小的垃圾回收暂停上界和垃圾回收的的占比。在个人电脑中或者嵌入式系统中,对小内存需求可能比较关心。

分代回收

  当一个叫做“分代回收”的垃圾收集被运用的时候,内存被划分成了不同的代,也就是划分出了不同的池来持有不同年龄的对象。比如,最广泛的使用的配置是分成两个代:年轻代和老年代。
  不同的算法运用在不同的代来执行垃圾回收。每一个算法基于各自代的特性来优化。在许多语言写成的应用程序中,这样的设计是基于弱年代假设(Weak Generational Hypothesis),包括Jav语言:
  1.越早分配的对象越容易失效。

  2.只有少数的老年代对象引用年轻代对象存在。

  年轻代回收相对频繁和快速,因为年轻代的空间通常很小并且包含了大量不再使用的对象。

  经过几次回收还存活的对象最终会被提升或者终生晋升到老年代。如图1。老年代通常比年轻代大并且增长缓慢。所以,老年代的垃圾回收不频繁,但是会占用更多的时间。


java 内存管理 —— 《Hotspot内存管理白皮书》

  年轻代垃圾回收算法选择通常会在速度上花费高昂的代价,因为年轻代的对象收集会非常频繁,另一方面,管理老年代垃圾回收的算法则更有效。因为老年代通常占据堆的大部分,并且老年代的算法必须在低回收密度下工作的很好。

4.J2SE5.0中的Hotspot VM的垃圾收集器

  J2SE 5.0 update 6发布的Hotspot VM包含了四中垃圾收集器。所有的垃圾收集器都是分代的。本节描述了分代和回收器的类型,并讨论了为什么对象的分配会频繁并且快速和高效,本文也提供了关于每个垃圾收集器的细节。

HotSpot中的分代

  HotSpot虚拟机被划分成了三个代:年轻代,老年代和一个永久代。大部分对象最初被分配到了年轻代,老年代包含了几次垃圾回收以后还存活的对象。此外一些大的对象一开始就被分配到老年代。永久带持有JVM比较容易找到并管理的对象,比如描述类和方法的对象,也包括类和方法自身。

  年轻代由叫做Eden区和两个较小的survivor区组成。如图2,大部分对象分配在Eden区。(注意,一些大的对象一开始就被分配到老年代)survivor区持有那些至少在一次垃圾收集中存活并且被认为在足够老并且晋升到老年代之前仍然有机会死亡的对象。在任何给定的时间内,一个survivor区总是持有存活的对象,而另外一个survivor区则是空置的,直到下一次回收器开始工作。


java 内存管理 —— 《Hotspot内存管理白皮书》

垃圾收集的类型

  当年轻代被占满的时候,年轻代的垃圾收集就会执行工作。(有时候也叫作次收集。次收集
垃圾收集按频率可分为: 次收集(Minor Collection):频繁发生在年轻代,收集快速消亡的对象;主收集(Major Collection):年轻代和年老代的全范围收集,频率很低)当老年代被占满或者永久带被占满的时候,会发生整个gc(有时候也叫做主收集),也就是说,所有的代都会被收集。通常,年轻代首先被收集。因为年轻代的算法通常是最高效的。接下来是使用“老年代垃圾收集算法”的垃圾收集器开始在老年代和永久代工作。如果在收集的过程中发生了压缩,每一个代内部都会各自压缩。

  如果年轻代优先被收集,老年代就会因为太满以至于无法接受来自年轻代晋升而来的对象。在这种情况下,除了CMS收集器运行之外,年轻代所有的垃圾回收算法将会被暂停。与之替代的是,老年代的算法将会被运用到整个堆。(CMS的老年代算法是一个特例,因为它无法回收年轻代)。

快速分配

  在下面关于垃圾收集器你将看到,在很多情况下都是从一块连续的很大的内存上面分配对象。利用一个叫做“空闲指针”的技术,使得在内存块上分配对象很高效。指针始终保持最后一个分配的对象的内存位置。当有新的对象需要分配内存时,只需要检查剩余的空间是否存放新的对象,如果能够,更新指针的位置并且初始化对象。

  对于多线程的应用程序来说,对象的分配需要保证线程安全。如果使用全局锁来保证这些,那么对象的分配就会成为瓶颈且性能下降。因此你,hotspot虚拟机采用了一个叫做“线程分配缓冲(TLAB)”的技术。通过给每个线程设置自己的缓存来分配对象来提高多线程的内存分配吞吐量。这样每个TLAB只会有一个线程分配对象,这样不需要锁,空闲指针会移动的很快。但当一个线程将自己的缓存用光以后再分配新的,则必须要同步,但是这并不频繁。HotSpot虚拟机会采取一系列的技术来减少由于使用了线程本地分配缓存而带来内存浪费。比如,TLAB的分配器只会造成Edon区平均大约少于百分之一的损耗。使用Edon和空闲指针能够做到每一个分配都高效,这只需要大约10个本地指令。

串行回收器

  在串行收集器中,所有的年轻代和老老年代都是串行的执行的(一次只用一个CPU),串行回收器会产生stop-the-world,也就是说,整个应用在垃圾回收期间会被挂起。

使用串行回收器的年轻代

  图3插图说明了串行回收器年轻代操作的情形。edon区活着的对象被拷贝到了Survivor区,有一种情况是其中一个区太大以至于不能完全拷贝到Survivor区,这些对象直接拷贝到了老年代。from区存活的相对来说比较年轻的对象也会被拷贝到另外一个Survivor区,相对比较老的对象则直接拷贝到老年代。注意:如果to区已经满了,edon区或者from区将不再拷贝,不管有多少对象存活。任何在edon区或者from区,经过拷贝过并且还存活的对象,在定义上是不存活。因此他们也不需要被检查。(这些对象就是在下图中标记为“x”对象,尽管事实上垃圾收集器并不会检查或者标记这些对象。)


java 内存管理 —— 《Hotspot内存管理白皮书》

  在年轻代的垃圾收集完成以后,Edon区和早先被占用的Survivor区被清空,早先空的Survivor区持有了活着的对象。基于这一点,两个Survivor区交换了角色。如图4


java 内存管理 —— 《Hotspot内存管理白皮书》

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

  在串行收集器中,老年代和永久带通过一种“标记-清除-压缩”的算法。在标记阶段,收集器识别哪些对象是存活的。在清除阶段,清除那些被识别为垃圾的对象。然后收集器则执行“滑动压缩”。把所有活着的对象移动到老年代起始的地方(永久代类似)。这样就在堆得末端留下一个相当大的连续的区块。如图5。压缩允许在老年代或者永久代使用空闲指针的技术分配任何预分配的对象。


java 内存管理 —— 《Hotspot内存管理白皮书》

什么时候使用串行回收器

  串行回收器是大部分运行在客户端类型机器的应用程序的选择,这些应用程序不要求有低延迟。在如今的硬件条件下,串行收集器能够很容易管理一个有64mb的内存,发生一次full gc相对最坏的暂停不会超过半秒的时间。

串行收集器的选择:

  在j2se5中,串行收集器自动被非server模式的机器所选择。如第五节描述的那样,在其他类型的机器上,需要使用-XX:+UseSerialGC来显示地指定。

#并行收集器

  如今,许多java应用程序运行在大内存和多核CPU上面。并行收集器,也就是熟知的高吞吐收集器。它利用了多核CPU的优势,而不是只在一个CPU上执行垃圾收集的工作。

年轻代的并行垃圾收集器

  年轻代的垃圾收集器是串行收集器的并行版本。它仍然是一个具有stop the world 暂停和对象拷贝的收集器。由于使用了多核CPU,在执行的时候确是并发的,这样减少了垃圾收集器的消耗,因此增加了应用程序的吞吐量。图6阐明了串行收集器与并行收集器在年轻代中的不同。


java 内存管理 —— 《Hotspot内存管理白皮书》

老年代使用并行收集器

  老年代的并行收集器工作模式与串行收集器的标记-清除-压缩算法相同。

什么时候使用并发收集器

  应用程序程序能够从运行在多个CPU的并发收集器受益,并且不会有长时间暂停的限制。但是在某些情况下,比如批处理任务,账单系统,工资系统,科学计算等系统中,这些应用系统的老年代垃圾收集回收虽然不频繁,但是耗费时间可能会很长。这个时候你就可以考虑使用并行的压缩回收器(下面将要描述的)而不是并发收集器了,因为早先的并发收集器适用于所有的代,并不仅仅是年轻代才能使用。

并发收集器的选择

  J2SE 5.0发行版中,并行回收器是server类型的机器的默认的收集器。(文章第五节定义)。在其他类型的机器上,使用XX:+UseParallelGC命令来显示地指定使用并行收集器。

并发压缩收集器

  J2SE 5.0 update 6 引进了并发压缩收集器。不同于并发收集器的地方在于它为老年代使用了新的算。注意: 并发压缩收集器终会替代并行垃圾回收器

年轻代使用并发压缩收集器

  年轻代的并发压缩收集器与年轻代使用并发收集器的算法一样。

老年代使用并发压缩收集器

  在并发压缩收集器中,老年代和永久带在stop-the-world的过程中,大部分并发模式都是滑动压缩。收集器分为三步来回收垃圾:第一步,每一个代都被逻辑地分为几个固定的区域。在标记阶段,初始的直接可到达的对象被划分到不同的垃圾收集线程。然后所有活着的对象被并发的标记。当一个对象被确定是活着的,关于这个区域的信息和这个对象的位置的数据将会被更新。汇总阶段:汇总阶段的操作基于区,而不是基于对象。由于上一次垃圾回收时的压缩操作, 一般来说代空间的左边区域存活对象的密度会较高. 这种密度高的区域中, 可以回收的空间不多, 所以压缩他们的可用空间的代价太高. 所以汇总阶段做的第一件事情就是测试区域密度。 从最左边的那个区域开始, 一直到找到一个点, 压缩这个点的右边的区域是代价是值得的. 这个点左边的区域叫做密度前缀 (Dense Prefix), 这些区域不会有新的对象写入。 这个点右边的区域将被压缩, 并清除所有死亡对象。汇总阶段计算并存储了每个压缩区域的存活对象的第一个字节的地址。注意: 汇总阶段目前的是实现是串行执行, 因为相对来说,标记和压缩阶段的并行执行更重要。

  在压缩阶段,垃圾回收使用从汇总阶段收集而来的数据来确定哪些区域,并且线程可以独立的拷贝数据到这些区域。这样就造就了在一端是密度很高的对象区块,另外一端是一个连续大的可用区块。

什么时候使用并发压缩算法

  与并行回收算法类似,并行压缩算法对在超过一个CPU允许的应用程序有益。除此之外,老年代的并行操作减少了暂停时间并且并发压缩的收集器更加适合对暂停时间有严格限制的应用。并发压缩算法可能不适用于那些运行在大型共享机的机器上面的应用,他们不允许单个应用程序长时间的垄断CPU。在这些机器上,就得考虑减少垃圾回收的线程(通过–XX:ParallelGCThreads=n)或者选择不同的回收器。

并行压缩收集器选择

  如果你想指定使用并行压缩收集器,使用-XX:+UseParallelOldGC 命令行选项。

并发的标记-清除收集器

  在很多应用程序中,端到端的吞吐量并与快速响应的时间相比并不重要。年轻代通常情况下并不会造成长时间的停顿。然后老年代虽然并不频繁,但是会造成长时间的停顿,尤其是涉及到很大的堆。为了解决这个问题,HotSpot VM引入了一种叫做并发的标记清除收集器,也叫作低延迟收集器。

年轻代使用并发标记清除收集器

  CMS与年轻代中的并行收集器一样。

老年代使用CMS收集器

  大多数的CMS收集器是在并行地执行。

  CMS收集器开始于一个叫做初始化标记的暂停。初始化标记是标记那些在应用程序中直接可达的活着的对象。然后,在并发标记阶段,收集器标记那些间接可达的活着的对象。由于在标记阶段发生的时候,应用程序不停地在运行并且更新引用,不是所有活着的对象都能在标记阶段被确定地标记。为了处理这个问题,应用程序将会暂停很短的时间,叫做remark。就是重新标记对在标记阶段中任何发生了改变的对象。因为remark阶段的暂停要比初始化标记的时间要长,所以会使用多线程运行以便提高效率。

  在remark标记的结束以后,所有活着的对象都确保被标记了。所以随后的并发清除阶段就是清除所有的垃圾。图7显示了使用串行的标记清除回收器与并行的标记清除回收器有何不同。


java 内存管理 —— 《Hotspot内存管理白皮书》
  由于一些诸如在remark阶段重新检索对象等额外的任务,垃圾回收器增加了一些额外的工作,同时也增加了一些简接消耗。所以对于大多数收集器来说,在正确性与暂停时间之间都会存在一种平衡和取舍。

  CMS收集器是唯一的不压缩的收集器。也就是说垃圾对象在释放以后,并不会把活着的对象移动到代的一端,参见图8

java 内存管理 —— 《Hotspot内存管理白皮书》
  这样做虽然节省了时间,但由于*空间不是连续的,收集器不再使用一个简单的指针来指示下一个可以使用的位置,使得下一个对象可以分配。相反,它现在需要空闲空间的列表。也就是说它现在需要一些列表把未分配的内存区域连接在一起,每一次需要分配对象,就必须在适当的列表(基于所需的内存)必须寻找一个区域足够容纳对象,分配到老年代。比用一个简单的bump-the-pointer技术更加昂贵。这也对年轻代的收集产生了额外的开销,因为大多数老年代的对象都是从年轻代晋升而来的。

  CMS垃圾回收器的另一个缺点是需要比其他垃圾回收器更大的堆。考虑到应用程序在标记阶段可以继续分配内存,从而老年代可能会持续增长。此外,尽管收集器保证在标记阶段期间识别所有活的对象,但一些对象可能在这个阶段成为垃圾,他们将不会再被标记,直到下一个老年代回收开始。这样的对象被称为漂浮垃圾。

  最后,由于缺乏压缩,碎片可能会产生。为了处理碎片,CMS回收器会指定一个常用的对象大小,估算出未来的需求,并且会分割或者合并空闲的内存块去符合需求。

  与其他的回收器不同,CMS回收器不会等到老年代的空间变满的时候才开始回收工作。它试图在足够早的时间就开始回收工作。否则,CMS回收器会比使用标记-清除-压缩算法的并行和串行垃圾回收器造成更多的暂停。为了避免它,CMS回收器会在达到某个阈值的时候时候启动回收操作,这个阈值基于前面垃圾回收的次数和垃圾回收的耗时来统计。当老年代被占用到超过了一个称之为初始化占用值的时候也会开始执行垃圾回收。初始化占用可以通过–XX:CMSInitiatingOccupancyFraction=n 这个命令行选项来设置,n是老年代对象大小的百分比,默认值是68。

  总之,并行收集器相比,CMS收集器降低了老年代的所带来的停顿,但令人感到戏剧性的是,它反而提升了年轻代的暂停时间,还减少了部分吞吐量,并且需要额外的堆空间。

增量模式

  CMS收集器可以使用一种模式,这个模式可以让并发阶段逐步完成,而不是一次整个完成。这个模式打算减少由长期并发阶段造成的影响,它采用了定期地暂停当前的并发阶段使得当前的回收工作被挂起,让出处理器来处理应用程序。它的工作是这样的,在年轻代回收操作工作时,回收器将老年代划分成不同的块单独回收。当应用程序要求回收器暂停时间要较短而又运行在小数量处理器的机器中时,这是很有用的。更多关于使用这个模型的信息,参见第九节“java5.0 虚拟机上的垃圾回收器调优”。

何时使用CMS收集器

  如果你的应用程序需要一个较短停顿的垃圾回收器,并且可以让垃圾回收器在应用程序运行过程中分享处理器资源,那么就合适使用CMS回收器(由于它的并发性,CMS回收器在回收周期使用的cpu周期与应用程序无关)。一般情况下,应用程序存在一个相对大的集合来存储长期存活的数据(一个足够大的老年代),并且运行它的机器拥有两个或更多的处理器,常常趋向于使用这个回收器。比如web服务器,CMS垃圾回收器通常被那些需要短时暂停的需求的应用程序所采用。它通常也适合那些在单个处理器上拥有合适老年代大的交互式应用程序。

选择CMS收集器

  如果你想要使用的CMS收集器,您必须通过指定的命令行选项xx:+ UseConcMarkSweepGC显式地选择它。如果你想要在增量模式下运行,使用-xx:+ CMSIncrementalMode选项。

5.人体工程学-自动选择和行为调优

  在J2SE 5.0的发行版中,垃圾收集器默认的值、堆大小、以及HotSpot VM的模式(客户端模式或者服务器模式)都是根据应用程序运行的平台或者操作系统自动选择的。这些自动化选项能够更好的匹配不同类型的应用程序的需要。当然比先前的发行版本中相比需要的命令行选项更少了。
  值得一提的是,一种新的回收器新的动态适配方法已经加入到并行垃圾收集器中。使用这种方法时,用户能指定期望的行为,并且垃圾回收器能动态的调整堆区域的大小以便试图与用户请求的行为取得一致。这种综合考虑了平台依赖的默认选项和使用用户期望的垃圾收集器的组合模式就叫做人体工程学。整个人体工程学的目标是使用最少的命令行来达到最好的JVM性能。

收集器自动选项的堆大小和虚拟机

一个服务器类型(Server模式)的机器定义如下:

  • 2个或者两个以上的物理处理器
  • 2g或者2g以上的物理内存
      这种服务器类型(Server模式)的定义适用于所有的平台。除了运行在32位Windows操作系统下。

      如果机器不是在服务器类型下的机器,默认的JVM,垃圾收集器和和堆大小如下:

  • 客户端模式的JVM

  • 串行收集器
  • 初始化堆大小4M
  • 堆最大64M
      在server模式的机器,JVM通常是Server模式,除非你想显示的使用client命令行来请求JVM使用client模式。在服务器类型的机器上运行JVM都是server模式的JVM,默认的垃圾收集器是并行收集器。否则是串行收集器。

      在server 类型的机器上运行任何一中JVM模式指定使用并行收集器,默认的初始化参数和最大堆得参数:

  • 初始化堆大小为物理内存的64分之一,上限是1G。注意最小的堆是32M。因为server类型的机器至少有2G内存,64分之一就是32M。
  • 最大的堆是物理内存的4分之一。上限是1G。

  否则,将按照非server类型的机器配置(4M初始堆内存和64M最大内存)默认的值通常会被命令行设置的值覆盖。有关选项在本文第八节说明。

基于行为的并发回收器的优化

  在J2SE 5.0版本中,添加了一种新的优化方法来并行垃圾收集器,它可以按照应用程序的期望的预期行为对垃圾进行收集。使用命令行选项指定所需的行为目标的最大暂停时间和应用程序吞吐量。

最大暂停时间

最大暂停的时间通过如下命令行设置:
-XX:MaxGCPauseMillis=n
这解释为提示并行收集器暂停时间被期望为n毫秒或者更少。并行收集器将调整堆大小和其他垃圾收集相关参数,试图保持垃圾收集停顿时间短于n毫秒。这些调整可能导致垃圾收集器来减少应用程序的整体吞吐量,并在某些情况下所需的暂停时间的目标不可能实现。
最大暂停时间的目标是分别适用于每一代。通常情况下,如果不满足我们的目标,代会被划分的更小以期望试图达到这个目标。没有默认设置最大暂停时间的目标。

吞吐量目标

  吞吐量目标是以一种测量垃圾收集花费的时间和和垃圾收集花费之外的时间(叫做应用程序花费时间)。这个目标可以使用如下命令行指定:
-XX:GCTimeRatio=n
  垃圾回收的时间和应用时间的比率:

  1/(1+ n)

  例如- xx:GCTimeRatio = 19设置了总时间的的5%的目标是垃圾收集的时间。默认的目标是1%(n = 99)。垃圾收集的时间是所有代的总时间。如果吞吐量的目标没有被满足,一代又一代的大小增加,以增加时间集合之间的应用程序可以运行。大的一代需要更多的时间来填满。

性能消耗的目标

  如果吞吐量和最大暂停时间都被满足,垃圾收集器会减少堆得大小直到其中一个目标不被满足(一般会是吞吐量)的临界值。未被达成的目标将会被放到一边。

目标优先级

  并行垃圾收集器首先试图满足最大暂停时间的目标。只有在最大暂停时间满足以后收集器解决吞吐量目标。同样,性能消耗目标是前两个目标已经达到之后才会思考的问题。

6.建议

  在前一节中描述的人体工程学中垃圾收集器,虚拟机以及堆大小的选择,对大部分应用程序是合理的。因此,最初的建议选择和配置一个垃圾收集器是什么都不做!即没有指定使用一个特定的垃圾收集器,等等。让系统根据应用程序正在运行的平台和操作系统自动选择。然后测试应用程序。如果它的性能是可以接受的,有够高的吞吐量和低暂停时间,这样就够了。你不需要排查垃圾收集器或修改选项。

  另一方面,如果您的应用程序似乎出现了与垃圾收集有关的性能问题,那么你能做的最简单的事情就是集合应用程序和平台特点,认为默认的垃圾收集器是否合适。如果不是,显式地选择你认为合适的收集器,然后看看是否成为可接受的性能。

你  可以使用如第7节中描述的那些工具测量和分析性能。根据结果,您可以考虑修改选项,比如那些控制堆大小和垃圾收集行为。一些最常用的具体选项部分8所示。请注意:最好的性能调优方法是测量第一,然后调整。(Measure using tests relevant for how your code will actually be used)。测试用例取决于你的代码如何运行。同时,谨防过度优化,因为应用程序的数据集,硬件,所以甚至垃圾收集器的实现!可能随时间改变。
  本节提供的信息选择一个垃圾收集器和指定堆大小。然后,提供建议,优化并行垃圾收集器,并给出一些建议关于如何处理outofmemoryerror错误。

何时选择不同的垃圾回收器

  在第四节中,介绍个每个回收器的适用情形。章节五描述了不同的平台上串行回收器和并行回收器的默认选择。如果你的应用程序或者环境特性与默认的回收器的适用情况不同,请使用以下的其中一个命令行选项来明确使用一个垃圾回收器:

   –XX:+UseSerialGC

   –XX:+UseParallelGC

   –XX:+UseParallelOldGC

   –XX:+UseConcMarkSweepGC

堆大小调整

  第五节告诉默认的初始和最大堆大小。这些默认大小可能对大多数情况来说会工作的很好,但是如果你的应用出现了性能问题(见第7节)或一个OutOfMemoryError(本节稍后讨论)并且已经通过分析确定是代或者整个堆大小的问题,您可以通过8节中指定的命令行选项修改大小。例如,默认最大堆大小64 mb的non-server-class机器通常是太小了,所以你可以通过- xmx选项指定一个更大的堆。除非你有长时间的暂停问题,尽量给予尽可能多的内存堆。吞吐量可用内存的数量成正比。有足够的可用内存影响垃圾收集的性能是最重要的因素。在已经决定你可以给整个堆的全部内存有多少后,然后您可以考虑调整各个代大小不同。第二个影响垃圾收集的性能最有影响力的因素是堆的年轻代比例。除非你找到老年代增长过快或这暂停时间过长的问题所在,尽量给予年轻代更多的多内存。然而,如果你使用的是串行收集器,不要给予年轻代超过总堆大小的一半的内存。

  当你使用一个并发的垃圾收集器,最好指定所期望的行为,而不是准确的堆大小值。让收集器自动和动态修改堆大小以实现这一行为,下面将讲到这一点。

并行收集器的调优策略

  如果(无论是自动或显式地)选择的是并行收集器或并行压缩收集器,然后为您的应用程序指定一个吞吐量目标(见第五节)。不要贸然的更改一个堆的最大值,除非你知道你需要一个大于默认最大堆大小的堆值。堆将增长或缩小规模来支持选择的吞吐量目标。堆得大小在初始化值和变化值之间的振荡是可以预期的。如果堆增长到最大值,在大多数情况下,这意味着在已经最大值的情况下还是不能达到吞吐量目标。此时为应用程序设置的最大的堆大小值来接*台上所有的物理内存但不包含引交换分区,再一次运行这个应用程序。如果吞吐量目标仍然没有被实现,那么应用程序的所期望的目标运行时间对于该平台上的可用的内存来说太高了。如果吞吐量目标可以被实现,但是暂停的时间太长了,会优先选择一个最大的暂停时间。选择一个最大的暂停时间意味着你的吞吐量目标将不会被实现,因此应当选择一个应用程序可以妥协的值。堆大小会在垃圾回收器试图满足相互竞争的目标之间进行摇摆,即使应用程序达到一个稳定的状态。这之间的压力来自达到吞吐量目标(这将会需要一个更大的堆)与最大暂停时间和最小内存需求(这将会需要一个更小的堆)。

如何处理内存溢出异常

  许多开发人员必须解决的一个常见的问题就是应用程序因为java.lang.OutOfMemoryError而终止。这个错误在没有足够的空间来分配一个对象时抛出。垃圾收集,不能分配任何进一步可用的的可用空间以适应一个新对象而且堆又不能进一步扩大。OutOfMemoryError错误并不一定意味着内存泄漏。这个问题可能是配置问题,例如如果指定的堆大小(如果未指定或默认大小)对应用程序来说是不够的。

  OutOfMemoryError错误诊断的第一步是检查错误消息。在抛出异常后,“java.lang.OutOfMemoryError”会提供进一步的信息。这里有一些常见的例子,额外的信息可能是什么,它可能意味着什么,以及如何应对:

Java堆空间

  这表明了一个对象无法在堆上分配。这个问题可能只是一个配置问题。你可以捕获到这个错误,例如,如果使用-Xmx命令行选项来指明最大的堆大小(或者是默认值)无法满足应用程序的需求。他也可能表明一个不再被使用的对象不被垃圾回收器回收,因为应用程序无意地保持了这些对象的引用。HAT工具(见章节七)可以用来观察所有的可达对象和明确哪一个引用来保持哪一个对象的存活。另一个潜在的错误来源有可能是在应用程序中过多地使用了 finalizers 以致于线程调用 finalizers 无法跟得上添加finalizers到队列的速度。Jconsole管理工具可以用来监控在销毁期间的对象的数目。

永久带空间

  这表明永久代已经满了。如前所述,堆得这部分区域是JVM的堆存储用来存储元数据的。如果一个应用程序加载大量的类,永久代就可能需要增加。可以通过指定的命令行选项-xx:MaxPermSize = n,其中n指定大小。

数组大小超过了VM的限制

  这意味着应用程序试图分配比堆大小的数组。例如,如果一个应用程序试图分配512 mb的数组但最大堆大小为256。 mb,那么就会抛出这个错误。在大多数情况下,问题很可能是堆大小是太小或应用程序中,数组大小被计算错误,使得数组大小很大。

  第7节中描述的一些工具可以用来诊断OutOfMemoryError问题。一些最有用的工具,这个任务是堆分析工具(HAT),jconsole管理工具,jmap工具组织选项。

分析回收器性能的工具

  有一系列的监控和诊断可以利用起来,来计算垃圾回收器的性能。本段提供了这些工具的简要说明。要获得更多的信息,请访问第九部分关于工具和故障排除的章节。

–XX:+PrintGCDetails 命令行

  一个最简单获取垃圾收集器初始化信息就是使用–XX:+PrintGCDetails命令。对于任何收集器来说,输出的结果包括了垃圾回收之前和之后各个代存活的对象的大小,每一个代可以用的空间以及垃圾回收所花费的时间。

–XX:+PrintGCTimeStamps 命令行

  输出垃圾回收器开始的时间戳。如果使用了PrintGCDetails的话还会输出一些额外的信息。时间戳能帮助你理清垃圾回收日志和其他日志事件的关系。

jmap

  jmap是一个命令行工具,包含在Solaris操作系统环境和linux(不包含windows)的Java 开发工具集(JDK)中。它会打印出运行中的JVM或者核心文件的内存相关统计数据。在不使用任何命令行选项的情况下,它会打印出所有被加载的共享对象,与Solaris的pmap工具相似的输出。对于更多的明确信息,可以使用 -heap,-histo,或 -permstat 选项。
  -heap 选项用来获取一些信息包含了垃圾回收器的名字,具体的算法细节(例如 并行垃圾回收器使用的线程数量),堆的配置信息,和堆的简单使用情况。

  -histo 选项可以用来获取堆上的类的直方图,对于每一个类,它会打印出堆中该类的实例数量,这些对象所占用的单位为字节的内存总数,和全合格的类名。当你试图理解堆的占用情况的时候,这个直方图会很有用。

  配置永久代的大小对于应用程序来说是很重要的,特别是动态加载一个很大数据量的类的时候(比如 java Server Pages(JSP)和web containers(web 容器))。如果一个应用程序加载了过多的类,那么将会抛出OutOfMemoryError。Jmap的 -permastat 选项可以用来获取永久代上的对象统计信息。

jstat

  jstat工具使用HotSpot JVM提供的内置仪器性能和运行应用程序的资源消耗信息。可以使用该工具在诊断性能问题,特别是与堆大小和垃圾收集有关的问题。它的一些关于垃圾收集许多选项可以打印统计行为和能力和使用不同的一代。

HPROF: Heap Profiler

  HPROF是一个简单的性能分析代理,附带在JDK 5.0中。它是一个动态链接库接口JVM使用Java虚拟机(JVM TI)工具界面。它写出概要信息到一个文件或一个套接字ASCII和二进制格式。这些信息可以进一步由前端工具分析器处理。
  HPROF能够提供CPU使用率,堆分配统计和监控争用配置文件。此外,它可以输出完整的堆转储和报告的所有Java虚拟机监视器和线程。HPROF是有用的在分析性能、锁争用内存泄漏等问题。参见9 HPROF链接文档。

HAT: Heap Analysis Tool

  堆分析工具(HAT)用来帮助调试无意地对象保留。这个术语用来描述一个不再被需要的对象由于被一个存活的对象所引用而保持存活。HAT提供了一个方便的手段来浏览对象在堆中的快照。这个工具允许一定数量的查询,包含“向我提供所有从根集合到对象的引用路径”,参见章节九的HAT文档链接。

8.与垃圾收集有关的关键选项

  许多命令行选项可以用来选择一个垃圾收集器,指定堆或代大小,修改垃圾收集行为,获得垃圾收集统计数据。本节显示了一些最常用的选项。更完整的列表和详细信息可用的各种选项,参见9。注意:你指定的数字可以以“m”或“m”mb,为千字节“k”或“k”,和“g”或“g”g。

垃圾收集器选项

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

垃圾回收器的统计分析选项

选项 描述
–XX:+PrintGC 输出每次垃圾收集的基础信息
–XX:+PrintGCDetails 输出每次垃圾回收的更多额外的信息
–XX:+PrintGCTimeStamps 输出每次垃圾回收事件开始的时间戳。使用–XX:+PrintGC 或者 –XX:+PrintGCDetails 来输出更多信息

堆和代大小

选项 默认 描述
–Xmsn 第五节 初始堆化大小,byte计数
–Xmxn 参见第五节 最大的堆大小,byte技术
–XX:MinHeapFreeRatio=minimum and –XX:MaxHeapFreeRatio=maximum 40最小,70最大 空闲空间占总空间比例的目标。这会运用于任何一代上。例如,如果最小值是30,并且空闲空间占该代上的空间比例小于30%,那么这个代空间就会扩展直到满足30%的空闲空间。近似的,如果最大值是60并且*空间的比例已经超过60%,代空间的大小就会收缩直到*空间只占到60%。
–XX:NewSize=n 平台依赖 默认年轻代的大小,byte计算
–XX:NewRatio=n Client JVM 为2,8为Server JVM 年轻代和老年代之间的比例。例如,如果n是3,那么Eden区的比例是1:3,合并后的大小和幸存者空间总大小的占年轻代和老年代四分之一。
–XX:SurvivorRatio=n 32 survivor区与Edon区的比例。例如,如果n是7,每个幸存者空间是年轻一代的九分之一(八分之一,因为有两个survivor空间)。
–XX:MaxPermSize=n 平台相关 永久带最大空间

并发或者并发压缩收集器的选项

选项 默认值 描述
–XX:ParallelGCThreads=n CPU的数量 垃圾收集器线程的数量
–XX:MaxGCPauseMillis=n 没有默认值 表明期望暂停时间少于n毫秒
–XX:GCTimeRatio=n 99 垃圾收集花在总时间的比例(1/(n+1))

CMS收集器选项

选项 默认值 描述
–XX:+CMSIncrementalMode 禁止 支持并发模式阶段逐步完成,并发阶段定期停止回收以便应用程序继续运行
–XX:+CMSIncrementalPacing 没有默认值 表明期望暂停时间少于n毫秒
–XX:ParallelGCThreads=n CPU的数量 年轻代的垃圾收集器和线程数和老年代并发收集器并发部分的线程数。

9.更多信息

Hotspot垃圾回收和性能调优

  • Java HotSpot中的垃圾收集

((http://www.devx.com/Java/Article/21977))

人体工程学

Server类型的侦探

(http://java.sun.com/j2se/1.5.0/docs/guide/vm/server–class.html)

垃圾收集器的人体工程学

(http://java.sun.com/j2se/1.5.0/docs/guide/vm/gc–ergonomics.html)
Java 5.0 虚拟机的人体工程学
(http://java.sun.com/docs/hotspot/gc5.0/ergo5.html)

选项

  • Java Hotspot VM 选项

(http://java.sun.com/docs/hotspot/VMOptions.html)

  • Solaris 和 Linux 选项

(http://java.sun.com/j2se/1.5.0/docs/tooldocs/solaris/java.html)

工具和问题诊断

Java2平台,5.0版本-问题定位和诊断指南
(http://java.sun.com/j2se/1.5/pdf/jdk50_ts_guide.pdf )

HPROF: A Heap/CPU Profiling Tool in J2SE 5.0
(http://java.sun.com/developer/technicalArticles/Programming/HPROF.html)
HAT:堆分析工具
(https://hat.dev.java.net/)

析构

如何处理JAVA析构中的内存留用问题:

(http://www.devx.com/Java/Article/30192)

杂项

• J2SE 5.0 发行版注解

(http://java.sun.com/j2se/1.5.0/relnotes.html)

• JavaTM 虚拟机

(http://java.sun.com/j2se/1.5.0/docs/guide/vm/index.html)

• Sun JavaTM 实时 System (Java RTS)

(http://java.sun.com/j2se/realtime/index.jsp)

• 关于垃圾收集的通用书籍:

Garbage Collection: Algorithms for Automatic Dynamic Memory Management by Richard Jones and Rafael Lins, John Wiley & Sons, 1996.

额外阅读-垃圾回收算法:

  任何一种垃圾收集算法一般要做2件基本的事情:(1)发现无用信息对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。
HotSpot使用的垃圾回收算法为分代回收算法(Generational Collector)
   大多数垃圾回收算法使用了根集(rootset)这个概念(有了这个概念应该就能解决面试中被问到的互为引用的孤独岛的情况);所谓根集就是正在执行的java程序可以访问的引用变量的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。垃圾收集首选需要确定从根开始哪些是可达的和哪些是不可达的,从根集可达的对象都是活动对象,它们不能作为垃圾被回收,这也包括从根集间接可达的对象。而根集通过任意路径不可达的对象符合垃圾收集的条件,应该被回收。下面介绍几个常用的算法。

1、引用计数法(referencecountingcollector)

引用计数法是唯一没有使用根集的垃圾回收得法,该算法使用引用计数器来区分存活对象和不再使用的对象。一般来说,堆中的每个对象对应一个引用计数器。当每一次创建一个对象并赋给一个变量时,引用计数器置为1。当对象被赋给任意变量时,引用计数器每次加1。当对象出了作用域后(该对象丢弃不再使用),引用计数器减1,一旦引用计数器为0,对象就满足了垃圾收集的条件。

基于引用计数器的垃圾收集器运行较快,不会长时间中断程序执行,适宜地必须实时运行的程序。但引用计数器增加了程序执行的开销,因为每次对象赋给新的变量,计数器加1,而每次现有对象出了作用域生,计数器减1。

2、tracing算法(tracingcollector)

   tracing算法是为了解决引用计数法的问题而提出,它使用了根集的概念。基于tracing算法的垃圾收集器从根集开始扫描,识别出哪些对象可达,哪些对象不可达,并用某种方式标记可达对象,例如对每个可达对象设置一个或多个位。在扫描识别过程中,基于tracing算法的垃圾收集也称为标记和清除(mark-and-sweep)垃圾收集器.

3、compacting算法(compactingcollector)

   为了解决堆碎片问题,基于tracing的垃圾回收吸收了compacting算法的思想,在清除的过程中,算法将所有的对象移到堆的一端,堆的另一端就变成了一个相邻的空闲内存区,收集器会对它移动的所有对象的所有引用进行更新,使得这些引用在新的位置能识别原来的对象。在基于compacting算法的收集器的实现中,一般增加句柄和句柄表。

4、coping算法(copingcollector)

   该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它开始时把堆分成一个对象面和多个空闲面,程序从对象面为对象分配空间,当对象满了,基于coping算法的垃圾收集就从根集中扫描活动对象,并将每个活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。

   一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象面和空闲区域面,在对象面与空闲区域面的切换过程中,程序暂停执行。

5、generation算法(generationalcollector) 分代回收算法(Generational Collector)

stop-and-copy垃圾收集器的一个缺陷是收集器必须复制所有的活动对象,这增加了程序等待时间,这是coping算法低效的原因。在程序设计中有这样的规律:多数对象存在的时间比较短,少数的存在时间比较长。因此,generation算法将堆分成两个或多个,每个子堆作为对象的一代(generation)。由于多数对象存在的时间比较短,随着程序丢弃不使用的对象,垃圾收集器将从最年轻的子堆中收集这些对象。在分代式的垃圾收集器运行后,上次运行存活下来的对象移到下一最高代的子堆中,由于老一代的子堆不会经常被回收,因而节省了时间。

6、adaptive算法(adaptivecollector)

在特定的情况下,一些垃圾收集算法会优于其它算法。基于adaptive算法的垃圾收集器就是监控当前堆的使用情况,并将选择适当算法的垃圾收集器。

java 8 中的垃圾收集

java8从Hotspot JVM中删除了永久代,所以我们不再需要为永久代设置大小,也就是不用设置PermSize和MaxPermSize。
在java8之前方法区是作为堆的永久代来实现的,启动JVM时我们需要设置永久代的大小,垃圾回收器也要回收这部分区域,而且会抛出内存溢出异常。借鉴于JRockit虚拟机,java8之后 Hotspot 虚拟机从堆中彻底删除了永久代。
—把方法区中的String和静态变量移到了堆中。
—把其他的东西(比如类结构)放到了本地内存中,JVM会直接负责这部分的内存回收。

总之,我们不再需要设置PermSize和MaxPermSize;方法区的内存溢出将不再出现,除非本地内存耗光。

本文引用自:https://juejin.im/post/58fca9465c497d00580068ff