Java的内存回收——垃圾回收机制

来源:互联网 发布:mac 配置hadoop 编辑:程序博客网 时间:2024/05/22 02:24

3、垃圾回收机制

       垃圾回收机制主要完成以下两件事情。
  1. 跟踪并监控每个Java对象,当某个对象处于不可达状态时,回收该对象所占用的内存空间。
  2. 清理内存分配、回收过程中产生的内存碎片。
       垃圾回收机制需要完成的这两方面工作工作量都不小,所以垃圾回收算法就成为了限制Java程序运行效率的重要因素。实现高效JVM的一个重要方面就是提供高效的垃圾回收机制,高效的垃圾回收机制既能够保证垃圾回收的快速运行,避免内存的分配和回收成为应用程序的性能瓶颈,又不能导致应用程序产生停顿。

3、1 垃圾回收机制

       JVM垃圾回收机制判断某个对象是否可以回收的唯一标准是:是否还有其他引用指向该对象?如果存在指向该对象,垃圾回收机制就不会回收该对象;否则,垃圾回收机制就会尝试回收它。
       实际上,垃圾回收机制不可能实时监测到每个Java对象的状态,因此当一个对象失去引用后,它也不会被立即回收,只有等垃圾回收机制运行时才会被回收。
       对于一个垃圾回收器的设计算法来说,大致有如下可供选择的设计。
  • 串行回收(Serial)和并行回收(Parallel):串行回收就是不管系统有多少个CPU,始终只用一个CPU来执行垃圾回收操作;而并行回收就是把整个回收工作拆分成多个部分,每个部分由一个CPU负责,从而让多个CPU并行回收。并行回收的执行效率很高,但复杂度增加,另外也会产生一些副作用,比如内存碎片会增加。
  • 并发执行(Concurrent)和应用程序停止(Stop-the-world):应用程序停止的垃圾回收方式在执行垃圾回收的同时会导致应用程序暂停。并发执行的垃圾回收机制需要解决和应用程序的执行冲突,因此并发执行垃圾回收的系统开销比应用程序停止更高,而且执行时也需要更多的内存。
  • 复制(Copying):将堆内存分为两个相同空间,从根开始访问每一个关联的可达对象,将所有的可达对象复制到另一块相同的内存中。对于复制算法而言,因此只需要访问所有的可达对象,将所有的可达对象复制完成后就回收整个空间,完全不用理会不可达对象,所以这种方式的优点就是垃圾回收过程不会产生碎片,遍历空间的成本较小,但是缺点也是很明显,需要复制数据和额外的内存。
  • 标记清除(mark-sweep):也就是不压缩(Non-Compacting)回收方式。垃圾回收器先从根开始访问所有的可达对象,将它们标记为可达状态,然后再遍历一次整个内存区域,对所有的没有标记为可达状态的对象进行回收处理。这种方式无须进行大规模的复制操作,而且内存利用率高,但是这种算法需要两次遍历内存空间,遍历的成本较大,因此造成应用程序暂停的时间随堆空间的大小线性增大,而且垃圾回收回来的内存往往不是连续的,因此整理后堆内存里的碎片更多。
  • 标记压缩(mark-sweep-compact):也就是压缩(Compacting)回收方式。这种方式充分利用上述两种算法的有点,垃圾回收器会先从根开始访问所有可达对象,将它们标记为可达状态。接下来垃圾回收器会将这些活动对象搬迁在一起,这个过程也被称为内存压缩,然后垃圾回收机制再次回收那些不可达对象所占用的内存空间,这样就避免了回收产生内存碎片。
       现行的垃圾回收器用分代的方式来采用不同的回收设计。分代的基本思路是根据对象生存时间的长短,把堆内存分成三个代:
  • Young
  • Old
  • Permanent
        垃圾回收器会根据不同代的特点采用不同的回收算法,从而充分利用各种算法的优点。

3、2 堆内存分代回收

       分代回收的一个依据即使对象生存时间的长短,然后根据不同代采取不同的垃圾回收策略。采用这种“分代回收”的策略基于如下两点事实。
  • 绝大多数的对象不会被长时间引用,这些对象在其Young期间就会被回收。
  • 很老的对象(生存时间很长)和很新的对象(生存时间很短)之间很少存在相互引用的情况。
       根据上面两点事实,对于Young代的对象而言,大部分对象都会很快就进入不可达状态,只有少量的对象能熬到垃圾回收执行时,而垃圾回收器只需保留Young代中处于可达状态的对象,如果采用复制算法只需要少量的复制成本,因此大部分垃圾回收器对Young代都采用复制算法

