前言
我们上一篇分析的是JVM的内存分布,分为堆内存、虚拟机栈、本地方法栈、方法区以及程序计数器等主要区域;各个区域的特点我也就不啰嗦了,想看的给大家直通车:
大鱼今天在家本来是闲暇的一天,很舒适,结果这个时候,妈妈敲门进来我房间了,咨询我有没有时间帮忙打扫一下父母的房间;(没有时间
当然我不能这么说了,我是个炒鸡孝顺的好孩子,当然了,妈妈,当然有时间了啊,now go,我的乖乖,这么乱的屋子,不对啊,平时都是很干净的啊(内心想逃,后悔,想拒绝
不对啊,妈,为什么房间这么乱啊,这有的东西我也不知道要不要扔掉啊,瞬间难到我了,你们生活中有没有遇到过类似的烦恼?
或者有没有遇到纠结一个东西要不要扔掉的时候,那时候你是如何做的呢?
我们知道在JVM内存中,实例对象基本都是存在于堆中的,那总不能无期限的往里面放吧,一些用不着的对象就需要随时回收掉,这样才能保证这个内存的均衡性,才能保证JVM的正常运行
那么问题来了,JVM如何知道哪些对象该回收、哪些不该回收,就像刚才大鱼不知道爸妈房间哪些东西该收拾、哪些不该收拾一个道理的,其实在JVM中是有两种解决办法的,分别是引用计数法和可达性分析法两种方法,来确定这些对象之中哪些是存活着的、哪些是已经死去的(不可能再被任何途径使用的对象)
问题明白了,下面就是来解决这个问题了,冲吧,干饭人
引用计数算法
这个其实很简单了,重点就是计数;给对象添加一个引用计数器,每引用一次,计数器加一;引用失效的时候,计数器减一;当计数器为0 的时候,则认为不可能被再次使用了;
我觉得不需要大鱼多解释了应该,这个应该及其好理解,但是,这种方法存在一个致命的问题:无法解决对象相互循环引用的问题
解释下这个循环引用问题
一起来看看下面这个例子
- public class ReferenceCountingGC {
- public Object instance = null;
- private byte[] bigSize = new byte[2 * 1024 * 1024];
- public static void main(String[] args) {
- ReferenceCountingGC o1 = new ReferenceCountingGC();
- ReferenceCountingGC o2 = new ReferenceCountingGC();
- o1.instance = o2;
- o2.instance = o1;
- o1 = null;
- o2 = null;
- //假设在这行发生了GC,o1和o2是否被回收
- System.gc();
- }
上面例子中o1和o2对象都分别将对方作为自己的属性注入,这也就是形成了所谓的循环引用;最后o1和o2对象都置为null,也就是栈中不再指向堆中的实例对象地址,但是他们还是会互相引用,所以不会被GC回收
再来看个图解版,加深理解
刚new的o1和o2对象是这个样子的:
分别引用了双方之后是这样子的状态:
最后置为null变成这个样子的:
是的,没错,最后就变成了如上图所示的尴尬境地,对象1和对象2在内部互相引用,永远失效不了,导致GC通过引用计数法判断他们的引用计数的时候,永远无法判断为0,也就是无法回收咯,不就造成了内存泄漏了吗
可达性分析法
上面说的引用计数法有缺点,而且这个问题还不小,所以现在使用这种方式来作为判断对象是否存活标准的比较少,多数使用的是另一种,可达性分析法;
先来解释下可达性分析法
基本思路就是通过一系列的”GC Roots“的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径就是引用链,当一个对象到GC Roots没有任何的引用链可达的时候,则证明这个对象是不可用的
什么意思呢?来个白话文版本的,就是选择一系列的基准点,这个点能通过引用链连接到的对象就被认为是可用的,只要是无法到达的,都被认为是不可用的,这个不可用并不一定代表对象死亡,只代表对象无法触达,无法再次引用
这就像递归定义的关系一样,如果只定义了递归项而不定义初始项的话,关系也就无从成立,无从开始;如果初始项定义漏掉了内容的话,递推的结果也会随之而漏掉;
什么是GC Roots
垃圾回收时,JVM会首先找到所有的GC Roots,这个过程叫做枚举根节点,这个过程需要暂停用户线程,也就是stop the world;然后再从GC Roots这些根节点向下搜索,可达的对象保留,不可达的便会回收掉
那么,到底什么是GC Roots呢?
GC Roots就是对象,就是JVM确定当前绝对不能回收的对象,只有找到这种对象,后面的搜索才会有意义,不能被回收的对象所依赖的对象也就必然不能回收
GC Roots是一种特殊的对象,是Java程序在运行过程中所必须的对象,而且必须是根对象
哪些对象可以作为GC Roots
基本可以作为GC Roots的对象基本分为两大类:全局对象和执行上下文;
全局对象
- 方法区静态属性引用的对象:全局对象的一种,Class对象本身很难被回收,回收的条件也是很苛刻,只要Class不被回收,静态成员不会被回收
- 方法区常量池引用的对象:全局对象,比如字符串常量池,常量初始化之后不会再次改变
执行上下文对象
- 方法栈的栈帧本地变量表引用的对象:线程方法执行的时候,会将方法打包成一个栈帧入栈执行,方法里得到的局部变量会存放到本地变量表中,只要方法未执行完,还没出栈,即本地变量表还会被访问,GC不应该回收
- JNI本地方法栈引用的对象:和上面同样的道理
- 被同步锁持有的对象:被synchronized锁住的对象不可回收,否则锁就失效了,那锁就没意义了
不可达的对象一定会回收吗?(缓刑阶段)
其实被判定为 不可达的对象,也不一定是”非死不可“的,还有一次复活机会,这时是处于缓刑阶段,要真正宣告一个对象死亡,至少要经历再次标记过程(其实就是finalize方法在搞怪)
我们在电视中也是经常见到类似的场景,一个人被判定死刑了,午时已到,立即执行,一般这个时候就会出来一个飞刀,刀下留人,皇上有旨;也有可能是一个飞刀,直接二话不说,噼里啪啦一顿操作,把人救走,是不是很熟悉
没错,这个过程就是finalize的内部过程,让被判定死刑的犯人”重获新生“
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链
第一次标记
筛选的条件是这个对象是否有必要执行finalize()方法;若对象未重写这个方法或者已被虚拟机调用过,虚拟机则认为没有必要执行,对象被回收
第二次标记
若这个对象有必要执行finalize方法,则这个对象会被放到一个F-Queue队列中,并在稍后由虚拟机自动创建的一个低优先级的finalizer线程去执行;
这里的执行指的是虚拟机会触发这个方法,但是不保证运行完成,这样做的原因是这个方法执行缓慢,也可能出现死循环,严重可能会导致回收系统崩溃
finalize是对象逃脱死亡命运的最后一次机会,稍后GC会对F-Queue中的对象进行二次标记,如果在这里面重新和GC Roots挂上引用关系,则可以逃脱被回收的命运;否则,就肯定GG了
方法区的回收
很多人认为方法区没有垃圾回收,Java虚拟机规范中也确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区中的垃圾回收的性价比一般比较低,在上面说的堆中进行一次垃圾回收会回收70—95的空间,而永久代中的垃圾回收的效率远低于此
方法区中的垃圾回收主要是两部分:废弃常量和无用的类;废弃常量的回收和Java堆中的对象类似,不多说了
但是判断一个类是否是无用的类,则条件比较苛刻,需要满足三个条件:
- 该类的所有实例都已经被回收,即Java堆中无该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问到该类的方法
虚拟机规范中说的是满足上面三个条件,便可以对无用的类进行回收,但是并不是必然回收;是否对类对类进行回收,可以根据虚拟机提供的参数来进行控制
在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGI这类频繁自定义ClassLoader的场景都需要虚拟机具备类的卸载功能,以保证永久代不会溢出
我爱总结
我爱总结之JVM如何判断哪些对象可以回收,总结很重要,整理思路,记得后续的温故而知新,GitHub地址在下面,我会把所有原创技术文章放到上面,持续不断的更新
引用计数法:存在循环引用的致命问题
可达性分析法:以GC Roots作为起点,可以达到的就不可回收,不可达到的暂定认为”死亡“;但是不是非死不可,有通过finalize方法加重新连接引用链的方法,让一个对象重新复活;但是不保证执行完成,这种方法是不靠谱的,也是不建议使用的
好了,以上就是全部内容了,我是小鱼仙,你们的学习成长小伙伴
我希望有一天能够靠写字养活自己,现在还在磨练,这个时间可能会有很多年,感谢你们做我最初的读者和传播者。请大家相信,只要给我一份爱,我终究会还你们一页情的。