Java内存管理白皮书


新生代老生代指定参数
串行收集器stop-the-worldmark-sweep-compact-XX:+UseSerialGC
并行收集器stop-the-world(多线程)mark-sweep-compact-XX:+UseParallelGC
并行压缩收集器stop-the-world(多线程)mark(多线程)-总结(单线程)-compact(多线程)(分区域选择性压缩)-XX:UseParallelOldGC
并发标记清除收集器stop-the-world(多线程)initial-mark concurrent-mark remark concurrent-sweep-XX:+UseConcMarkSweepGC

1.   介绍

Java2平台的优点之一是它执行自动内存管理,从而把开发人员从复杂的显示内存管理中解放了出来。

       本文提供了一个全面的视角来介绍Sun的J2SE5.0 发行版在JavaHotSpot虚拟机中的内存管理机制。其中描述了现有的垃圾收集器如何执行内存管理工作,给出了一些关于如何选择和配置垃圾收集器以及设置存储区域大小的指导。同时本文内容也可以作为一份手册,列出了影响垃圾收集器行为的常用配置,另外提供了大量更为详细的文档资料的链接。

       本文第二章的目标读者是那些刚刚接触自动内存管理的技术人员。文中简单地讨论了相对于需要开发人员显示释放内存而言,自动内存管理有哪些优点。第三章介绍了一些基本的垃圾收集概念,设计选择以及性能指标。还介绍了一种通用的内存组织方式,该方法将内存划分为称为年代的不同的区域,而年代的划分是基于对象生命周期的长短。在降低垃圾收集的停顿时间和总的开销方面,这种按年代来划分内存的方式在大量的应用中被证明了是富有成效的。

       后续章节提供的内容针对的是HotSpot虚拟机。第四章描述了现有的四种垃圾收集器,包括一种在J2SE 5.0 update 6中新引入的收集器。文章介绍了这些垃圾收集器共同采用的一些内存组织方式。对于每一种收集器,归纳了其所采用的收集算法,以及何时应该选择某种特定的收集器。

       本文第五章描述了J2SE 5.0中的一种新技术,包括(1) 基于应用所运行平台和操作系统,对于垃圾收集器,堆大小,HotSpotVM(client或者server模式)的自动选择;(2) 基于特定的用户预期的动态垃圾收集器调优技术。这种技术被称为ergonomics。

       本文第六章提供了垃圾收集器的选择和配置推荐。同时提供了一些应对OutOfMemoryErrors的建议。第七章简要地介绍了一些评估垃圾收集器性能的工具,第八章列出了用于选择垃圾收集器和定制其行为的最常用的命令行选项。最后在第九章提供了更详细文档的链接,这些链接的内容覆盖了本文所讨论的各个方面。

2.   显示与自动内存管理

内存管理是识别,释放那些不再被引用的对象所占用的内存,并且让这些内存可以被后续的分配请求所使用的过程。在一些程序设计语言中,内存管理是程序开发人员的责任。这项工作的复杂性导致了一些常见的错误产生,而这些错误可以导致无法预期的和容易产生错误的程序行为,甚至是系统崩溃。结果是开发人员的很大一部分时间花费在调试和修复这些错误上面。

显示内存管理在程序中经常发生的一个问题是悬挂引用(dangling references)。有可能出现的情况是释放了一个被其他对象引用的对象的内存空间。如果引用对象试图访问被引用对象的内存空间,而此时该空间已经被分配给了一个新的对象,其结果是无法预期的。

显示内存管理的另一个常见问题是内存泄露。这些泄露发生在当某块内存已经被分配,但是既没有被引用也没有被释放。例如,你想释放被一个链表占用的空间,但是你犯了一个错误,仅仅释放了链表的第一个元素所占有的存储空间,链表的其余元素所占有的空间不再被引用,但程序开发人员却再也无法使用这些空间,这样的内存空间既无法被使用也无法被释放。如果足够的泄露发生,便可以持续的消耗内存直到所有的可用内存都被耗尽。

现在普遍使用的另一种内存管理方式,尤其是在现代面向对象语言中,是被一种被称为垃圾收集器的程序来自动管理。自动内存管理使得更加抽象的接口和更加可靠的代码称为可能。

垃圾收集避免了悬挂引用的问题,因为一个仍然在某处被引用的对象从不会被当成垃圾收回并且当成可用内存来分配。垃圾收集同时解决了内存泄露问题,因为垃圾收集自动释放所有不再被引用的内存。

3.  垃圾收集的概念

垃圾收集器的职责是

1.    分配内存

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

3.    回收从当前运行的代码的引用出发,不可达的对象所占用的内存空间

被引用的对象被称为存活的, 不再被引用的对象被认为是死亡的,又被称为垃圾。发现和回收被这些死亡对象所占用的内存空间的过程叫做垃圾收集。

垃圾收集解决了大部分,但不是全部,的内存分配问题。例如,你可以无限地创建对象,并且持续的引用它们,直到没有更多的内存可以分配。垃圾收集也是一项复杂的任务,会耗费大量的时间和资源。

内存的组织,分配和回收所使用的具体算法由垃圾收集器控制,对于程序开发人员是透明不可见的。空间一个很大的内存池中分配,这个内存池被称为堆。

垃圾收集的时机由垃圾收集器决定。典型情况下,整个堆或者其中一部分的垃圾收集工作是在该区域的内存耗尽或者达到一个使用阈值的情况下被触发。

