文章目录
前言
了解 JVM GC 前需要知道的。
判断对象可回收方法
GC 主要发生在运行时数据区里的堆和方法区,但主要关注的区域还是堆。那么什么是垃圾对象呢?简单的说就是某个线程在堆上创建对象使用后不再需要,则可认为该对象就是垃圾对象。
引用计数法
引用计数法就是在 JVM 层面为对象添加一个引用计数器,每当有一个地方引用它,计数器值 +1;当引用失效时,计数器值 -1;任何时刻计数器值为 0 时则表明该对象不再需要。
大多数情况这个方法是可以发挥作用的,但在循环引用的情况下就不行了:
public class Student {
// friend 字段
public Student friend = null;
public static void test() {
Student a = new Student();
Student b = new Student();
a.friend = b;
b.friend = a;
a = null;
b = null;
// gc
}
}
上面代码中的 a 和 b 虽然被赋值为 null
,但堆里对象的 friend
字段相互引用,而且没有地方能再对他们修改,这样计数器就永远不会为 0,垃圾收集器将无法回收他们。
- 优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
- 缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为 0。
可达性分析算法
可达性分析算法也叫根搜索算法,JVM 使用的就是该算法。
这个算法的基本思路就是通过一系列称为 “GC Roots” 的根对象作为起始节点集 (GC Root Set),从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为 “引用链” (Reference Chain),如果某个对象到 GC Roots 间没有任何引用链相连,则说明此对象不再被使用,也就可以被回收了。
要进行可达性分析就需要先枚举根节点 (GC Roots),在枚举根节点过程中,为防止对象的引用关系发生变化,需要暂停所有用户线程 (垃圾收集之外的线程),这种暂停全部用户线程的行为被称为 Stop The World。
- 优点:解决引用计数法的问题
- 缺点:STW
基础名词
JVM 是以可达性分析算法查找垃圾对象,下面名词都是以此为前提。
读/写屏障(Load/Store Barrier)
// 读屏障
public Field getField() {
// 返回数据时先进行处理
loadBarrier(this.field);
return this.field;
}
// 写屏障,分为写前和写后
public void setField(Field field) {
// 写前
beforeStoreBarrier(field);
this.field = field;
afterStoreBarrier(field);
}
有点类似于 AOP。
根节点(GC Roots)
当前 GC 不会回收的对象。
GC Roots 主要来自上图中的发出箭头的对象:
- 在虚拟机栈 (栈帧中的本地变量表) 中引用的对象,比如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,比如 Java 类的引用类型静态变量。
- 在本地方法栈中 JNI (即通常所说的 Native 方法) 引用的对象。
三色标记法
三色标记法是为了减少 GC 时发生的 STW(Stop The World) 时间。三色标记法将对象分为三种颜色,黑色、灰色和白色。
- GC 开始时,所有待处理的对象都是白色;
- 从 GC Roots 开始遍历对象,对象所有引用都遍历后标记为黑色,还未遍历完的对象标记为灰色;
- 标记完成后,对象就只有黑色和白色,白色就是要清除的垃圾数据,因为应用代码无法再操作这些对象。
当 GC 线程与应用线程并发执行时,那就需要读/写屏障帮忙,防止对象漏标导致需要的对象被清除。
安全点 Safepoint
当需要 GC 时,应用线程需要执行到某个位置才能停止,这个位置需要记录该线程的调用栈、寄存器等一些重要信息,否则等 GC 完无法重新执行该线程,而这个位置就是安全点。
算法关注点
GC 算法主要关注两点,是否高吞吐量,是否低延迟。
吞吐量
吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值。
吞 吐 量 = 用 户 代 码 运 行 时 间 ( 用 户 代 码 运 行 时 间 + G C 时 间 ) 吞吐量 = \frac{用户代码运行时间}{(用户代码运行时间 + GC 时间)} 吞吐量=(用户代码运行时间+GC时间)用户代码运行时间
虚拟机总共运行了 100 秒钟,其中垃圾收集花掉 1 秒钟,那吞吐量就是 99%。
延迟
延迟就是当发生 GC 时,应用线程被暂停的时间。
收集算法
标记-清除算法
从 GC Roots 进行扫描,对存活对象进行标记,完成后遍历堆,把未被标记的对象回收。
缺点:
- 若堆较大,则遍历时间长;
- 对象清除后,留下的空缺位置造成内存不连续。
标记-整理算法
从 GC Roots 进行扫描,对存活对象进行标记,完成后移动存活对象,并按内存地址次序依次排列。
缺点:
- 需要移动存活对象
标记-复制算法
复制算法简单来说就是把内存一分为二,但只使用其中一份,在垃圾回收时,将正在使用的那份内存中存活的对象复制到另一份空白的内存中,最后将正在使用的内存空间的对象清除,完成垃圾回收。
缺点:内存缩小为原来的一半
分代算法
首先这不是一种新算法,它是一种思想。现在使用的Java虚拟机并不是只是使用一种内存回收机制,而是分代收集的算法。就是将内存根据对象存活的周期划分为几块。一般是把堆分为新生代、和老年代。短命对象存放在新生代中,长命对象放在老年代中。
对于不同的代,采用不同的收集算法:
- 新生代:由于存活的对象相对比较少,因此可以采用复制算法, 该算法效率比较快。
- 老年代:由于存活的对象比较多哈,可以采用标记-清除算法或是标记-整理算法。