JVM——引用计数算法与可达性分析算法

前几篇博客我们一起认识了JVM的内存模型(程序计数器、虚拟机栈、本地方法栈、方法区与堆),了解了它们的内存结构与分配,同时也略带提到关于内存的回收。

JVM——内存模型(一):程序计数器

JVM——内存模型(二):虚拟机栈与本地方法栈

JVM——内存模型(三):堆与方法区

有内存分配就肯定有内存回收,这个大家都知道,可哪些东西需要回收?什么时候进行回收呢?又怎么样回收呢?

之前我们介绍了Java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈这三个区域随线程而生,随线程而灭。栈中的栈帧则随着方法的进入和退出而有条不紊地执行着出栈和入栈的操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具有确定性,在这几个区域内就不需要过多的考虑回收的问题,毕竟等方法结束或者线程结束的时候,内存自然也跟着回收了。

但是Java堆和方法区却并不是这样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我等程序员只有在程序处于运行期间的时候才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器最关注的,也就是者两个内存区域了。

我们知道,在堆里面,存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收之前,第一件事情就是看看谁还“活着”,谁已经“死去”(即不再被任何途径使用的对象)。

那怎么判断呢对象的存活状态呢?这里有两种方法:引用计数法可达性分析算法。

1.引用计数算法

所谓引用计数法,看字面意思就知道是靠“引用”和“计数”。大部分教科书是如何描述引用计数法的呢?如下:

引用计数法:给对象添加一个引用计数器,每当有一个地方引用它,计数器值就加一;相反的,当引用失效的时候,计数器值就减一;任何时刻计数器为0的对象就是不可能再被使用的。

 也就是说,当计时器的数值为0的时候,这个对象就可以被回收了。

客观的说,引用计数算法的实现比较简单,判定效率也很高,在大部分情况下它都是一个不错的算法。

不过,现在主流的Java虚拟机里却没有选用引用计数算法来管理内存,最主要的原因就是它很难解决对象之间相互循环引用的问题。

什么叫相互循环引用呢?举个例子

Node a=new Node();
Node b=new Node();
a.next=b;
b.next=a;

这时候a和b就属于互相引用。这时候将a和b为null。

a=null;
b=null;

这个时候a和b都不再使用了,但是它们之间的引用依旧无法消除,因为a=null与b=null只是撤销了指向内存的句柄,内存依旧在互相引用。如下图:

由此我们看出,a和b在置为null之后,这两个对象已经不可能再被访问了,但是因为它们互相引用着对方,导致它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。虚拟机这么牛气,自然不会用这样的方法来判断对象的存活状态了,那用什么呢?那自然就是上文提到的可达性分析算法啦。

2.可达性分析算法

