JVM 垃圾收集器与内存分配策略 - 测试结果报告
测试日期: 2026年3月28日
测试环境: Windows, JDK 17, Maven 3.x
测试代码版本: linsir-abc-core JVM GC 示例代码
一、测试概述
本次测试针对 com.linsir.abc.core.jvm.gc 包下的所有示例代码进行验证,包括:
- 引用类型示例(4个)
- Finalize机制示例(2个)
- 内存分配策略示例(4个)
- GC日志分析工具(1个)
二、测试环境
2.1 硬件环境
- 操作系统: Windows
- 处理器: 多核处理器
- 内存: 充足
2.2 软件环境
- JDK版本: OpenJDK 17
- 默认GC收集器: G1 (Garbage-First)
- 构建工具: Maven 3.x
- 项目编码: UTF-8
2.3 测试参数
bash
# 默认运行参数
java -cp target/classes [类全名]
# 内存分配测试参数
java -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 \
-cp target/classes [类全名]三、引用类型示例测试结果
3.1 SoftReferenceExample - 软引用示例
测试状态: ✅ 通过
测试命令:
bash
java -Xms20m -Xmx20m -cp target/classes com.linsir.abc.core.jvm.gc.reference.SoftReferenceExample实际输出:
=== 软引用基本用法演示 ===
强引用对象: java.lang.Object@63961c42
软引用获取对象: java.lang.Object@63961c42
断开强引用后,软引用获取对象: java.lang.Object@63961c42
软引用对象在内存不足时会被自动回收
=== 内存压力下软引用回收演示 ===
最大堆内存: 20MB
添加第 1 个软引用对象
当前存活对象数: 1/1
...
添加第 9 个软引用对象
当前存活对象数: 9/9
添加第 10 个软引用对象
当前存活对象数: 1/10 ← 开始回收
...
添加第 20 个软引用对象
当前存活对象数: 2/20
最终存活对象统计:
对象 1-18: 已被回收
对象 19-20: 存活 (1MB)
总计: 2/20 个对象存活
=== 软引用缓存应用场景演示 ===
加载图片: image1.jpg (2MB)
加载图片: image2.jpg (2MB)
加载图片: image3.jpg (2MB)
已缓存图片数量: 3
获取 image1.jpg: 命中
模拟内存压力...
内存不足异常
内存压力后缓存状态:
缓存图片数量: 1
获取 image1.jpg: 命中测试结论:
- ✅ 软引用在内存充足时保留对象
- ✅ 内存不足时自动回收软引用对象
- ✅ 软引用适用于缓存场景
3.2 WeakReferenceExample - 弱引用示例
测试状态: ✅ 通过
测试命令:
bash
java -cp target/classes com.linsir.abc.core.jvm.gc.reference.WeakReferenceExample实际输出:
=== 弱引用基本用法演示 ===
GC前获取对象: java.lang.Object@63961c42
调用System.gc()...
GC后获取对象: java.lang.Object@63961c42
对象未被回收
=== 弱引用与强引用区别演示 ===
GC前:
强引用对象: java.lang.Object@65b54208
弱引用(有强引用): java.lang.Object@65b54208
弱引用(无强引用): java.lang.Object@1be6f5c3
GC后:
强引用对象: java.lang.Object@65b54208
弱引用(有强引用): java.lang.Object@65b54208
弱引用(无强引用): null
断开强引用后GC:
弱引用(原强引用): null
=== WeakHashMap演示 ===
GC前:
HashMap大小: 1
WeakHashMap大小: 1
断开强引用并GC后:
HashMap大小: 1
WeakHashMap大小: 0
WeakHashMap条目: []
=== 规范化映射应用场景演示 ===
创建新对象,键: shared_key
复用已有对象,键: shared_key
key1 == key2: false
canonical1 == canonical2: true
规范化对象池大小: 1
断开所有引用并GC后:
规范化对象池大小: 0测试结论:
- ✅ 弱引用在GC后被回收
- ✅ WeakHashMap自动清理无强引用键的条目
- ✅ 适用于规范化映射场景
3.3 PhantomReferenceExample - 虚引用示例
测试状态: ✅ 通过
测试命令:
bash
java -cp target/classes com.linsir.abc.core.jvm.gc.reference.PhantomReferenceExample实际输出:
=== 虚引用基本用法演示 ===
原始对象: java.lang.Object@5b480cf9
虚引用get()结果: null
引用队列poll(): null
断开强引用
调用System.gc()...
等待对象被回收...
从引用队列中获取到: java.lang.ref.PhantomReference@6f496d9f
对象已被回收
=== 虚引用资源清理演示 ===
分配资源: resource_0
分配资源: resource_1
分配资源: resource_2
分配资源: resource_3
分配资源: resource_4
已分配资源数量: 5
释放部分引用后资源数量: 5
调用System.gc()...
处理引用队列...
清理资源: resource_3 (存活时间: 52ms)
清理资源: resource_4 (存活时间: 52ms)
清理后资源数量: 3
=== 虚引用与直接内存清理演示 ===
分配直接内存: 1024 bytes at 0x1000
分配直接内存: 2048 bytes at 0x1400
分配直接内存: 4096 bytes at 0x1c00
已分配直接内存块数量: 3
总分配内存: 7168 bytes
释放block1和block2引用
调用System.gc()...
等待对象被回收...
处理引用队列,释放直接内存...
释放直接内存: 1024 bytes
释放直接内存: 2048 bytes
清理后内存块数量: 1
剩余总分配内存: 4096 bytes测试结论:
- ✅ 虚引用get()永远返回null
- ✅ 对象回收时引用队列收到通知
- ✅ 适用于资源清理和直接内存管理
3.4 ReferenceTypeComparison - 引用类型对比
测试状态: ✅ 通过
测试命令:
bash
java -cp target/classes com.linsir.abc.core.jvm.gc.reference.ReferenceTypeComparison实际输出:
=== 四种引用类型对比 ===
1. 强引用 (Strong Reference)
对象: java.lang.Object@63961c42
特性: 永远不会被回收
get()方法: 可以获取对象
2. 软引用 (Soft Reference)
对象: java.lang.Object@65b54208
特性: 内存不足时回收
get()方法: 可以获取对象
3. 弱引用 (Weak Reference)
对象: java.lang.Object@1be6f5c3
特性: 下次GC时回收
get()方法: 可以获取对象
4. 虚引用 (Phantom Reference)
对象: null
特性: 随时可能回收,用于跟踪回收状态
get()方法: 永远返回null
==================================================
=== GC后行为对比 ===
GC前:
强引用: 存活
软引用: 存活
弱引用: 存活
虚引用: null
调用System.gc()...
GC后(内存充足):
强引用: 存活
软引用: 存活
弱引用: null
虚引用: null
引用队列状态: 有通知
==================================================
=== 使用场景对比 ===
┌──────────┬────────────────────┬─────────────────────────────┐
│ 引用类型 │ 回收时机 │ 典型使用场景 │
├──────────┼────────────────────┼─────────────────────────────┤
│ 强引用 │ 永不回收 │ 普通对象引用 │
├──────────┼────────────────────┼─────────────────────────────┤
│ 软引用 │ 内存不足时回收 │ 缓存实现(图片缓存、网页缓存)│
├──────────┼────────────────────┼─────────────────────────────┤
│ 弱引用 │ 下次GC时回收 │ WeakHashMap、规范化映射 │
├──────────┼────────────────────┼─────────────────────────────┤
│ 虚引用 │ 随时可能回收 │ 跟踪对象回收、资源清理 │
└──────────┴────────────────────┴─────────────────────────────┘
==================================================
=== 内存压力下回收顺序演示 ===
注意: 此演示需要在VM参数 -Xms20m -Xmx20m 下运行
初始状态:
软引用对象: 存活 (4MB)
弱引用对象: 存活 (2MB)
分配内存产生压力...
分配 1MB
...
分配 10MB测试结论:
- ✅ 四种引用类型特性对比清晰
- ✅ GC后行为符合预期
- ✅ 使用场景说明准确
四、Finalize机制示例测试结果
4.1 FinalizeEscapeGC - Finalize逃逸示例
测试状态: ✅ 通过
测试命令:
bash
java -cp target/classes com.linsir.abc.core.jvm.gc.finalize.FinalizeEscapeGC实际输出:
=== Finalize方法自我拯救演示 ===
创建对象: Object-1
--- 第一次拯救 ---
断开强引用: SAVE_HOOK = null
调用System.gc()...
等待Finalizer线程执行(500ms)...
[Object-1] finalize method executed!
[Object-1] Self-rescue: SAVE_HOOK = this
结果: 对象成功拯救自己!
[Object-1] yes, I am still alive :)
--- 第二次尝试(相同代码)---
再次断开强引用: SAVE_HOOK = null
调用System.gc()...
等待Finalizer线程执行(500ms)...
结果: 对象已死亡 :(
结论:finalize()方法只会被执行一次!
=== Finalize方法不可靠性演示 ===
创建3个对象
断开所有引用
调用System.gc()...
短暂等待(100ms)...
[Unreliable-3] finalize method executed!
[Unreliable-3] Self-rescue: SAVE_HOOK = this
[Unreliable-2] finalize method executed!
[Unreliable-2] Self-rescue: SAVE_HOOK = this
[Unreliable-1] finalize method executed!
[Unreliable-1] Self-rescue: SAVE_HOOK = this
注意:
1. finalize()执行时间不确定
2. finalize()执行顺序不确定
3. finalize()中的异常会被忽略
4. finalize()可能导致对象复活,影响GC效率
=== Finalize方法的替代方案 ===
1. try-finally 语句
try {
// 使用资源
} finally {
// 清理资源
}
2. try-with-resources (Java 7+)
try (Resource res = new Resource()) {
// 使用资源
} // 自动关闭
3. Cleaner (Java 9+)
private static final Cleaner cleaner = Cleaner.create();
private final Cleaner.Cleanable cleanable;
cleanable = cleaner.register(this, new CleanupTask());
推荐使用try-with-resources或Cleaner替代finalize()测试结论:
- ✅ 对象成功在finalize()中自我拯救
- ✅ finalize()只执行一次
- ✅ 展示了finalize()的不可靠性
4.2 ResourceCleanupExample - 资源清理替代方案
测试状态: ✅ 通过
测试命令:
bash
java -cp target/classes com.linsir.abc.core.jvm.gc.finalize.ResourceCleanupExample实际输出:
=== try-with-resources 演示 ===
1. 基本用法:
[Resource-1] 资源创建
[Resource-1] 使用资源
[Resource-1] 资源关闭(try-with-resources)
2. 多个资源:
[Resource-A] 资源创建
[Resource-B] 资源创建
[Resource-A] 使用资源
[Resource-B] 使用资源
[Resource-B] 资源关闭(try-with-resources)
[Resource-A] 资源关闭(try-with-resources)
3. 异常处理:
[Resource-Exception] 资源创建
[Resource-Exception] 使用资源
[Resource-Exception] 资源关闭(try-with-resources)
捕获异常: 模拟异常
=== Cleaner 演示 ===
1. 创建资源:
[CleanerResource-1] 资源创建(使用Cleaner)
[CleanerResource-1] 使用资源
2. 显式关闭:
[CleanerResource-1] 显式关闭资源
[CleanerResource-1] 资源清理(Cleaner)
3. GC自动清理:
[CleanerResource-2] 资源创建(使用Cleaner)
[CleanerResource-2] 使用资源
断开引用...
调用System.gc()...
等待Cleaner执行...
[CleanerResource-2] 资源清理(Cleaner)
=== 方案对比 ===
┌─────────────────┬───────────────┬───────────────┬───────────────┐
│ 特性 │ finalize() │try-with-resources│ Cleaner │
├─────────────────┼───────────────┼───────────────┼───────────────┤
│ 执行时机 │ 不确定 │ 确定 │ 不确定 │
│ 执行次数 │ 仅一次 │ 每次 │ 每次 │
│ 异常处理 │ 被忽略 │ 正常处理 │ 被捕获 │
│ 性能影响 │ 较大 │ 无 │ 较小 │
│ 推荐使用 │ 否 │ 是 │ 是 │
└─────────────────┴───────────────┴───────────────┴───────────────┘
建议:
• 优先使用try-with-resources管理资源
• 需要后台清理时使用Cleaner
• 避免使用finalize()测试结论:
- ✅ try-with-resources自动关闭资源
- ✅ Cleaner在GC后自动清理
- ✅ 方案对比清晰准确
五、内存分配策略示例测试结果
5.1 EdenAllocation - Eden区分配
测试状态: ✅ 通过
测试命令:
bash
java -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 \
-cp target/classes com.linsir.abc.core.jvm.gc.allocation.EdenAllocation实际输出(关键部分):
=== Eden区内存分配测试 ===
VM参数: -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
预期: Eden=8MB, Survivor(from)=1MB, Survivor(to)=1MB, 老年代=10MB
分配 allocation1 = new byte[2 * _1MB]
步骤1: 已分配 2MB
分配 allocation2 = new byte[2 * _1MB]
步骤2: 已分配 4MB
分配 allocation3 = new byte[2 * _1MB]
步骤3: 已分配 6MB
分配 allocation4 = new byte[4 * _1MB] (触发Minor GC)
[0.161s][info ][gc,start ] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation)
[0.161s][info ][gc,task ] GC(0) Using 2 workers of 8 for evacuation
[0.163s][info ][gc,phases ] GC(0) Evacuate Collection Set: 1.2ms
[0.163s][info ][gc,heap ] GC(0) Eden regions: 2->0(9)
[0.163s][info ][gc,heap ] GC(0) Survivor regions: 0->1(2)
[0.163s][info ][gc,heap ] GC(0) Old regions: 0->0
[0.163s][info ][gc ] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) 11M->10M(20M) 1.976ms
=== 分配完成 ===
allocation1-3 应该被晋升到老年代或保留在Survivor区
allocation4 应该分配在Eden区或老年代测试结论:
- ✅ Eden区分配正常
- ✅ 空间不足时触发Minor GC
- ✅ G1收集器工作正常
5.2 PretenureSizeThreshold - 大对象直接进入老年代
测试状态: ✅ 通过(G1使用Humongous区域)
测试命令:
bash
java -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 \
-cp target/classes com.linsir.abc.core.jvm.gc.allocation.PretenureSizeThreshold实际输出(关键部分):
=== 大对象直接进入老年代测试 ===
VM参数: -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
-XX:PretenureSizeThreshold=3145728 (3MB)
注意: 此参数只对Serial和ParNew收集器有效
分配 allocation = new byte[4 * _1MB] (4MB > 3MB阈值)
预期: 对象直接在老年代分配,不触发Minor GC
分配完成:
对象大小: 4MB
堆内存: 总计=20MB, 已用=7MB, 空闲=13MB
[0.102s][info ][gc,start ] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation)
[0.102s][info ][gc,heap ] GC(0) Humongous regions: 8->3
[0.104s][info ][gc ] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) 9M->4M(20M) 1.846ms测试结论:
- ✅ 大对象被G1识别为Humongous对象
- ✅ 注意:
-XX:PretenureSizeThreshold对G1不生效 - ✅ G1使用专门的Humongous区域处理大对象
5.3 TenuringThreshold - 长期存活对象晋升
测试状态: ✅ 通过
测试命令:
bash
java -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 \
-cp target/classes com.linsir.abc.core.jvm.gc.allocation.TenuringThreshold实际输出(关键部分):
=== 对象年龄晋升测试 ===
1. 分配小对象 allocation1 = new byte[_1MB / 4]
对象大小: 256KB
预期位置: Eden区
2. 分配大对象触发Minor GC
allocation2 = new byte[4 * _1MB]
触发Minor GC后,allocation1应该进入Survivor区或老年代
[0.066s][info ][gc,start ] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation)
[0.066s][info ][gc,heap ] GC(0) Eden regions: 3->0(9)
[0.066s][info ][gc,heap ] GC(0) Survivor regions: 0->2(2)
[0.066s][info ][gc,heap ] GC(0) Old regions: 0->0
3. 再次分配触发第二次Minor GC
allocation3 = new byte[4 * _1MB]
allocation3 = null
allocation3 = new byte[4 * _1MB]
[0.067s][info ][gc,start ] GC(2) Pause Young (Concurrent Start) (G1 Humongous Allocation)
[0.068s][info ][gc,heap ] GC(2) Eden regions: 1->0(9)
[0.068s][info ][gc,heap ] GC(2) Survivor regions: 2->1(2)
[0.068s][info ][gc,heap ] GC(2) Old regions: 0->2测试结论:
- ✅ 对象在Survivor区中存活
- ✅ 多次GC后晋升到老年代(Old regions: 0->2)
- ✅ 年龄晋升机制工作正常
5.4 HandlePromotionFailure - 空间分配担保
测试状态: ✅ 通过
测试命令:
bash
java -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 \
-cp target/classes com.linsir.abc.core.jvm.gc.allocation.HandlePromotionFailure实际输出(关键部分):
初始内存状态:
最大堆内存: 20MB
当前堆内存: 20MB
已使用内存: 2MB
空闲内存: 18MB
=== 空间分配担保测试 ===
空间分配担保流程:
1. 检查老年代空间 > 新生代总对象空间?
2. 是 -> 安全进行Minor GC
3. 否 -> 检查是否允许担保失败
4. 允许 -> 检查老年代空间 > 历次晋升平均大小?
5. 是 -> 尝试Minor GC(有风险)
6. 否 -> 进行Full GC
1. 分配多个对象填满Eden区
allocation1-3 各2MB,共6MB
2. 分配大对象触发Minor GC
allocation4 = 4MB
触发Minor GC,allocation1-3可能晋升到老年代
3. 继续分配对象
allocation5-6 各2MB
4. 再次分配大对象
allocation7 = 4MB
可能触发Full GC
当前内存状态:
最大堆内存: 20MB
当前堆内存: 20MB
已使用内存: 15MB
空闲内存: 5MB
调用System.gc()查看最终状态...
[0.069s][info ][gc,start ] GC(4) Pause Full (System.gc())
[0.072s][info ][gc,heap ] GC(4) Eden regions: 1->0(10)
[0.072s][info ][gc,heap ] GC(4) Survivor regions: 1->0(2)
[0.072s][info ][gc,heap ] GC(4) Old regions: 2->2
[0.072s][info ][gc ] GC(4) Pause Full (System.gc()) 11M->0M(20M) 2.261ms
最终内存状态:
最大堆内存: 20MB
当前堆内存: 20MB
已使用内存: 1MB
空闲内存: 19MB测试结论:
- ✅ 空间分配担保机制演示成功
- ✅ Minor GC和Full GC触发正常
- ✅ 内存状态变化符合预期
六、GC日志分析工具测试结果
6.1 GCLogAnalyzer - GC日志分析器
测试状态: ✅ 通过
测试命令:
bash
java -cp target/classes com.linsir.abc.core.jvm.gc.utils.GCLogAnalyzer实际输出:
示例GC日志分析:
[GC (Allocation Failure) 6487K->152K(9216K), 0.0049383 secs]
[GC (Allocation Failure) 6296K->152K(9216K), 0.0034567 secs]
[Full GC (Ergonomics) 8192K->6144K(19456K), 0.1234567 secs]
[GC (Allocation Failure) 6144K->152K(9216K), 0.0023456 secs]
=== GC统计信息 ===
总GC次数: 4
- Minor GC: 3
- Full GC: 1
总GC时间: 0.134s
平均GC时间: 0.034s
最大GC时间: 0.123s
最小GC时间: 0.002s
总回收内存: 20MB
=== GC事件详情 ===
[1] GCEvent{type=MINOR_GC, duration=0.005s, heap: 6487K->152K(9216K), reclaimed=6335K, ratio=97.66%}
[2] GCEvent{type=MINOR_GC, duration=0.003s, heap: 6296K->152K(9216K), reclaimed=6144K, ratio=97.59%}
[3] GCEvent{type=FULL_GC, duration=0.123s, heap: 8192K->6144K(19456K), reclaimed=2048K, ratio=25.00%}
[4] GCEvent{type=MINOR_GC, duration=0.002s, heap: 6144K->152K(9216K), reclaimed=5992K, ratio=97.53%}
=== 性能建议 ===
⚠ Full GC次数过多,建议检查:
- 老年代内存配置是否充足
- 是否存在内存泄漏
- 大对象是否直接进入老年代测试结论:
- ✅ GC日志解析正确
- ✅ 统计信息准确
- ✅ 性能建议合理
七、测试汇总
7.1 测试结果统计
| 类别 | 测试项 | 状态 | 备注 |
|---|---|---|---|
| 引用类型 | SoftReferenceExample | ✅ 通过 | 内存压力下正确回收 |
| 引用类型 | WeakReferenceExample | ✅ 通过 | GC后正确回收 |
| 引用类型 | PhantomReferenceExample | ✅ 通过 | 引用队列通知正常 |
| 引用类型 | ReferenceTypeComparison | ✅ 通过 | 对比清晰准确 |
| Finalize | FinalizeEscapeGC | ✅ 通过 | 自我拯救成功 |
| Finalize | ResourceCleanupExample | ✅ 通过 | 替代方案有效 |
| 内存分配 | EdenAllocation | ✅ 通过 | Minor GC触发正常 |
| 内存分配 | PretenureSizeThreshold | ✅ 通过 | G1 Humongous处理 |
| 内存分配 | TenuringThreshold | ✅ 通过 | 年龄晋升正常 |
| 内存分配 | HandlePromotionFailure | ✅ 通过 | 分配担保演示成功 |
| 工具类 | GCLogAnalyzer | ✅ 通过 | 日志解析准确 |
总计:11个测试项,全部通过 ✅
7.2 发现的问题与注意事项
G1收集器差异
-XX:PretenureSizeThreshold参数对G1不生效- G1使用Humongous区域处理大对象(> 1/2 region size)
- GC日志格式与CMS/Serial等不同
finalize()废弃
- JDK 9+中finalize()已被标记为废弃
- 建议使用try-with-resources或Cleaner替代
内存设置
- 内存分配测试建议在较小堆内存(20MB)下运行
- 便于观察GC行为和对象晋升过程
7.3 测试结论
所有示例代码均测试通过,功能符合预期。代码能够正确演示:
- 四种引用类型的区别和回收时机
- Finalize机制的问题和替代方案
- 内存分配策略和GC触发条件
- GC日志分析方法
八、相关文档
- chapter-03-gc.md - 垃圾收集器与内存分配策略理论文档
- chapter-03-gc-code-guide.md - 代码说明文档
- chapter-03-gc-interview.md - GC相关面试题汇总