JVM 是怎么把“送”出去的内存又“要”回来的

虚拟机内存回收

之前我们知道了对象是怎么诞生,然后在内存中安家的,不过对象终归有一天也会“死亡”,那它“死亡”的时候,虚拟机都干了什么呢?又怎么判断一个对象”死“没”死“呢?今天一起学习一下虚拟机是怎么把分配出去的内存,“拿”回来的。

想搞懂内存回收,顺着三个问题往下捋就可以很清楚的梳理好这块内容。

目录:

  1. 哪些内存需要回收?
  2. 什么时候回收?
  3. 怎么回收?

内存回收

自动收集程序运行产生的“垃圾”,回收这部分内存这种思想并不是起源于 Java ,而是早 Java 30 年的 Lisp 语言*(作者:John McCarthy)*。

我们也先思考 Lisp 作者对垃圾收集,内存回收提出的三个问题

  1. 哪些内存需要回收?
  2. 什么时候回收?
  3. 怎么回收?

哪些内存需要回收?

这里在回顾一下 Java 程序在运行时都有哪些数据区域,如果没跟 lvgo 一起学的同学,可以看看之前的文章 《 你创建的 Java 对象都搁哪了》

PC寄存器(程序计数器)、本地方法/虚拟机栈、方法区、堆这大区域

程序计数器、虚拟机栈、本地方法栈3个区域都是线程私有的,他们的产生和消失都随着线程的创建和销毁而变化。

所以 方法区 这两块内存区域就是需要进行回收的内存。同时这部分内容和程序运行密切相关,具有不确定性,比如你写了1w个类,但实际只使用1个,那在堆和方法区中也只有1个对象的内存大小的区域被使用。所以需要动态的管理这部分内存。

什么时候回收?

在堆中的实例对象,如果想要把这部分内存回收,一定要确定这里所保存的实例对象哪些还在被使用,哪些已经没有用了,即允许回收。

所以回收的条件就是对象已经没有再被引用了。

对象引用知识补充

JDK2的时候,对象的引用只有两种情况

  1. 被引用
  2. 没被引用

后来有了更多场景的需要,

  • 如果内存还够用,即使我没被引用,也不要回收我。
  • 如果已经做了一次回收,还是不够用,那就把没被引用的我回收吧。

在这种需求提出后 JDK2之后,Java 补充了对象引用的概念,将引用分为强、软、弱、虚四种。

了解对象引用概念之前先喝杯咖啡提提神。

4个不同性格的人喝咖啡

从前有甲乙丙丁四个人去咖啡厅喝咖啡,这四兄弟吧,不知道咖啡厅的规矩,跟我似的,也没去过啊,不知道。进去了之后这 甲乙丙丁 四兄弟就找个位置拿着咖啡在那喝。

结果不巧今天咖啡厅爆满!服务员就想着找一些喝完不走的人聊聊,把它们聊走,

也不知怎么的,就盯上这四兄弟了,就过来问 :“甲先生,你喝完了吗?今天人有点多,你看看…”,“管谁叫先生呢,我是你大姐!没喝完呢,你看不见呐,不走”。 势,服务员不敢惹。

接着他就朝着 走过来,他还没说话, 先开口:“刚你跟我大姐说的我听见了,你看这么的行不行,你先去找别人,如果你找完别人之后位置还不够,来找老妹儿,老妹儿给你让这个位置 ” ,服务员一看 是个 妹子,就答应她走开了。

这回儿服务员大哥已经有点累了,想赶快找到新位置出来,不然一会内存溢出了擦,来到 面前,“兄弟,今天餐厅位置不够了,你看你让一下子行不行”,说着用手拍着 的肩膀, 一看这架势,有点害怕,赶紧说:“行行行”,服务员一看,这 也太 了。

服务员大哥准备乘胜追击来到 面前,发现这个 有点问题,怎么说呢,就像 幽灵 一样若隐若现,完事服务员过去拍了一下他,这不拍不知道,一拍差点裤衩子都吓掉了,这压根就没人,甚至那杯咖啡他都拿不起来,服务员只好瑟瑟发抖的说“这地方我先收拾了啊” 然后就灰溜溜的跑开了,不一会这个位置就给其他客人坐上了。

大哥找了一会安排了一些人坐下,发现位置还是不够,于是他想起 说的话,就过去给 说了现在的情况,然后 也是个守信用的 妹子,就收拾东西走开了。

不过当 走开之后,服务员发现还有 1 个顾客没有位置坐!实在没办法了,谁也别喝了,关门吧,人都从咖啡厅里溢出去了。

所以这个故事告诉了我们什么?

咖啡厅里有鬼