满足一个内存分配请求是一项困难的任务,其中包括在堆中寻找一块特定大小的内存区块。对于大多数动态内存分配算法来说,一个主要的问题是在避免内存碎片的同时,还要保持分配和回收的效率。

理想的垃圾收集器的特性

一个垃圾收集器必须同时具有安全和缜密的特性。即存活的对象必须不能被错误的释放,同时垃圾不能在经历少数几轮收集以后仍然未被回收。

理想的垃圾收集器的另一个特性是,在不引入让应用长时间暂停的情况下,保持高效地运行。在大多数计算机相关的系统中,时间,空间和频率需要有一个权衡。例如,如果一个堆的空间很小,收集过程将很快,但是堆也会被快速的填满,因此也就需要更加频繁的收集。反之,一个大堆需要很长时间才能将其填满,收集操作也不会频繁发生,但是一次收集也会花费较长的时间才能完成。

垃圾收集器的另一个理想的特性是碎片的限制。当垃圾对象的内存被释放后,被释放的空间有可能位于各个区域的小块空间中,以至于在任何连续的空间中都找不到足够的空间来分配给一个大对象。减少碎片的一种方法被称为压缩,将在下面各种介绍垃圾收集器的设计选择时讨论。

可扩容性同样很重要。对于在多处理器上运行的多线程应用来说,空间分配不应该成为扩容的瓶颈,同时垃圾收集同样不应该成为瓶颈。

设计选择

在设计和选择一种垃圾收集算法的时候必须做出一些选择,包括:

1.    串行还是并行

选择串行收集,同一时间只能执行一项任务。例如,即使在多处理器的系统上,也只有一个处理器可以被用来执行收集工作。选择并行收集,垃圾收集的任务会被分解为多个部分,这些子任务可以在多个处理器上并行执行。任务的并行执行使得收集工作更快地得以完成,并行需要付出的成本是增加了复杂性以及潜在的碎片。

2.    并发还是stop-the-world

当采用stop-the-world模式的垃圾收集执行的时候,应用的执行被完全地暂停。另外一个选择是,一个或者更多的垃圾收集任务可以并发的执行,即与应用同时执行。通常情况下,一个并发垃圾收集器可以并发执行大部分的工作,但是偶尔也不得不引入一些短暂的stop-the-world的暂停 (将应用暂停执行)。

stop-the-world垃圾收集器比起并发收集器要简单,因为在收集的过程中,堆被冻结并且对象不再会被改变。但是缺点是对于一些应用来说,应用的暂停执行是不能接受的。于此相对应,当垃圾收集与应用并发执行的时候,暂停的时间会短一些,但是收集器必须更加小心,因为在收集的过程中,对象有可能同时被应用程序更新。这些会给并发收集器带来一些成本,从而会影响性能以及需要一个更大的堆。

3.    压缩,不压缩还是拷贝

在垃圾收集器已经确定了内存中的哪些对象仍然存活,哪些是垃圾后,收集器便可以压缩内存,将存活的对象移动到一起,将剩余的空间回收。压缩以后,在第一处空闲的位置,为对象分配空间是非常容易和快速的。可以用一个简单的指针来跟踪下一处空闲的区域,来为对象分配空间。与压缩收集器相反,非压缩收集器在适当的地方回收垃圾对象占用的空间,而不是像压缩收集器那样移动所有存活的对象来创建一大块回收的内存空间,但是缺点是潜在的内存碎片。通常情况下,在适当的地方释放空间比在一个压缩的堆上需要花费更大的代价。为此需要在堆上寻找一块足够大的连续区域来容纳新对象。第三种方案是拷贝收集器,这种收集器拷贝所有存活的对象到一个不同的内存区域。这样做的好处是,原始区域可以被认为是空的和可用的,可以快速和简单的执行接下来的分配任务,但是缺点是需要更多的时间来进行拷贝,以及可能需要额外的空间。

性能度量

1.    吞吐量– 在一段较长的时间里,花费在非垃圾收集上的时间所占总时间的百分比。

2.    垃圾收集开销– 与吞吐量相反,花费在垃圾收集上的时间占总时间的百分比。

3.    停顿时间– 当垃圾收集发生的时候,应用执行被停止的时间长度。

4.    收集频率– 相对于应用执行,收集任务多久发生一次。

5.    Footprint – 大小的度量,例如堆大小。

6.    及时性– 对象变为垃圾与垃圾对象所占用内存被回收之间的时间间隔。

一个交互型应用可能需要更短的停顿时间,而对于非交互型应用来说,总执行时间更加重要。一个实时应用可能在垃圾收集所导致的停顿时间和垃圾收集时间所占用的比例上都有一个较小的上限要求。一个个人电脑或者嵌入式系统对于Footprint更加关心。

分代收集

当使用被称为分代收集的技术时,内存被划分为不同的分代,即不同的内存池保存不同年代的对象。例如,应用最广泛的划分拥有两个代:一个保存年轻对象,一个保存年老对象。

不同分代中可以使用不同的算法来执行垃圾收集。每一个最优的算法都是基于对特定分代中观察到的特定的特性。分代垃圾收集利用了被称为”弱代假说”的观察结论,该结论适用于使用包括Java等多种编程语言编写的应用:

1.    大多数被分配的对象不会被引用很久,即大多数对象在年轻时便死去

2.    年老对象很少引用年轻的对象

新生代收集发生的相对频繁,并且更加高效和快速,因为新生代的空间通常比较小,并且包含大量不再被引用的对象。