1、Young代
       对Young代采用复制算法只需遍历那些处于可达状态的对象,而且这些对象的数量很少,可复制成本也不大,因此可以充分发挥复制算法的优点。
       Young代由一个Eden区和两个Survivor区构成。绝大多数对象先分配到Eden区中(有一些大的对象可能会直接被分配到Old代中),Survivor区中的对象都至少在Young代中经历过一次垃圾回收,所以这些对象在被转移到Old代之前会先保留在Survivor空间中。同一时间两个Survivor空间中有一个用来保存对象,而另外一个是空的,用来在下次垃圾回收时保存Young代中的对象。每次复制就是将Eden和第一个Survivor区的可达对象复制到第二个Survivor区,然后清空Eden和第一个Survivor区。Young代的分区如下图所示。


2、Old代
       如果Young代中的对象经过数次垃圾回收依然没有被回收掉,即这个对象经过足够长的时间还处于可达状态,垃圾回收机制就会将这个对象转移到Old代。随着时间的流逝,Old代的对象会越来越多,因此Old代的空间要比Young代的空间更大。处于这两点考虑,Old代的垃圾回收具有如下两个特征。
  • Old代垃圾回收的执行频率无须太高,因此很少有对象死掉。
  • 每次对Old代执行垃圾回收都需要更长的时间来完成。
       基于以上考虑,垃圾回收器通常会使用标记压缩算法,这算法可以避免复制Old代的大量对象,而且由于Old代的对象不会很快死亡,回收过程中不会大量地产生内存碎片,因此相对比较划算。

3、Permanent代
       Permanent代主要用于装载Class、方法等消息,默认为64MB,垃圾回收机制通常不会回收Permanent代中的对象。对于那些需要加载很多类的服务器程序,往往需要加大Permanent代的内存,否则可能会因为内存不足而导致程序终止。
       当Young代的内存将要用完时,垃圾回收机制会对Young代进行垃圾回收,垃圾回收机制会采用较高的频率对Young代进行扫描和回收。因为这种回收的系统开销比较小,因此被称为次要回收(minor collection)。当Old代的内存将要用完时,垃圾回收机制会进行全回收,也就是对Young代和Old代都要进行回收,此时回收成本就大得多了,因此也称为主要回收(major collection)。

3、3 常见的垃圾回收器

1、串行回收器(Serial Collector)
       串行回收器对Young代和Old代的回收都是串行的(只使用一个CPU),而且垃圾回收执行期间会使得应用程序产生暂停。具体策略为:Young代采用串行复制算法,Old代采用串行标记压缩算法
       假设程序Young代的内存分配示意图如下所示。所有划叉的区域代表表示不可达对象,空白区域代表可达对象。


       对于上图所示的内存分配示意图,垃圾回收器将会采用下图所示的方式进行回收。

       系统将Eden区中的活动对象直接复制到初始为空的Survivor区中(也就是To区),如果有些对象占用空间特别大,垃圾回收器会直接将其复制到Old代中。
       对于Form Survivor区中的活动对象(该对象至少经历过一次垃圾回收),到底是复制到To Survivor区中,还是复制到Old代中,则取决于这个对象的生存时间:如果这个对象的生存时间较长,它将复制到Old代中;否则,将被复制到To Survivor区中。
       完成上面复制之后,Eden和From Survivor区剩下的对象都是不可达对象,系统直接回收Eden区和From Survivor区的所有内存,而原来空的To Survivor区则保存了活动对象。在下一次回收时,原本的From Survivor区变为To Survivor区,原本的To Survivor区则变为From Survivor区。完成后的内存分配示意图如下所示。

       串行回收器对Old代的回收采用串行、标记压缩算法(mark-sweep-compact),这个算法有三个阶段:mark(标识可达对象)、sweep(清除)、compact(压缩)。在mark阶段,回收器会识别出哪些对象仍然是可达的,在sweep阶段将会回收不可达对象所占用的内存空间,在compact阶段回收器执行sliding compaction,把活动对象往Old代的前段移动,而在尾部保留一块连续的空间,以便下次为新对象分配内存空间。