总结一下,甲乙丙丁分别对应的 强软弱虚 四种引用类型

  • 甲:强引用(Strongly Re-ference)

    只要我的咖啡(引用)还在,就没人敢动他,很强势。

  • 乙:软引用(SoftReference)

    一个讲道理的引用,你先去收拾别的地方,如果你收拾完了地方还不够,我再让我的位置,不然你就让我多待一会

  • 丁:弱引用(Weak Reference)

    这个引用的生命只能坚持到诞生之后的第一个 GC 就嗝屁了,来收拾你就得让,管你喝完没喝完。

  • 丙:虚引用(Phantom Reference)

    幽灵一般的存在,最弱的一种引用关系,随时可能被回收,你不能用它来获取一个实例,这个引用的作用是为了对象被回收是接到通知。

这4种引用强度依次逐渐减弱。强 > 软 > 弱 > 虚

怎么回收?

回收堆内存

关于判断堆中对象是否存活(被引用)的方法,有两种

引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。

JVM没有采用过这种方式,原因之一就是循环引用问题

可达性分析算法

注:摘自周志明《深入理解 Java 虚拟机(第三版)》3.2.2 可达性分析算法部分

通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

图片来自周志明《深入理解 Java 虚拟机(第三版)》

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。

    方法执行时相关的数据

  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。

    静态变量

  • 在方法区中常量引用的对象,譬如字符串常量池(StringTable)里的引用。

    常量

  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。

    虚拟机本地方法持有的对象

  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。

  • 所有被同步锁(synchronized关键字)持有的对象。

  • 反映Java虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。

除了固定的 GC Roots 以外,这部分集合还会根据选用的垃圾回收器和当前区域的不同,临时增加一些 GC Roots 对象。具体的体现为分代收集和局部回收。

对象真正死亡(即可被回收)

标记:在可达性分析之后,即使对象被标记为不可达,也不会立即回收掉该对象,而是标记为可回收(笔者自释)。

标记确认:在已经标记的对象集合中,会进行一次筛选,筛选条件是当前对象是否有必要执行 finalize() 方法。如果有必要执行则继续 存活,如果没有必要则 死亡

判断当前对象是否有必要执行 finalize() 方法的条件(二次确认对象已死亡)是

  1. 对象没有重写 finalize() 方法
  2. finalize() 方法已经被执行过一次

满足以上两个条件的话虚拟机则会认为没有必要执行 finalize() 方法,即对象已死亡。

如有逃过这次的 死亡 时机,只要当前对象在重写的 finalize() 方法中(一定要在 finalize()方法中,这是唯一一个能拯救自己的机会)与 GC Roots 集合中对象的引用链发生关联,即当前对象再次产生引用关系,即可逃离这次垃圾回收。

所以实际上,我们可以通过重写 finalize() 来“拯救”一次对象的死亡,也只能拯救一次,因为宣布其真正死亡还有第二个条件,就是 finalize() 方法已经执行过一次。

回收方法区

注:以下摘自周志明《深入理解 Java 虚拟机(第三版)》3.2.5 回收方法区

有些人认为方法区(如HotSpot虚拟机中的元空间或者永久代)是没有垃圾收集行为的。

《Java虚拟机规范》中提到过可以不要求虚拟机在方法区中实现垃圾收集,事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK 11时期的ZGC收集器就不支持类卸载)。

方法区垃圾收集的“性价比”通常也是比较低的:在Java堆中,尤其是在新生代中,对常规应用进行一次垃圾收集通常可以回收70%至99%的内存空间,相比之下,方法区回收囿于苛刻的判定条件,其区域垃圾收集的回收成果往往远低于此。

方法区数据

方法区中保存的是类信息和运行时常量池两部分内存。

回收运行时常量池

运行时常量池的回收比较简单,只需判断当前常量池中的字面量是否有被引用即可。常量池中其他类(接口)、方法、字段的符号引用也与此类似。

回收类信息(类卸载)

判断一个类型信息是否可以被回收需要同时满足以下三个条件,

  1. 该类信息对应的所有实例被回收
  2. 加载该类的加载器被回收
  3. 对应的 java.lang.Class 对象没有在任何地方被引用

具体内容如下:

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
  2. 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

多了解一点:

其中第三点与我们经常见到的操作诸如 spring 这种大量使用反射的框架、JDK 的动态代理、以及 CG lib 这种操作字节码的框架基本上都需要 JVM 拥有类卸载的功能,否则会导致一些自定义加载器加载的临时类信息占据着方法区的内存,带来不必要的压力。

(正文完)


下一篇学习具体的垃圾回收算法,欢迎关注加群一起学习,虚拟机的学习之路不在枯燥,不在孤单。

推荐阅读

1. JVM 你知道不
2. 你创建的 Java 对象搁哪了
3. JVM 中对象咋创建啊,又怎么访问啊

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值