经过数轮新生代垃圾收集之后仍然存活的对象最终会被升级到老生代。如图1所示。老生代通常比新生代更大,并且对象增长的更加缓慢。结果老生代收集发生的不那么频繁,但是明显需要花费更长的时间来完成收集工作。


通常新生代的垃圾收集算法更加关注速度,因为新生代垃圾收集更加频繁。另一方面,老生代的垃圾收集算法更加关注空间效率,因为老生代占用了堆的大多数空间,并且老生代的算法需要在低垃圾密度的情况下有出色的表现。

4.  J2SE 5.0HotSpot VM中的垃圾收集器

到J2SE 5.0 update 6为止,Java HotSpot虚拟机共包括四种垃圾收集器。这四种收集器都是基于分代的思想。本章描述了分代和收集的类型,讨论了对象收集之所以高效和快速的原因。随后详细描述了每种收集器的实现细节。

HotSpot中的分代

JavaHotSpot虚拟机的内存被划分为三个代:新生代(young generation),老生代(old generation)和永久代(permanent generation)。大部分对象初始被分配在新生代上。经过几轮新生代垃圾收集后仍然存活的对象被迁移到老生代中,而一些大对象则直接被分配在老生代中。永久代保存了一些JVM用于让管理垃圾收集器高效工作的对象,例如描述类和方法的对象以及类和方法自身。

新生代由一个伊甸园区(Eden)和两个较小的存活区(survivor)组成,如图2所示。大多数对象初始被分配在伊甸园区 (正如前面提到的,一些大对象直接在老生代分配)。存活区中保存的对象至少逃过了一次新生代的垃圾收集,并且有可能在被认为足够的老而移入老生代之前死亡。在任何时刻,仅有其中一个存活区容纳这样的对象,而另一个存活区保持为空直到下一次垃圾收集。


垃圾收集的类型

在新生代被填满后,一次新生代的垃圾收集工作就会被执行(称为minor collection),这次收集仅限于新生代。当老生代或者永久代被填满后,称为full collection的垃圾收集会被执行(叫做major collection)。即所有的分代都会被执行垃圾收集。通常情况下,新生代首先被收集,而使用的收集算法也是针对于新生代本身的,因为这种算法可以保证高效率地在新生代中收集垃圾。下文提到的老生代垃圾收集算法被同时应用于老生代和永久代。如果执行压缩,每个区域将各自执行压缩操作。

有时,如果新生代首先被回收,老生代有可能太满而无法接收从新生代移动过来的所有对象。在这种情况下,除了CMS垃圾收集器,新生代的垃圾收集算法不会被执行。取而代之的是,老生代的垃圾收集算法会先在整个堆中执行。(CMS老生代垃圾收集算法是个特例,它不会收集新生代)

快速分配

从下面描述的垃圾收集器中可以看到,很多情况下内存中存在大块连续的区域可用于对象的分配。使用一种简单的bump-the-pointer技术可以高效的在这些区域完成分配工作。这种技术利用了保存指向上一次分配对象的末尾位置的指针。当有一个新的分配请求需要满足时,所要做的就是检查剩余的空间是否可以容纳下新的对象,如果可以,则更新这个指针以及初始化新对象。

对于多线程应用来说,分配内存的操作需要保证线程安全。如果全局锁被用于确保线程安全,一个区域内内存的分配将会变成瓶颈并且会损耗性能。因此HotSpot JVM采用了一种被称为Thread-Local AllocationBuffers(TLABS)的技术。这种技术通过为每一个线程单独分配一块可以分配的缓存(例如,分代中的一小块区域),来提高多线程内存分配的吞吐量。因为在一个TLAB中仅有一个线程可以进入并且分配内存,分配工作可以利用bump-the-pointer技术快速的完成,而不需要使用锁。只有在一个线程填满了属于它的TLAB而需要分配一个新的内存区域的情况下才需要进行同步,而这种情况发生的频率并不高。几种使用TLAB技术来最小化内存浪费的技术已经被使用。例如,由收集器确定的TLABs的大小,在平均情况下仅会浪费不到1%的伊甸园区域。TLABs和bump-the-pointer技术的联合使用使得内存分配变得更加高效,大约仅需要10条本地指令。

串行收集器

使用串行垃圾收集器,新生代和老生代的垃圾收集都是使用stop-the-world的方式串行执行(使用一个CPU)。即当垃圾收集发生的时候,应用的执行被暂停。

1.    使用串行收集器的新生代收集过程

图3描绘了使用串行垃圾收集器的新生代的垃圾收集过程。在伊甸园区存活的对象被拷贝到初始为空的存活区,在图上标记为To。而一些太大而无法放入存活区的对象被直接拷贝到老生代。在另一个已经被使用的存活区(图中标记为From)中的仍然年轻的对象被拷贝到另一个存活区To,而相对较老的对象被拷贝到了老生代。注意:如果存活区To被填满,伊甸园区和From存活区的对象无论经历过几次垃圾收集,都被拷贝到老生代中。当伊甸园区和From存活区中存活的对象被移走以后,剩余的对象均被当做垃圾而不需要再次进行检查。(这些对象在图中被标记为X,而实际上收集器并不对这些对象进行检查和标记)。


当新生代的垃圾收集完成后,伊甸园区和From存活区变为空,而之前为空的存活区To中包含了存活的对象,此时,两个存活区的角色进行了互换。如图4所示。


2.    使用串行收集器的老生代收集过程

