深入理解java虚拟机系列第三版读后小记:(三) java的死亡判断
深入理解java虚拟机系列第三版读后小记:三 java的死亡判断
前言
上文介绍了java对象在内存分配的区域,本文将介绍jvm如何判断对象是否存活。
vm判断对象死亡的两种方式
引用计数法
jvm判断java对象是否死亡的两种算法之一就是引用计数法。引用计数法的原理也很简单:在对象里添加一个引用计数器,若有其他地方引用这个对象,该计数器加1,引用失效时就减1,当计数器为0时,说明该对象已死亡,无法再被使用,然后被gc回收掉。
优点:原理简单,判定效率也很高
然而实际上很少java虚拟机使用这种方法去判定java对象是否存活。因为它有一个致命的缺点,无法解决循环引用。如果了解过spring ioc的同学知道有个循环依赖的概念。其实对于java对象,循环引用也是正常的。所以若存在这样的场景,A对象被B对象所引用,所以A的引用计数器+1,而B的对象又被A对象所引用,B的引用计数器+1.这就造成了闭环,A和B的引用计数器就一直无法变0,就永远不能被jvm回收掉。
!](https://img-blog.****img.cn/20200521161113184.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3o1OTEwNDU=,size_16,color_FFFFFF,t_70)
还有缺点就是必须多消耗一块内存用来存储引用计数器,当然这个缺点对于无法解决循环引用而言无足轻重。
可达性分析算法
当前的主流jvm使用的判断对象是否存活几乎都是可达性分析算法。其算法的原理是:通过一系列被称为“GC Roots”作为根节点,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,只需判断对象到GC roots之间是否存在引用链,就可以判断对象是否存活,如果没有引用链,或者用图论表述为不可达,那此对象可判断为死亡。
如图,object5,6,7与GCroot是不可达,所以判断为死亡。
并不是所有对象都能充当GCroots,能够充当GCroots的对象:
- 虚拟机栈(栈帧中的本地变量表)引用的对象,如线程被调用栈堆上的局部变量,参数,临时变量等。
- 方法区内静态属性引用的对象,如java类的静态引用变量。
- 方法区内的常量引用对象,如字符串常量池里引用的对象。
- 本地方法栈JNI(即常说的native方法)所引用的对象
- 虚拟机内部引用,如基本数据类型对应的class对象,一些常驻异常的对象(如,nullpointException,OutOfMemeryException),还有系统加载器。
- 所有被同步锁(synchronize关键字)持有的对象。
- 反映jvm内部情况 JMXBean,JVMT1中注册的本地回调,代码缓存等。(这个就属于反映jvm运行的状况,笔者不会对其关注)。
以上都是固定的能成为GCroots的对象,当然还有不固定的,这些动态变更的gcroots主要是由于用户所选的gc收集器的不同以及当前回收内存区域的不同,所以会临时产生一些对象加入gc roots,这种情况大多出现在跨代回收。
java的引用
jdk1.2之前,java的引用就相当于指针一样,存储者引用对象的地址,所以这种情况下对象状态只有被引用和未被引用两种状态,这样的定义太过狭隘了,比如说这种场景:资源不紧张的时候,想让对象继续活着存储到堆上,紧张的时候再回收掉,如鸡肋一样的场景。所以jdk1.2之后java对其引用进行扩充了,将引用分为以下四种:
- 强引用,传统的引用类型,也是最常见的引用类型,如new一个对象一样的引用关系。无论任何情况下,只要存在强引用,gc就不会去回收这个对象。
- 软引用,软引用就是所谓的鸡肋,还有用但非必须的对象,内存充足是不会进行回收,内存紧张的话就会回收掉。
- 弱引用,弱引用和软引用一样,但其强度低于弱引用,软引用只会活到下次gc之前(相当于这次gc不会回收,但下次就会回收掉)。所以下次gc开始的时候,无论内存是否充足都会回收掉软引用的对象。
- 虚引用。最弱的引用,不会影响对象的生存周期,也无法通过虚引用来获取一个对象实例,也活不到xiacgc,只有一个标识做用,就是被gc回收的时候这个对象会收到一个通知。
被判断死亡的java对象必死无疑?
无论是引用计数还是可达性分析,被标记的回收对象也不是立马就被回收,相当于法院宣布死刑犯,也不是当场执行处决。所以jvm判断对象已死是有两个步骤:
- 对象可达性分析后不可达,进行第一次标记
- 判断是否有必要执行对象的finalize()方法
对于标记算法,搬砖猿是无能为力的,因为是jvm自主运行的。所以让对象死里逃生只有在finalize()方法上下功夫。
jvm判断不执行finalize()的条件是该对象没有覆盖finalize()或者finalize()被jvm调用过。满足两者之一 ,jvm就会认为没有必要执行finalize()。
如果jvm判断要执行finalize(),也不会是立即执行,而是加入到一个FQUEUQ队列,然后开启一个低优先级的线程去执行finalize(),但并不会保证等待它执行结束,原因也是显而易见的。担心对象的finalize()执行缓慢,或者极端情况下死循环,导致其他对象无限等待,最终造成内存崩溃的情况。
所以在finalize()方法里只有让gcroot引用链引用到此对象,比如在finalize()让其他对象引用自己的某个属性,这样就有可能起死回生。如果第二次标记的时候,对象还没挂在引用链上,那就只能被回收了。
方法区的回收
其实有不少读者误认为方法区(元空间)是不会gc的,虚拟机规范中也没有明确要求方法区内要进行gc,事实上也确实有不支持方法区上回收或不完整的实现方法区回收的垃圾回收器。毕竟在java堆上,一次垃圾回收,尤其是新生代可以回收70%-99%的内存空间。而且回收方法区的条件过于苛刻,所以回收成功远远低于其他区域。
方法区回收主要分为两块:
- 废弃常量的回收,顾名思义,只要这常量没有对象引用就可以被回收掉,举例:字符串不被引用时,就可移除常量池。
- 不再使用的类型,相对于回收常量而言,回收不再使用的类型条件就相当苛刻了:
- 该类的所有实例都被回收了,也就是堆中不存在该类或者派生子类任何实例
- 加载该类的类加载器被回收了,这个条件除非专门设计的可替换类加载器的场景,否则很难实现。
- 该类的java.lang.class对象没有被任何地方引用,即无法在任何地方通过反射访问该类的方法。
只要满足以上三个条件,就可以回收方法区。既然条件这么苛刻,那为何还要回收方法区,因为现在大多框架越来越频繁的使用反射,动态代理和cglib字节码生成这种频繁的自定义类加载器,如果没有类型卸载的能力,可能会对方法区造成过大的压力。
总结
本文主要介绍了java对象的引用类型,以及判断java存活的两种算法。