【JVM】自动内存管理之垃圾收集(GC)

使用Java最方便的莫过于不用关心内存泄漏(极个别情况还是会发生内存泄漏),也不用担心使用了某个野指针导致程序crash,这些都要归功于JVM强大的垃圾收集Garbage Collection(GC)。

通俗地理解,垃圾收集是JVM把我们本应该手动释放的内存包揽过去自行管理的过程,属于自动内存管理,它极大地避免人为错误地释放内存或者内存泄漏。一个对象,程序员负责让它生,至于它怎么死就交给垃圾收集器,垃圾收集器就是使用各种策略进行垃圾收集的具体实现。因此,垃圾收集任务的基本流是:发现一个没用的对象,对它的内存进行回收。这个基本流对应着三个技术问题:

(1)如何判断对象已经没用?

(2)可以马上进行回收吗?

(3)如何高效进行回收?

明白这三个问题就能理解垃圾收集是如何工作的了,接下来说的是它的实现技术。

一、 如何判断对象已经没用?

如何判断一个对象已经不可用,目前主要有两种算法:引用计数算法和可达性分析算法。

【引用计数算法】

我们知道,如果一个对象没有任何指向它的引用,说明这个对象不可能被访问,也就是说这个对象没用了,不回收的话浪费内存。这就很容易想到用引用计数算法来判断一个对象的死活,当有一个地方引用它,引用计数加一,当一个地方对它的引用失效,引用计数器减一,当引用计数器为0时,说明对象已经没用,可以对它进行回收。

Object object=new Object();//在内存中创建一个对象,该对象的引用计数器为1
Object temp=object;//对象object的引用计数器加一
temp=null;// 对象object的引用计数器减一
object=null;// 对象object的引用计数再减一,此时为0,可以进行回收了

这个算法非常容易理解,也简单高效,但JVM并没有使用这个算法,这是为什么?

原因在于引用计数算法存在一个缺陷,当对象之间互相循环引用的话,它没法进行准确的判断。例如:

class MyObject{
    MyObject next;
}
MyObject A=new MyObject();//对象1被创建,引用计数为1
MyObject B=new MyObject();//对象2被创建,引用计数为1
A.next=B;               //对象2引用计数加1,为2
B.next=A;               //对象1引用计数加1,为2
A=null;                 //对象1引用计数减1,为1
B=null;                 //对象2引用计数减1,为1

我们发现,A,B的引用计数都不为0,使用引用计数算法的垃圾收集器不会对A,B进行回收,但是可以看出,A,B两个对象已经没用,不会有任何地方可以访问它们了。

【可达性分析算法】 现在的虚拟机基本都是使用这个算法判断对象是否应该被回收,这个算法解决了对象相互循环引用的问题,它通过可达性分析来判断对象是否已经“死去”,就像一颗有根树一样,每一个可用的对象都挂在树上,根是一系列成为GC Roots的起点,如果根节点能够达到该对象节点,说明对象是存活的,如果无法到达,则说明对象已经不可用,应该对其进行回收。如下图:

GC Roots

从图中可以发现,当A=null;B=null;时,切断其到GC Roots的引用链,GC Roots已经不可到达A,B,next对象,尽管A和B相互循环引用,但可以判断A和B已经不可访问,可以对其进行回收了。

二、 可以马上进行回收吗?

在JDK 1.2后,引用按强到弱分为:强引用、软引用、弱引用、虚引用。有些对象虽然已经“死去”,不马上对其进行回收是考虑到,对象可能会被重新创建,如果内存不是特别紧张,可以不对它进行回收,这样下次要使用时可以直接引用,只有内存紧张时,才对其进行回收,这能避免不必要的创建和回收工作。

【强引用】 垃圾收集器永远不会回收被强引用的对象,例如A= new MyObject()就是强引用,不难想象,回收这样的对象会引发严重的后果。

【软引用】 可以使用SoftReference实现软引用,表明这个对象还有用但不是必需的,在内存不足的时候,垃圾收集器可以对其进行回收,内存充足时一般不会对其进行回收。

【弱引用】 可以使用WeakReference实现弱引用,下次垃圾回收时,无论内存是否足够,都会对弱引用的对象进行回收。

【虚引用】 可以使用PhantomReference来实现虚引用,它是最弱的一种引用,不能通过虚引用取得对象的实例,虚引用通常的作用是被回收时可以收到系统通知。

三、 如何高效进行回收?

不同的收集算法各有优点,没有哪一种是万能的,也没有哪一种是最高效的,要根据不同的情况进行选择,一般来说,垃圾收集器会使用多种垃圾收集算法进行内存回收。

【标记-清除算法】

顾名思义,首先标记需要被回收的对象,然后对被标记的对象进行统一回收。这个算法非常简单直观,但效率不高,另外还会产生大量不连续的内存碎片。
比如一块内存如下:0代表未使用,1代表存活对象,X代表可回收(下同)
回收前:1x11xxx1011xx1x1
回收后:1011000101100101
可以发现,回收后产生了很多不连续的内存碎片。

【标记-整理算法】

过程与标记-清除算法一样,但是接下来还会让存活的内存移动到一端,是内存分为存活对象和未使用两大部分。使用标记-整理算法回收效果如下:
回收前:1x11xxx1011xx1x1
回收后:1111111100000000
这个算法能够避免产生内存碎片,但这个算法比复制算法慢。

【复制算法】

这个算法将内存分为大小相同的两块,例如A和B,内存分配时总是只使用其中一块,当进行回收时,把A存活对象复制到B,然后清除A,A和B相互使用,这样就能避免产生内存碎片,效率也高。
使用复制算法回收效果如下:
回收前:1x11xxx1011xx1x1 0000000000000000
回收后:0000000000000000 1111111100000000
可以注意到,实际上能使用的内存只有原来的一半,这样的牺牲值不值呢?
实际上并不一定要按照1:1进行划分,但是发生意外情况时要进行分配担保。

【分代收集算法】

这个算法可以说是策略性地使用上面得算法,根据对象生存时间的不同进行划分,在不同的层次选择适合的算法。如果把堆分为新生代和老年代,在新生代区域会有大量的对象死去,则使用复制算法比较高效,老年代大部分对象都会存活,因此使用标记-清除算法或者标记-整理算法。

参考资料:
《深入理解Java虚拟机》机械工业出版社 周志明 2013

【2016-04-15 15:23:33】

发表评论

电子邮件地址不会被公开。 必填项已用*标注