Java垃圾回收机制详解
Java垃圾回收机制详解
什么是垃圾回收
垃圾回收(Garbage Collection,GC)是JVM自动管理内存的机制,负责回收不再使用的对象所占用的内存。
如何判断对象已死
引用计数法
每个对象维护一个引用计数器,当有引用指向它时计数器+1,引用失效时-1。
public class ReferenceCountingDemo {
public static void main(String[] args) {
Object obj = new Object(); // 引用计数 = 1
Object another = obj; // 引用计数 = 2
obj = null; // 引用计数 = 1
another = null; // 引用计数 = 0,可回收
}
}
缺点:无法解决循环引用问题。
可达性分析(JVM采用)
从GC Roots出发,遍历所有引用链,不可达的对象判定为垃圾。
public class ReachabilityDemo {
private static Object gcRoot; // 静态变量是GC Root
public static void main(String[] args) {
Object obj1 = new Object(); // 可达
Object obj2 = new Object(); // 可达
gcRoot = obj1; // obj1通过GC Root可达
obj2 = null; // obj2不可达,可回收
}
}
GC Roots包括:
- 虚拟机栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
垃圾回收算法
标记-清除(Mark-Sweep)
// 标记阶段:遍历所有对象,标记存活对象
// 清除阶段:遍历堆内存,回收未标记对象
// 缺点:产生内存碎片
复制算法(Copying)
// 将内存分为两块,每次只使用一块
// GC时将存活对象复制到另一块,然后清除当前块
// 新生代采用此算法(Eden:S0:S1 = 8:1:1)
标记-整理(Mark-Compact)
// 标记存活对象,然后将所有存活对象向一端移动
// 清除边界以外的内存
// 老年代采用此算法
分代收集
// 新生代:对象生命周期短,采用复制算法
// 老年代:对象生命周期长,采用标记-整理算法
public class GenerationalDemo {
public static void main(String[] args) {
// 短命对象,新生代回收
for (int i = 0; i < 100000; i++) {
String temp = "temp" + i; // 每次循环结束可回收
}
// 长命对象,进入老年代
static List<Object> longLived = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
longLived.add(new Object());
}
}
}
垃圾收集器
Serial收集器
单线程,简单高效,Client模式下默认。
# 使用Serial收集器
java -XX:+UseSerialGC MyApp
ParNew收集器
Serial的多线程版本,配合CMS使用。
# 使用ParNew收集器
java -XX:+UseParNewGC MyApp
Parallel Scavenge收集器
关注吞吐量,适合后台计算任务。
# 使用Parallel收集器
java -XX:+UseParallelGC MyApp
# 设置吞吐量
java -XX:GCTimeRatio=99 MyApp # GC时间不超过1%
CMS收集器(Concurrent Mark Sweep)
以最短停顿时间为目标。
# 使用CMS收集器
java -XX:+UseConcMarkSweepGC MyApp
# 设置并发线程数
java -XX:ParallelCMSThreads=4 MyApp
G1收集器(Garbage First)
JDK9+默认,兼顾吞吐量和停顿时间。
# 使用G1收集器
java -XX:+UseG1GC MyApp
# 设置最大停顿时间
java -XX:MaxGCPauseMillis=200 MyApp
# 设置堆大小
java -Xmx4g -XX:+UseG1GC MyApp
ZGC(JDK11+)
超低停顿时间(<10ms)。
# 使用ZGC
java -XX:+UseZGC MyApp
GC日志分析
# 开启GC日志(Java 9+)
java -Xlog:gc*:file=gc.log:time,uptime,level,tags MyApp
# 开启GC日志(Java 8)
java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log MyApp
GC日志示例:
[2024-01-15T10:30:45.123+0800][0.123s][GC (Allocation Failure) [PSYoungGen: 32768K->5120K(38400K)] 32768K->5120K(125952K), 0.0056789 secs]
内存分配策略
// 对象优先在Eden区分配
public void edenAllocation() {
// 小对象直接在Eden分配
Object obj = new Object();
}
// 大对象直接进入老年代
public void bigObjectAllocation() {
// 大于PretenureSizeThreshold的对象直接进老年代
byte[] bigArray = new byte[4 * 1024 * 1024]; // 4MB
}
// 长期存活对象进入老年代
// 对象每经历一次Minor GC年龄+1,默认15岁进入老年代
// -XX:MaxTenuringThreshold=15
常用GC调优参数
# 堆大小
-Xms512m # 初始堆大小
-Xmx1024m # 最大堆大小
-Xmn256m # 新生代大小
# 新生代比例
-XX:NewRatio=2 # 老年代:新生代 = 2:1
-XX:SurvivorRatio=8 # Eden:S0:S1 = 8:1:1
# GC相关
-XX:+PrintGCDetails # 打印GC详情
-XX:+PrintGCTimeStamps # 打印GC时间戳
总结
理解GC机制对于Java性能调优至关重要。选择合适的收集器和参数配置,可以在吞吐量和响应时间之间找到平衡点。