使用串行收集,老生代和永久代使用了一种被称为mark-sweep-compact的算法进行垃圾收集。在标记阶段,收集器确定哪些对象是存活的。而在清除阶段,收集器需要在整个分代中确定垃圾。最后收集器执行滑动压缩(sliding compaction),将所有存活的对象移动到老生代的一端(在永久代中同样如此),剩余可用的连续区域在相对应的另一端。如图5所示。这种压缩允许未来在老生代和永久代中的内存分配使用bump-the-pointer技术。


3.    何时使用串行收集器

串行收集器适用于运行在client-style的机器上,以及对应用的暂停时间没有要求的应用中。在如今的硬件条件下,串行收集器可以在配置64MB堆的应用中良好运行,并且可以完成最坏情况下少于半秒的完整收集(fullcollection)。

4.    如何选用串行收集器

在J2SE 5.0发行版中,串行收集器在非服务器级别的机器上被自动选为默认的垃圾收集器。在其他机器中,可以通过命令行参数-XX:+UseSerialGC来显示的指定使用串行收集器。

并行收集器

现在,很多应用都运行在拥有大量物理内存和多CPU的机器上。相比串行收集器同一时间只能有一个CPU工作而其他CPU空闲的情况,并行收集器,也被称为吞吐量收集器,可以充分利用多CPU的优势。

1.    使用并行收集器的新生代收集过程

并行收集器使用的是串行收集器在新生代所使用的收集算法的并行版本。该版本仍然是一个使用stop-the-world模式的拷贝收集器,区别是可以利用多CPU在新生代中并行执行收集任务,从而可以降低垃圾收集的开销以及增加应用的吞吐量。图6描述了新生代中串行收集器和并行收集器的差别。


2.    使用并行收集器的老生代收集过程

并行收集器在老生代的垃圾收集使用了与串行收集器相同的串行mark-sweep-compact收集算法。

3.    何时选择并行收集器

并行收集器可以在拥有多CPU的机器以及没有停顿时间限制的应用上发挥优势,虽然频率不高,但是老生代的收集仍会发生,而且时间比较长。适合并行收集器的应用类型包括批处理,计费,薪酬统计,科学计算等等。

你可能会选择下面介绍的并行压缩收集器取代并行收集器,因为前者可以在所有的区域执行并行收集,而不仅仅在新生代。

4.    如何选用并行收集器

在J2SE 5.0发行版中,并行收集器自动被选择为服务器级别机器中的默认垃圾收集器。在其他机器上,可以利用命令行参数-XX:+UseParallelGC显示的指定并行收集器。

并行压缩收集器

       并行压缩收集器在J2SE 5.0 update6中引入。它与并行收集器的区别是采用了一种新的老生代垃圾收集算法。注意:最终并行压缩收集器会取代并行收集器。

1.    使用并行压缩收集器的新生代收集过程

并行压缩收集器在新生代中使用了与并行收集器同样的算法。

2.    使用并行压缩收集器的老生代收集过程

并行压缩收集器使用了stop-the-world模式,在大多数时候并行以及使用滑动压缩的方式对老生代和永久代进行收集。收集过程划分为三个阶段。第一阶段,每一个分代被逻辑上划分为固定大小的区域。在标记阶段,通过应用代码可以到达的初始存活对象集合被划分到各个垃圾收集线程中去,然后所有存活的对象被并行的标记。一旦一个对象被确定为存活,该对象所在区域的数据就会被更新,其中包括了对象的大小以及所在的位置。

总结阶段(The summary phase)操作的对象是这些区域,而不是区域中的对象。由于前一次的压缩,每一个分代左边一些区域存活对象的密度较大。可以从这些存活对象密度较大的区域中恢复的可用空间的量太小,而不值得去压缩这些区域。因此总结阶段所作的第一件事是从最左边的第一个区域开始,逐个的检查这些区域的密度,直到找到一个起始点,该点右侧的区域都可以从中恢复出足够的可用空间,并且值得花费成本来压缩它们。该点左边的区域被称为dense prefix,其中没有对象被移动。该点右边的区域将被压缩以消除所有死亡的对象。总结阶段计算并存储每一个压缩过的区域的存活对象的第一个字节的位置。注意:总结阶段现在被实现为串行模式;并行是可能的,但是对于性能不如标记阶段和压缩阶段那么重要。

在压缩阶段(The compaction phase),垃圾收集器使用总结阶段得到的数据来确定需要被填充的区域,线程可以独立地将数据拷贝到这些区域中。由此产生了一个主要密度在一端,而另一端存在一个单一的较大的空白区域的分代。

3.    何时选用并行压缩收集器

与并行收集器相比,并行压缩收集器更适于那些运行在超过一个CPU的机器上的应用。由于并行压缩收集器降低了应用的停顿时间,因此更适合于那些有停顿时间限制的应用。并行压缩收集器不适用于那些运行在大型共享机器(例如SunRays)上的应用,因为这些机器不允许一个单独的应用长时间垄断CPU资源。在这些机器上,可以考虑减少垃圾收集器的线程数,或者选择其他类型的收集器。

4.    如何选用并行压缩收集器

如果你打算选用并行压缩收集器,就必须显示的通过命令行参数-XX:UseParallelOldGC来指定。

并发标记清除收集器

对于许多应用来说,响应时间比端到端的吞吐量更重要。通常情况下,新生代的垃圾收集不会造成长时间的停顿。而老生代的收集虽然频率不高,但却会造成长时间的停顿,尤其是当堆空间较大的情况。为了解决这种问题,HotSpot JVM采用了一种叫做并发标记清除concurrent mark-sweep(CMS)的收集器,这种收集器也被称为低延迟收集器。

