垃圾收集算法
一. 需要回收哪些对象?
JVM的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭 ,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。而Java堆区和方法区的内存分配和回收是动态的,正是垃圾收集器所需关注的部分
1.如何判断对象已死?
1.1 引用计数算法
引用计数是垃圾收集器中的早期策略,他的实现方式是为每个对象都添加一个引用计数器,当一个对象被创建时,就将这个计数器的值设置为1,当任何其它变量引用这个对象的时,计数器的值加1,如果这个对象实例的某个引用超过了他的作用域或者被设置为一个新值时,对象实例的引用计数器减1,任何引用计数器为0的对象实例可以被当作垃圾收集
虽然这种算法实现占用了一些额外内存,但是他原理简单,效率也高。但是这种算法很难解决对象的循环引用问题,使对象永远得不到回收,类似于死锁一样得不到释放
public class ReferenceFindTest {
public static void main(String[] args) {
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();
object1.object = object2;
object2.object = object1;
object1 = null;
object2 = null;
}
}
1.2 可达性分析算法
主流的商用编程语言(Java、C#)都是使用这个算法来判断对象是否存活
可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象
那些对象可以作为GC Roots 对象
-
虚拟机栈中引用的对象(栈帧中的本地变量表)
-
方法区中类静态属性引用的对象 如:Java类中引用类型的对象
-
方法区中常量引用的对象 如:字符串常量池的引用
-
本地方法栈中JNI(Native方法)引用的对象
-
被同步锁(synchronized)持有的对象
2. finalize机制
就算通过可达性分析算法判定为不可达的对象,并不是非死不可的,要真正的宣告一个对象的死亡,至少要经历两次标记过程:
第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记
第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()
方法,在finalize()
方法中没有重新与引用链建立关联关系的,将被进行第二次标记
-
如果这个对象没有覆盖
finalize()
,或者finalize()
已经被虚拟机调用过:虚拟机将这两种情况都视为没有必要执行finalize()
方法 -
如果这个对象被判定为有必要执行
finalize()
方法,那么这个对象将会放置在一个叫F-Queue的队列中,并在稍后由虚拟机自动创建的优先度比较低的Finalizer
线程去执行它。这里所谓的“执行”是虚拟机会触发这个方法,但并不会承诺等待它运行结束,这样做的原因是,如果一个对象在finalize()
中执行缓慢,或者发生了死循环(更极端的情况),将很可能导致F-Queue中队列中的其他对象永久处于等待,甚至导致整个内存回收系统崩溃 -
finalize()
方法是对象逃脱死亡命运的最后一次机会,稍后GC将会对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()
方法中拯救自己,只要重新与引用链上的任何一个对象建立关联就行,如果成功,那在第二次标记时它将被移出“即将回收”的集合;如果对象在这个时候还没有逃脱,那基本上它就真的被回收了
第二次标记成功的对象将真的会被回收,如果对象在finalize()
方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活,虽然可以用这种方法拯救对象。但是尽量避免使用,因为他运行代价高,不确定性大无法保证各个对象的调用顺序。
3. 强软弱虚 引用
无论是引用计数算法还是可达性分析算法,判断对象存放都与引用相关
Java中的四种引用:强引用、软引用、弱引用、虚引用
- 强引用:强引用指的是代码中普遍存在的,类似于
Object obj=new Object();
只要引用还存在,垃圾回收器永远不会回收掉被引用的对象 - 软引用:软引用是用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统进入即将发生OOM之前,将会把这些对象列入回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常,在JDK1.2之后,提供
SoftReference
类来实现软引用 - 弱引用:弱引用也是用来描述非必须对象的,但是他的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾回收器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供
WeakReference
类来实现弱引用 - 虚引用:虚引用也称为幽灵引用,他是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对齐生存时间造成影响,也无法通过虚引用获取一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知在JDK1.2之后,提供
PhantomReference
类来实现虚引用
二. 垃圾收集算法
1. 标记-清除算法
标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记
,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如下图所示
缺点:
- 标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于堆中的大多数对象都是朝生夕死的,因此标记和清除这两个动作的执行效率会随着对象数量的增长而降低
- 内存空间不连续、碎片化问题。碎片空间太多,以至于后面要给大对象分配内存时找不到连续的内存空间,此时不得不提前触发另一次垃圾回收
2. 标记-复制算法
复制算法的设计原理就是将内存分为大小相等的两块,每次只使用一块,当一块空间满了,就标记存活对象把他们复制到另一块空闲的空间中,然后再把这一块中所有的垃圾对象清除掉
标记-复制算法解决了 标记-清除算法回收后出现内存碎片的问题,也解决了回收大量对象时效率低的问题,但是一共有两大缺点:
- 可用内存空间浪费一半,导致触发回收的动作频繁
- 如果存活对象过多,那么复制阶段的工作量变得庞大
现在的Java虚拟机都优先采用了这种收集算法去回收新生代,ibm公司对新生代的对象做出了“朝生夕灭”的诠释 即:新生代98%的对象都活不过第一轮,因此不需要 1:1的比例去划分内存
HotSpot虚拟机通过这种策略设计了新生代的内存布局,将新生代的内存分成了一个Eden区和两个Survivor区来匹配这种标记-复制算法。第一次垃圾收集将eden的存活对象,复制到其中一个Survivor区,然后直接清理掉 Eden,第二次垃圾收集时将 Eden 和 Survivor 中仍然存活的对象一次性复制到另一块 Survivor 上,然后直接清理掉 Eden 和已用过的那块 Survivor。HotSpot 默认Eden 和 Survivor 的大小比例是 8:1,即每次新生代中可用空间为整个新生代的 90%
我在之前的文章中讲到了 新生代的内存分布,有兴趣的可以看一下,传送门:
3. 标记-整理算法
标记整理算法的标记操作和 “标记-清除” 算法一致,但是后续操作不只是直接清理可回收对象,而是让所有存活的对象都向一端移动,再清理掉边界以外的可回收对象,并更新引用其对象的指针。
优点:
- 弥补了“标记-清除”算法,内存区域分散的缺点
- 弥补了“标记-复制”算法内存减半的代价
缺点:
- 效率不高,对于“标记-清除”而言多了整理工作
- 因为需要更新对象指针地址,所以需要 stop the world,影响用户体验