java Hotspot 内存管理白皮书(中文翻译)
转自: http://my.oschina.net/u/568779/blog/166891
1引言
一个健壮的 Java™2平台,Standard Edition (J2SE™)拥有一个自动内存管理机制,它为开发者们屏蔽了复杂的内存管理步骤。
本文提供了一个关于java Hotspot 虚拟机中内存管理机制的简单概述,它描述了一个可用于垃圾回收的内存管理器,并且提供了关于选择和配置一个回收器以及设置内存区域大小的回收操作。它同样可以作为一个参考书,本文列举了与垃圾回收器行为相关的一些最常用的方法,并且描述了他们之间千丝万缕的关系。
第二章节中,我们为读者展示最新的自动内存管理器的概念,在那里我们将讨论手动为数据分配内存空间对于程序员的好处。
第三章节中,我们将简述通常的垃圾回收器的概念,设计的选择,以及性能指标。本章节中,我们还引入了称之为“代”的,一个常用的内存组织方式,它将基于对象的预期寿命来将不同的对象划分到不同的区域中。这种按照代来划分的方式已经被证明了能够有效地减少垃圾回收器的暂停时间和让每一个应用拥有一个良好的性价比。
在剩余的章节中,我们将会提供HotSpot的一些信息。在第四章节中,我们将描述 javaSE 5.0 update 6中已经存在的4种垃圾回收器,和他们的内存管理方式,还总结了每一种类型的垃圾回收器的最佳应用场合。
第五章节描述了javaSE 5.0 如何为基于操作系统的,运行于虚拟机上的应用程序自动选择垃圾回收器,设置堆大小,以及动态内存回收如何满足用户的期望。我们称这个技术为人体工程学。
第六章提供了配置和选择一个垃圾回收器的建议,并且提供了一些处理OutOfMemoryError异常的方法。
第七章简单介绍了一些用来评估垃圾回收器性能的工具。
第八章列出了一些涉及到垃圾回收器的选择,和行为的常用的命令行选项。
最后的第九章提供了一些本文涉及到的文档的链接。
2手动VS自动内存管理
在内存管理过程中,我们认识到当我们分配的对象已经不再被使用的时候,我们就要释放这个对象所占用的内存空间,(标记这块内存区域)为后续的内存分配让出空间。在一些编程语言中,内存管理是程序员的职责(由程序员自己来管理内存的使用),但是由于这个任务的复杂性会导致许多常见的错误,可能会导致意外或错误的程序行为以及崩溃。基于以上的原因,大部分的开发时间往往都耗费在调试和纠正这种错误上。
在手动管理内存的过程中,一个空悬的引用通常会造成这样的问题。当这个对象占用的内存空间已经被释放掉的时候,其他的对象可能还拥有这个对象的引用。如果其他的对象试图访问原始的对象,但是原来的空间已经被分配给新的对象,那么结果就是不可预知的。
手动内存管理还存在另一个常见的问题是内存泄漏,这些泄漏发生在内存已经被分配并不再被引用但是却没有被释放的时候。例如,如果你打算释放一个链表所引用的所有空间,但是你错误地只是释放了链表的第一个元素,剩下的列表元素虽然不再被引用,但是他们却离开了整个程序的控制范围,他们所占用的内存,既不可能被再次使用也不可能被回收。内存泄漏发生的时候,程序可以继续运行,直到耗尽所有可用的内存。
一种称之为垃圾回收器的自动内存管理程序目前已经成为大多数现代面向对象的语言用来替代之前的内存管理方式。自动内存管理器提升了代码的抽象性和接口性以及可靠性。
垃圾回收器避免了引用悬空的问题,因为一个被引用的对象将永远不会被垃圾回收器收回,所以他所占用的内存就不会被视为空闲区。垃圾回收器也解决了内存泄漏的问题,因为它会自动释放所有不再被引用的对象所占用的内存。
3垃圾回收器的概念
一个垃圾回收器的职责有:
分配内存
确保任何被引用的对象保留在内存中
释放所有在执行的代码中不可达的引用所指向的对象所使用的内存
我们称一个被引用的对象称之为存活的对象,一个不再被引用的对象称之为死亡对象,并标记为垃圾。一个为对象寻找和释放其所占用的内存空间的过程,我们称之为垃圾回收。
垃圾回收器只能解决一部分而不是所有的内存分配问题。例如,你可以创建一个对象,无限期地引用他们,直到没有更多可用的内存。垃圾回收器本身也是一个既占用时间也消耗资源的复杂任务。
垃圾回收器处理着一个用于组织内存的分配和释放空间的精确的算法,并且向程序员屏蔽掉这一个过程。空间通常来自于一个称之为堆的内存池中。
垃圾收集的时间由垃圾收集器决定,通常情况下,当整个堆或者他的子集被填满或达到了某个百分比占用的阀值的时候会发生垃圾收集事件。
为了分配请求的任务,寻找一块具有一定规模的未使用是的内存块是一个难题,最常用的内存分配算法的主要问题是避免内存碎片,同时保持高效地分配和释放。
理想的垃圾收集器的特点
一个垃圾回收器应当是既安全又全面的。这意味着,存活的数据永远不会被错误的释放,
并且垃圾不应超过一定收集周期之后仍然没有被收集。同时也意味着垃圾回收器应该是高效的,不应当使用应用程序的暂停来完成他的垃圾回收工作。
然而,在大多数实时操作系统中,时间,空间和频率往往是相互取舍的,举个例子,如果堆空间过小,收集工作会更快,但是堆也会被迅速地填满,因此需要更频繁的收集操作,相反地,一个足够大的堆空间,需要更长的时间来填满,因此收集频率会更低,但是收集工作会耗费更多的时间。
理想的垃圾回收器另一个的特点是碎片的限制。当内存上的垃圾对象被释放的时候,释放出的空闲区域可能会是一小块一小块的不连续空间,这样会导致任何一个连续的区域中没有一个区域有足够的空间用于分配一个大的对象。有一种消除碎片的方法称之为压缩,我们将会在下面的各种垃圾回收器的设计中讨论到。
可扩展性也很重要,在多处理器的操作系统上,分配操作不应当成为多线程应用程序的瓶颈,收集操作也是如此。
设计选择
当我们设计或者选择一个垃圾回收器算法的时候,我们必须从以下选项中作出一个选择。
串行或者并行:
在串行回收中,同一时间只能有一件事情发生。例如,即使当多个CPU可以用的时候,也只有其中的一个用于执行回收操作。当我们使用并行收集的时候,垃圾回收任务可以分割成几个部分,在不同的CPU中,同时并行执行这些子部分。并发操作可以更迅速地完成收集到错,不惜牺牲掉一些额外的复杂性和潜在的碎片。
并发或者停掉整个世界:
当通过“停止世界”的方式来运行垃圾回收器,垃圾回收器会在收集过程中暂停应用程序。另外,一个或者多个垃圾回收器任务可以并发执行,就是说可以与应用程序一起同时运行。通常情况下,一个并发的垃圾回收器可以并发执行大多数的工作,然是同时也可能偶尔需要做短时的“停止世界”的暂停操作。“停止世界”的垃圾回收器是简单的并发回收器,因为堆在收集过程中是被冻结且对象不能改变的,但是他的缺点是,我们可能不希望某一些应用程序在运行过程中被暂停。相应的,暂停时间较短的并发进行垃圾收集器,但是收集器必须格外小心,因为在同一时间,应用程序更新操作的对象和垃圾收集器操作的对象可能正在同时进行。这样会增加并发回收器的运行开销 ,这会影响到收集器的性能和需要更大的堆。
压缩 VS 不压缩 VS 拷贝:
在垃圾回收器已经决定哪些内存中的对象是活着的,哪些是垃圾之后,他就可以开始压缩内存了,移动所有存活的对象到一起(移动到一个连续的空间中),并且完全回收其余的内存。压缩之后,我们可以非常容易和快速地将一个新的对象分配在第一个空闲的内存位置上,并且利用一个简单的指针来跟踪对象的下一个位置来作为下一次分配的起始地点。与压缩收集器相反,非压缩收集器释放垃圾对象所占用的内存空间之后并不会像压缩收集器一样移动所有的存活对象来释放一块连续的空闲区域。这样做的好处是可以更快地完成垃圾收集,但是缺点是会造成潜在的碎片。通常来说,从内存就地释放的堆中分配内存比已经压缩过的堆中分配内存更昂贵。因为它必须从堆中搜索出一块连续并且足够大的内存来容纳新的对象。
第三种方法就是拷贝回收器,他复制或者调整活着的对象到另一个内存区域中,它的好处就是对象所在的旧内存区域可以被认为是空闲的内存区域,可以非常快速和容易地进行后续的分配,但缺点就是拷贝旧内存区域中的对象到新的区域中需要额外的时间和空间。
性能指标:
利用几个指标来估量垃圾回收器的性能,包括:
吞吐量—在一段足够长的时间内,减去垃圾回收器时间后剩余的时间与所占总体时间的比例(不包含垃圾回收器的时间)
垃圾回收器的开销—吞吐量的补数,也就是说,垃圾回收器所占时间与总时间的比例。
暂停时间—应用程序执行过程中,垃圾回收器执行时,应用程序暂定的时间。
回收效率--相对于应用程序的执行,回收操作发生的次数。
足迹—大小的度量单位,如堆大小。
迅速--一个对象变成垃圾和内存变成可用之间的时间
一个交互的应用程序可能需要短暂的暂停时间,然而对于非交互性的应用程序,整体的执行时间将会更重要。一个实时的应用程序会要求在任何时期,垃圾回收器暂停时间和花费在收集过程中的时间比例都要有一个最小的上限。一个足够小的空间会成为一个运行在小型个人电脑或者嵌入式系统的主要关注点。
代回收
当一个被称之为代回收的技术被使用的时候,内存将会根据代来划分,也就是说,将不同年龄的对象划分到不同的对象池中。例如,最常用的配置含有两代,一个是年轻代,一个是老旧代。
在执行垃圾回收的时候,不同的代会使用不同的算法,不同的算法是基于不同代的特点进行优化的。通常的垃圾回收器会利用这些观察结果,比如弱代假说,它被应用于许多编程语言编写的应用程序,包含java编程语言:
大部分配给对象的内存,不会被长时间的引用(既不会存活很久),这意味着,这些对象 是英年早逝的。
很少存在从老一代到新一代的引用。
年轻代的垃圾回收会相对频繁、高效、快速,这是因为年轻代的内存空间通常比较小并且被认为其中的大部分对象不会存在一个长时间的引用。
一些年轻代中的对象会存活下来,最终促进收集器将他们放入到老一代当中。见图一,老一代的回收器比年轻代的大并且增长相对缓慢得多,因此,老一带的回收行为比较难以发生,但是一旦发生了,就会耗费比较长的时间来完成。
为年轻代选择的垃圾回收器算法通常是速度优先的,因为年轻代的回收速度非常频繁。另一方面,老一代的回收器通常使用更节省内存的算法,因为老一代占据了大部分的堆并且老一代算法必须在垃圾密度低情况下的工作得很好。
4 Java SE 5.0 HotSpot JVM 的垃圾回收器
java HotSport 虚拟机包含了4种垃圾回收器,比如java SE 5.0 update 6.这些垃圾回收器都是基于代垃圾回收器的。在这一章节中,我们将讨论不同类型的代回收器,并且讨论为什么对象分配通常是快速和高效的。下面会提供每一种收集器的详细信息。
HotSpot的代划分
Java HotSpot 虚拟机中的内存,被分配到三个代中:一个年轻代,一个老旧代,一个永久代。大部分的对象将被初始化并分配在年轻代中,老旧代中包含的对象来自于部分存活的年轻代,还有一些大的对象也会被直接分配到老一代中。永久代持有的对象是方便于垃圾回收器管理的对象,比如类和方法的描述,以及自己的类和方法。
年轻代包含一个被称之为Eden的空间和两个较小的幸存空间,如图2,大部分的对象都被初始化分配在Eden区域中(正如前面所说,一些比较大的对象可能会直接分配在老一代),幸存空间至少保持一个年轻代收集器运行时候,幸存下来的对象,这些对象被赋予更多的死亡机会之前,被认为“足够老”,则会被晋升到老一代。在任何给定的时间汇总,生存空间中的一个保持着所有“幸存”的对象,而另一个则是空置的,直到下一次回收器开始工作。
垃圾回收器的类型
当年轻代的空间被填满的时候,年轻代的回收器(有时被称为次要的回收器)就会在此时开始工作。当老一代或者永久代的空间被填满的时候,所有的垃圾回收器就会开始工作,这就是说,所有代上的回收器都会开始工作。,一般情况下,年轻代的垃圾回收器会先开始工作,
使用的收集算法是专门为年轻代进行设计的算法,因为年轻代需要一个最高效的算法来识别年轻代中的垃圾对象。然后,老一代和永久代上的垃圾回收器将根据为他们设计的算法开始进行工作。如果选择了压缩空间,那么每一代上的空间都将被压缩。
有的时候老一代的空间太满以至于不能接受来自于年轻代中转移过来的所有对象,特别是年轻代的回收器是优先工作的时候。在这种情况下,除开CMS之外的所有回收器,在年轻代上的回收算法都被停止执行,作为替代,老一代的回收器算法通常会在整个堆中执行(老一代上的CMS回收器是一个特殊的回收器,因为他无法在年轻代上展开回收工作)。
快速分配
正如你所看到的如下所述的垃圾回收器,在多数情况下,内存上存在着大量连续的内存块来分配给对象。使用一个简单的凹凸指针技术在这些块上分配对象是高效的,这就是说始终保持跟踪在之前分配的对象后面。当一个新的分配请求需要执行的时候,所有需要做的事情就是检查代上剩余的内存是否满足需要分配的对象的要求,如果满足,则更新指针的位置,并且初始化对象。
对于多线程的应用程序,分配操作必须是线程安全的。如果使用一个全局锁来保证这一件事,那么在代上分配空间将成为一个瓶颈,并且降低性能。为了解决这个问题,HotSpot JVM 使用一个称之为 线程本地分配缓存(Thread-Local- Allocation Buffer)的技术,它给每一个线程提供一个独有的缓存(一小部分的代空间),来提升多线程分配的吞吐量。由于每一个TLAB上只有一个线程进行分配操作,分配操作可以利用凹凸指针,无需使用任何锁定,使操作迅速到位。只有在少数情况下,当一个线程的的TLAB填满和需要一个新的TLAB的时候,必须使用同步来予以确认。由于使用了TLAB技术,使得(JVM)最大限度地减少了内存浪费。例如,由选择器分配大小的TLAB,平均浪费的空间小于Eden的1%。结合使用TALB和 基于凹凸指针的线性分配技术,使整个分配生效,仅仅只需要大约10个本地指令。
串行回收
使用串行回收器,在“暂停世界”策略中,年轻代和老旧代之间的操作是串行完成的(使用一个单一的CPU)。这意味着,回收器工作的时候,应用程序应当停止执行。
使用串行回收器的年轻代
图3展示了一个使用串行回收器的年轻代的工作过程。Eden中存活的对象被复制到一个刚初始化而且空着的幸存空间(图中的To 标签),除非那些太大的对象而无法放到To空间中,这样的对象直接复制到老一代的空间之中。那些已经在幸存空间中占用空间的还活着的对象(from 标签)还相对年轻的话将被拷贝到另一个幸存空间中,当这些活着的对象相对老的话则拷贝到老一代的空间中。注:如果To空间已经变满,那些来自Eden和From的活对象将不会被拷贝到To中而是放入年老区中,不管他们在多少次的年轻代回收操作中幸存下来。任何剩余在Eden 或者 From 空间的对象在活着的对象被拷贝之后,根据定义他们都是“死”对象,我们不需要去检查他们。(在图中垃圾回收器会给他们打上叉标记,虽然在事实上回收器不会去检查和标记这些对象)。
在年轻代的垃圾回收器结束之后,Eden 和 From空间已经清空,仅仅是To空间中包含活着的对象,在这一点上,幸存空间充当着一个交换空间的角色。
老旧代上的串行垃圾回收器
老旧代和永久代上的串行垃圾回收器使用一种叫 标记-清理-压缩(mark-sweep-compact)的算法。在标记阶段,收集器识别哪些对象是存活的。清理阶段,回收器将扫描整个代上的空间,识别出垃圾对象。随后,回收器执行滑动压缩,移动或者的对象前进到老旧代空间的前面部分(永久代也是如此),在空间的尾部留下一块连续的空间块。如图5,压缩操作使得老旧代和永久代上,使用凹凸指针就能快速地完成分配操作。
什么时候使用串行回收
串行回收器是大多数运行在客户端的机器上的应用程序的选择,而且他们没有低暂停时间的要求。在今天的硬件中,串行回收器可以非常高效地管理大多数拥有64MB堆空间和全收集最坏情况下的暂停时间少于半秒 的重要应用程序。
串行回收器的选择
在j2SE5.0版本中,串行回收器会被没有指名“server-class”的机器自动选择作为默认的垃圾回收器,这将会在第五章节中说到。在其他的机器上,串行回收器同意通过使用 -XX:+UseSerialGC命令行选项来明确使用。
并行回收器
在今天,许多java 应用程序运行在拥有足够多的内存和多核心的cpu机器上,并行回收器,也叫吞吐回收器,被设计用来发挥多个cpu的特点来承担单个CPU中垃圾回收器的工作。
使用并行回收器的年轻代
年轻代上的并行回收器使用的并行算法借鉴了串行回收器。它仍然是一个使用了“停止世界”和复制的回收器,但是年轻代上的并行回收器在执行过程中使用到了多个cpu,降低了垃圾回收器的时间上限,并且增加了应用程序的吞吐量。图6展示了年轻代上串行回收器和并行回收器之间的不同之处。
使用并行回收器的老旧代
老旧代上的并行垃圾回收器使用和串行回收器一致的 标记-清理-压缩(mark-sweep-compact) 算法。
什么时候采用并行选择器
并行回收器适合那些运行在拥有多个CPU的机器上且对暂停时间没有限制的应用程序,虽然这种情况很少,但是有可能会很长,老一代的回收器将仍然工作。那些合适使用并行回收器的应用程序的例子有,批处理,记账,工资单,科学计算等等。
你应当考虑选择并行压缩回收器(在下面描述)来替代并行回收器,因为 form空间执行的并发回收操作是在多个代空间之间的,不仅仅是在年轻空间。
并行空间的选择
在J2SE 5.0 发行版,在server-class机器上并行回收器是默认的垃圾回收器,在其他的机器中,你可以通过执行 -XX:+UseParallelGC 命令行选项来明确打开并行选择器
并行压缩选择器
并行压缩选择器是J2SE 5.0 update 6中引入的,它和并行选择器的区别是它在老旧代上的垃圾回收上采用了一个新的算法。注:最后,并行压缩选择器终将要替代并行选择器。
使用并行压缩算法的年轻代
年轻代上的并行压缩算法采用和年轻代上的并行算法是一样的。
使用并行压缩算法的老旧代
使用并行压缩算法的老旧代和永久代在回收时使用 “停止世界”,多数并发出现在滑动压缩上。选择器采取三个阶段。首先,每一代的空间在逻辑上划分为固定大小的区域。在标记阶段,存活对象的初始集合 应用程序代码中,可达的存活对象,在垃圾回收器的线程之间,然后所有的存活对象会在这个阶段中标记出来。当一个对象被标记为存活,它的所在的区域的数据将会被更新,更新的内容是这个对象的大小和位置。
总结阶段的操作是基于区域,而不是对象。由于在上一次的压缩中,每一代的左侧区域通常是密集的,包含着大部分存活的对象。一些可以被清空的密集区域上进行压缩操作是不值得的。所以总结阶段要做的第一件事是检查区域中的密度,从最左边的一个开始,直到它到达一个点,这个点所在的空间可以从其所在的区域中清空和那些空间中的右侧是值得压缩的区域。在这个点的左侧区域都打上密集的前缀,并且不会移动这些区域上的任何一个对象。这个点的右侧区域都将被压缩,消除所有的死亡空间。总结阶段统计并且存储每一个区域的第一个存活对象的第一个字节的新位置。注:总结阶段当前被实现为串行阶段;并行是可行的,但是对于标记和压缩阶段并行不是性能的重要部分。
在压缩阶段,垃圾回收器线程使用总结阶段的数据来确定被填充的区域,然后线程可以独立地拷贝数据到区域中。这会造成堆的前面是密集的对象,后面是空闲的块。
什么时候使用并行压缩回收器
在不止一个cpu的机器上运行的应用程序适合使用并行压缩回收器。在老旧区域上的并行操作降低暂定时间并且使得有暂停时间限制的应用程序上并行压缩的选择器比并行选择器更合适。并行压缩选择器不适合那些运行在大型共享机器上的应用程序,因为在那些机器上面没有任何一个单独的应用程序可以独占好几个CPU,这个会延长时间的周期。在这样的机器上,要么减少垃圾回收器使用的线程个数(使用–XX:ParallelGCThreads=n这个选项)要么选择别的回收器。
如何选择并行压缩回收器
如果你要使用并行压缩回收器,你必须明确地调用 -XX:+UseParallelOldGC 这个命令行选项。
并发 标记-清除(CMS)回收器
对于一些端对端的应用程序来说,吞吐量并以是一个快速响应的一个重要因素。年轻代回收器通常不会造成一个长时间的暂停。然而老旧代回收器在少数情况下可能会导致一个长时间的暂停,特别是涉及到一个很大的堆的时候。为了解决这个问题,HotSpot JVM 包含了一个称之为 并发标记-清除(CMS)的回收器,也叫低延迟回收器。
使用CMS的年轻代回收器
CMS回收器在年轻代上的操作和并发回收器所做的一样。
使用CMS回收器的老旧代
应用程序上老旧代的大多数回收操作使用CMS回收器来并发完成。
CMS回收器的回收周期以一个称之为初始化标记的短暂暂停开始,它确定在程序代码中可达的存活对象的集合。接着,在整个并发标记阶段回收器将根据这个集合,标记出所有或者的的对象。因为在并发标记阶段期间,应用程序会运行和更新引用,因此在并发标记阶段无法保证所有的存活对象都会被标记。为了处理这个问题,应用程序会再次暂停,称之为再标记,此时将访问和标记那些在并发标记阶段被更新的的对象。因为再标记的暂停比初始标记更可靠,这回提升多线程会并发执行的效率。
再标记阶段的末尾,堆中所有的存活对象可以保证都被标记了,于是随后的并发清除阶段将会回收所有被定义的垃圾。图7展示了老旧代上使用串行标记-清除和并行标记-清除回收器的不同点。
因为一些任务,比如再标记阶段中再次访问对象,提升了回收器要完成的工作数量,这同样会提升了上限。这是一个大多数的回收器试图降低暂停时间的典型权衡。
CMS回收器是唯一一个不采用压缩的回收器。这意味着,在清空死亡对象占用的空间之后,它不会移动存活对象到老旧代的一端。如图8
这样会节约时间,但是因为空闲空间不是连续的,回收器将不再能使用一个简单的指针来标明下一个待分配对象可以使用的空闲位置。为了解决它,我们现在要使用一个空闲空间的链表。也就是说,他需要创建一些指向内存中尚未分配区域的链表,每当需要为一个对象分配空间的时候,一个适当的链表(基于内存所需的数量)必须搜索一个足够大的区域来持有这个对象,正因如此,在老旧区域上分配空间是一个相对简单凹凸指针技术更昂贵的操作。这同样会额外提升年轻代上回收操作的上限,当来自年轻代上的对象提升到老旧代上的时候,这样的分配会大量地出现。
另一个CMS回收器的缺点是,它需要一个比别的回收器更大的堆需求。考虑到应用程序会在标记阶段继续运行,它将继续分配内存,这会导致老旧代的持续增长。虽然收集器保证在标记阶段定义出所有的存活对象,在这个阶段一些对象会成为垃圾并且不能被再次利用直到下一次回收器工作,这些对象被成为 漂浮垃圾。
缺少压缩过程,最终还是会出现碎片。为了处理碎片,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 虚拟机(不管是客户端,还是服务器)都是根据应用程序所运行的平台和操作系统进行自动选择的。当不设置任何命令行选项的时候,自动选择的结果会更适合各种类型的应用程序。
另外,并行垃圾回收器添加了一个新的动态调整回收。依靠这个功能,只要用户指定期望的行为,垃圾回收器器动态调整每一个区域的堆大小,来符合行为的需求。这种组合了依靠平台来默认选择和依靠预期行为垃圾回收器调整的东西被叫做人工智能。人工智能的目的就是为最小化命令行调整的JVM提供一个优异的性能。
回收器,堆大小和虚拟机的的自动选择
一个服务器类的机器将根据以下之一的条件进行定义
2个或2个以上物理处理器
2个或2个以上千兆字节的物理内存
这个服务器类机器的定义适用于所有平台,除开window操作系统上运行的32位平台。
在非服务器类的机器上,默认的JVM选项,垃圾回收器,和堆大小如下
客户端JVM
串行垃圾回收器
初始堆大小为4MB
最大堆大小为64MB
在服务器类服务器,JVM将永远使用服务器JVM直到你显示指定-client 命令行选项来使用客户端JVM,在一个服务器类机器上运行服务器JVM,默认的垃圾回收器是并行回收器,否则默认的回收器是串行回收器。
在一个服务器类机器上使用并行垃圾回收器来运行这些JVM(客户端JVM或者服务器JVM),堆的默认初始值和最大值如下
初始堆大小是物理内存的1/64 ,最多是1GB。(注意:;最小的初始堆大小是32MB,因为一个服务器类的机器定义的是最少含有2GB的内存,并且2GB的 1/64 是32MB)
堆最大是物理内存的 1/4,最多1GB。
另外,这些默认值同样适用于 非服务器类机器(初始化4MB堆大小和最大64MB堆大小)。默认值可以通过命令行选项来重定义,相关选项在第8章给出。
调节并行回收器的默认行为
在j2SE5.0的发行版本中,添加了一个调节并行回收器的新方法,基于应用程序对于垃圾回收器的期望行文,使用命令行选项来指定这个期望的行为来达成最大暂定时间和应用程序吞吐量的目标。
最大暂停时间目标
最大暂停时间目标的指定使用下面的命令行选项
-XX:MaxGCPauseMillis=n
这个解释将对并行回收器的暂定时间进行提示,最大暂停时间是n秒或者少于n。并行回收器将调整堆大小和其他的垃圾回收相关的选项来试图保持垃圾回收器的暂停时间小于n秒。这个调整会导致垃圾回收器降低应用程序的全局吞吐量,在一些情况下预期的暂停时间目标可能无法被实现。
最大暂停时间的目标策略分别被运用在每一个代上。一般地,如果目标没有被达成,代会变得更小来试图达成目标。默认最大暂停时间没有被设置。
吞吐目标
最大吞吐目标意思是依据花费在垃圾回收和花费在非垃圾回收器(代指应用程序时间)上的时间。这个目标可以通过以下命令行选项来明确指定
- XX:GCTimeRatio=n
垃圾回收器和应用程序时间的比值是
1 / (1 + n)
例如:-XX:GCTimeRatio=19 设置的目标是垃圾回收器占用所有时间的5%,默认的目标是1%(n=99)。这个花费在垃圾回收器上的时间,是指所有代上垃圾回收器的时间总和。如果目标的吞吐量没有被完成,代空间将会增长使得在 两次垃圾回收工作期间,应用程序的运行时间得以增长。,一个巨大的代空间需要更多的时间来填满。
足迹目标
当吞吐量和最大暂停时间的目标被完成,垃圾回收器会降低堆的大小直到目标没有被完成。目标的完成边界就是足迹的到达点。
目标的顺序
并行回收器以完成最小暂停时间为第一目标,只有在这个目标完成之后才会去追求吞吐量的目标,足迹目标只有在前两个目标被完成后才开始考虑。
6建议
建议这一章补充了之前的章节介绍了自动配置垃圾回收器,虚拟机和堆大小的选择,覆盖了绝大部分的应用程序的合理配置。因此,初始的垃圾回收器的选择和配置什么都没做。这就是说,不需要特别指定垃圾回收器,注:让系统基于平台和操作系统为你的应用程序进行自动选择。然后测试你的程序。如果它的性能是可以接受的,拥有足够高的吞吐量和足够低的暂停时间。你就不需要修改垃圾回收器的选项。
另一方面,如果你的应用程序试图通过垃圾回收来获取高性能,那么你最优先做的事情就是思考根据你的应用程序和你的平台所提供的默认垃圾回收选择器是否是合适的。如果不合适,那么你要明确地选择一个你认为合适的选择器,并且查看你的平台是否兼容。
你可以使用章节七给出的工具来测量和分析性能。基于工具给出的结果,你可以考虑修改选项,例如控制堆大小或者垃圾回收器的行为。章节八中将给出一些通用的指定选项。请注意:最合适的性能调节,应当是先测量,然后再调节。测量的测试你实际上使用的代码相关。另外,请勿过度优化,因为应用程序的数据集,硬件等等,甚至垃圾回收器的实现都会随着时间的改变而改变。
这一章节提供了关于选择垃圾回收器和指定堆大小的信息。然后提供了调节并行垃圾回收器的选项,和提供了一些关于处理 OutOfMemoryErrors的建议。
何时选择一个不同的垃圾回收器
在章节四中,介绍个每个回收器的适用情形。章节五描述了不同的平台上串行回收器和并行回收器的默认选择。如果你的应用程序或者环境特性与默认的回收器的适用情况不同,请使用以下的其中一个命令行选项来明确使用一个垃圾回收器。
–XX:+UseSerialGC
–XX:+UseParallelGC
–XX:+UseParallelOldGC
–XX:+UseConcMarkSweepGC
堆大小
章节五描述了默认的初始和最大堆大小。这些值符合大多数的应用程序,但是如果你的性能分析出现了问题(见章节七)或者出现了OutOfMemoryError(将会在下面进行描述)显示你的问题出现在某一个代或者所有的堆的大小上,你可以通过章节八中提供的命令行选项来修改你的大小。例如,默认的最大堆大小是64MB这个在非服务器类机器上通常太小了,因而你可以通过 -Xmx 来指定一个更大的值。除非你的问题与长的暂停时间相关,不然你可以试图使用所有可以使用的内存。吞吐量与可以使用的内存成正比。拥有足够的可利用内存是影响垃圾回收器性能的重要因素。
在明确了你能够为所有的堆提供多少的内存之后,你可以开始考虑调整不同的代上的大小了。如下描述,让回收器自动和动态修改堆大小来完成这个行为。
调整并行回收器的策略
如果垃圾回收器的选择(自动或者明确使用)是并行回收器或者并行压缩回收器,那么开始并且指定一个满足应用程序的吞吐量目标(见章节五)。不要选择一个最大的堆大小除非你明白你需要一个比默认最大堆大小还大的堆。堆会自动增长或者收缩它的大小来支持选择的吞吐量目标。在初始化期间和改变应用程序的行为来符合期望的期间堆大小会有一个摆动。
如果堆大小增长到它的最大值,在绝大多数的情况下这意味着在当前的最大堆大小下吞吐量目标无法被实现。为应用程序设置最大的堆大小值来接近平台上所有的物理内存但不包含引交换分区。再一次运行这个应用程序。如果吞吐量目标仍然没有被实现,那么应用程序的目标运行时间对于该平台上的可用的内存来说太高了。
如果吞吐量目标可以被实现,但是暂停的时间太长了,选择一个最大的暂停时间。选择一个最大的暂停时间意味着你的吞吐量目标将不会被实现,因此应当选择一个应用程序可以妥协的值。
堆大小会在垃圾回收器试图满足相互竞争的目标之间进行摇摆,即使应用程序达到一个稳定的状态。这之间的压力来自达到吞吐量目标(这将会需要一个更大的堆)与最大暂停时间和最小足迹(这将会需要一个更小的堆)。
如何处理OutOfMemoryError
一个许多开发者经常碰见的问题是,应用程序因为java.lang.OutOfMemoryError而终止。这个错误在没有足够的内存来为对象进行分配的时候抛出来。这就是说,垃圾回收器不能找到更多的空间来容纳这个新的对象,并且堆无法进一步扩大。一个OutOfMemoryError 并不能直接确认是内存泄露的问题。这个问题也许是一个配置问题,例如指定的堆大小(如果不指定的话则是默认的)不能满足应用程序的需求。
诊断OutOfMemoryError的第一步是检查所有的错误信息。在异常信息中,进一步的信息会在“java.lang.OutOfMemoryError”之后提供。这里有一些常见的例子包含了这些额外信息的内容、意义和处理方式。
Java heap space(java 堆空间)
这表明了一个对象无法在堆上分配。这个问题可能只是一个配置问题。你可以捕获到这个错误,例如,如果使用 -Xmx命令行选项来指明最大的堆大小(或者是默认值)无法满足应用程序的需求。他也可能表明一个不再被使用的对象不被垃圾回收器回收,因为应用程序无意地保持了这些对象的引用。HAT工具(见章节七)可以用来观察所有的可达对象和明确哪一个引用来保持哪一个对象的存活。另一个潜在的错误来源有可能是在应用程序中过多地使用了 finalizers 以致于线程调用 finalizers 无法跟得上添加finalizers到队列的速度。Jconsole管理工具可以用来监控在销毁期间的对象的数目。
PermGen space(永久代空间)
这表明了永久代的空间已经满了。如之前的描述,这是JVM存储元数据的堆区域。如果一个应用程序加载一个很大数量的类,那么永久代会自动增长。你可以指明永久代的大小通过命令行选项 –XX:MaxPermSize=n ,n是指明的大小。
Requested array size exceeds VM limit(请求的数组大小超过虚拟机的限制)
这表明了应用程序试图分配一个数组空间大于了堆的大小。例如,如果一个应用程序试图分配一个512MB的数组,但是最大的堆大小只有256MB,那么这个错误将会被抛出。在大多数情况下,这个问题原因可能是堆大小太小或者是一个BUG导致了应用程序试图创建一个被计算错误的巨大数组。
在章节七中介绍的一些工具可以用来诊断 OutOfMemoryError 问题。在这些有用的工具中有一些是 堆分析工具(HAT Heap Analysis Tool),比如jconsole管理工具和使用-histo选项的jmap工具。
7评估垃圾回收器性能的工具
各种诊断和监测工具可以用来评估垃圾收集的性能,本章节提供了一些简要的概述来描述其中的一些工具,更多的信息请参见章节九中的“工具和故障排除”连接。
–XX:+PrintGCDetails 命令行选项
一个简单的用来获取回收器的初始信息的方法是指定 –XX:+PrintGCDetails 命令行选项。对于任意一个回收器,这个的输出信息包含了每一个代在垃圾回收之前和之后存活的对象大小,每一个代上的所有可用空间,回收器消耗的时间。
–XX:+PrintGCTimeStamps 命令行选项
这会输出每一个回收器的启动时间戳,另外如果你使用–XX:+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的简单分析代理。他是一个使用java虚拟机工具接口(Java Virtual Machine Tools Interface )的动态连接库接口。它会输出分析信息到一个文件或者套接字中,以ASCII 或者二进制的格式。这些工具可以进一步地使用一个前端分析工具来处理。
HPROF能够展示CPU的使用率,堆分配统计,和监控竞争配置。此外,它可以输出完整的堆垃圾和报告java虚拟机上所有的监控器和线程的状态。HPROF在分析新能,锁竞争,内存泄露和其他问题的时候很有用。参见章节九HPROF文档的连接。
HAT:堆分析工具(Heap Analysis Tool)
堆分析工具(HAT)用来帮助调试无意的对象保留。这个术语用来描述一个不再被需要的对象由于被一个存活的对象所引用而保持存活。HAT提供了一个方便的手段来浏览对象在堆中的快照。这个工具允许一定数量的查询,包含“向我提供所有从根集合到对象的引用路径”,参见章节九的HAT文档链接。
8垃圾收集相关关键选项
我们可以使用一些命令行参数选项来选择垃圾回收器,指定堆或者代上的大小,调整垃圾回收器的行为,和活的垃圾回收器的统计信息。这一章节展示了其中的最广泛使用的选项。对于关于多方面的有效选项的更多完整列表和详细信息,见章节九。注:指定的数量时以“m”或者“M”结尾,代表兆字节,“k”或者“K”结尾代表千字节,“g”或者”G”结尾代表千兆字节。
垃圾回收器的选择
选项 |
选择的垃圾回收器 |
–XX:+UseSerialGC |
Serial 串行 |
–XX:+UseParallelGC |
Parallel 并行 |
–XX:+UseParallelOldGC |
Parallel compacting 并行压缩 |
–XX:+UseConcMarkSweepGC |
Concurrent mark–sweep (CMS) 并发标记清除 |
垃圾回收器的统计
选项 |
描述 |
–XX:+PrintGC |
打印每一个垃圾回收器的基础信息 |
–XX:+PrintGCDetails |
打印每一个垃圾回收器更详细的信息 |
–XX:+PrintGCTimeStamps |
打印每一个垃圾回收器开始事件的时间戳。使用–XX:+PrintGC和–XX:+PrintGCDetails 可以在每一次垃圾回收器开始的时候打印这些内容。 |
堆和代的大小
选项 |
默认值 |
描述 |
–Xmsn |
See Section 5 |
堆的初始大小,单位为字节, |
–Xmxn |
See Section 5 |
堆的最大大小,单位为字节 |
–XX:MinHeapFreeRatio=minimum and –XX:MaxHeapFreeRatio=maximum |
40 (min) 70 (max) |
空闲空间占总空间比例的目标。这会运用于任何一代上。例如,如果最小值是30,并且空闲空间占该代上的空间比例小于30%,那么这个代空间就会扩展直到满足30%的空闲空间。近似的,如果最大值是60并且自由空间的比例已经超过60%,代空间的大小就会收缩直到自由空间只占到60%。 |
–XX:NewSize=n |
平台相关 |
默认的年轻代上的初始大小,单位为字节 |
–XX:NewRatio=n |
2 on client JVM, 8 on server JVM |
年轻代和老旧代的比例。例如,如果n=3,那么比例就是1:3并且Eden空间和幸存(survivor)空间一共占年轻代和老旧代的总空间的 1/4。 |
–XX:SurvivorRatio=n |
32 |
幸存空间和 Eden空间的比例,例如,如果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的个数 |
年轻代上的并行回收器的线程个数,和老旧代上的并行部分的线程个数 |
posted on 2013-11-14 03:01 Step-BY-Step 阅读(683) 评论(0) 编辑 收藏 举报