我是一个垃圾收集器——上篇

来源:互联网 发布:mac相簿和相册的区别 编辑:程序博客网 时间:2024/05/16 01:53
面试官:垃圾回收介绍一下

垃圾回收算法算是一个常问的知识点了,今天我们听听垃圾收集器的叙述


幸福的Java程序员

我是一个垃圾收集器,自从有了我,Java程序员们不知道要比C++程序员幸福多少倍,从此再也不用为每一个new操作去写配对的delete/free代码了(没有语言 鄙视)

他们的生活是如此的美好,可是这些脏活累活全都由我来干了

神圣的使命

我从出生以来,身上就肩负着回收垃圾的使命,所谓垃圾,其实就是不再被程序大哥所需要的对象,官方一点就是这个对象不再被程序所引用了。

说来他们这些对象也挺可怜的,需要的时候就是对象,不需要的时候就是垃圾。

为了完成这个使命,我需要把下面的东西给搞清楚
① 哪些内存需要回收?
② 什么时候回收?
③ 如何回收?

艰难的第一步

——- 确定要回收的区域

为了完成这个神圣的使命,我面临的第一个问题就是哪些内存需要回收?

虚拟机老大掌控者操作系统boss给他分的一亩三分地,他自己起了个名叫运行时数据区

在Java程序运行过程中,虚拟机老大把他所拥有的土地分为了几个区域,分别叫方法区、堆、虚拟机栈、本地方法栈和程序计数器。如下图:

这里写图片描述

虚拟机栈、本地方法栈和程序计数器这几个区域是线程小哥私有的,线程结束时内存自然就跟随着回收了,所以我不用去管他们的内存应该什么时候回收

而Java堆和方法区这两块地很特殊

一是因为他们是线程共享的区域,线程结束和他们内存回不回收没有关系

二是因为在这篇土地上生长的东西,它在编译期不确定,比如说堆上的Java对象,有时候我只有在程序运行期间才能知道会创建哪些对象(多态),这样我就无法知道我要回收哪些东西了,所以才需要我去解决这些问题

如果在编译器知道也就不需要我在运行期这么不停的干活了,我也没有必要存在了

所以我就重点关注这两块区域

判断对象是否已“死”

终于弄清要回收那块区域了,但是新的问题又来了,我该如何确定堆中的对象是否已经死亡了呢?垃圾收集器心里想到。

垃圾收集器想了想,咦,我可以给每一个对象添加一个引用计数器,每当有一个地方引用它时,计数器就加 1,当引用失效时,计数器值就减 1,如果计数器为 0 的对象就是不可能再被使用的。

垃圾收集器满心欢喜,把这个方案递交给Java虚拟机老大,虚拟机老大看看了,说:“嗯,不错,方案挺好的,但是有一个问题”,“什么问题?”垃圾收集器问道

“你想想看,如果两个对象互相引用的话,那么当其他地方对你的这个对象的引用断开的时候,由于两个对象相互引用,所以两个对象各自的计数器的值为 1 ,但是这个时候没有其他地方引用你的这两个对象了,你的两个对象只是互相引用,是不是该回收,但是没法回收”

虚拟机老大看垃圾收集器一脸懵逼,随手就写了一段代码

