JVM垃圾回收(上):面试必问,白话文讲解,小白请进

JVM垃圾回收(上):面试必问,白话文讲解,小白请进

首页卡牌对战小龙飞飞游戏更新时间:2024-07-04

垃圾回收

其实讲到垃圾回收,不外乎弄懂三个哲学问题:

回收哪?

讲到这个,我就又要上 JVM 内存分布的图了,把图摊开说大事(需要详细了解这些内存分布的请移步前面我的魔性介绍JAVA跨平台根本原因,面试必问JVM内存结构白话文详解来了 )。

首先把程序计数器排除,(再啰嗦一遍它的作用,程序计数器存放的是下一条字节码指令执行的地址,存放地址的地方,因此只需要一块较小的内存空间,几乎忽略不计,它的作用是当前线程所执行的字节码行号指示器,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成),这么小的地方,可怜的连 OutOfMemoryError 都不会有的地方,不值得垃圾回收器出马,pass掉。

接着,栈区,包括本地方法栈和虚拟机栈。遥望栈当年,入栈出栈不亦乐乎,半点不留数。栈是个喜欢自己玩的区域,数据在里面也就是玩玩而已,进去了入栈了转一圈计算完了又出栈了,所以这块内存也不用垃圾回收器操心人家累了自己会出来。

因此上面提到的,都不需要垃圾回收,随线程生随线程死。

然后,方法区,这个老哥,存的是主要是类信息和运行时常量池,类信息也就算了,撑死能有多大,编译完就可以确定大小并且不会改变的东西。主要是这个运行时常量池坏事,以前的垃圾回收器不管这部分内存,如果运行时好死不死有很多的常量产生,那么常量池就会变得很大最后内存溢出,还经常引发事故,后来JVM重视这一块也开始垃圾回收。

最后,堆区,这个就比较 happy 了,大量 OOM 的地方,运行期间可能会有数不清的对象产生,不产生我写个死循环new也要产生,是垃圾回收期的亲儿子重点照顾对象。

所以,回收哪?不就是回收方法区和堆区吗老弟。

回收谁?

关于回收谁,方法区和堆区里面有谁呢?

方法区的常量,从常量数据类型看分为基本类型和引用类型两种常量,基本类型肯定不用担心,JAVA里面八种基本类型说1就是1,和别的对象什么的没有牵扯,说回收也就回收了;引用类型可能和别的对象藕断丝连,不能轻易斩断。

堆区里面全是对象,一个个的对象,互相牵扯,要回收任何一个对象都需要看看他还有没有被别人引用,不能轻易回收。

所以,总的来看,回收引用类型的数据是关键。

怎么断定对象已死?

突然想起了一个笑话,有村民举报野外有一具尸体,狄仁杰和李元芳去办案,狄仁杰指着尸体说:“我断定此人已死,元芳你怎么看?”。笑话有点冷。

对象已死,简单理解就是在任何地方都不需要引用这个对象了,这个需求其实很简单,让我们自己来设计的话,就是将所有跟此对象有牵扯的对象都用一个值统计一下,被引用一次 1,取消引用-1。

引用计数法

算法思想:这个方法,就是给对象加一个引用计数器,每当有一个地方引用,计数器加1;当引用失效,计数器减1;当计数器为0表示这个对象已死没有再被使用。

这个算法简单高效粗暴,但是不能解决互相循环引用的问题,伪代码也就是如下情况:

