【JavaEE初阶】JVM-垃圾回收机制(高频面试题)

目录

垃圾回收(GC)

JVM中各部分的回收的情况

GC工作过程

一、找到垃圾(不再使用的对象)

方案一:引用计数器(Python,PHP采用了这个方案)

引用计数器的弊端

方案二:可达性分析(Java采用了这个方案)

可达性分析的步骤

面试小Tips

二、释放垃圾

方案一:标记-清除

方案二:复制算法

复制算法的缺点

方案三:标记-整理

方案四:分代回收(Java的方案)

分代回收具体流程

不同区域的释放算法


垃圾回收(GC)

  • 垃圾回收是Java释放内存的手段
  • 例如,在C语言中,用malloc来申请内存,申请之后一定要手动调用free来进行资源释放,否则就会出现内存泄漏问题
  • 手动释放内存太麻烦,太容易出错了
  • 所以,Java引入了垃圾回收,进行自动释放
  • JVM会识别出 某个内存是不是后续不再使用了,然后回收(自动释放)

Java之后的各种编程语言都引入了垃圾回收,当前最主流的方案还是GC

  • 在Java17 及以上的版本,可以做到让STW大部分情况下 < 1ms的时间
  • 论性能,Java确实不如C++,但经过多年的发展,其实也没有差太多
    • 比如同一段程序
    • 用C++实现是1个单位时间
    • 用Java大概是1.5-2个单位时间
    • 用Go大概是4-5个单位时间
    • 用Python大概是100个单位时间

为啥C++不引入GC呢?

  • C++标准委员会否决了引入GC的方案
  • 因为C++主打的就是“和C兼容”、“极致的性能”
  • GC的代价太大,C++不愿承担~
  • 因为GC还有一个臭名昭著的STW(syop the world)问题
    • 触发了大规模GC
    • 其他业务就会因为GC动弹不得(在阻塞等待GC完)
    • 等GC结束了,其他业务代码才能继续走
      • 就像你在电脑前打游戏
      • 你麻麻要你让让
      • 那你只能等她清理完再继续呀~

JVM中各部分的回收的情况

区域内存回收方式
程序计数器

线程销毁了,自然就释放了

不需要GC

方法执行结束,栈帧就结束了,栈随之释放
元数据区存放类对象,一般不会释放

有很多新对象的创建,也会伴随着旧对象的消亡

使用GC来释放对象

  • 虽然说起来是“回收内存”
  • 本质上是“回收对象”(以对象为单位释放)
  • 不会出现把一个对象释放一半的情况

GC工作过程

  • 找到垃圾(不再使用的对象)
  • 释放垃圾(对应的内存释放掉)

一、找到垃圾(不再使用的对象)

方案一:引用计数器(Python,PHP采用了这个方案)

  • 每个对象在new的时候,都搭配一个小的内存空间,里面存放一个整数
  • 用这个整数来记录 这个对象被引用了多少次
  • 每次进行引用赋值时,都会自动触发引用计数的参数修改
  • 在Java中,要使用,某个对象,一定是通过引用来完成的
  • 如果引用计数为0,就说明没有引用指向这个对象,那这个对象就是垃圾咯~

引用计数器的弊端

弊端一:内存消耗更多

  • 计数空间会占用内存,特别是对象本身比较小的时候,引用计数消耗的空间占比就比较大
    • 假设引用计数占4字节
    • 对象本身是8字节
    • 计数引用就相当于提高了50%的空间占用率 -> 这个比例相当高了

弊端二:可能出现循环引用的问题

例如这段代码

class Test{
    Test t = null;
}

Test a = new Test();
Test b = new Test();

a.t = b;
b.t = a;

a = null;
b = null;
  • class Test{
        Test t = null;
    }

  • Test a = new Test();

  • Test b = new Test();

  • a.t = b;   //b中的值放到a.t里
  • b.t = a;

  • a = null;
  • b = null;

此时,这俩对象的引用计数不为0,但是,这俩对象都无法使用

方案二:可达性分析(Java采用了这个方案)

  • 引用计数,是有空间开销的
  • 可达性分析,用时间换空间

可达性分析的步骤

  • 1.以代码中的某些特定对象,作为遍历的“起点” => GCRoots
    • 1)栈上的局部变量
    • 2)常量池引用指向的对象
    • 3)静态成员(引用类型)
  • 2.对这些“起点”,尽可能地进行遍历,来判定某个对象是否能访问到
  • 3.每次访问到一个对象,都会把这个对象标记成“可达
    • 当完成所有对象的遍历后
    • 没有被标记成“可达”的对象 就是“不可达”
    • JVM自身是知道自己有多少个对象的,标记完“可达”的,剩下的那些就是“不可达”的
    • “不可达”的就是接下来要回收的垃圾 
  • 很好地解决了 引用计数中 内存占用和循环引用的问题

以t为起点,遍历t内的所有对象

再比如这段代码:

class Node{
    String val;
    Node left;
    Node right;
}