public class GC{    public Object instance = null;    public static void main(String[] args){        GC objA = new GC();        GC objB = new GC();        // 让 objA指向的对象和 objB 指向        // 的对象相互引用        objA.instance = objB;        objB.instance = objA;        // 让 objA 和 objB 引用失效        objA=null;        objB=null;        // 如果此时发生GC,objA 和 objB        // 所引用的对象能否被回收?        System.gc();    }}

“你看啊,当你new 两个GC对象的时候,分别 objA 和 objB 引用了堆中的两个对象

然后你又让objA所指向的对象中的 instance 指向了 objB 所指向的对象,并且让objB所指向的对象中的 instance 指向了 objA 所指向的对象

当你objA=null 和 objB=null 的时候,断开了 objA 和 objB 对GC对象的引用(黑色的线),但是你的GC对象的引用计数器的值还是不为0,因为他们互相还持有对方的引用

看着垃圾收集器还是不懂的样子,Java虚拟机老大随手画了一个图:

这里写图片描述

“看见了吧,当objA和objB指向堆中的对象的那条链断了,是不是就该回收了,但是你的对象的计数器现在还不为0(相互引用各自为1)”虚拟机老大解释道

“哦,原来还有这个坑,那该怎么办呢?”垃圾收集器说道

Java虚拟机老大思索了一会说道:“我们可以通过可达性分析来判定对象是否存活”,”可达性分析?什么鬼”,垃圾收集器暗自思索道

接着Java虚拟机老大又说:“所谓可达性分析,其实就是我先选取一系列对象的引用(不是对象)作为根节点(GC Roots),然后以根节点为起始点,向下搜索,搜索不到的对象我们就认为它不可达

说着说着,顺手就画了个图

这里写图片描述

“看到了吧,我从GC Roots往下搜素,object4、 object5和 object6我搜索不到,所以他们被称为不可达对象,这样是不是就解决了对象循环引用而迟迟不能被回收的问题了”Java虚拟机说道

“哦?能再解释一下吗?”垃圾收集器说道

Java虚拟机还是很有耐心的,说道,“咱们还是拿最初相互引用的例子来说吧

objA 指向的对象和objB指向的对象相互引用,但是在objA=null;objB=null; 后objA 和 objB 那两条链断了(这里objA 和 objB 就是根节点),只留下对象相互引用,是不是没办法从根节点搜索到这两个对象了”

Java虚拟机又随手画了个图

这里写图片描述

“看见了吗”一旦 objA 和 objB 那两条链断开,那么堆中创建的两个对象就不可达了,这时即使他们相互引用,也被认为是“垃圾”,也要进行回收的。

“哦,明白了,这个方法很棒,可是什么样的对象会被认为是根节点呢?”垃圾收集器问道

“好问题,看来你很聪明嘛!”Java虚拟机老大说道,“可作为GC Roots的对象有以下几种:

① 虚拟机栈(栈帧中的本地变量表)中引用的对象
咱举例中的objA 和 objB 就是 main函数栈帧中的本地变量表中的变量(关于虚拟栈的相关知识请读者参考相关博客)

② 方法区中类静态属性引用的对象

③ 方法区中常量引用的对象

④本地方法栈中JNI(即一般说的Native方法)引用的对象

方法区回收的苛刻条件

前面确定堆中对象是否已死可把我累坏了,但紧接着的问题又是一个难题,方法区的内存可该怎么回收呀?

方法区里方的可都是一些难缠的家伙,像什么运行时常量池啦,什么字段和方法数据,这些都是类的结构信息,轻易是不会被回收的

但是如果我想回收也是可以的,只不过费点劲而已,哎,JVM规范都没有要求让我回收这块区域,我这不是自己给自己找活干嘛,谁让我做事都这么要求完美呢

我细数了一下方法区可以回收的东西,一个是常量,另一个就是无用的类

先解决常量这个家伙吧,这个家伙解决起来挺easy,如果当前系统没有一个String对象叫做“abc”,那么我就认为它可以回收了

比如说,String s = “abc”, 然后s=null;在常量池中的常量“abc”没有人引用它了,所以我就把它给回收得了

另一个就是无用的类了,这个可麻烦了,我该如何判定一个类是无用的呢?
让我想想

首先这个类如果无用,那么这个类的实例应该都已经被回收(类是在方法区,而类的实例在堆中)

其次,加载该类的ClassLoader也应该被回收

最后,该类对应的 java.lang.Class 对象没有在任何地方被引用,也就是说无法在任何地方通过反射访问该类的方法

满足上面的三点我才放心把这个类给回收了

夜色降临,今天也忙了一天了,终于迈出了第一步,我也该歇歇了

未完,待续

本文参考《深入理解Java虚拟机》 周志明著,强烈推荐大家看

原创粉丝点击