Java 垃圾回收机制

1,599 阅读9分钟
原文链接: www.jianshu.com

Java的内存分布

在JVM中,内存是按照分代进行组织的。

其中,堆内存分为年轻代和年老代,非堆内存主要是Permanent区域,主要用于存储一些类的元数据,常量池等信息。而年轻代又分为两种,一种是Eden区域,另外一种是两个大小对等的Survivor区域。之所以将Java内存按照分代进行组织,主要是基于这样一个“弱假设” - 大多数对象都在年轻时候死亡。同时,将内存按照分代进行组织,使得我们可以在不同的分代上使用不同的垃圾回收算法,使得整个内存的垃圾回收更加有效。

年轻代的垃圾回收

在年轻代上采用的垃圾回收算法是“Mark-Copy”算法,并不同于我们前面所了解的任何一种基本垃圾回收算法,但是Mark算法是一样的,基于根对象找到所有的可达对象,具体可看Mark-Sweep算法中的Mark步骤. 而对于Copy算法,它仅仅是简单的将符合一定年龄的对象从一个分代拷贝到另一个分代。具体的回收过程如下:

首先,新对象的内存分配都是先在Eden区域中进行的,当Eden区域的空间不足于分配新对象时,就会触发年轻代上的垃圾回收(发生在Eden和Survivor内存区域上),我们称之为"minor garbage collection".同时,每个对象都有一个“年龄”,这个年龄实际上指的就是该对象经历过的minor gc的次数。如图1所示,当对象刚分配到Eden区域时,对象的年龄为“0”,当minor gc被触发后,所有存活的对象(仍然可达对象)会被拷贝到其中一个Survivor区域,同时年龄增长为“1”。并清除整个Eden内存区域中的非可达对象。

当第二次minor gc被触发时(如图2所示),JVM会通过Mark算法找出所有在Eden内存区域和Survivor1内存区域存活的对象,并将他们拷贝到新的Survivor2内存区域(这也就是为什么需要两个大小一样的Survivor区域的原因),同时对象的年龄加1. 最后,清除所有在Eden内存区域和Survivor1内存区域的非可达对象。

当对象的年龄足够大(这个年龄可以通过JVM参数进行指定,这里假定是2),当minor gc再次发生时,它会从Survivor内存区域中升级到年老代中,如图3所示。

其实,即使对象的年龄不够大,但是Survivor内存区域中没有足够的空间来容纳从Eden升级过来的对象时,也会有部分对象直接升级到Tenured内存区域中。

年老代的垃圾回收

当minor gc发生时,又有对象从Survivor区域升级到Tenured区域,但是Tenured区域已经没有空间容纳新的对象了,那么这个时候就会触发年老代上的垃圾回收,我们称之为"major garbage collection".

而在年老代上选择的垃圾回收算法则取决于JVM上采用的是什么垃圾回收器。通过的垃圾回收器有两种:Parallel Scavenge(PS) 和Concurrent Mark Sweep(CMS)。这两种垃圾回收器的不同更多的是体现在年老代的垃圾回收过程中,年轻代的垃圾回收过程在这两种垃圾回收器中基本上是一致的。

就像其名字所表示的那样,Parallel Scavenge垃圾回收器在执行垃圾回收时使用了多线程来一起进行垃圾回收,这样可以提高垃圾回收的效率。而Concurrent Mark Sweep垃圾回收器在进行垃圾回收时,应用程序可以同时运行。

Parallel Scavenge

PS垃圾回收器在年老代上采用的垃圾回收算法可以看作是标记-清除算法标记-压缩算法的结合体。

首先,PS垃圾回收器先是会在年老代上使用标记-清除算法来回收掉非可达对象所占有的空间,但是我们知道,标记清除算法的一个缺陷就是它会引起内存碎片问题。继而有可能会引发连续的major gc。假设当前存在的内存碎片有10M,但最大的内存碎片只能容纳2M的对象,这个时候如果有一个3M的对象从Survivor区域升级到Tenured区域,那Tenured区域也没有办法存放这个3M的对象。结果就是不断的触发major gc,直到Out of Memory。所以,PS垃圾回收器在清除非可达对象后,还会进行一次compact,来消除内存碎片。

Concurrent Mark Sweep

CMS垃圾收集器相比于PS垃圾收集器,它成功的减少了垃圾收集时暂停应用程序的时间,因为CMS在进行垃圾收集时,应用程序是可以并行运行的。下面让我们来看看它是怎么做到的。

