目录
关于 GC 的三个问题
关于 GC,先思考三件事(官方开头):
- 问题一 —— 哪些内存需要回收?
- 问题二 —— 什么时候回收?
问题三 —— 如何回收?
问题一:哪些内存需要回收?
当然是垃圾需要回收!!!判断是否垃圾的方式有两种:
- 引用计数法(无法回收循环引用对象,了解即可)
-
对象中添加一个引用计数器
-
有一个地方引用它时,计数器值 + 1
-
引用失效,计数器值 - 1
-
计数器值为 0,表示对象“已死”,需要回收
-
- 可达性分析
-
通过一系列称为“GC Roots”的根对象作为起始节点集
-
从这些节点开始,根据引用关系向下搜索
-
如果某个对象到 GC Roots 间没有任何引用链,即对象不可达,需要回收
-
问题二:什么时候回收?
先标记后回收,下面是标记流程:
根据不同的垃圾收集器,标记后可能还要先整理 or 复制,最后才真正开始回收。
问题三:如何回收?
这里只讲回收算法,实现算法的垃圾收集器另开一章。
JVM 遵循“分代收集理论”将堆内存分为新生代和老年代。那什么是“分代收集理论”?
分代收集理论有三个假说:
弱分代假说:绝大多数对象都是朝生夕灭。
JVM 据此设计了一个 Young Generation,每次回收只关注少量存活对象。
强分代假说:熬过越多次 GC 的对象就越难以消亡。
JVM 据此设计了一个 Old Generation,难以消亡的对象,使用较低的频率回收这个区域。
跨代引用假说:存在互相引用关系的两个对象,应该倾向于同时生存或同时消亡。
在 JVM 中,新生代对象可能被老年代对象引用,因为这种情况的低频性,为此扫描整个老年代,代价太大。
所以,在新生代上建立一个全局数据结构“记忆集”(Remembered Set);
Remembered Set 把老年代划分成若干小块,标识出老年代哪块内存会存在跨代引用;
发生 MinorGC 时,只有包含了跨代引用的小块内存里的对象被加入到 GC Roots 进行扫描。
- “标记-清除”算法(apply to 老年代)
标记:从 GC Roots 节点开始进行扫描,对所有存活的对象进行标记,将其记录为可达对象。
清除:对整个堆内存空间进行扫描,如果发现某个对象未被标记为可达对象,那么将其回收。
TIPS
只是把垃圾对象的首地址和尾地址进行了保存,等到再次分配内存时,直接去地址列表中分配,并非真正清除数据(提高效率)。
缺点
标记和清除随着对象数量的增长,效率降低;
产生大量不连续的内存碎片,影响大对象的分配;
- “标记-整理”算法(apply to 老年代)
标记:从 GC Roots 节点开始进行扫描,对所有存活的对象进行标记,将其记录为可达对象。
整理:让所有存活对象都向空间内存一端移动,然后清理掉边界以外的内存。
优缺点
STW 时间增加,但比起复杂的内存分配和访问是值得的,因为内存分配和访问的频率要比垃圾收集的频率高得多;
整个程序的总吞吐量更高。吞吐量 = 应用执行总时间 - GC 总时间 / 应用执行总时间;
如果不移动对象,存在大量内存碎片,会导致大对象没有空间可以分配,增加 GC 次数,应用总 GC 时间增加,吞吐量降低;
- “标记-复制”算法(apply to 新生代)
标记:从 GC Roots 节点开始进行扫描,对所有存活的对象进行标记,将其记录为可达对象。
复制:只使用 Eden 和 Survivor from 来存放对象,垃圾收集时将存活对象复制到 Survivor to,然后清理 Eden 和 Survivor from。
垃圾收集器
下面是虚拟机截止 JDK11 Product 级别的垃圾收集器。
讨论这些垃圾收集器之前,先了解一下垃圾收集器的三项指标。
下面是几款垃圾收集器的对比:
从 JDK9 开始就已经弃用 CMS,将 G1 作为默认垃圾收集器。下面着重介绍 G1 收集器,CMS 四个阶段类比 G1 即可。
附:垃圾收集算法细节(面试加分)