public class MyObject { public Object ref = null; public static void main(String[] args) { MyObject myObject1 = new MyObject(); MyObject myObject2 = new MyObject(); myObject1.ref = myObject2; myObject2.ref = myObject1; myObject1 = null; myObject2 = null; }

那么A和B的引用计数器就都是1,为 null 主动触发GC也不能回收,JAVA 里面对象那么多,可见这种方式,在JVM里面是行不通的,那就优化。

可达性分析算法

算法思想:两个对象之间互相循环引用不好解决,那就给他们同一个父祖先,这个对象不管在哪里,只要这个对象到达GC Roots有路,也就是有引用链,那么这个对象就是可达的。反之,如果这个对象到达GC Roots没路了,不可达,那么这个对象已死不可用。图中 Object5,6,7就因为不可达,所有可以被回收。

那么问题来了,这个GC Roots又是何方神圣?

其实这个就是对象,在Java语言中,可作为GC Roots的对象包括下面几种:

......

被这些对象引用的对象,都是可达的。

可达性分析算法的实现

GC Roots的标记和存储

算法有了,怎么实现呢?一步一步来,首先进行标记,这么多对象都可以作为GC Roots,那么就标记下来到底哪些是那些不是。两种方法,第一种还是暴力遍历,第二种就是准确式的遍历:

拿栈区来举例子,所有的引用都进行遍历,遇到数字地址就判断是否是一个引用(这里会涉及上下边界检查(GC堆的上下界是已知的)、对齐检查(通常分配空间的时候会有对齐要求,假如说是4字节对齐,那么不能被4整除的数字就肯定不是指针),是的话就放GCRoots;这样会有问题

  1. 就是这个引用是不是指向了堆对象不知道,如果是个无效的引用那么还是没法回收。
  2. 其次,这样其实很慢,遍历就要考虑时间问题,对象的状态在这段时间内的变化问题,上面看到方法区的内容也放到了GC Roots,这个地方多的时候上百兆内容,遍历的话很耗时。

我们在学习mysql,redis一些数据备份的时候,肯定接触过快照的概念,拍个快照然后接着该干嘛干嘛。

  1. 类加载完成时候,HotSpot就把对象内什么偏移量是什么类型数据计算出来,对象引用自然也算出来
  2. JIT编译,也会在特定位置记录下栈和寄存器的哪些位置是引用;

也就是在整个Java生命周期里面JVM都在维护这张表,所以GC Roots 标记方式以及存储的数据结构就都明确了。

GC Roots遍历前

首先绕了这么一大圈,明确一下,GC ROOTS这个是用来判断垃圾回收时回收哪些对象的,从目的出发,遍历这个GC ROOTS,没在这里面的对象都可以进行回收。

遍历的话,肯定要遍历最新准确的的 GC ROOTS 的存储结构 OopMap,那么问题来了,什么时候GC Roots是最新的准确的?

方法执行的过程中, 引用关系时刻在发生变化,那么保存的 OopMap表 的更新就要随着变化,如果每个对象引用关系变一下都要触发 OopMap表 的更新,那维护这个的成本也太高了,而且GC也不是时刻都在发生的,没有必要。这个OopMap表准确性只要保证 GC之前遍历的时候他是准确的就好了。GC又是什么时候触发的,所以这里就引入了安全点的概念,安全点决定GC的时机,GC来临之前肯定要先进行 OopMap表 的更新。

GC Roots遍历前前

安全点 SafePoint 触发GC,程序只有在到达了安全点才会暂停进行GC,不是随心所欲的,安全点的选择就很关键了,安全点太少了GC等待时间太长,太多了增大了运行时的压力,这个点的选择程序会以他自己“是否具有让程序长时间执行的特征”为标准来界定的,长时间执行最明显的就是指令序列复用,例如方法调用,循环跳转,异常跳转等。

为了让线程在安全点处才停顿,有两种办法:

  1. 抢断式中断

就是在GC的时候,让所有的线程都中断,如果这些线程中发现中断地方不在安全点上的,就恢复线程,让他们重新跑起来,直到跑到安全点上。

  1. 主动式中断

当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。

现在都是第二种方式,原因我猜测主要是因为第一种是阻塞的,第二种是非阻塞的效率更高。

GC Roots遍历中

以上都是遍历前的准备工作,现在要开始正经遍历了,遍历期间又要注意什么呢?知道了什么时候开始GC,那么就可以进行拍快照存储OopMap了,就又有一个问题,别嫌弃问题多,给女孩子拍照尚且还要等妹子摆好pose,比好剪刀手呢,因此快照肯定不能就随手咔嚓一下。

我们需要得到准确的可达性分析结果,那就不可以出现在分析期间对象引用关系还在疯狂的变化情况,因此在进行遍历 GC Roots 的时候,需要将所有的用户线程都停下来,JVM 有个浪漫的说法叫做“stop the world”,这是 GC 进行时需要停顿的一个最主要原因,即使是 CMS 号称不停顿 GC 的收集器这一步停顿遍历根结点也是不可避免的。

四种引用类型

字节面试官问这个问题,我是始料未及的,我只之前粗略的知道这四种,把四种名字说出来了,然后说是垃圾回收时根据内存情况回收不同类型,牙缝里就再也蹦不出一个字了,因为哪种类型什么时候回收我浑然忘光了。

JAVA中的引用就相当于C中的指针。

java.lang.ref包是JDK1.2引入的,包结构和类分布如下:

要我一直死记概念是很难的,年纪大了越来越不喜欢这种记忆的活,因此还是从推理的角度来看这个引用问题。我们平时中新建对象 Object object = new Object();上面那么花里胡哨的引用我是没有用过的,相信大部分人也不经常用,然而存在即合理,Java代码里面很多封装类的源码里面就用到了。

为什么要设计这四种引用类型呢?JDK1.1就是只一种的,加的原因猜测有如下三:

  1. 拓展引用类型,使得引用类型更加丰富,便于拓展Java语言一些别的功能;
  2. 垃圾回收总的来说对程序员是比较不透明的,除了最开始的强引用,其他的三个对垃圾回收敏感,给程序员自己选择的权利,可以定义一些对象的垃圾回收方式;
  3. 增大回收效率,每种对象回收的时机不一样,遍历的时候可以针对性遍历;

强引用

Object obj = new Object();

生命周期:这样的常规引用,只要引用还在,就永远不会回收对象。

软引用

Object a = new Object(); ReferenceQueue<Object> queue = new ReferenceQueue<>(); SoftReference<Object> sf = new SoftReference<Object>(a,queue);

生命周期:在发生内存溢出之前,进行回收,如果这次回收之后还没有足够的内存,则报OOM。

具体实现:如代码所示,软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。后续,我们可以调用 ReferenceQueue 的poll()方法来检查是否有它所关心的对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个 Reference 对象。

应用场景:软引用通常用来实现内存敏感的缓存。如果还有空闲内存,就可以暂时保留缓存,直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。这样就保证了使用缓存的同时,不会耗尽内存。

弱引用

生命周期:生命周期比软引用短,生存到下一次垃圾回收之前,无论当前内存是否够用,都回收掉被弱引用关联的对象。

Object a = new Object(); ReferenceQueue<Object> queue = new ReferenceQueue<>(); WeakReference<ReferenceCountingGC> wf = new WeakReference<>(a,queue);

具体实现:如代码,弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中,后续和软引用差不多。

应用场景:

  1. 同样可用于内存敏感的缓存;
  2. ThreaLocal中的map实现,此map继承了弱引用WeakReference,防止map中的key引用的对象无法被回收;

//继承了弱引用WeakReference static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }

虚引用

虚引用也叫幻象引用,通过 PhantomReference 类来实现。不会对对象的生命周期有任何影响,也无法通过它得到对象的实例,唯 一的作用也就是在对象被垃圾回收前收到一个系统通知

应用场景:可用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知。

怎么回收公众号《阿甘的码路》下一篇会继续更,这些内容要往深了写内容很深,能坚持看下来的人肯定是狠人。写作不易,一键三连是美德,喜欢可以关注。

关注我,一起成长

查看全文
大家还看了
也许喜欢
更多游戏

Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved