目录
垃圾回收(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✿✿ヽ(°▽°)ノ✿













4956

被折叠的 条评论
为什么被折叠?



