本文作者:@Ryan Miao 本文链接:https://www.cnblogs.com/woshimrf/p/jvm-garbage.html

1. 垃圾收集算法

1.1 标记-清除算法

最基础的收集算法是“标记-清除”(Mark-Sweep)算法,分两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

不足:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能导致以后在程序运行过程需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一个的垃圾收集动作。

1.2 复制算法

为了解决效率问题,一种称为复制(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活着的对象复制到另外一块上,然后再把已经使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。代价是内存缩小为原来的一半。

商业虚拟机用这个回收算法来回收新生代。IBM 研究表明 98%的对象是“朝生夕死“,不需要按照 1-1 的比例来划分内存空间,而是将内存分为一块较大的”Eden“空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另外一个 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。Hotspot 虚拟机默认 Eden 和 Survivor 的比例是 8-1.即每次可用整个新生代的 90%, 只有一个 survivor,即 1/10 被”浪费“。当然,98%的对象回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10%的对象存活,当 Survivor 空间不够时,需要依赖其他内存(老年代)进行分配担保(Handle Promotion).

如果另外一块 survivor 空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

1.3 eden survivor 复制过程概述

Eden Space 字面意思是伊甸园,对象被创建的时候首先放到这个区域,进行垃圾回收后,不能被回收的对象被放入到空的 survivor 区域。

Survivor Space 幸存者区,用于保存在 eden space 内存区域中经过垃圾回收后没有被回收的对象。Survivor 有两个,分别为 To Survivor、 From Survivor,这个两个区域的空间大小是一样的。执行垃圾回收的时候 Eden 区域不能被回收的对象被放入到空的 survivor(也就是 To Survivor,同时 Eden 区域的内存会在垃圾回收的过程中全部释放),另一个 survivor(即 From Survivor)里不能被回收的对象也会被放入这个 survivor(即 To Survivor),然后 To Survivor 和 From Survivor 的标记会互换,始终保证一个 survivor 是空的。

为啥需要两个 survivor?因为需要一个完整的空间来复制过来。当满的时候晋升。每次都往标记为 to 的里面放,然后互换,这时 from 已经被清空,可以当作 to 了。

1.4 标记-整理算法

复制收集算法在对象成活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费 50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都 100%存活的极端情况,所以,老年代一般不能直接选用这种算法。

根据老年代的特点,有人提出一种”标记-整理“Mark-Compact 算法,标记过程仍然和标记-清除一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理端边界以外的内存.

1.5 分代收集算法

当前商业虚拟机的垃圾收集都采用”分代收集“(Generational Collection)算法,这种算法根据对象存活周期的不同将内存划分为几块。一般把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代,每次垃圾收集时都发现大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率较高,没有额外的空间对它进行分配担保,就必须使用”标记-清理“和”标记-整理“算法来进行回收。