啥是垃圾?
不再使用的内存
啥是垃圾回收机制?
自动释放不用的内存
注意: GC 主要是针对 堆 进行的
GC的基本操作单位是 对象, 即GC'回收的是整个对象都不使用的情况
GC 的优缺点
好处: 省心, 写代码简单, 不易出错
缺点: 需要消耗额外资源, 有额外性能开销 , 此外, 易触发 STW 问题
STW 问题
Stop The World
如果内存中垃圾很多, 此时触发一次 GC 操作, 开销可能非常大, 还可能会触发一些锁操作, 因此会导致业务代码无法正常执行
GC 实际工作过程
- 找到垃圾 / 判定垃圾
- 进行垃圾 (对象) 的释放
找到垃圾 / 判定垃圾
关键是看这个有没有引用 "指向" 它, 没有引用指向的对象, 自然不可能被用到, 就是垃圾了
两种典型实现:
- 引用计数 (py / php 做法)
给每个对象分配一个计数器, 每次创建一个引用指向该对象, 计数器加一, 每次删除一个该对象的引用, 计数器减一, 当计数器为0, 该对象自然就是垃圾了问题 :
- 内存空间利用率低 (计数器本身需要存储)
- 存在循环引用的问题 (py / php 使用引用计数, 需要搭配其他机制来避免循环引用)
- 可达性分析 (Java 做法)
将整个 Java 中所有的对象, 通过 链式 / 树形 结构, 整体给串起来
可达性分析,就是把所有的对象被组织起来的结构视为树, 从树的根节点出发, 遍历树, 所有能被访问到的对象, 标记成 "可达"
JVM 再比对自己手里的对象名单, 将不可达的对象进行回收(每次 new 一个对象的时候, JVM 都会有记录)
- 可达性分析类似树遍历, 相对于引用计数会慢一些 (因此不必一直执行, 每隔一段时间执行一次就ok ---- "虽迟但到" )
- 可达性分析遍历的起点 ---- GCroots (一个代码中会有很多起点, 每个起点往下扫一遍, 就完成一次分析)
清理垃圾 (此处为几种垃圾回收算法)
- 标记清理
简单粗暴的做法, 如果灰色区域为垃圾, 白色不是, 则直接清除
问题:
- 会产生零散的内存碎片
2.复制算法
把内存空间均分成两份, 每次将不是垃圾的对象复制到另外一边, 然后把整个含垃圾的区域清理掉
问题 :
- 空间利用率低 (每次只能用到一半空间)
- 如果垃圾少, 有效对象多, 复制成本就会很大
- 整理标记
类似顺序表删除中间元素, 将不是垃圾的对象从前往后依次排开, 清理剩余空间
- 保证了空间利用率, 也解决了内存碎片问题
- 显而易见, 本做法效率也不高
分代回收
基于上述基本策略, 搞了一个符合策略 "分代回收"
历史基本规律: 如果一个东西存在的时间比较长, 那么大概率还会继续长时间存在下去
该规律对于 Java 同样有效, Java 对象要么生命周期特别长, 要么特别短
引入概念: 年龄 ---- 熬过 GC 的轮次 (没被清理)
将堆分成一系列区域Minor GC : 新生代 GC
Full GC : 老年代 GC
- 刚创建的对象, 年龄为 0 ,放到伊甸区
- 熬过一轮 GC ,放入幸存区 (复制算法)
- 幸存区中对象, 周期性接收 GC 考验, 如果变成垃圾, 就被释放, 如果不是垃圾, 就拷贝到另外一个幸存区 (俩幸存区同一时刻只使用一个), 在二者间来回拷贝 (复制算法)
- 在幸存区中经过了多轮考验后, 进入老年代
- 老年代也要周期性 GC 扫描, 只是频率更低
- 如果老年代的对象是垃圾了, 使用标记整理的方式进行释放