在主流的商用程序语言(如Java,C#,以及古老的Lisp)的主流实现中,都是通过可达性分析来判断对象是否存活的。

这个算法的基本思路就是通过一系列的称谓“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路称为引用链,当一个对象到GC Roots没有任何引用链相连的时候,(用图论的话来说)即从GC Roots到这个对象不可达,则证明这个对象是不可用的。

举个例子:

从上图我们可以看到,对象Object5、Object6、Object7、虽然相互间有关联,但是它们到GC Roots是不可达的,因此他们将会被判定为可回收的对象。

那什么可以作为GC Roots呢?

在Java语言中,可以作为GC Roots的对象包括以下几种

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

3.生存还是死亡

上文说到可达性分析算法中不可达的对象就是可回收,那么它一定会被回收吗?

其实,即使再可达性分析算法中不可达的对象,它们也并非是“非死不可”的。这种情形好比咱们在生活中经常见的情况:(假设本帅松是一名警察)在经过一系列的排查之后,我们在人海中抓住一个嫌疑犯,这个时候我们要把他抓回警局,但却不能立马将他定位犯罪者。虽然种种迹象都表明他与本案有关,但是他此时仍然是一个嫌疑犯,还没有下定论,等到断定此人就是此案的犯罪者的时候,这个时候才可以将它移送到相关机构接受审判并定罪。

对于可达性分析算法中不可达的对象,它们也不会立刻就被回收,这个时候它们暂时处于“嫌疑人”状态,到真正宣告一个对象死亡,至少要经历两次标记过程。

如果对象在进行可达性分析之后被发现没有与GC Roots相连的引用链,那么它将会被第一次标记,并且进行一次筛选,筛选的条件就是此对象是否有必要执行finalize()方法。

以下两种情况虚拟机将视为没有必要执行finalize()方法:

  • 当对象没有覆盖finalize()方法
  • finalize()方法已经被虚拟机调用过

如果这个对象被判定为有必要执行finalize()方法,那么这个对象就会被放置在一个叫做F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。

这里所说的“执行”是指虚拟机会触发这个方法,但是并不承诺会等待它执行完毕。为什么呢?

你想,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(甚至其它更加极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待状态,甚至可能导致整个内存回收系统奔溃。

finalize()方法是对象逃脱死亡命运的最后一次机会。因为在执行了finalize()方法之后,GC将会对F-Queue队列中的对象进行第二次小规模的标记,如果对象能够在finalize()中成功地拯救自己,即只要重新与引用链上的任一对象建立关联即可,比如将自己(this关键字)赋值给某一个类变量或者对象的成员变量,那么在第二次标记的时候,它就会被移出“即将回收”的集合,即移出F-Queue队列。如果这个时候,对象还没有逃脱,那么它就基本上就要被回收了。

这样说大家可能不好理解。下面来看一个小栗子。


public class FinalizeEscapeGC {
	public static FinalizeEscapeGC SAVE_HOOK=null;
	public void isAlive() {
		System.out.println("Yes,i'm alive.");
	}
	@Override
	protected void finalize() throws Throwable{
		super.finalize();
		System.out.println("finalize method executed!");
		FinalizeEscapeGC.SAVE_HOOK=this;
	}
	
	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		SAVE_HOOK=new FinalizeEscapeGC();
		SAVE_HOOK=null;
		System.gc();
		//因为finalize方法的优先级很低,所以暂停0.5秒来等待它。
		//Thread.sleep(500);
		if(SAVE_HOOK!=null)
		{
			SAVE_HOOK.isAlive();
		}else {
			System.out.println("no,i am dead.");
		}
		//下面这段代码与上面的完全相同,但是这次自救会失败
		SAVE_HOOK=null;
		System.gc();
		//因为finalize方法的优先级很低,所以暂停0.5秒来等待它。
		//Thread.sleep(500);
		if(SAVE_HOOK!=null)
		{
			SAVE_HOOK.isAlive();
		}else {
			System.out.println("no,i am dead.");
		}
	}

}

运行结果如下: 

咦,怎么与我们预想的结果不一样?

上文我们说到,finalize()方法的优先级很低,因此两次都会是dead。

要想要出现预想的结果,我们只需要在GC的时候让线程睡眠一会儿。即上文注释中的Thread.sleep(500)

这回我们将Thread.sleep(500)这个语句的注释取消,再来看看结果。

好了,这就是我们要的结果了。

从运行结果我们可以看出,SAVE_HOOK对象的finalize()方法确实是被GC收集器触发过,并且在被收集前成功逃脱了。

另外一个值得注意的地方,代码中有两段完全一样的代码片段,执行结果确实一次逃脱成功,而一次逃脱失败,这是因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动就失败了。

注:finalize() 的运行代价昂贵,不确定性大,无法保证各个对象的调用顺序,很多时候建议用try-catch代替。

好啦,以上就是关于如何判断对象的存活状态的两种算法的相关知识总结,如果大家有什么更具体的发现或者发现文中有描述错误的地方,欢迎留言评论,我们一起学习呀~~

Biu~~~~~~~~~~~~~~~~~~~~宫å´éªé¾ç«è¡¨æå|é¾ç«gifå¾è¡¨æåä¸è½½å¾ç~~~~~~~~~~~~~~~~~~~~~~pia!

猜你喜欢

转载自blog.csdn.net/Searchin_R/article/details/84980843