1. 浅堆(Shallow Heap) #
技术定义 #
浅堆(Shallow Heap)指一个对象自身占用的内存空间,包含:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
关键特性 #
- 不包含引用对象:仅计算对象自身内存
- 类型敏感:String对象的浅堆 ≠ Integer对象的浅堆
- 内存对齐:JVM按8字节对齐分配内存
计算示例 #
// 32位JVM中的Integer对象
对象头(8B) + int value(4B) + 对齐填充(0B) = 12B
// 64位JVM关闭压缩指针的HashMap$Node
对象头(16B) + 4个引用字段(32B) + int hash(4B) = 52B → 对齐填充后56B
实战意义 #
- 快速定位「对象膨胀」问题
- 分析集合类基础内存开销
2. 保留集(Retained Set) #
核心概念 #
保留集指某个对象被GC回收时,会连带回收的所有对象集合
当对象X被回收时:
- 保留集 = {X,Y,Z,M,N}(所有被X直接或间接独占引用的对象)
- 排除被其他存活对象引用的对象
3. 深堆(Retained Heap) #
技术定义 #
深堆(Retained Heap)= 保留集中所有对象浅堆之和 - 被其他对象共享的部分
在上例中:
- 对象A的深堆 = A.shallow + B.shallow + C.shallow
- 对象D不计入(被外部对象E引用)
内存分析价值 #
- 识别真正内存占用大户
- 发现「看似小对象实则大内存」的场景
- 内存泄漏排查的核心指标
诊断工具对比 #
工具 | 浅堆分析 | 深堆分析 | 支配树 |
---|---|---|---|
MAT | ✅ | ✅ | ✅ |
JProfiler | ✅ | ✅ | ✅ |
VisualVM | ✅ | ❌ | ❌ |
4. 对象的实际大小 #
与浅堆的差异对比 #
对象的实际大小 = 浅堆 + 所有可达对象的浅堆总和
计算规则详解 #
// Person对象结构
class Person {
String name; // 引用类型
int age; // 基本类型
Address addr; // 引用类型
}
// 计算步骤:
// 1. Person浅堆 = 对象头 + 字段存储空间
// 2. 实际大小 = Person.shallow + name.shallow + addr.shallow + ...
内存快照分析 #
使用MAT工具查看对象内存分布
5. 支配树(Dominator Tree) #
核心算法原理 #
支配树中的节点满足:从GC Roots到该节点的所有路径都必须经过其支配者
MAT实战应用 #
- 打开支配树视图
- 按Retained Heap排序
- 定位可疑对象
// 典型内存泄漏支配树结构
ThreadLocalMap --支配--> Entry --支配--> ValueObject
优化指导价值 #
- 识别关键内存持有路径
- 快速定位集合类泄漏
6. 内存泄漏(Memory Leak)VS 内存溢出(OOM) #
本质区别对比 #
维度 | 内存泄漏 | 内存溢出 |
---|---|---|
发生条件 | 对象不再使用但无法回收 | 内存空间不足以创建新对象 |
持续时间 | 长期累积 | 可能瞬时发生 |
检测难度 | 需要内存分析工具 | 往往有明确错误日志 |
典型错误 | 无直接报错 | java.lang.OutOfMemoryError |
关联性图解 #
7. Java中内存泄露的8种情况 #
8.1 静态集合类 #
static List<Object> cache = new ArrayList<>(); // 添加后未移除
8.2 单例模式 #
class Singleton {
private static final instance = new Singleton();
private byte[] data = new byte[1024 * 1024]; // 大对象常驻内存
}
8.3 内部类持有外部类 #
class Outer {
class Inner {
// 隐式持有Outer.this引用
}
}
8.4 未关闭连接 #
8.5 变量不合理的作用域 #
public class DataProcessor {
private byte[] buffer = new byte[1024 * 1024]; // 本应作为局部变量
public void process() {
// 长期持有大数组引用
}
}
内存泄漏原理:
https://i.imgur.com/3GjZx4k.png
将本应方法内使用的临时变量提升为成员变量,导致对象生命周期被意外延长
8.6 改变哈希值 #
class Student {
int id;
@Override
public int hashCode() {
return id;
}
void updateId(int newId) {
this.id = newId; // 修改哈希关键字段
}
}
HashSet<Student> set = new HashSet<>();
set.add(student);
student.updateId(100); // 导致无法通过remove()删除
哈希表结构变化:
对象存储在旧桶中,但查询时到新桶查找,导致永远无法被访问
8.7 缓存泄露 #
// 错误实现
Map<String,BigObject> cache = new HashMap<>();
// 正确姿势
Map<String,SoftReference<BigObject>> cache = new WeakHashMap<>();
缓存策略对比:
8.8 监听器和其他回调 #
public class Service {
private List<Listener> listeners = new ArrayList<>();
public void addListener(Listener l) {
listeners.add(l);
}
// 缺少removeListener方法
}
监听器泄漏链:
9. 内存泄露案例分析 #
场景描述 #
某电商系统频繁Full GC,监控显示老年代内存持续增长
MAT分析步骤 #
- 获取堆转储文件
<font style="background-color:rgb(252, 252, 252);">jmap -dump:format=b,file=heap.hprof <pid></font>
- 支配树定位问题
https://i.imgur.com/5WQY7vG.png - 发现可疑对象
ConcurrentHashMap$Node[]
Retained Heap: 1.2GB
- 引用链追溯
- 根本原因:
静态缓存使用强引用,未设置过期策略
10. 常见问题与解决方案 #
问题现象 | 分析工具 | 解决方案 |
---|---|---|
频繁Full GC | GC日志+MAT | 检查深堆>1MB的对象 |
PermGen OOM | JVisualVM | 检查类加载器引用链 |
堆内存持续增长 | JProfiler | 对比两次内存快照 |
线程数暴涨 | jstack | 定位线程创建堆栈 |
操作指南:
11. 高频面试问题与解答 #
Q1:浅堆和深堆的核心区别是什么? #
答:
- 浅堆测量对象自身内存占用(约几十字节)
- 深堆计算对象被回收时释放的总内存(可能达GB级)
- 示例:ArrayList浅堆约40字节,深堆包含所有元素内存
Q2:如何用MAT检测内存泄漏? #
答:
- 打开支配树视图
- 按Retained Heap降序排序
- 检查可疑对象的GC Root路径
- 对比多个堆快照的增量变化
Q3:ThreadLocal为什么会导致内存泄漏? #
答:
- 线程池场景下,线程长期存活导致Entry无法回收
- 必须显式调用remove()清理条目
Q4:WeakHashMap如何解决缓存泄漏? #
答:
- Key使用弱引用,不影响GC回收
- 当Entry的Key被回收时,自动移除整个Entry
- 需要保证Key是外部对象的唯一引用
全文总结:
通过浅堆/深堆分析可精准定位内存问题,结合支配树和引用链分析能快速识别泄漏点。8种典型场景覆盖90%的泄漏情况,MAT等工具是解决问题的利器。建议开发者在以下场景主动检查内存:
- 使用全局集合时
- 涉及外部资源操作时
- 实现回调机制时
- 使用缓存组件时