从它的名字可以看出,CMS垃圾收集器在年老代上采用的垃圾回收算法是标记-清除算法。但是,它跟标准的标记-清除算法略有不同。它主要分为四个阶段:

  1. Initial Mark阶段 - 这个阶段是Stop-The-World的,它会暂停应用程序的运行,但是在这里阶段,它不会标记出在Tenured区域中所有的可达对象。它只会从根对象开始出发,标记到根对象的第一层孩子节点即停止。然后恢复应用程序的运行。所以,这个暂停应用程序的时间是很短的。
  2. Concurrent Mark阶段 - 在这个阶段中,CMS垃圾回收器以Initial Mark阶段标记的节点为根对象,重新开始标记Tenured区域中的可达对象。当然,在这个阶段中是不需要暂停应用程序的。这也是它称为"Concurrent Mark"的原因。这同时也造成了一个问题,那就是由于CMS垃圾回收器和应用程序同时运行,Concurrent Mark阶段它并不保证在Tenured区域的可达对象都被标记了 - 应用程序一直在分配新对象。
  3. Remark阶段 - 由于Concurrent Mark阶段它并不保证在Tenured区域的可达对象都被标记了,所以我们需要再次暂停应用程序,确保所有的可达对象都被标记。为了加快速度,这里也采用了多线程来同时标记可达对象。
  4. Concurrent Sweep阶段 - 最后,恢复应用程序的执行,同时CMS执行sweep,来清除所有非可达对象所占用的内存空间。

从下图可以看到PS和CMS垃圾收集器的区别:

黑色箭头代表应用程序的运行,绿色箭头代表CMS垃圾收集器的运行。一根线条表示单线程,多个线条表示多线程。

所以,相比于PS垃圾收集器,CMS垃圾收集器成功的减少了应用程序暂时的时间。

Garbage First(G1)垃圾收集器

但是很不幸的是,CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是由于它没有Compact阶段,它还是存在着内存碎片问题。于是,为了去除内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优点,JAVA7发布了一个新的垃圾收集器 - G1垃圾收集器。它会在未来逐步替换掉CMS垃圾收集器。

G1垃圾收集器和CMS垃圾收集器有几点不同。首先,最大的不同是内存的组织方式变了。Eden,Survivor和Tenured等内存区域不再是连续的了,而是变成了一个个大小一样的region - 每个region从1M到32M不等。

一个region有可能属于Eden,Survivor或者Tenured内存区域。图中的E表示该region属于Eden内存区域,S表示属于Survivor内存区域,T表示属于Tenured内存区域。图中空白的表示未使用的内存空间。G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。这种内存区域主要用于存储大对象-即大小超过一个region大小的50%的对象。

在G1垃圾收集器中,年轻代的垃圾回收过程跟PS垃圾收集器和CMS垃圾收集器差不多,新对象的分配还是在Eden region中,当所有Eden region的大小超过某个值时,触发minor gc,回收Eden region和Survivor region上的非可达对象,同时升级存活的可达对象到对应的Survivor region和Tenured region上。对象从Survivor region升级到Tenured region依然是取决于对象的年龄。

对于年老代上的垃圾收集,G1垃圾收集器也分为4个阶段,基本跟CMS垃圾收集器一样,但略有不同:

  1. Initial Mark阶段 - 同CMS垃圾收集器的Initial Mark阶段一样,G1也需要暂停应用程序的执行,它会标记从根对象出发,在根对象的第一层孩子节点中标记所有可达的对象。但是G1的垃圾收集器的Initial Mark阶段是跟minor gc一同发生的。也就是说,在G1中,你不用像在CMS那样,单独暂停应用程序的执行来运行Initial Mark阶段,而是在G1触发minor gc的时候一并将年老代上的Initial Mark给做了。
  2. Concurrent Mark阶段 - 在这个阶段G1做的事情跟CMS一样。但G1同时还多做了一件事情,那就是,如果在Concurrent Mark阶段中,发现哪些Tenured region中对象的存活率很小或者基本没有对象存活,那么G1就会在这个阶段将其回收掉,而不用等到后面的clean up阶段。这也是Garbage First名字的由来。同时,在该阶段,G1会计算每个 region的对象存活率,方便后面的clean up阶段使用 。
  3. Remark阶段 - 在这个阶段G1做的事情跟CMS一样, 但是采用的算法不同,G1采用一种叫做SATB(snapshot-at-the-begining)的算法能够在Remark阶段更快的标记可达对象。
  4. Clean up/Copy阶段 - 在G1中,没有CMS中对应的Sweep阶段。相反 它有一个Clean up/Copy阶段,在这个阶段中,G1会挑选出那些对象存活率低的region进行回收,这个阶段也是和minor gc一同发生的,如下图所示:

从上可以看到,由于Initial Mark阶段Clean up/Copy阶段都是跟minor gc同时发生的,相比于CMS,G1暂停应用程序的时间更少,从而提高了垃圾回收的效率。