如果这题都不会面试官还会继续问我 JVM 嘛:如何判断对象是否可回收
不懂就问,世界都快毁灭了我为啥还要在这背八股写论文(滑稽)
首先,对于 JVM 来说,什么是垃圾?
简单说就是内存中已经不再被使用到的空间就是垃圾
其次,什么是垃圾收集(Garbage Collection,下文简称 GC)?
简单来说,就是清除垃圾占用的空间,从而给新生的对象腾出内存空间。
垃圾收集发生在哪个内存区域?
回顾下 Java 内存运行时区域,程序计数器、虚拟机栈、本地方法栈 这 3 个区域是线程私有的,随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑如何回收的问题,当方法结束或者线程结束时,内存自然就跟随着回收了。
而 堆 和 方法区 这两个区域是线程共有的,有着很显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。所以一般垃圾收集器所关注的就是对这部分内存的管理。
确定了垃圾收集发生的具体地点,还需要考虑的其实无非就是以下三个问题:
哪些内存需要回收?(GC 目标) 什么时候回收?(GC 时间) 如何回收?(GC 方法)
毫无疑问,垃圾收集器在进行回收前,第一件事情就是要确定 GC 目标,判断这些对象之中哪些还存活着,哪些已经死去成为垃圾(即不可能再被任何途径使用的对象)。所以,我们今天这篇文章的目的,就是探讨如何判断对象是否已经成为垃圾。
老规矩,背诵版在文末。点击阅读原文可以直达我收录整理的各大厂面试真题
引用计数法
众所周知,在 Java 中,如果要操作对象是必须通过引用来进行的。
因此,如果某个对象已经不存在任何引用指向它了,就说明这个对象已经没有作用了,就是一个垃圾了。
所以,很显然的一个办法就是通过引用计数来判断一个对象是否可以回收。
算法的实现非常简单:在对象中添加一个字段作为引用计数器,每当有一个地方引用这个对象时,计数器的值就加一;当引用失效时,计数器值就减一;计数器为零的对象就是可以被回收的对象。
But!!!目前主流的 JVM 里面都没有选用引用计数算法来管理内存。
主要原因是,这个看似简单的算法需要考虑很多例外情况,比如,单纯的引用计数就很难解决对象之间相互循环引用的问题。
所谓对象之间的相互循环引用,举个简单的例子,如下代码:对象 objA 和objB 都有字段 instance,赋值令 objA.instance=objB
及 objB.instance=objA
,除此之外,这两个对象再无任何引用
当我们令 objA 和 objB 为 null 的时候,意思就是不想再让他俩指向的对象被其他人访问了,虚拟机可以回收掉了,但是它们因为互相引用着对方,导致它们的引用计数都不为零,所以引用计数算法也就无法回收它们
可达性分析法
当前主流的 JVM 都是通过可达性分析(Reachability Analysis
)算法来判定对象是否可回收的。
这个算法的基本思路也不难,可以理解为一棵多叉树:就是通过一系列称为 “GC Roots” 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为 “引用链”(Reference Chain),如果某个对象到 GC Roots 间没有任何引用链相连,或者用说从 GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的。
如图所示,对象 object 5、object 6、object 7 之间虽然互有关联,但是它们到 GC Roots 是不可达的,因此它们将会被判定为可回收的对象
这里要说明下,GC Roots 存的到底是引用还是对象?
答案是引用,指向对象的引用。至于为什么很多人会直接说是对象,主要原因就是因为 Java 中引用无法脱离对象存在,没有指向对象的引用是没有任何意义的。
另外,GC Roots 到底存放在哪里呢?
事实上,GC Roots 本身是没有存储位置的~ 他们都是字节码加载运行过程中加入 JVM 中的一些普通引用,只不过被认为是 GC Roots 罢了。
哪些对象的引用可以作为 GC Roots ?
下面所列举出来的对象,指向它们的引用,就可以作为 GC Roots:
1)在虚拟机栈中引用的对象
举个例子,a 是栈帧中的本地变量,指向了 name = Jack 这个 User 对象。当 a = null 时,由于此时 a 充当了 GC Root 的作用,a 与原来指向 User 对象断开了连接,所以这个 User 对象会被回收。
2)在本地方法栈中 JNI 引用的对象(Java Native Interface,Java 本地接口,即通常所说的 Native 方法)
3)类的静态变量引用的对象(JDK 1.7 开始静态变量的存储从方法区移动到堆中)
如下代码所示,首先,存储在虚拟机栈中的变量 a 是一个 GC Root 对吧,当 a = null 时,那么 a 原来指向的 name = Jack 的 User 对象与 GC Root 断开了连接,所以 name = Jack 的 User 对象会被回收。
而由于我们给 s 赋值了变量的引用,s 在此时是类静态属性引用,充当了 GC Root 的作用,它指向的 name = Tom 的 User 对象依然存活
4)常量引用的对象(运行时常量池属于方法区的一部分,另外,其中的字符串常量池从 JDK 1.7 开始由方法区移动到堆中)
如下代码所示,常量 s 指向的对象并不会因为 a 指向的对象被回收而回收
5)JVM 内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。这个很好理解,如果这些核心的系统类对象被回收了,程序就没办法运行了。
6)所有被同步锁(synchronized 关键字)持有的对象
7)反映 JVM 内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等(这个说实话我不懂,也没去查资料,感觉不是很重要,哈哈哈哈,偷个懒吧)
另外,需要注意的是,除了这些固定的 GC Roots 集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象引用 “临时性” 地加入,共同构成完整 GC Roots 集合。
简单介绍下,众所周知分代收集机制,如最典型的只针对新生代的垃圾收集,这时候该怎么判断 GC Roots?
判断堆区中的所有对象作为 GC Roots?显然这样作会导致 GC Roots 包含过多对象而过度膨胀 只判断新生代中的对象作为 GC Roots?每个内存区域都不是孤立封闭的,所以某个区域里的对象完全有可能被位于堆中其他区域的对象所引用,这时候就需要将这些关联区域的对象也一并加入 GC Roots 集合中去,才能保证可达性分析的正确性。这有个专业术语:局部回收(Partial GC),目前最新的几款垃圾收集器无一例外都具备了局部回收的特征,当然它们在实现上也做出了各种优化处理
对象可回收,就一定会被回收吗?
并不是,对象的 finalize 方法给了对象一次垂死挣扎的机会。
当对象不可达(可回收)并发生 GC 时,会先判断对象是否执行了 finalize 方法,如果未执行,则会先执行 finalize 方法!
所以,我们可以在此方法里拯救这个濒死的对象。
具体做法就是将当前对象与 GC Roots 关联,这样执行 finalize 方法之后,GC 会再次判断对象是否可达,如果不可达,则会被回收,如果可达,则不回收!
需要注意的是,这个保命套路只能用一次,因为 finalize 方法只会被执行一次,如果第一次执行 finalize 方法此对象变成了可达确实不会回收,但如果对象再次被 GC,则会忽略 finalize 方法,对象会被直接回收掉!
最后放上这道题的背诵版:
🥸 面试官:如何判断对象是否可回收?
类似提问:
如何判断对象是否存活? 如何判断对象是否为垃圾? 😎 小牛肉:两种方法,引用计数法和可达性分析法。
所谓引用计数法就是在对象中添加一个字段作为引用计数器,每当有一个地方引用这个对象时,计数器的值就加一;当引用失效时,计数器值就减一;计数器为零的对象就是可以被回收的对象。
虽然这个算法简单但是无法解决对象之间的循环引用问题,所以目前主流的 JVM 用的都是可达性分析算法。
这个算法的基本思路可以理解为一棵多叉树:就是通过一系列称为 “GC Roots” 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,如果从 GC Roots 到某个对象不可达时,则说明此对象是可以被回收的。
在虚拟机栈中引用的对象 在本地方法栈中引用的对象 在方法区中类的静态变量引用的对象(JDK 1.7 开始静态变量从方法区移动到了堆中) 在方法区中常量引用的对象 JVM 内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器等。这个很好理解,毕竟如果这些核心的系统类对象被回收了,程序就没办法运行了 所有被同步锁(synchronized 关键字)持有的对象 ......
心之所向,素履以往,我是小牛肉,小伙伴们下篇文章再见 👋