1.    使用CMS收集器的新生代收集过程

CMS收集器在新生代采用了和并行收集同样的方式进行垃圾收集。

2.    使用CMS收集器的老生代收集过程

CMS收集器在老生代的收集过程与应用并发执行。

CMS收集器的整个收集过程以一个短暂的停顿开始,这一停顿被称为初始化标记(initial mark),目的是标记从应用的代码中可以直接到达的存活对象的一个初始集合。然后,在并发标记阶段(concurrent marking),收集器标记从初始集合中的对象可以传递到达的所有存活对象。因为在标记的过程中,应用在同时运行和更新引用的字段,因此在并发标记阶段结束以后,不是所有存活对象都会保证被标记。为了解决这个问题,应用开始了第二次停顿,这一阶段叫做重复标记(remark),再次访问在并发标记阶段中任何被修改过的对象。因为重新标记阶段的工作量比初始标记要大得多,因此会采用多线程并行执行来提高效率。

在重新标记结束后,堆中所有存活的对象都会被保证得到标记,由此在接下来的并发清除阶段就可以回收所有被确定为垃圾的对象。图7说明了老生代中的串行mark-sweep-compact收集器和CMS收集器的区别。


由于一些额外的任务,例如在重新标记阶段重新访问对象,增加了收集器的工作量,因此开销也会增大。这通常也是大多数想降低停顿时间的收集器需要权衡的因素。

CMS收集器是唯一一个没有使用压缩的收集器。当它释放死亡对象的空间以后,没有将存活对象从老生代的一端移动到另一端。如图8所示。


这种做法节省了时间,但是由于可用空间不是连续的,收集器也就无法使用一个简单的指针来标记可以为下一个新对象分配空间的起始位置。因此就需要采用自由空间列表。这些自由空间列表将内存中没有被分配的区域链接起来,每一次当需要为一个对象分配内存时,就会搜索一个合适的列表(基于所需的内存大小),该列表包含了足以容纳这个对象的空间。这样做的结果是,在老生代上分配空间比起使用简单的bump-the-pointer技术来说要昂贵得多。这种做法同样会给新生代的收集带来额外的开销,因为大多数在老生代中的内存分配是发生在新生代的收集时,将对象从新生代移入老生代的时候。

CMS收集器的另外一个缺点是,比起其他收集器,他需要更大的堆空间。因为应用得以在标记阶段并发执行,由此可以持续的分配内存,也就会持续的增加老生代的大小。而且虽然收集器保证会在标记阶段确认所有的存活对象,但是一些对象有可能会在在标记阶段变为垃圾而不会被回收,直到下一次的老生代回收。这种对象被称为floating garbage。

最终,内存碎片有可能由于没有进行压缩而产生。为了处理碎片,CMS收集器会跟踪常见对象大小,评估未来的需求,划分或者合并空闲的区域来满足需要。

不像其他收集器,CMS收集器不会在老生代变满的时候才启动老生代的垃圾收集。而是试图在足够早的时候提前启动从而可以在老生代变满之前完成收集。否则CMS收集器就恢复到了被串行和并行收集器所采用的更加费时的stop-the-worldmark-sweep-compact算法。为了避免这样的情况发生,CMS收集器是基于之前垃圾收集的时间和老生代被填满的时间来启动收集过程。CMS收集器也会在老生代的使用量超过了被称为initiating occupancy阈值后启动收集工作。Initiating occupancy可以通过命令行参数-XX:CMSInitiatingOccupancyFraction=n来设定,其中n是老生代被占用的百分比,默认是68。

总而言之,与并行收集器相比,CMS收集器降低了老生代的停顿时间,有时降低的幅度是非常大的,而付出的成本是新生代的停顿时间会稍微延长,吞吐量会有些降低,以及而外的堆空间。

3.    增量模式

CMS收集器可以被设置为让垃圾收集的并发阶段采用分时间段增量完成的工作模式。这种模式通过周期性的暂停并发收集阶段而将系统资源还给应用执行,从而可以减小漫长的并发阶段所带来的影响。这种特性对于运行在处理器数量较少(例如一个或者两个),需要更低的停顿时间的应用来说是非常有用的。这种模式更详细的用法可以参考第九章中引用的文章“Tuning Garbage Collection with the 5.0 Java™ Virtual Machine”。

4.    何时使用CMS收集器

如果你的应用需要更低的停顿时间,并且可以允许在应用执行的同时,与垃圾收集器一同分享处理器资源,便可以选择CMS收集器。(由于并发的需要,在一个收集周期,CMS收集器会占用应用的CPU周期)。典型的场景是,应用有一个相对较大的存活时间很长的对象集合(一个很大的老生代),应用所运行的系统拥有两个或者更多的CPU,这样的应用可以从CMS收集器中获益。

5.    如何选用CMS收集器

如果你想使用CMS收集器,就必须显示的在命令行中指定参数-XX:+UseConcMarkSweepGC。如果你希望他工作在增量模式,也必须通过添加命令行参数-XX:+CMSIncreamentalMode来选用。

5.  Ergonomics-自动选择与行为调节

在J2SE 5.0发行版中,垃圾收集器,堆大小以及HotSpot虚拟机(client 还是 server)是根据应用所运行的平台和操作系统而自动选择的。比起之前的发行版,这些自动选择需要更少的命令行参数,并且可以更好地满足不同类型系统的需要。

