常见算法分析(大白话理解可达性分析算法)
常见算法分析(大白话理解可达性分析算法)通过一系列被称为「GC Roots」的根对象作为起始节点集,从这些节点开始,通过引用关系向下搜寻,搜寻走过的路径称为「引用链」,如果某个对象到GC Roots没有任何引用链相连,就说明该对象不可达,即可以被回收。这个算法的基本思路是:这种算法实现简单,判断高效,但是有一些缺点:主流的商用JVM实现都没有采用这种方式。目前主流的商用JVM都是通过可达性分析来判断对象是否可以被回收的。
垃圾收集(Garbage Collection,下文简称GC)是Java有别于其他编程语言的一大特点,GC主要考虑的有三个问题:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
今天咱们主要聊聊JVM是如何判断对象可以被回收的。
常用的算法有两种:
- 引用计数算法
- 可达性分析算法
在对象中添加一个引用计数器,每当新加一个引用时,计数器就加1,当引用失效时,计数器就减1。任何时刻只要计数器为0就代表对象没有引用可以被回收。
这种算法实现简单,判断高效,但是有一些缺点:
主流的商用JVM实现都没有采用这种方式。
可达性分析算法
目前主流的商用JVM都是通过可达性分析来判断对象是否可以被回收的。
这个算法的基本思路是:
通过一系列被称为「GC Roots」的根对象作为起始节点集,从这些节点开始,通过引用关系向下搜寻,搜寻走过的路径称为「引用链」,如果某个对象到GC Roots没有任何引用链相连,就说明该对象不可达,即可以被回收。
初看这段话是不是一脸懵呢?笔者当初也是的,完全不知道什么意思,后面才慢慢理解。
要想理解可达性算法,首先要想明白几个问题:
1、什么是对象可达?对象可达指的就是:双方存在直接或间接的引用关系。
根可达或GC Roots可达就是指:对象到GC Roots存在直接或间接的引用关系。
如下代码:
public class MyObject {
private String objectName;//对象名
private MyObject refrence;//依赖对象
public MyObject(String objectName) {
this.objectName = objectName;
}
public MyObject(String objectName MyObject refrence) {
this.objectName = objectName;
this.refrence = refrence;
}
public static void main(String[] args) {
MyObject a = new MyObject("a");
MyObject b = new MyObject("b");
MyObject c = new MyObject("c");
a.refrence = b;
b.refrence = c;
new MyObject("d" new MyObject("e"));
}
}
创建了5个对象,他们之间的引用关系如图:
假设a是GC Roots的话,那么b、c就是可达的,d、e是不可达的。
2、GC Roots是什么?垃圾回收时,JVM首先要找到所有的GC Roots,这个过程称作 「枚举根节点」 ,这个过程是需要暂停用户线程的,即触发STW。
然后再从GC Roots这些根节点向下搜寻,可达的对象就保留,不可达的对象就回收。
那么,到底什么是GC Roots呢?
GC Roots就是对象,而且是JVM确定当前绝对不能被回收的对象(如方法区中类静态属性引用的对象 )。
只有找到这种对象,后面的搜寻过程才有意义,不能被回收的对象所依赖的其他对象肯定也不能回收嘛。
当JVM触发GC时,首先会让所有的用户线程到达安全点SafePoint时阻塞,也就是STW,然后枚举根节点,即找到所有的GC Roots,然后就可以从这些GC Roots向下搜寻,可达的对象就保留,不可达的对象就回收。
即使是号称几乎不停顿的CMS、G1等收集器,在枚举根节点时,也是要暂停用户线程的。
GC Roots是一种特殊的对象,是Java程序在运行过程中所必须的对象,而且是根对象。
那么,哪些对象可以成为GC Roots呢?
3、哪些对象可以作为GC Roots?可以作为GC Roots的对象可以分为两大类:全局对象和执行上下文。
下面就一起来理解一下为什么这几类对象可以被作为GC Roots。
1、方法区静态属性引用的对象
全局对象的一种,Class对象本身很难被回收,回收的条件非常苛刻,只要Class对象不被回收,静态成员就不能被回收。
2、方法区常量池引用的对象
也属于全局对象,例如字符串常量池,常量本身初始化后不会再改变,因此作为GC Roots也是合理的。
3、方法栈中栈帧本地变量表引用的对象
属于执行上下文中的对象,线程在执行方法时,会将方法打包成一个栈帧入栈执行,方法里用到的局部变量会存放到栈帧的本地变量表中。只要方法还在运行,还没出栈,就意味这本地变量表的对象还会被访问,GC就不应该回收,所以这一类对象也可作为GC Roots。
4、JNI本地方法栈中引用的对象
和上一条本质相同,无非是一个是Java方法栈中的变量引用,一个是native方法(C、C )方法栈中的变量引用。
5、被同步锁持有的对象
被synchronized锁住的对象也是绝对不能回收的,当前有线程持有对象锁呢,GC如果回收了对象,锁不就失效了嘛。
尾巴总结,可达性分析就是JVM首先枚举根节点,找到一些为了保证程序能正常运行所必须要存活的对象,然后以这些对象为根,根据引用关系开始向下搜寻,存在直接或间接引用链的对象就存活,不存在引用链的对象就回收。