1. 引言
Java平台一个最大的优势是在于它的自动内存管理,这样可以使得Java的开发者不用自己去编写代码来进行内存的管理,从而从复杂的内存管理的工作抽身出来专注于业务逻辑的开发。
这篇文章主要是针对sun公司J2SE5.0发布版本的HotSpot虚拟机的内存管理做一个大致的介绍。主要介绍了内存管理中一些可用的垃圾回收器(garbage collector),以及提供一些在垃圾回收器的选择和配置、垃圾回收器操作内存的大小等方面的意见和建议。同时也提供了资源信息,比如说一些影响垃圾回收器工作的最常用的虚拟机配置选项、还有一些相关详细信息的文档资源的链接。
第二节主要是针对对自动内存管理不熟悉的读者。主要包括自动内存管理相对于开发者手动内存管理的好处的讨论。第三节主要介绍了垃圾回收的概念、设计的选择和性能特点。同时也介绍了一种常用的基于对象生命周期的分代内存管理(也就是把内存划分成为多个不同的区域,每一个区域的对象的生命周期不同)。基于分代的内存管理被证明在不同应用程序当中,都能有效的降低垃圾回收的时候的程序停顿的时间和总体资源的消耗。
文章余下的部分主要提供了主要针对HotSpot虚拟机的相关信息。第四节描述了四种可用的垃圾回收器,包括了在J2SE5.0 update6版本中新推出的一种,以及详细描述了基于分代的内存管理。对于每一种垃圾回收器,第四节总结了使用的相应的垃圾回收算法以及使用的时期。
第五节描述了一种在J2SE5.0 中引进的新技术,这种新技术被称为(ergonomics,就是让内存管理更加人性化),主要包含了两种情况的结合:(1)根据应用程序运行的平台和操作系统来自动选择所使用的垃圾回收器、堆的大小、以及虚拟机的版本(客户端或者的服务器端);(2)根据用户指定的垃圾回收器的行为参数(比如说用户可以设定垃圾回收的暂停时间的上限下限、垃圾回收所花费的时间的比值等)来动态的进行垃圾回收的调整。
第六节提供了一些垃圾回收器的配置和选择的意见和建议。同时也提供了一些遇到OutOfMemoryErrors异常该如何做的建议。第七节简单介绍了一些可用于评估垃圾回收性能的工具,第八节列举出了一些和垃圾回收器的选择以及控制垃圾回收器的工作情况的一些命令。最后第九节提供了一些在本篇文章中所涉及到的话题的相关详细参考文档的链接。
2. 手动VS自动的内存管理
内存管理主要完成的工作是,识别出已经分配给对象的内存在什么时候不再被应用程序需要,然后释放已经分配给该对象的内存,以方便后续的内存分配。在一些其他的编程语言当中,内存的管理是有开发者自己完成的。内存管理的复杂性会导致一些常见的错误,这些常见的错误可能会引起一些不可预见的不正确的程序行为甚至程序崩溃。结果就是,开发者大部分的开发时间都花费在调试和修改这种错误上了。
在手动内存管理当中经常发生的一个问题就是(dangling references)悬空指针引用。在内存释放的时候可能出现一个对象A释放的内存空间M在别的对象B中仍然保留着该内存空间M的引用,这个时候内存空间M已经被操作系统回收了,然而对象B仍然保留着内存空间M的引用,该引用就是(dangling references)。这时候就会出现这种情况,当对象B试图使用该引用去访问原来内存空间的内容的时候,但是该内存空间原来的内容已经被清空回收,现在已经分配给了一个新的对象,结果就出现不正当的内容的访问,就会导致程序出现不可预见的情况,而且这种情况不是我们想要的。
另外一种情况就是我们常见的内存泄露,这种情况发生在一块已经分配的内存不再被程序使用,但是却没有被释放掉的情况。举个例子,比如你需要使用一个链表,但是你犯了一个错误,那就是在释放内存的时候只释放了链表第一个节点的内存,这样就会导致余下的节点的不能再被程序访问到,也不能被覆盖掉,如果这种情况持续出现,就会导致不断的内存消耗,直到没有内存可以用。
一种现在经常被面向对象语言使用的可以实现内存管理的方案就是自动内存管理(也成为垃圾回收器)。自动的内存管理允许更高级的抽象接口和更可靠的编码(也就是说提供了更多的业务逻辑处理的接口,对内存的管理进行了更加高级的封装,从而实现自动内存管理,这样开发者就可以更专注与业务逻辑的实现,而不用担心复杂的内存管理)。
垃圾回收避免了上述提到的(dangling references)悬空指针引用的问题,这是因为仍然被引用的内存空间被认为是非空闲绝对不会被垃圾回收。同时垃圾回收也解决了内存空间泄露的问题,因为不在被引用的内存空间将被自动释放。
3. 垃圾回收的概念
一个垃圾回收器主要负责下面几个任务:
- 内存分配
- 保证仍在被对象引用的内存空间不被释放
- 回收不再被对象引用的内存空间
(在这里我们认为对象和上述提到的内存空间是等价的也就是说 【对象~内存空间】)被引用的对象(内存空间)是被认为是存活的。不再被引用的对象(内存空间,后续都是一样的)认为是死掉了,也就是说应用程序不会再使用,就会被认为是垃圾。寻找和释放这种空间的过程就是垃圾回收。
垃圾回收解决了许多内存管理的问题,但是不是全部。你可以通过不停的创建对象并引用他们直到没有更多的内存可用。垃圾回收自身也是一个需要耗费时间和资源的复杂的任务。
用于组织内存、分配和释放内存的算法由垃圾回收器实现,对于程序员来说是不可见的。内存的分配来自一个叫做堆(heap)的一块大的内存池。
垃圾回收所需要的时间由相应的垃圾回收器所决定。典型的,当整个堆或者是堆的一部分已经被分配完或者是堆的使用率达到了一个阀值的时候,就会进行垃圾回收。
完成一个内存分配的请求,就是需要在堆中找到一块大小合适的且未被使用的内存,这个过程是一个非常困难的过程。大多数动态内存分配算法的主要问题是如何在保持内存分配和回收能高效进行的同时还能避免内存碎片。
期望的垃圾回收器的特征
一个垃圾回收器必须是安全的和综合性的。也就是说,存活的对象或者数据一定不会被错误的释放,以及垃圾对象或者数据不应该在少数几个垃圾回收周期后还没有被回收。
还有就是期望垃圾回收器高效的运行,不会导致太长的停顿时间(在这段时间内应用程序不会运行)。然而对于大多数计算机系统来说,都存在时间、空间、和回收频率之间的一个平衡。例如,如果堆的大小很小,回收过程将会很快但是堆也很容易被填满,这样需要更频繁的回收,回收的频率就更高。相反的,一个大的堆被填满需要更长的时间所以回收的频率相对较低,但是回收的过程也会花费更多的时间。
另外一个期望的垃圾回收的特征就是内存碎片化的限制。当垃圾对象所占用的内存被释放掉了之后,这些被释放掉的内存空间可能分散在整个堆上的多个不连续的区域,这样就会导致在处理一个大对象的内存分配请求的时候,在堆上没有相应大小的连续的空闲内存用于分配。一种消除碎片化的方法叫做内存紧缩,将会在接下来的几种垃圾回收器的设计中讨论。
可扩展性也是很重要的。内存分配不应该成为在多处理器系统上多线程程序可扩展性的瓶颈,与此同时垃圾回收也不应该成为此类瓶颈(译文翻译过来是这样的,有点难以理解,结合后面的多线程的垃圾回收,我认为表达的意思是垃圾回收器应该支持多线程的内存分配和回收这样可以充分利用多处理器的特点,分配和回收效率更高;或者说在可能允许的条件下可以让应用程序和内存分配、垃圾回收并发执行,具体参考下面“设计的选择的介绍”)。
设计的选择
在设计和选择垃圾回收器的回收算法的时候,需要做出一系列的选择:
- 串行 VS 并行
串行的垃圾回收(也可以说是单线程的垃圾回收)。例如,即使是在多处理器的情况下,也只有一个处理器在用于垃圾回收。当并行垃圾回收(多线程的垃圾回收)被使用,垃圾回收的任务被分为多个子任务,这些多个子任务可以再多个处理器上并发的执行。这种并发的操作可以使得垃圾回收更快,但是会增加一些额外的复杂性和潜在的内存碎片化。
- 并发 VS 停止应用程序(stop-the-world)两种垃圾回收机制
当停止应用程序模式的垃圾回收机制开启时,在垃圾回收的过程中应用程序的执行被完全的阻塞。然而并发的垃圾回收机制,一个或者多个垃圾回收的任务或者线程可以并行的执行,也就是说、同时和应用程序一起执行。典型的并发机制的垃圾回收机制,大部分工作都是在并发的执行,但是偶尔也不得不停止应用程序一小会儿做一些必须要做的工作。停止应用程序机制的垃圾回收方式比并发机制的垃圾回收机制简答,因为垃圾回收过程中堆中的内存不会被应用程序修改,回收更方便彻底。它的不好之处在于会导致应用程序暂停一段时间,给用户带来不好的体验。相应的如果垃圾回收和应用程序并行,这样可以缩短应用程序的停顿时间,但是会增加回收的复杂性,因为在回收的过程中堆中的内存使用情况可能会被应用程序修改。这样会增加并发收集器的时间和空间上的开销从而影响性能,而且需要更大的堆空间。
- 内存紧缩 VS 非内存紧缩 VS 内存复制
在垃圾回收器扫描了堆中哪些对象是存活的以及哪些对象是垃圾对象之后,就可以进行内存的紧缩,把所有存活的对象移动到一起,然后回收剩下的所有的内存空间。内存紧缩之后,可以通简单的指针移动来更快更方便的进行下一次内存的分配。相反的,非内存压缩的垃圾回收,只会将垃圾对象占用的空间原地释放,不会移动所有的存活的对象到一起从而可以得到一片连续的大的内存空间。这样做的好处是垃圾回收很快,但是缺点是潜在的内存碎片化。总的来说,从非紧缩的堆中分配一块内存空间比在紧缩的堆中分配一块内存空间需要花费的更多。在这种情况下为了满足下一次的内存分配请求,可能要搜寻整个堆上所有的空闲的内存空间。第三种内存复制方式的垃圾回收,也就是说将该内存区域(不是整个堆,在这种情况下堆可能被分成大小相等的多块内存区域),存活的对象全部复制到另一块空闲的内存区域上,这样的好处是刚才进行回收的内存区域被认为是空闲的可用的,这样就可以进行接下来的更快的内存分配,但是缺点是需要额外的时间进行复制,以及额外的空间存放复制的对象。
性能度量标准
多个度量的标准被用例评估垃圾回收器的性能,包括:
- 吞吐量 — 没有花费到垃圾回收的和总的时间的百分比,应该是在运行了很长一段时间之后的统计。
- 垃圾回收消耗 — 和吞吐量相反,是花费到垃圾回收上的时间和总的时间的百分比。
- 暂停时间 — 在垃圾回收的时候应用程序停止执行的时间。
- 垃圾回收的频率 — 在一定时间内垃圾回收的次数,和应用程序执行有关。
- 资源计量(footprint足迹) — 一种大小的计量方式,如堆的大小。
- 敏捷度 — 是指当一个对象变成垃圾对象到该对象被回收的这段时间的大小。
一个交互程序也许就需要更小的应用程序的停顿时间,然而总得执行时间对于一个非交互程序来说更加重要。一个实时的应用程序也许更加需要在垃圾回收暂停应用程序时间以及花费在垃圾回收上的时间比值这两个方面有一个上限值。在个人电脑或者是嵌入式系统上的应用程序最关心的就是一个更小的资源计量(也就是说虚拟机使用资源情况)。
分代垃圾回收
在使用分代垃圾回收的虚拟机当中,内存被划分成为不同的代,也就是说不同的代的内存区域中存放的对象的年纪(对象存活的时间)也不同。例如,最广泛应用的是将内存划分成为两个代:一个用于存放年轻的(存在时间不久)的对象,另外一个用于存放老的(存在时间较长)的对象。
不同的分代可以使用不同的垃圾回收算法进行垃圾的回收,每一种算法将会根据不同分代的特点进行优化。分代垃圾回收利用了一种被称为“弱分代假设”(weak generational hypothesis)的特点,视应用编写的语言而定,包括java编程语言:
- 大多数被分配内存的对象不会被引用(存活)太久,也就是说这些对象被引用的时间很短,所以就会很快称为垃圾回收的对象。
- 存在部分老年对象到年轻对象的引用。
young generation的垃圾回收的频率相对较高、也比较高效和快,因为young generation的内存空间常常较小,而且貌似包含许多不再被引用的对象。
在young generation中的对象如果经过一定次数的垃圾回收之后如果仍然被引用,那么这些对象就会被复制到old generation当中,如figure 1所示。old generation一般都比young generation的内存空间大,空间被占用的速度也相对较慢。所以old generation的垃圾回收的频率相对较低,但是垃圾回收过程占用的时间也相对较多。
由于young generation垃圾回收的频率相对较高,所以young generation需要选择速度更快的垃圾回收算法。相反的,old generation需要选择空间更高效的垃圾回收算法,因为old generation占据了堆空间的绝大部分,所以垃圾回收算法必须在低垃圾密度(垃圾占用的空间比)的情况下也能工作的很好。
4. J2SE5.0中的HotSpot JVM的垃圾回收器
自J2SE5.0 update 6版本开始HotSpot虚拟机中包含四种垃圾回收器。所有的垃圾回收器都是基于分代的。这一节将要介绍各个分代以及其回收器的类型,然后讨论为什么对象的分配常常很快和高效,然后详细的介绍每一个垃圾回收器。
HotSpot的分代
在HotSpot中的内存被划分成为三个代:young generation、old generation、permanent generation。大部分的对象最初被分配在young generation。
争取在这周翻译完!