JAVA 垃圾回收机制(二) --- GC回收具体实现

1,131 阅读6分钟

JAVA 垃圾回收机制系列文章

系列文章分3个部分

Java JVM -- 看这篇就够了

JAVA 垃圾回收机制(一) --- 对象回收与算法初识

JAVA 垃圾回收机制(二) --- GC回收具体实现

JAVA 垃圾回收机制(一) --- 对象回收与算法初识 中,我们已经知道了 GC 回收时的一些原理和算法总结。这一章,我们来看看 虚拟机中到底是怎么实现这些步骤的。

一、枚举跟节点

从上篇知道了 可达性分析中,有个叫 GC Roots节点的名词;那在进行 GC 时,就需要对所有对象找到这个 GC Roots 的节点,而在找的同时,是不能出现对象引用关系在不断变化的。这点是导致GC进行时必须停顿所有的Java执行线程的其中一个重要原因。 那既然无法避免这种情况,就需要如何去优化这个时间了,毕竟你突然写到一半,来了个停顿5分钟,谁都会崩溃的。

1.1 准确式GC

因为并不需要一个不漏的检查所有的执行上下文和引用位置,虚拟机中,可以一组被称为 OopMap 的数据结构来达到目的。在类加载完的时候,HotSpot (虚拟机的一种)就已经把对象内的偏移量上是什么类型的数据计算出来了,即位置也会被 OopMap 记录下来。

1.2 安全点

有了OopMap 的帮忙下,可以快速且准备的找到 GC Roots 枚举,但可能由于引用关系的变化,或者说 OopMap 内容变化的指令非常多,如果为每一条指令都生成对应的 OopMap ,那将会需要大量的额外控件,这样GC成本也会变高。 所以,系统并没有为每条指令都生成OopMap,而是在“特定的位置”记录了这些信息,比如方法的调用、循环跳转、异常跳转等,这些位置被称为安全点,只有在达到安全点才会去停顿。跑到安全点的防范有两种:

  1. 抢断式中断:在GC发生时,首先把所有的线程全部中断,如果发现有线程中断的地方不在安全点上,则恢复线程,让它“跑”在安全点上。不过现在没有虚拟机采用这种方式。
  2. 主动式中断:线程给自己设置一个标志位,当轮询到这个标志位,就主动挂起,标志位和安全点在同一个位置。

1.3 安全区域

上面貌似解决了问题,当如果线程处于 sleep 或者 Blocked 状态时,此时系统又发出了GC,这时安全点就没啥用了;所以,这里又设置了一个安全区域;安全区域是指在一段代码中,引用关系不会发现变化。在这个区域 GC 都是安全的,当在这个区域发现GC,线程可以不管自己的安全点的状态,当要离开这段区域时,需要检查是否完成根节点枚举,没有则等待,只有收到可以离开安全点的信号为止。

二、内存分配和回收策略

内存分配在上一章也讲了不少;从大方向来讲,对象主要分配在 堆 上,对象主要分配在新生代的 Eden 分区上,如果启动了本地线程缓冲,将按线程有线分配在CLAB上;少数情况下也会分配在老分区上。 首先先要理解,新生代和老年代都是一个内存空间,由参数配置,只是可以根据算法,决定对象是在新生代还是在老年代的内存区域!!!

举个简单例子:假如虚拟机中设置了新生代的内存大小为10M,老年代的也为10M,Eden 和 Survivor 为 8:1 的关系,那么Eden就只有8M,Survivor 为1M;下面创建4个对象

public static void test(){
	// 假设 _1MB 字符创为 1MB
	byte[] b1 = new byte[2*_1MB];
	byte[] b2 = new byte[2*_1MB];
	byte[] b3 = new byte[2*_1MB];
	byte[] b4 = new byte[4*_1MB];
}

当执行 test() ,当要分配 b4 对象时,会执行一次 Minor GC,原因是 Dden 才 6M,被b1,b2,b3填充之后,已经没有数据去填充 b4了,就会触发 GC,而b4没办法,只有移动到老年代的内存区域了。

2.1 大对象直接进入老年代

从上面可以看,假如一个 2MB 的数据,这个短命的大对象在新生去上朝生夕死,很容易触发 GC ,所以可以通过设置PretenureSize 设置 阈值,对象大于这个参数,直接进去老年代

2.2 长期存活的对象将进入老年代

一块内存可以分为3个区域,一个 Eden 和两个 Survivor 区,当对象在 Eden 创建,并经理了第一次 GC 之后仍然存活,并且能被 survivor 区容纳的话,将移到 survivor 区;对象在Survivor 区中“熬过”一次,年龄增加1,当增加到 15 岁(默认,这个阈值可以通过 -XX:MaxTenuringThreshold 设置),就会晋升成老年代的对象。 从这里来看,可以得到两个结论

  • 新生代:对象少,垃圾多
  • 老年代:对象多,垃圾少 (毕竟经历了10几次GC的老油条)

2.3 动态对象年龄判断

上面说到默认年龄为 15 才进入老年代,其实并不然,只要Survivor 中的对象总和大于Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。如下:

public static void test(){
	// 假设 _1MB 字符创为 1MB
	byte[] b1 = new byte[_1MB/4];
	byte[] b2 = new byte[_1MB/4];
	// b1 + b2 的总和大于 Survivor 的一半
	byte[] b3 = new byte[4*_1MB];
	byte[] b4 = new byte[4*_1MB];
}

当运行 test() ,发现 Survivor 依旧为0,b1和b2进入进入老年代,因为它们是同龄的。

2.4 空间分配担保

在发生GC前,虚拟机会先检查老年代的可用连续空间是否大于新生代的所有对象,如果成立,则发生 GC 是安全的。如果不成立,则会看是否允许担保失败,如果不允许,则进行 Full GC;如果允许,则会继续检查老年代的可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则进行 Minor GC,如果小于,则进行 Full GC ,但如果某次 Minor GC 后存活的对象大于平均值,会导致担保失败,失败之后,也会进行Full GC。

此致,Java 的GC 机制就分析完啦