GC
- 一、判定对象是否是垃圾
- 1.引用计数法
- 2.可达性分析算法
- 二、垃圾回收算法
- 1.标记清除
- 2.标记整理
- 3. 复制
- 4. 分代垃圾回收
- 1.尝试在伊甸园分配
- 2.大对象直接晋升至老年代
- 3.多次存活的对象
- 4.老年代连续空间不足,触发 Full GC
链接: jvm学习笔记(一) ----- JAVA 内存
链接: jvm学习笔记(三) ----- 垃圾回收器
一、判定对象是否是垃圾
1.引用计数法
- 有一个地方引用对象,计数加一,当计数为零表示可以回收
- 缺点是难以解决对象之间的循环引用问题
2.可达性分析算法
- java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。它从一系列 GC Roots 出发,边标记边探索所有被引用的对象
- 从 GC Root对象 为起点,看是否能沿着引用链找到该对象,找不到,表示可以回收
- GC Root对象 包括栈帧中的局部变量、方法区中的静态变量、方法区中的常量、本地方法栈中JNI引用的对象
- 为了防止在标记过程中堆栈的状态发生改变,Java 虚拟机采取安全点机制来实现 Stop-the-world (应用程序的线程全部停止) 操作,暂停其他非垃圾回收线程
- 当然,安全点的初始目的并不是让其他线程停下,而是找到一个稳定的执行状态。在这个执行状态下,Java 虚拟机的堆栈不会发生变化。这么一来,垃圾回收器便能够“安全”地执行可达性分析
二、垃圾回收算法
1.标记清除
第一遍标记、第二遍收集。缺点是会产生内存碎片,碎片过多,仍会使得连续空间少
2.标记整理
第一遍标记、第二遍整理,整理是指存活对象向一端移动来减少内存碎片,相对效率较低
3. 复制
开辟两份大小相等空间,一份空间始终空着,垃圾回收时,将存活对象拷贝进入空闲空间,优点是不会有内存碎片,但占用空间多。
4. 分代垃圾回收
- 大部分的 Java 对象只存活一小段时间,而存活下来的小部分 Java 对象则会存活很长一段时间。
- 根据对象的特点分代(分区域)来进行,分为新生代和老年代,新生代对象一般很少存活,采用『复制算法』、老年代对象生存时间长,适合采用『标记-清除算法』或『标记-整理算法』
- 堆内存分为『新生代』和『老年代』,『新生代』又分为『伊甸园』和两个『幸存区』。新生代内存不足触发的 GC 称为 Minor GC ,暂停时间很短,老年代内存不足触发的 GC 称为 Full GC 暂停时间较长,一般是新生代 GC 的几十倍,它们使用的垃圾回收算法不同,见之前的介绍。
1.尝试在伊甸园分配
对象优先在『伊甸园』分配,当『伊甸园』没有足够的空间时,触发 Minor GC ,将『伊甸园』和『幸存区 From』中仍然存活的对象利用 复制算法 移入『幸存区 To』,然后交换『幸存区 From』和『幸存区 To』的位置。
默认情况下,Java 虚拟机采取的是一种动态分配的策略(对应 Java 虚拟机参数 -XX:+UsePSAdaptiveSurvivorSizePolicy),根据生成对象的速率,以及 Survivor区的使用情况动态调整 Eden 区和 Survivor 区的比例。当然,你也可以通过参数 -XX:SurvivorRatio 来固定这个比例。但是需要注意的是,其中一个 Survivor 区会一直为空,因此比例越低浪费的堆空间将越高。
情况1:伊甸园空间还够,新对象在伊甸园能够存储的下,这时候不会发生GC。图中白色区域是空闲空间、蓝色矩形表示已创建对象。
情况2:伊甸园空间不够了。
标记可回收的对象,图中用黄色表示,这时候会用户线程会被暂停(Stop The World)。
触发新生代的垃圾回收,称为 Minor GC ,幸存对象移入『幸存区 To』,注意这里用的是复制算法,因此在幸存区没有碎片。
最后的结果,注意 GC 完成后,From 和 To 交换了位置,另外幸存区的对象开始记录寿命。
2.大对象直接晋升至老年代
当对象太大,伊甸园包括幸存区都存放不下时,这时候老年代的连续空间足够,此对象会直接晋升至老年代,不会发生 GC
结果:
测试:
- 预先定义一组大小
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _6MB = 6 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
在运行时添加如下 JVM 参数:
-XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
参数含义 :
-XX:+UseSerialGC 是指使用 Serial + SerialOld 回收器组合
-XX:+PrintGCDetails -verbose:gc 是指打印 GC 详细信息
-Xms20M -Xmx20M -Xmn10M 是指分配给 JVM 的最小,最大以及新生代内存
-XX:SurvivorRatio=8 是指『伊甸园』与『幸存区 From』和『幸存区 To』比例为 8:1:1
- 最开始,没什么对象:
public static void main(String[] args) {
}
测试结果:
- 当代码改为
public static void main(String[] args) {
byte[] obj1 = new byte[_7MB];
}
可以预料到,因为「eden space 8192K, 30% used」已经放不下 7MB 的对象,必然会触发新生代的 GC:
- 可以看到,结果是一部分旧的对象进入了幸存区「from space 1024K, 73% used」,而伊甸园里放入了 7MB 的对象「eden space 8192K, 88% used」再放入一个 512KB 的对象
public static void main(String[] args) {
byte[] obj1 = new byte[_7MB];
byte[] obj2 = new byte[_512KB];
}
可以看到,伊甸园几乎被放满了「eden space 8192K, 98% used」,但毕竟没有满,所以没有触发第二次 GC继续放入一个 512KB 的对象.
- 果然触发了第二次 GC,其中一个 512KB 的对象进入了幸存区「from space 1024K, 52% used」而那个 7MB 的对象晋升至了老年代「tenured generation total 10240K, used 7848K」
public static void main(String[] args) {
byte[] obj1 = new byte[_7MB];
byte[] obj2 = new byte[_512KB];
byte[] obj3 = new byte[_512KB];
}
3.多次存活的对象
在幸存区历经多次 GC 还存活的对象会晋升至老年代,默认晋升的阈值是 15,也就是说只要经历 15 次回收不死,肯定晋升,但注意如果目标 survivor 空间紧张,也不必等足 15 次,可以提前晋升
-XX:MaxTenuringThreshold=threshold
Sets the maximum tenuring threshold for use in adaptive GC sizing. The largest value is 15. The default value is 15 for the parallel (throughput) collector, and 6 for the CMS collector.
-XX:TargetSurvivorRatio=percent
Sets the desired percentage of survivor space (0 to 100) used after young garbage collection. By default, this option is set to 50%.
4.老年代连续空间不足,触发 Full GC
public static void main(String[] args) {
byte[] obj1 = new byte[_8MB];
byte[] obj2 = new byte[_8MB];
}
第一个 8MB 直接进入老年代,第二个 8MB 对象在分配时发现老年代空间不足,只好尝试先进行一次 Minor GC ,结果发现新生代没有连续空间,只好触发一次 Full GC ,最后发现老年代也没有连续空间,这时出现 OutOfMemoryError
果把代码改为下面的样子,则只会触发 Minor GC ,之后,老年代能够容纳 obj2,所以不会触发 Full GC
public static void main(String[] args) {
byte[] obj1 = new byte[_8MB];
obj1 = null;;
byte[] obj2 = new byte[_8MB];
}