# 对象存活
在最开始用c/c++语言编程时,申请/回收内存空间都需要手动进行,难免会有疏忽导致内存溢出的问题,Java设计之初就想彻底解决这个问题,让内存回收自动化,开发者无需再担心内存的回收问题,下面一起看看Java虚拟机是如何标记待回收对象实例的
# 引用计数算法(Reference Counting)
当虚拟机想知道一个对象实例是否可以被回收,那么最简单的办法,就是在对象中添加一个引用计数器,当有一个地方引用了该对象,计数器+1,当引用失效了,计数器-1,那么当计数器为零的时候,这个对象就可以被判定为不再使用,可以被回收了
这种算法虽然占用了一些额外内存空间来计数,但它原理简单,判定效率也高,大多数情况下都是一种不错的算法,不过有个场景这个算法却无能为力了,如上图,对象实例A仅有一个引用指向对象实例B,反之同样,外部也无任何其它对这2个对象实例的依赖,2个对象实例的引用计数器却均不为0,因此虚拟机使用引用计数算法,是无法回收掉它们占用的内存空间的
# 可达性分析算法(Reachability Analysis)
实际上,当前主流的虚拟机语言,都是使用可达性分析算法来判定对象是否存活的,如Java,C#等等
这个算法的基本思路是从称为GC Roots的根对象作为起始节点集,根据引用关系向下搜索遍历,搜索过程走过的路径称为引用链(Reference Chain),如果某个对象与根对象之间无任何引用链相连,可以判定这个对象是不可达的,需要被回收,如上图左侧对象均可达,右侧就可以被回收了
那么固定可作为GC Roots的对象主要有以下几种:
- 在虚拟机栈(栈帧中的局部变量表)中引用的对象(比如各个线程中被调用的方法栈中使用到的参数/局部变量/临时变量等)
- 在方法区中类静态属性引用的对象(比如Java类引用静态类型变量)
- 在方法区中常量引用的对象(比如字符串常量池(String Table)里的引用)
- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象
- Java虚拟机内部的引用(如基本数据类型对应的Class对象,一些常驻的异常对象等,还有系统类加载器)
- 所有被同步锁(synchronized关键字)持有的对象
- 反映Java虚拟机内部情况的JMXBean/JVMTI中注册的回调/本地代码缓存等
# 引用的类型
在前面提到的2种解决对象是否可被回收的判定条件里,都是通过判断一个对象是否被引用,然而某些场景下(比如缓存功能),我们希望在内存空间充足的情况下,能适当保留一些对象,当内存空间紧张时再去回收掉这部分对象,那么从JDK1.2开始,Java对引用的概念进行了扩充,将引用强度由强到弱分成以下4种:
- 强引用(Strongly Reference):是最传统的,类似
Object o = new Object();
这种 - 软引用(Softly Reference):还有用,但非必须的,系统发生OOM前,会把这些对象进行第二次回收,如果这次回收还没有足够内存,才会抛OOM
- 弱引用(Weak Reference):非必须的,这些对象只会生存到下次回收之前
- 虚引用(Phantom Reference):为一个对象设置这个引用只是为了让它能够在被回收前收到一个系统通知
# 生存还是死亡?
即便是一个对象被算法判定为不可达,可以回收了,也不是必定死亡的,要真正回收掉它,至少要经历两次标记过程:如果一个对象被可达性分析为没有与GC Roots相连,会被第一次标记,随后进行一次筛选,条件是此对象是否有必要执行finalize()方法,假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机会把这两种情况都认为是没有必要执行
如果这个对象被判定为有必要执行,它会被放置在一个名为F-Queue的队列中,稍后由一条虚拟机自动建立的,低调度优先级的Finalizer线程去执行它们的finalize()方法,这里说的执行仅仅是指虚拟机会触发这个方法,却并不承诺一定会等待finalize()方法运行结束,因为假如某个对象的finalize()方法执行缓慢,或者发生了死循环,就会导致F-Queue队列中其它对象永久处于等待被回收,甚至导致内存回收子系统的崩溃,稍后收集器会对F-Queue中的对象做二次小规模标记,如果这时一个对象重新与引用链上任何一个对象建立了关联,它就能被移除即将回收的集合,仍然存活了
# 总结
以上就是虚拟机垃圾收集器如何对待回收对象实例做标记的过程了,两种算法各自优缺点也一目了然,都是根据引用关系来判定一个对象是否存活,这让我想起了迪斯尼的一部电影《Coco》(《寻梦环游记》),当你已故,但只要你的亲人还记得你,你就能在另一个空间一直”活“着,而被遗忘的时候,才是真正逝去了