2、并行回收器
       并行回收器对于Young代采用与串行回收器基本相似的回收算法,只是增加了多CPU并行的能力,即同时启动多线程并行来执行垃圾回收。并行回收器对于Old代采用与串行回收器完全相同的回收算法,不管计算机有几个CPU,并行回收器依然采用单线程、标记整理的方式进行回收。

3、并行压缩回收器(Parallel Compacting Collector)
       并行压缩算法是从JDK5 update 6开始引入的,它和并行回收器最大的不同是对Old代的回收是用来不同的算法。
       并行压缩回收器对Young代采用与并行回收器完全相同的回收算法。并行压缩回收器的改变主要是体现在对Old代的回收上,系统首先将Old代划分为几个固定大小的区域。在mark阶段,多个垃圾回收线程会并行标记Old代中的可达对象。当某个对象被标记为可达对象时,还会更新该对象所在区域的大小,以及该对象的位置信息。在summary阶段,直接操作Old代的区域,而不是单个的对象。由于每次垃圾回收的压缩都是在Old代的左边部分存储大量的可达对象,对这样的高密度可达对象的区域进行压缩往往很不划算。所以summary阶段会从最左边的区域开始检测每个区域的密度,当检测到某个区域中能回收的空间达到某个数值时(也就是可达对象的密度较小时),垃圾回收器会判定该区域,以及该区域右边的所有区域都应该进行回收,而该区域左边的区域都会被标识为密集区域,垃圾回收器既不会吧新对象移动到这些密集区域中,也不会对这些密集区域进行压缩。summary阶段目前还是串行操作,虽然可以使用并行方式实现,但是重要性不如对mark和压缩阶段的并行重要。
       最后是compact阶段,回收器利用summary阶段生成的数据识别出有哪些区域是需要装填的,多个垃圾回收线程可以并行地将数据复制到这些区域中。经过这个过程后,Old代的一端会密集地存在大量的活动对象,另一端则存在大块的空闲块。

4、并发标识-清理(Mark-Sweep)回收器(CMS)
       CMS回收器对Young代的回收方式和并行回收器的回收方式完全相同。由于对Young代的回收依然采用复制回收算法,因此垃圾回收时依然会导致程序暂停,除非依靠多CPU并行来提高垃圾回收的速度。通常来说,建议适当加大Young代的内存。如果Young代的内存足够大就不用频繁地进行垃圾回收了,而且增大垃圾回收的时间间隔后可以让更多的位于Young代中的Java对象自己死掉,从而避免复制。但将Young代的内存设置得过大也有一个坏处:当垃圾回收器回收Young代的内存时,复制成本会显著上升(复制算法必须等Young代满了之后才开始回收),所以回收时会让系统的暂停时间显著加大。
       CMS对Old代的回收多数是并发操作,而不是并行操作。垃圾回收开始时需要一个短暂的暂停,此阶段成为初始标识(initial mark)阶段,这个阶段仅仅标识出那些被直接引用的可达对象。接下来进入并发标识阶段(concurrent marking phase),垃圾回收器会依据在初始标识中发现的可达对象来寻找其他的可达对象。由于在并发标识阶段应用程序会同时运行,无法保证所有的可达都被标识出来,因此应用程序会再次很短地暂停一下,多线程并行地重新标识之前可能因为并发而漏掉的对象,这个阶段被称为再标识(remark)阶段。
       由于CMS不会进行内存压缩,也就是说,不可达对象占用的内存被回收以后,垃圾回收器不会移动可达对象占用的内存。由于Old代的可用空间是不连续的,因此CMS垃圾会回收器必须保存一份可用空间的列表。当需要分配对象时,垃圾回收器就是要通过这份列表找到能容纳新对象的空间,这样就会使得分配内存时的效率下降,从而影响了Young代回收过程中将Young代
对象移动到Old代的效率。
       对于CMS回收器而言,当垃圾回收器执行并发标识时,应用程序在运行的同时也在分配对象,因此Old代也同时在增长。而且,虽然可达对象在标识阶段会被识别出来,但有些在标识阶段成为垃圾的对象并不能立即被回收,只有等到下次垃圾回收时才能被回收。因此CMS回收器较之前的几种回收器需要更大的堆内存。








1 0