Node build(){
    Node a = new Node("a");
    Node b = new Node("b");
    Node c = new Node("c");
    Node d = new Node("d");
    Node e = new Node("e");
    Node f = new Node("f");
    Node g = new Node("g");

    a.left = b;
    a.right = c;
    b.left = d;
    b.right = e;
    e.left = g;
    c.rihgt = f;
    reture a;
}

fun(){
    Node root = bulid();
}

以root为起点,进入build()内部进行对象遍历(深度优先/广度优先~),判定每一个对象的可达性

经过上面一通遍历,发现对象都是可达的~

  • 要是我让root.right.right = null(也就是把c和f的链接断开
  • 这样的操作会使f不可达
  • 下一轮GC中,f会被当成垃圾回收掉

除非以下这样表达,f依旧可达

Node f = root.right.right;
root.right = null;
  • 此时c也会被当成垃圾

可达性分析 是一个周期性的过程

  • 每隔一定时间,就会触发一次这样的可达性分析的遍历
  • 这也是 C++非常嫌弃GC的原因
    • 每次触发GC,都得遍历一遍所有对象,如果对象特别多,这个过程就会非常消耗时间和资源

面试小Tips

在深入理解Java虚拟机这节,面试的时候如何回答,要看面试官是咋问的

  • 问题:介绍一下垃圾回收的基本策略
    • 可以把两个方案都回答
  • 问题:介绍一下Java的垃圾回收机制
    • 介绍可达性分析即可

二、释放垃圾

当已经知道哪些对象是垃圾后,如何进行释放呢?

方案一:标记-清除

  • 把垃圾对象的内存直接释放,但是这样做会导致内存碎片问题

垃圾释放后,内存碎片的情况

  • 由于内存申请,申请的都是连续的内存
  • 比如,申请1MB的内存,必须是连续的,不能是多个碎片零零散散的
    • 内存碎片如果非常多,就算总的空闲空间很大
    • 但是想申请一个稍微大一点的内存空间都会失败
      • 例如,总的空闲空间是4G
      • 但是如果都是碎片,就会使得申请1G空间都有可能失败

方案二:复制算法

  • 把申请的内存分成等大的 两个内存空间
  • 一次只使用其中一半
  • 不是垃圾的对象拷贝到另一侧

  • 这一侧的内容整体释放掉,此时就可以确保空闲的内存是连续的

复制算法的缺点
  • 内存的空间利用率很低(至少浪费一半内存)
  • 一旦不是垃圾的对象特别多,复制的成本就会很高(尤其是这样的对象包含 大体量 的对象时)
  • 因此,使用复制算法的前提是 去除垃圾后,剩下有用的数据少(这样复制更方便)

方案三:标记-整理

  • 优点:解决内存碎片 & 保证内存利用率
  • 类似于顺序表的“搬运”

缺点:内存搬运的操作,开销还是挺大的,复制的成本问题仍然存在

方案四:分代回收(Java的方案)

  • “代” => 对象的年龄(GC的轮次)
    • 某个对象,经历一轮GC可达性分析后,不是“垃圾”,则该对象年龄 +1
    • 初始年龄为 0
  • 空间分成两种区域
    • 新生代(年龄小的对象)
    • 老年代(年龄大的对象)

  • 针对不同年龄的对象采取不同的策略
  • 因为不同年龄的对象,特征是不同的(经验性的规律)
    • 一个年龄大的对象(多次可达性分析后还存在),后续大概率还会继续存在很久
    • 一个年龄小的对象,很有可能很快就没用了
  • 针对不同年龄的对象,处理方式不同
    • 老年代,GC频次可以降低
    • 新生代,GC频次会比较高
分代回收具体流程
  • 新建的对象放到“伊甸区
    • 绝大部分的伊甸区对象,活不过第一轮
  • 活过第一轮的对象放到“幸存区”
    • 伊甸区 => 幸存区 使用复制算法(复制的对象规模很小,复制的开销可控)
  • 幸存区的对象,也要经历GC扫描
    • 每一轮GC都会消灭一大波对象
    • 剩余的对象再次通过 复制算法复制到另一侧的幸存区
  • 如果这个对象在幸存区经历了多次复制,都存活下来了,这个对象年龄就大了 =>晋升到老年代
    • 老年代也是要定时GC的,只是频率没有那么高
  • 一个对象的成长历程:
    • 伊甸区 => 幸存区 => 幸存区 …… => 幸存区 => 老年代
不同区域的释放算法

  • 新生代中的对象大部分都会快速消亡,使得每次复制的开销都可控
  • 老年代的对象大部分生命周期较长,使得整理的开销也都可控

以上的GC过层仅作参考,大部分都已经过时了,但是禁不住面试要考啊~

嗨喽呀!集智慧与努力于一身的勇士!

恭喜你通过本关!

希望你在以后不管是学习还是生活都迎难而上,永不言弃!

我们在下一个关卡见面吧~再见~

END✿✿ヽ(°▽°)ノ✿

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值