另外,一种新的动态调节垃圾收集过程的方法已经加入了并行垃圾收集器中。通过这种方法,用户可以指定期望的行为,垃圾收集器会通过动态的调整堆的大小来试图达到用户所期望的行为。跨平台的默认选择与基于期望行为的垃圾收集自动调节一起被称为Ergonomics。Ergonomics的目标是通过最小的命令行参数来提供一个拥有良好性能的JVM。

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

服务器端机器的定义是

1.    拥有两个或者更多的CPU

2.    拥有2G或者更多的物理内存

这一服务器端机器的定义适用于除了运行于windows操作系统的32位平台之外的所有平台。

非服务端机器对于JVM类型,垃圾收集器,堆大小的默认值为

1.    client JVM

2.    串行收集器

3.    初始堆大小为4M

4.    最大堆大小为64M

在服务端机器上,JVM类型总是server JVM,除非你手动通过命令行参数-client指定选用client JVM。在运行server JVM的服务端机器上,默认的垃圾收集器是并行收集器。反之,默认的为串行收集器。

        无论是client jvm还是server jvm,采用并行垃圾收集的服务端机器的默认初始和最大的堆大小为

1.    初始堆大小是物理内存的1/64,最大不超过1G。(注意因为服务端机器的最少拥有2G的内存,所以JVM的最小初始堆大小为32M)。

2.    最大堆大小为物理内存的1/4,最大不超过1G。

基于行为的并行收集器调优

在J2SE 5.0发行版中,并行垃圾收集器采用了一种新的基于应用的垃圾收集器特定行为的调优方法。可以用命令行参数来指定最大停顿时间和应用的吞吐量。

1.    最大停顿时间期望

最大停顿时间是通过如下命令行参数指定

-XX:MaxGCPauseMillis=n

该参数通知并行收集器所期望的最大停顿时间不能超过n毫秒。并行收集器会通过调整堆大小以及其他垃圾收集相关的参数来试图在执行垃圾收集时,应用的停顿时间少于n毫秒。这些调节可能会造成整个应用吞吐量的下降,然而一些情况下期望的停顿时间仍然无法得到满足。

最大停顿时间这一目标被分别应用于堆中的每一个分代中。如果目标没有达到,虚拟机会通过减少分代的大小来再次试图达到目标。最大停顿时间没有默认值。

2.    吞吐量期望

吞吐量是通过花费在垃圾收集上的时间和非垃圾收集的时间来度量。吞吐量通过如下命令行参数指定

-XX:GCTimeRatio=n

垃圾收集的时间与应用执行的时间之比为

1/ (1 + n)

例如,-XX:GCTimeRatio=19设置了垃圾收集的执行时间占总的CPU时间为5%。默认目标是1%(n=99)。垃圾收集的时间为各个分代中垃圾收集的总时间。如果吞吐量的目标没有被达到,JVM通过增加分代的大小来努力增加相邻两次垃圾收集之间应用的持续运行时间。一个更大的堆需要更长的时间来填满。

3.    Footprint期望

如果吞吐量和最大停顿时间的目标得到满足,垃圾收集器会减小堆的大小,直到一个目标(总会是吞吐量)无法被达到。无法被达到的目标会重新被垃圾收集器调整而达到。

4.    期望的优先级

并行垃圾收集器会首先试图满足停顿时间的目标。然后才会考虑实现吞吐量的目标。同样地,在前两个目标实现以后才会考虑footprint的目标。

6.  推荐

上一章中描述的ergonomics技术所选择的自动垃圾收集,虚拟机工作模式(client orserver)以及堆大小已经可以适用于很大一部分应用。因此首先的推荐是不要做任何关于选择和配置虚拟机的事情。也就是说,不要指定垃圾收集器的使用方式。让系统根据所运行的平台和操作系统自动做出选择。然后测试你的应用。如果应用关于吞吐量和停顿时间的性能可以被接受,你就不再需要做额外的事情,不需要排查问题和修改虚拟机配置。

另一方面,如果你的应用看起来有与垃圾收集器相关的性能问题,那么最简单的办法是思考默认选择的垃圾收集器是否适合于你的应用以及平台的特点。如果答案是否,那么显示指定你所认为合适的收集器,并观察性能是否可以接受。

你可以使用第七章中介绍的工具来测量和分析性能。根据测量的结果,你可以考虑修改配置项,例如堆大小或者垃圾收集器的行为。第八章中介绍了一些最常用的配置项。请注意:性能调优的最佳实践是首先要测量,然后才是调优。同时也需要避免过度调优,因为应用的数据集,硬件甚至是垃圾收集器的实现,都会随着时间而变化。

这一部分提供了一些垃圾收集器选择和堆大小设置的信息。然后提供了一些并行垃圾收集器的调优建议,并且给出了一些应对OutOfMemoryErrors错误的指导。

何时该选择一个不同的垃圾收集器

第四章中说明了每一种收集器的使用场景。第五章中描述了每一种平台上默认选择的串行或者并行收集器。如果你的应用或者环境特性需要选择另外一种不同于默认的收集器,可以通过如下命令行参数来指定.

-XX:+UseSerialGC

-XX:+UseParallelGC

-XX:+UseParallelOldGC

-XX:+UseConcMarkSweepGC

堆大小调节

第五章讲述了默认的初始堆大小和最大堆大小,这些配置对于大多数应用来说是适合的,但是如果你对于OutOfMemoryErrors错误的分析结果显示,问题有可能出在特定分代或者整个堆的大小上,你可以通过修改第八章中所描述的命令行参数来解决问题。例如,通常对于一个非服务器端机器来说,默认的64M的堆大小是不够的,你可以通过命令行参数-Xmx来指定一个更大的容量。除非你遇到了停顿时间上的问题,那就试着为堆分配尽可能多的内存。吞吐量与可用内存成正比。拥有足够的可用内存是影响垃圾收集性能最重要的因素。

