本博客为《深入理解java虚拟机》的学习笔记,所以大部分内容来自此书。
一 是否可以回收?
Java中对象能否被回收取决于对象是“死”还是“活”,下面我们就介绍一下判定算法。
1 引用计数
1) 算法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器加一;当引用失效时,计数器减一;计数器的值为0时,对象就是不能被使用的了,即处于可以被回收的状态了。
2) 分析
这种算法的优点就是简单,但java中无法使用这种方法,因为java中允许对象循环引用,例如Student、Teacher这种模型,我们可以再在Student中引用Teacher,同时在Teacher中引用Student,此时计数器的值无法恢复到0,即无法被回收,这显然与我们的预期不一致。
2 可达性分析
java、C#等主流语言的实现中,均是通过可达性分析来判断对象是否存活的。
1) 算法
通过一系列被称之为“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称之为引用链(Reference Chain),当一个对象与GC Roots没有引用链相连时,则证明是不可用的。模型如下:
2) GC Root
Java中可以作为GC Root对象有以下几种:
虚拟机栈(栈帧中的本地变量表)引用的对象。
方法区中类的静态属性引用的对象。
方法去中常量引用的对象。
本地方法栈中JNI(即一般说的native方法)引用的对象。
3) 引用
JDK1.2之后,java支持以下几种引用:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference),他们的强度一次递减。
a) 强引用
只要强引用存在,垃圾收集器就不会回收他们。常用的Object obj = new Object()就属于强引用。
b) 软引用
对于软引用关联的对象,在系统将要发生内存溢出前,将会把这些对象列入回收返回,进行第二次回收。如果这次回收后依然没有足够的内存,才会抛出内存溢出异常。可以通过SoftReference类可以实现软引用。
由此可见,软引用是用来描述还有用但是非必须的对象。
c) 弱引用
使用弱引用关联的对象,只能存活到下一次垃圾收集发生之前,即垃圾收集器工作时,总会收集这部分对象。可以通过WeakReference类来实现弱引用。
由此可见,弱引用也是用来描述还有用但是非必须的对象。
d) 虚引用
虚引用也称幽灵引用或幻影引用,是最弱的一种引用关系。虚引用不会对对象的生命周期产生影响,无法通过虚引用获得一个对象的实例。
对一个对象设置虚引用关联的唯一目的是能够在对象呗垃圾收集器回收时收到一个系统通知。可以通过PhantomReference类来实现虚引用。
3 生死判定
通过可达性分析,在引用链上的对象都不能被回收,但是不在引用链上的对象也不是说一定是“非死不可”的。整个判定过程需要经历两次标记:
1) 第一次标记
如果通过可达性分析后,发现一个对象与GC Root不存在相关联的引用链,那么他将被第一次标记,并且进行一次筛选,即是否有必要执行此对象的finalize()方法。以下两种情况被认为是没有必要执行:对象没有覆盖Object的finalize()方法,或者finalize()方法已经被虚拟机执行过。
如果对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列中,并在稍后由一个虚拟机自动创建、低优先级的Finalizer线程去执行对象的finalize()方法。
finalize()方法时对象逃脱死亡的最后一次机会,如果对象希望拯救自己,那么需要将自己与引用链上的任何一个对象关联起来。例如:将this赋值给某个类变量或者某个对象的成员变量。
注意:虚拟机会触发执行finalize()方法,但是并不会保证等待这个方法执行结束;原因是:如果一个对象在finalize方法中执行缓慢或者死循环,可能会导致F-Queue中其他的对象永久的处于等待,甚至导致整个内存回收崩溃。
2) 第二次标记
GC将会对F-Queue中的对象进行第二次标记,如果对象在finalize()方法中拯救了自己,那么此次标记中会将此对象移除“即将回收”的集合;如果对象在finalize()方法没有拯救自己,那么基本上就认为这个对象已经死了,可以被回收了。
4 回收方法区
java虚拟机规范中没有要求在方法区实现垃圾回收。在这一部分进行垃圾回收的性价比一般较低。
永久代的垃圾回收主要包括:废弃常量和无用的类。
1) 回收废弃常量
废弃常量的回收和java堆的的回收非常类似。
以字符串为例,如果常量池中的字符串“abcd”已经没有任何对象引用了,即没有其他地方引用这个“字面量”,如果发生内存回收,那么此“abcd”字符串将会被回收。
常量池中的其他类、接口、方法、字段的符号引用也与之类似。
2) 回收无用的类
类只有满足以下条件才能算作是无用的类。
该类的所有实例已经被回收,即java堆中不存在该类的任何实例。
加载该类的ClassLoader已经被回收。
该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射方位该类的方法。
和堆中对象不同的是,虚拟机可以对无用的类进行回收,但并不是不用了就一定会被回收。
二 如何回收?
常见的垃圾收集算法有:标记-清除算法(Mark-Sweep)、复制算法(Coping)、标记-整理算法(Mark-Compact)。
1 标记-清除算法
1) 算法
标记-清除分为标记、清除两个阶段。
标记出所有需要回收的对象,在标记完成后统一回收被标记的对象。标记的过程就是前面描述的“生死判定”过程。
2) 不足
ÃÂ 效率不高,标记和清除两个过程效率都不高。
ÃÂ 标记-清除后悔产生大量不连续的内存碎片。
3) 模型
2 复制算法
复制算法主要是解决标记清楚算法效率问题而设计的。
1) 算法
将内存按照容量划分成大小相等的两块每次只使用其中的一块,当一块使用完了,将还存活的对象复制到另外一块上,然后将已经使用过的内存空间一次清理掉。
2) 分析
每次对其中的一个块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只需要移动指针到堆顶,然后按顺序分配内存即可。此种方式实现简单、运行高效。
缺点是内存缩小了一半。
3) 模型
4) 改进
a) 改进余地
绝大多数数对象都是朝生夕死的,所以按照1:1的比例来划分内存空间是非常浪费的。一般的做法是:将内存划分成一块较大的Eden空间和两块Survivor空间,每次使用Eden和一块Survivor。当回收时将存活的对象一次性的复制到另外一块Survivor中,然后一次性的清理掉Eden和刚才使用的Survivor空间。
Hotspot虚拟机默认的Eden : Survivor = 8 : 1,即每次使用整个内存空间的90%,浪费10%的空间。
b) 改进模型
c) 新的问题
如果某次gc后发现存活对象的内存大于survivor的大小,将会需要老年代及进行担保。
如果每次gc后存活的对象较多,频繁的复制到另一个survivor中也会导致性能降低。
3 标记-整理算法
复制算法的两个问题,导致了其不适合作为老年代的回收算法。
1)Â Â Â Â Â Â 算法
和“标记-清除”算法一样,标记-整理算法也是通过上述“生死判定”过程对对象进行标记,接着将所有存活的对象向一端移动,然后清理掉边界之外的内存。
2)Â Â Â Â Â Â 模型
4 分代收集算法
根据对象存活周期的不同,将内存划分为几块,一般将java堆分为新生代、年老代,并根据各个年代的特点采用合适的收集算法。
新生代中,绝大多数对象都是迅速死去,只有少量存活,适合采用优化后的复制算法,只需要付出少量的复制成本就可以完成收集。
年老代因为对象存活率高,并且没有额外的空间对它进行分配担保,所以就必须采用标记-清除、标记-整理算法进行回收。