目录
一、对象引用
二、堆区和方法区回收
1. 堆区回收
2. 方法区回收
三、垃圾回收算法
1. 算法总结
2. 算法相关细节
四、垃圾收集器
1. 新生代收集器
2. 老年代收集器
3. 混合式收集器G1
4. 低延迟收集器
五、参考资料
一、对象引用
判定对象是否存活和引用离不开关系。JDK 1.2之后,有四种引用:强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference),其引用强度依次减弱(强软弱虚)。
若下代码所示,是四种引用的使用方式。
package com.cmmon.instance.gc;
import com.alibaba.fastjson.JSON;
import org.junit.jupiter.api.Test;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
/**
* @author tcm
* @version 1.0.0
* @description JDK1.2的4种引用
* @date 2023/5/24 17:25
**/
public class ReferenceTest {
/**
* 强引用:new的对象,只要有引用链,则GC永远不会回收
*/
@Test
public void testStronglyReference() {
// new创建对象,是强引用
Map<String, String> map = new HashMap<>();
map.put("name", "stronglyReference");
System.out.println(JSON.toJSONString(map.keySet()));
}
/**
* 软引用:内存不足时,GC会回收SoftReference所引用的对象
* 适用场景:缓存
*/
@Test
public void testSoftReference() throws InterruptedException {
// 软引用
SoftReference<Map<String, String>> softReference = new SoftReference<Map<String, String>>(new HashMap<>());
Map<String, String> map = softReference.get();
if (map == null) {
// 下面代码,这样做,仍然可能为null(概率很小)
softReference = new SoftReference<Map<String, String>>(new HashMap<>());
}
map = softReference.get();
map.put("name", "test");
System.out.println(JSON.toJSONString(map.keySet()));
}
/**
* 弱引用:内存是否足够都要回收,则只能生存到下一次GC回收WeakReference所引用的对象
* 适用场景:Debug、内存监视工具等程序中(一般要求即要观察到对象,又不能影响该对象正常的GC过程)
*/
@Test
public void testWeakReference(){
WeakReference<Map<String, String>> weakReference = new WeakReference<Map<String, String>>(new HashMap<>());
Map<String, String> map = weakReference.get();
if (map == null) {
// 下面代码,这样做,仍然可能为null(概率很小)
weakReference = new WeakReference<Map<String, String>>(new HashMap<>());
}
map = weakReference.get();
map.put("name", "test");
System.out.println(JSON.toJSONString(map.keySet()));
}
/**
* 虚引用:无法通过虚引用来获取一个对象实例,目的回收时通知系统
* 以下代码报错:java.lang.NullPointerException
*/
@Test
public void testPhantomReference(){
PhantomReference<Map<String, String>> phantomReference = new PhantomReference<Map<String, String>>(new HashMap<>(), new ReferenceQueue());
Map<String, String> map = phantomReference.get();
if (map == null) {
phantomReference = new PhantomReference<Map<String, String>>(new HashMap<>(), new ReferenceQueue());
}
map = phantomReference.get();
map.put("name", "test"); // 报空指针异常
System.out.println(JSON.toJSONString(map.keySet()));
}
}
二、堆区和方法区回收
1. 堆区回收
对象“死去”,说明没有任何引用指向该对象,即:对象没有被任何引用使用。那么,如何判定一个对象是否存活呢?对象存活算法有两种:引用计数算法、可达性分析算法(目前使用),如下图所示。
目前HotSpot虚拟机采用可达性分析算法来判定对象是否存活,某对象没有任何引用链(Reference Chain) ,则对象“死去”,如下图所示。
根据上述,哪些内容可作为GC Roots?如下图所示。
同时,准确且完整的GC Roots至关重要。目前所有收集器在进行枚举GC Roots对象时,都需要暂停用户线程来获取整个堆区原始快照(SATB:Snapshot At The Beginning),以达到GC Roots的准确性;不同的收集器,存在跨代和跨区引用问题(记忆集解决),以达到完整。详细见收集器章节。
一个对象最多经历两次标记,如下图所示。其中,finalize()方法在Object中,只能被调用一次,第二次调用时则失效,所以:拯救对象再次能活,可以在finalize()方法再次引用(不推荐使用),测试代码如下。
package com.cmmon.instance.gc;
/**
* @author tcm
* @version 1.0.0
* @description TODO
* @date 2023/4/19 9:53
**/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
private static final int _1MB = 1024 * 1024;
private byte[] space1 = new byte[4 * _1MB];
public void isAlive() {
System.out.println("是的,我还活着!");
}
/**
* 系统只能调用一次,推荐不使用该方法
* @throws Throwable
*/
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("进入finalize()方法");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC();
// 第一次自救,则成功
SAVE_HOOK = null;
/*
注意:若使用-XX:+DisableExplicitGC禁止人工触发GC,
则不会触发Full GC,更不会进入finalize()方法,SAVE_HOOK = null时,则已经死了
*/
System.gc();
Thread.sleep(1000 * 6);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("我已经死了!");
}
// 第二次自救,则失败
// SAVE_HOOK = null;
// System.gc();
// Thread.sleep(5000);
// if (SAVE_HOOK != null) {
// SAVE_HOOK.isAlive();
// } else {
// System.out.println("2我已经死了!");
// }
}
/*
执行结果:
[GC (System.gc()) [PSYoungGen: 4592K->1173K(37888K)] 4592K->1181K(123904K), 0.0283448 secs] [Times: user=0.00 sys=0.00, real=0.04 secs]
[Full GC (System.gc()) [PSYoungGen: 1173K->0K(37888K)] [ParOldGen: 8K->1042K(86016K)] 1181K->1042K(123904K), [Metaspace: 3060K->3060K(1056768K)], 0.0072510 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
进入finalize()方法
是的,我还活着!
[GC (System.gc()) [PSYoungGen: 1310K->64K(37888K)] 2353K->1106K(123904K), 0.0005835 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 64K->0K(37888K)] [ParOldGen: 1042K->1039K(86016K)] 1106K->1039K(123904K), [Metaspace: 3061K->3061K(1056768K)], 0.0057221 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
2我已经死了!
Heap
PSYoungGen total 37888K, used 1638K [0x00000000d6380000, 0x00000000d8d80000, 0x0000000100000000)
eden space 32768K, 5% used [0x00000000d6380000,0x00000000d6519b40,0x00000000d8380000)
from space 5120K, 0% used [0x00000000d8880000,0x00000000d8880000,0x00000000d8d80000)
to space 5120K, 0% used [0x00000000d8380000,0x00000000d8380000,0x00000000d8880000)
ParOldGen total 86016K, used 1039K [0x0000000082a00000, 0x0000000087e00000, 0x00000000d6380000)
object space 86016K, 1% used [0x0000000082a00000,0x0000000082b03fe0,0x0000000087e00000)
Metaspace used 3068K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 324K, capacity 386K, committed 512K, reserved 1048576K
*/
}
2. 方法区回收
方法区回收主要是废弃的常量、不再使用的类型。需要主要的是:JDK7版本方法区内的常量池、静态变量移至堆中;JDK8使用元空间(Metaspace)代替永久代,主要存储类信息。
三、垃圾回收算法
1. 算法总结
如下表所示,垃圾回收算法总结。
算法 | 特点 |
标记-清除 (Mark-Sweep) | 1.标记存活的对象,统一回收所有未被标记的对象; 2.最基础的收集算法; 3.缺点:内存碎片化,大对象分配时没有足够连续空间,则提前触发GC; 对象数量增加时,标记和清除两个过程执行效率低 4.适用:老年代; 5.采用该算法收集器:CMS。 |
标记-复制 (Mark-Copy) | 1.“半区复制”:内存按大小相等分两块 (使用一块;存活对象复制到另一块); 2.“Appel式回收”:新生代分为一个Eden区、两个Survivor区 (每次可用一个Eden区、一个Survivor区;存活对象复制到另一个Survivor区) 3.缺点:“半区复制”内存浪费大,缩小一半; “Appel式回收”,默认90%可用(Eden:Survivor=8:1) 4.适用:新生代; 5.采用该算法收集器:Serial、ParNew。 |
标记-整理 (Mark-Compact) | 1.标记后不直接清理,而是所有存活对象向内存空间一端移动(整理)后,再清除; 2.整理时,需要移动对象,则必须暂停用户线程才能进行 (注意:最新的ZGC、Shenandoah收集器使用读屏障解决该问题); 3.缺点:移动对象时,需暂停用户线程(Stop the World);效率低; 4.适用:老年代; 5.采用该算法收集器:Parallel Old。 |
需要注意的是,分代收集是上面三种算法的混合使用。分代收集存在跨代引用问题,用记忆集(Remembered Set)解决。如下图所示,是分代收集假说及注意问题。
2. 算法相关细节
算法的实现细节,如:GC Root枚举、安全点、安全区域、记忆集、写屏障、三色标记等,如下如图所示。
其中,从GC Roots扫描整个堆的对象图时,按照“是否访问过” 用三种颜色标记。标记整个堆的对象是很耗时,用户线程停顿时,导致用户体验差。那么,与用户线程并发来进行可达性分析时会出现问题,如下图所示。
如下图所示,并发时的解决方案。
四、垃圾收集器
下图所示是HotSpot虚拟机的垃圾收集器,图中连线说明可搭配使用。其中JDK9废弃Serial + CMS、ParNew + Serial Old。
垃圾收集器三大指标:内存占用(Footprint)、吞吐量(ThroughPut)、延迟(Latency),一款优秀的收集器最多同时达成两项。而理想中的收集器其收集速率与内存分配速率相当,而不是整堆清除。
虚拟机吞吐量(ThroughPut) = CPU运行用户代码时间 / ( CPU运行用户代码时间 + CPU运行垃圾回收时间 )。停顿时间短适合用户交互或保证服务响应的程序,提升交互体验;吞吐量高适合运算量大的服务,提高利用资源效率。
各类收集器并发情况,如下图所示。Shenandoah、ZGC两款只有初始标记、最终标记这部分是固定的,与堆容量、堆的对象数量没有关系。
1. 新生代收集器
新生代收集器 | 特点 |
Serial | 1.JDK1.3之前新生代收集器; 2.单线程收集,整个GC过程必须暂停用户线程; 3.算法:标记-复制; 4.缺点:整个GC过程必须暂停用户线程。 |
ParNew | 1.JDK5新生代收集器; 2.多线程收集,整个GC过程必须暂停用户线程; 3.算法:标记-复制; 4.缺点:整个GC过程必须暂停用户线程; 5.与Serial比较:多线程,其他都一样; 6.只与CMS老年代收集器联合使用。 |
Parallel Scavenge | 1.JDK7新生代收集器; 2.多线程收集,整个GC过程必须暂停用户线程; 3.算法:标记-复制; 4.缺点:整个GC过程必须暂停用户线程; 5.Parallel Scavenge关注点是达到可控的吞吐量; CMS关注点尽可能缩短停顿时间。 |
2. 老年代收集器
3. 混合式收集器G1
4. 低延迟收集器
低延迟收集:Shenandoah(OpenJDK支持)、ZGC(OracleJDK12支持),这两款收集器抛弃了分代收集、记忆集等概念。
五、参考资料
JVM垃圾收集器(很全很全) - 简书
关于垃圾收集算法与垃圾收集器ParNew与CMS_parnew收集器_秋天的一亩三分地的博客-CSDN博客
详解ZGC垃圾收集器-CSDN博客
69.G1垃圾回收的详细过程 -了解_simpleGq的博客-CSDN博客
G1垃圾回收器_赵军林的博客-CSDN博客
垃圾收集算法——分代收集算法(Generational Collection)。_分代收集算法根据各个年代的特点采用最适当的收集算法_孤芳不自賞的博客-CSDN博客https://www.cnblogs.com/yufengzhang/p/10571081.html
为对象分配内存——TLAB_usetlab_chengqiuming的博客-CSDN博客
JVM调优常用参数_printreferencegc_point-break的博客-CSDN博客