在决定了你可以为堆分配的内存总数后,你可以开始考虑调整各个分代的大小。第二个影响垃圾收集性能的因素是新生代的大小。除非你遇到了过度的老生代收集或者应用停顿时间,为新生代分配尽可能多的内存。然而,当你使用串行收集器的时候,为新生代分配的内存不要超过整个堆的一半。

如果你使用的是并行垃圾收集器,最好是指定你期望的行为而不是具体的堆大小。让收集器自动动态地修改堆大小来达到你所期望的行为。

调节并行收集器的策略

不论是默认还是显示地指定,如果所选择的垃圾收集器是并行收集器或者并行压缩收集器,那么去指定一个满足你应用的吞吐量目标。除非你确信需要一个比默认最大堆还要大的堆大小,否则不要为堆设置最大值。因为堆会根据吞吐量的目标来增加或者减小堆到一个合适的大小。在初始化或者应用的行为发生变化的时候,堆的大小有可能出现一些波动。

如果堆增长到最大值,通常意味着在最大值的范围内无法实现预设的吞吐量目标。将最大堆大小设置为所在平台的总共物理内存数,重新执行应用,如果仍然无法达到所期望的吞吐量,那么说明对于当前应用运行的平台来说,所设置的吞吐量目标太高了。

如果不但吞吐量的目标无法达到,还会出现过长的停顿时间,那么可以设置一个最大停顿时间的目标。选择了一个最大停顿时间的目标就意味着吞吐量的目标无法被达到,因此需要在两者之间做一个权衡,设置一个合适的停顿时间预期。

垃圾收集器尽力去满足这些相互冲突的目标时,即使应用已经达到稳定,堆的大小也会出现波动。达到最大吞吐量(需要一个尽可能大的堆)的压力会和最大停顿时间以及最小footprint(需要一个尽可能小的堆)相互冲突。

如何应对OutOfMemoryError

一个很多开发人员不得不面对的问题是应用由于发生java.lang.OutOfMemoryError错误而终止。这个错误的抛出是由于不能为对象分配足够的空间。也就是说,垃圾收集无法找到可用的空间来容纳新对象,并且堆也无法被继续增大。OutOfMemoryError错误的抛出不意味着内存泄露的发生。也许仅仅是一个配置问题,例如所设置的堆大小(或者没有指定情况下的默认值)对于应用来说不足。

诊断OutOfMemoryError错误的第一步是检查全部的错误消息。在异常消息中,进一步的信息在”java.lang.OutOfMemoryError”之后。一些常见的错误信息的含义以及应对措施如下:

1.    Java heap space

这种情况说明对象无法在堆上分配。也许仅仅是一个配置问题。你可以看到这个错误。例如,如果通过命令行参数-Xmx所指定(或者默认选择)的最大堆的大小无法满足应用的需要。这种情况也许暗示着应用程序无意地引用了不再被使用的对象,而这些对象是无法被当做垃圾而回收的。第七章介绍的HAT工具可以让你看到所有可达的对象,以及对象相互之间的引用关系。这个错误的另一个根源可能来自于应用对于finalizers的大量使用,使得调用finalizers的线程跟不上加入到队列中的finalizers的速度。JConsole可以被用来监控等待被终结的对象数量。

2.    PermGen space

这种情况意味着永久代被填满。正如前面描述的,永久代是堆上JVM存储元数据的区域。如果一个应用程序加载了大量的类,那么永久代所需空间有可能需要增加。你可以通过命令行参数-XX:MaxPermSize=n来指定永久代的大小。

3.    Requested array size exceeds VM limit

这种情况意味着应用试图分配一个比堆还要大的数组。例如,应用程序试图分配一个512M大小的数组,而最大堆却只有256M,这时OutOfMemoryError错误就会被抛出。大多数情况下这种问题发生的原因要么是堆空间太小,要么是应用程序中分配了一个错误大小的数组。

可以利用第七章中介绍的工具来诊断OutOfMemoryError问题。一些最有用的诊断工具有Heap AnalysisTool(HAT),jconsole,jmap。

7.  评估垃圾收集性能的工具

本章介绍一些可以用于评估垃圾收集性能的检测和监控工具。更多的信息在第九章的”工具和问题排查”中介绍。

-XX:+PrintGCDetails命令行选择

获得垃圾收集器的初始信息最简单的方法之一是通过命令行参数-XX:+PrintGCDetails。对于每一次的收集,输出信息包括了例如垃圾收集前后,堆中每一个分代中存活对象的大小,可用空间的大小,以及垃圾收集的执行时间等数据。

-XX:PrintGCTimeStamps命令行参数

命令行参数-XX:PrintGCTimeStamps可以使得输出信息中包含每一次垃圾收集的起始时间戳。这个时间戳可以帮助你将垃圾收集日志和其他日志事件相关联。

Jmap

Jmap是一个包含在Solaris操作环境和JDK的Linux发行版中的实用命令行工具。该工具为JVM或者核心文件打印了一些内存相关的统计数据。如果该命令在没有任何命令行参数的情况下使用,则会打印一个已经加载的共享对象清单,就像Solaris的pmap工具的输出一样。想要查看更多的信息,可以添加-heap, -histo或者-permstat参数。

