浅谈垃圾回收算法

 

一、首先我们需要如何判断一个对象已死?

  • 引用计数法

引用计数描述的算法为: 给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数 器为0的对象就是不能再被使用的,即对象已""。 引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法。比如Python语言就采用引用计 数法进行内存管理。

但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问 题。即JVM并不使用引用计数法来判断对象是否存活。

  • 可达性分析算法

在上面我们讲了,Java并不采用引用计数法来判断对象是否已"",而采用"可达性分析"来判断对象是否存活(同样 采用此法的还有C#Lisp-最早的一门采用动态内存分配的语言)。    此算法的核心思想为 : 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径 称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(GC Roots到这个对象不可达)时,证明此对象 是不可用的。以下图为例:

浅谈垃圾回收算法

 

对象Object5-Object7之间虽然彼此还有关联,但是它们到GC Roots是不可达的,因此他们会被判定为可回收对象。

Java语言中,可作为GC Roots的对象包含下面几种:

1. 虚拟机栈(栈帧中的本地变量表)中引用的对象

2. 方法区中类静态属性引用的对象

3. 方法区中常量引用的对象

4. 本地方法栈中JNI(Native方法)

引用的对象JDK1.2以前,Java中引用的定义很传统 : 如果引用类型的数据中存储的数值代表的是另一块内存的起始地址,就 称这块内存代表着一个引用。这种定义有些狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态。我们希望能描述这一类对象 : 当内存空间还足够时,则能保存在内存中;如果内存空间在进行垃圾回收后还是非常 紧张,则可以抛弃这些对象。很多系统中的缓存对象都符合这样的场景。 在JDK1.2之后,Java对引用的概念做了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用的强度依次递减。

1. 强引用 : 强引用指的是在程序代码之中普遍存在的,类似于"Object obj = new Object()"这类的引用,只 要强引用还存在,垃圾回收器永远不会回收掉被引用的对象实例。

2. 软引用 : 软引用是用来描述一些还有用但是不是必须的对象。对于软引用关联着的对象,在系统将要发 生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收。如果这次回收还是没有足够的内 存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。

3. 弱引用 : 弱引用也是用来描述非必需对象的。但是它的强度要弱于软引用。被弱引用关联的对象只能生 存到下一次垃圾回收发生之前。当垃圾回收器开始进行工作时,无论当前内容是否够用,都会回收掉只 被弱引用关联的对象。在JDK1.2之后提供了WeakReference类来实现弱引用。

4. 虚引用 : 虚引用也被称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的 存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引 用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了 PhantomReference类来实现虚引用。

二、垃圾回收算法

  • 标记-清除算法

"标记-清除"算法是最基础的收集算法。算法分为"标记""清除"两个阶段 : 首先标记出所有需要回收的对象,在标 记完成后统一回收所有被标记的对象(标记过程见3.1.2章节)。后续的收集算法都是基于这种思路并对其不足加以改 进而已。

"标记-清除"算法的不足主要有两个 :

1. 效率问题 : 标记和清除这两个过程的效率都不高

2. 空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较 大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。

浅谈垃圾回收算法

  • 复制算法(新生代回收算法)

 

"复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一 块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存 区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂 情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。算法的执行流程如下图 :

浅谈垃圾回收算法

现在的商用虚拟机(包括HotSpot都是采用这种收集算法来回收新生代)

新生代中98%的对象都是"朝生夕死"的,所以并不需要按照1 : 1的比例来划分内存空间,而是将内存(新生代内存) 分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,每次使用Eden和其中一块Survivor(两个 Survivor区域一个称为From区,另一个称为To区域)。当回收时,将EdenSurvivor中还存活的对象一次性复制 到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。 当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保

HotSpot默认EdenSurvivor的大小比例是8 : 1,也就是说Eden:Survivor From : Survivor To = 8:1:1。所以每次新生代可用内存空间为整个新生代容量的90%,而剩下的10%用来存放回收后存活的对象。

HotSpot实现的复制算法流程如下:

1. Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触 发Minor gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象, 则直接复制到To区域,并将EdenFrom区域清空。

2. 当后续Eden又发生Minor gc的时候,会对EdenTo区域进行垃圾回收,存活的对象复制到From区域,并将 Eden和To区域清空。

3. 部分对象会在FromTo区域中复制来复制去,如此交换15(JVM参数MaxTenuringThreshold决定,个参数默认是15),最终如果还是存活,就存入到老年代

浅谈垃圾回收算法

  • 标记-整理算法(老年代回收算法)

复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。 针对老年代的特点,提出了一种称之为"标记-整理算法"。标记过程仍与"标记-清除"过程一致,但后续步骤不是直接 对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。流程图如下:

浅谈垃圾回收算法

  • 分代收集算法

当前JVM垃圾收集都采用的是"分代收集(Generational Collection)"算法,这个算法并没有新思想,只是根据对象存 活周期的不同将内存划分为几块。 一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采 用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。

面试题 : 请问了解Minor GCFull GC么,这两种GC有什么不一样吗

1. Minor GC又称为新生代GC : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝生夕灭的特 性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。

2. Full GC 又称为 老年代GC或者Major GC : 指发生在老年代的垃圾收集。出现了Major GC,经常会伴随 至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)Major GC的速度一般会比Minor GC10倍以上。