JVM垃圾收集器

Java虚拟机(JVM)的垃圾收集(GC)是自动内存管理的核心机制,理解不同的垃圾收集器及其工作原理对于编写高性能Java应用至关重要。


常见垃圾收集器

Serial收集器

特点

  • 单线程收集器

  • 新生代使用标记-复制算法

  • 老年代使用标记-整理算法

  • 适合客户端应用或小内存环境

启用参数

1
-XX:+UseSerialGC

代码示例:

1
2
3
4
5
6
7
8
9
10
11
public class SerialGCExample {
public static void main(String[] args) {
// 模拟产生大量临时对象
for (int i = 0; i < 1000000; i++) {
String temp = new String("Object-" + i);
if (i % 10000 == 0) {
System.gc(); // 建议JVM执行GC
}
}
}
}

Parallel/Throughput收集器

特点

  • 多线程收集器(并行收集)

  • 新生代使用标记-复制算法

  • 老年代使用标记-整理算法

  • 注重吞吐量(Throughput)

启用参数

1
2
-XX:+UseParallelGC
-XX:+UseParallelOldGC

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ParallelGCExample {
private static final List<byte[]> LIST = new ArrayList<>();

public static void main(String[] args) throws InterruptedException {
// 模拟内存分配和释放
while (true) {
byte[] data = new byte[1024 * 1024]; // 1MB
LIST.add(data);
if (LIST.size() > 100) {
LIST.clear();
}
Thread.sleep(10);
}
}
}

CMS(Concurrent Mark-Sweep)收集器

特点

  • 以获取最短回收停顿时间为目标

  • 老年代使用并发标记-清除算法

  • 与用户线程并发执行大部分工作

  • Java 14中被移除

工作阶段

  1. 初始标记(Initial Mark)

  2. 并发标记(Concurrent Mark)

  3. 重新标记(Remark)

  4. 并发清除(Concurrent Sweep)

启用参数

1
-XX:+UseConcMarkSweepGC

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CMSGCExample {
private static final Map<Integer, String> CACHE = new HashMap<>();

public static void main(String[] args) {
// 模拟缓存系统
for (int i = 0; i < 1000000; i++) {
CACHE.put(i, "Value-" + i);
if (i % 10000 == 0) {
System.out.println("Cache size: " + CACHE.size());
// 模拟缓存清理
CACHE.entrySet().removeIf(entry -> entry.getKey() < i - 50000);
}
}
}
}

G1(Garbage-First)收集器特点

  • Java 9+的默认收集器

  • 将堆划分为多个大小相等的Region

  • 可预测的停顿时间模型

  • 同时管理新生代和老年代

  • 采用标记-整理和复制算法

启用参数:

1
-XX:+UseG1GC

配置参数:

1
2
3
-XX:MaxGCPauseMillis=200 // 目标暂停时间(毫秒)
-XX:G1HeapRegionSize=N // Region大小(1-32MB)
-XX:InitiatingHeapOccupancyPercent=45 // 触发并发周期的堆占用百分比

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class G1GCExample {
private static final List<byte[]> BIG_OBJECTS = new ArrayList<>();
private static final List<byte[]> SMALL_OBJECTS = new ArrayList<>();

public static void main(String[] args) throws InterruptedException {
// 模拟混合大小对象分配
new Thread(() -> {
while (true) {
byte[] big = new byte[1024 * 1024 * 10]; // 10MB
BIG_OBJECTS.add(big);
if (BIG_OBJECTS.size() > 10) {
BIG_OBJECTS.clear();
}
}
}).start();

new Thread(() -> {
while (true) {
byte[] small = new byte[1024]; // 1KB
SMALL_OBJECTS.add(small);
if (SMALL_OBJECTS.size() > 10000) {
SMALL_OBJECTS.clear();
}
}
}).start();

Thread.sleep(Long.MAX_VALUE);
}
}

ZGC和Shenandoah

ZGC特点

  • 低延迟(目标<10ms)

  • 可扩展(支持TB级堆)

  • 并发处理(大部分工作与应用线程并发)

  • Java 15开始生产可用

启用参数

1
-XX:+UseZGC

Shenandoah特点:

1
2
3
低暂停时间
并发压缩
与ZGC类似但实现不同

启用参数

1
-XX:+UseShenandoahGC

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ZGCShenandoahExample {
private static final int SIZE = 1000000;
private static final Object[] HOLDER = new Object[SIZE];

public static void main(String[] args) throws InterruptedException {
// 模拟大规模对象分配和更新
for (int i = 0; i < SIZE; i++) {
HOLDER[i] = new byte[1024]; // 1KB对象
if (i % 1000 == 0) {
Thread.sleep(1); // 稍微减慢速度
}
}

// 随机更新对象
Random random = new Random();
while (true) {
int index = random.nextInt(SIZE);
HOLDER[index] = new byte[1024];
}
}
}

如何选择合适的垃圾收集器

  1. 吞吐量优先:Parallel GC

  2. 低延迟优先:G1 GC(堆<6GB)、ZGC/Shenandoah(堆>6GB)

  3. 小内存/客户端应用:Serial GC

  4. CMS:已过时,不推荐使用


监控和分析GC

启用GC日志

1
2
3
4
-XX:+PrintGCDetails 
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:<file-path>

Java 9+更推荐使用

1
-Xlog:gc*:file=<file-path>:time:filecount=5,filesize=10M

使用JVM工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class GCMonitoringExample {
public static void main(String[] args) {
// 打印内存信息
System.out.println("Max Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
System.out.println("Total Memory: " + Runtime.getRuntime().totalMemory() / 1024 / 1024 + "MB");
System.out.println("Free Memory: " + Runtime.getRuntime().freeMemory() / 1024 / 1024 + "MB");

// 模拟内存分配
List<byte[]> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
byte[] data = new byte[1024 * 1024]; // 1MB
list.add(data);

// 打印内存变化
if (i % 10 == 0) {
System.out.println("After allocation " + (i + 1) + "MB");
System.out.println("Free Memory: " + Runtime.getRuntime().freeMemory() / 1024 / 1024 + "MB");
}
}
}
}

GC调优建议

  1. 避免频繁Full GC

    • 合理设置新生代大小(-Xmn)

    • 避免大对象直接进入老年代

  2. 合理设置堆大小

    • 初始堆(-Xms)和最大堆(-Xmx)设为相同值

    • 避免堆自动扩展带来的性能波动

  3. 注意对象分配模式

    • 避免过早晋升(PretenureSizeThreshold)

    • 注意大对象分配

  4. 选择合适的GC算法

    • 根据应用特点(吞吐量vs延迟)选择
  5. 监控和观察

  • 使用VisualVM、JConsole、GC日志等工具持续监控