参数-heap被用来获得包括垃圾收集器,算法相关的细节(例如并行垃圾收集所使用的线程数),堆配置,以及堆的使用情况等信息。

参数-histo使你可以获得一个堆中类的直方图。可以打印出堆中每一个类的所有实例的数量,这些对象占用的总的内存字节数,以及类的全限定名称。直方图在理解堆如何被使用时是非常有用的。

Jstat

工具Jstat使用一个HotSpot JVM内建的指令来提供运行中的应用的性能和资源占用信息。该工具可以在诊断性能,尤其是与堆大小和垃圾收集相关的问题时使用。其中的一些配置项参数可以打印垃圾收集行为以及堆的各个分代的容量和使用相关的统计信息。

HPROF:Heap Profiler

HPROF是一个跟随JDK 5.0一起分发的简单的事件探查器代理,作为使用Java VirtualMachine Tools Interface(JVM TI)的接口的动态链接库。它以ASCII格式或者二进制格式将探查信息写入一个文件或者一个Socket。这些信息可以进一步被探查器前端工具进行处理。

HPROF可以用于描述CPU的使用情况,堆分配的统计信息,以及监视器争夺的简介。另外,该工具可以将对完整地导出,并且可以报告Java虚拟机中所有监视器和线程的状态。在分析性能,锁争夺,内存泄露和其他问题时,HPROF是非常有用的。

HAT:Heap Analysis Tool

Heap Analysis Tool(HAT)可以用来帮助调试unintentional objectretention(无意识的对象保留)问题。这个术语被用于描述一个不再被需要,但是由于通过一个存活对象的某些路径所引用而仍然存活的对象。HAT提供了一个方便的手段来浏览使用HPROF生成的一个堆快照中的对象拓扑图。该工具允许若干查询,例如“为我显示从跟路径到达这个对象的所有引用路径。”在第九章中可以查看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:PrintGCDetail

输出每次垃圾收集的详细信息

-XX:PrintGCTimeStamps

输出每次垃圾收集时间的起始时间戳。配合使用参数-XX:PrintGC或者-XX:PrintGCDetails来显示每一次垃圾收集的开始时间。

堆和分代大小

配置项

默认值

描述

-Xmsn

见第五章

堆初始字节大小

-Xmxn

见第五章

堆最大字节大小

-XX:MinHeapFreeRatio=minimum

-XX:MaxHeapFreeRatio=maximum

40(min)

70(max)

堆中自由空间的比例区间。每一个分代单独设置。例如,如果最小设置为30,而一个分代中自由空间小于30%,则该分代会自动扩充到自由空间占比达到30%。同样,如果最大值设为60,而自由空间占比超过60%,分代的大小会缩减到自由空间仅占60%。

-XX:NewSize=n

Platform-dependent

默认新生代的字节大小

-XX:NewRatio=n

2 client VM

8 server VM

新生代与老生代的比例。例如,n=3表示伊甸园区与存活区的大小总和占据了四分之一的新生代与老生代的空间之和。

-XX:SurvivorRatio=n

32

存活区与伊甸园区的大小比值。例如,n=7表示表示每一个存活区占到新生代的九分之一大小。(有两个存活区)

-XX:MaxPermSize=n

Platform-dependent

永久代的最大值

并行和并行压缩收集器的参数

配置项

默认值

描述

-XX:ParallelGCThreads=n

CPU的数量

垃圾收集器的线程数

-XX:MaxGCPauseMillis=n

没有默认值

通知收集器所期望的停顿时间少于n毫秒

-XX:GCTimeRatio=n

99

通知收集器所期望的垃圾收集时间不要超过1(1+n)%

CMS收集器的参数

配置项

默认值

描述

-XX:CMSIncrementalMode

没有启用

开启增量模式,使得垃圾收集的并发阶段定期暂停,将处理器资源让给应用执行。

-XX:CMSIncrementalPacing

没有启用

Enables automatic control of the amount of work the CMS collector is allowed to do before giving up the processor, based on application behavior

-XX:ParallelGCThreads=n

CPU的数量

新生代并发收集以及老生代的并发收集阶段所使用的垃圾收集线程数。

 

9.  更多信息

HotSpot垃圾收集和性能调优

Java HotSpot虚拟机中的垃圾收集

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

Java 5.0虚拟机的垃圾收集调优

(http://java.sun.com/docs/hotspot/gc5.0/gc_tuning_5.html)

Ergonomics

服务端机器探测

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

垃圾收集器Ergonomics

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

Java 5.0虚拟机中的Ergonomics

(http://java.sun.com/docs/hotspot/gc5.0/ergo5.html)

配置项

Java虚拟机配置项

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

Solaris和Linux配置项

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

Windows配置项

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

工具和问题排查

Java2平台,标准版5.0问题排查与诊断指导

(http://java.sun.com/j2se/1.5/pdf/jdk50_ts_guide.pdf)

HPROF: 一个J2SE5.0中的堆/CPU事件探查工具

(http://java.sun.com/developer/technicalArticles/Programming/HPROF.html)

Hat: 堆分析工具

(https://hat.dev.java.net/)

Finalization

Finalization, 线程和基于Java技术的内存模型

(http://devresource.hp.com/drc/resources/jmemmodel/index.jsp)

How to HandleJava Finalization's Memory–Retention Issues

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

杂项

J2SE 5.0发行版说明

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

Java虚拟机

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

Sun Java实时系统

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

垃圾收集相关:Algorithms for Automatic Dynamic Memory Management by Richard Jones and Rafael Lins, JohnWiley & Sons, 1996.

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值