堆存储文件分析
字段Shallow Size 计算逻辑Retained Size 计算逻辑对象头(12B) + 引用字段(6 * 4B=24B) + 填充(4B) = 40B自身(40B) + 所有独家支配的子对象总和 = 96BString (sn)固定结构:对象头+字段(hash,value引用,coder等)=24B自身(24B) + 其独家支配的byte[3]byte[3]对象头(12
一、每个字段后面的shallow和retained怎么算出来的
核心概念回顾
-
Shallow Size (浅堆):对象自身占用的内存,不包括它引用的其他对象。
-
Retained Size (保留堆):当这个对象被垃圾回收时,能释放的总内存大小。它包括自身以及所有仅通过该对象引用链访问到的对象(即被它“独家支配”的对象)。
图片中显示的是每个字段所指向的对象的 Shallow 和 Retained 大小,而不是字段引用本身的大小(一个引用在64位压缩指针下固定占4字节)。
逐行计算解析
我们来逐一分析图片中的每一行:
1. LimpSearchResult(Shallow: 40B, Retained: 96B)
这是根对象。
-
Shallow (40B):
-
对象头: 12B (8B Mark Word + 4B Klass Pointer)
-
实例数据: 该对象有多个字段(
sn,modelId,status,deviceIp,key,ip等),每个字段是一个引用,占4B。从图上看至少有6个引用字段,共6 * 4B = 24B。 -
小计:
12B + 24B = 36B -
对齐填充: 36B 不是8的倍数,需填充到
40B。
-
-
Retained (96B):
-
这是最关键的数字。它等于该对象自身的 Shallow Size (40B) 加上 所有被它独家支配的对象的 Shallow Size。
-
从下方字段看,
sn(48B),modelId(16B),status(16B),deviceIp(56B),ip(56B) 的 Retained Size 之和已经远超96B。但请注意,Retained Size 不能简单相加,因为对象之间可能有共享。 -
更合理的解释是:
LimpSearchResult对象支配了这些字段对象,但这些字段对象(如两个String)可能共享了底层的byte[],或者工具计算其独家支配子树的总和为96B。96B是这个对象被回收后能释放内存的总量。
-
2. sn = 'The' java.lang.String(Shallow: 24B, Retained: 48B)
这是一个String对象。
-
Shallow (24B):
-
对象头: 12B
-
实例数据:
-
char[] value(引用) -> 4B -
int hash-> 4B -
byte coder-> 1B (JDK9+用于表示编码) -
boolean hashIsZero-> 1B (可能是缓存哈希值计算状态的字段)
-
-
小计:
12B + 4B + 4B + 1B + 1B = 22B -
对齐填充: 22B -> 填充至
24B。
-
-
Retained (48B):
-
回收这个String能释放48B。这包括它自身(24B) + 它独家支配的
char[](或byte[]) 对象。 -
从下面展开的字段可以看到
value = byte[3](Shallow: 24B)。因为只有这个String引用了这个数组,所以回收String也会回收该数组。 -
24B (String) + 24B (byte[]) = 48B。
-
3. value = byte[3](Shallow: 24B, Retained: 24B)
这是存储字符串"The"内容的字节数组。
-
Shallow (24B):
-
对象头: 12B
-
数组长度: 4B
-
实例数据:
3 bytes* 1B = 3B -
小计:
12B + 4B + 3B = 19B -
对齐填充: 19B -> 填充至
24B。
-
-
Retained (24B):
-
这个数组没有引用其他对象,所以它的Retained Size等于它的Shallow Size。
-
4. modelId = java.lang.Integer(Shallow: 16B, Retained: 16B) & status = java.lang.Integer(Shallow: 16B, Retained: 16B)
以 modelId为例:
-
Shallow (16B):
-
对象头: 12B
-
实例数据:
int value-> 4B -
小计:
12B + 4B = 16B(正好是8的倍数,无需填充)。
-
-
Retained (16B):
-
Integer对象内部只有一个int,没有引用其他对象,所以 Retained = Shallow。
-
5. deviceIp = '10.10.101.223' java.lang.String(Shallow: 24B, Retained: 56B) & ip = '10.10.101.223' java.lang.String(Shallow: 24B, Retained: 56B)
这两个String内容相同,但可能是不同的对象实例(新建了两次),所以各有各的Retained Size。
-
Shallow (24B): 计算方式同
sn字符串,结构固定为24B。 -
Retained (56B):
-
回收这个String能释放56B。这包括它自身(24B) + 它独家支配的
char[](或byte[]) 对象。 -
字符串 "10.10.101.223" 长度为14(字符),内部存储它的数组是
byte[14]。 -
一个
byte[14]的 Shallow Size 计算:-
对象头: 12B
-
数组长度: 4B
-
数据:
14 bytes* 1B = 14B -
小计:
12B + 4B + 14B = 30B -
对齐填充: 30B -> 填充至
32B。
-
-
24B (String) + 32B (byte[]) = 56B。
-
6. key = null object
引用为 null,不指向任何对象,因此没有内存占用可计算。
总结与启示
|
字段 |
Shallow Size 计算逻辑 |
Retained Size 计算逻辑 |
|---|---|---|
|
LimpSearchResult |
对象头(12B) + 引用字段(6 * 4B=24B) + 填充(4B) = 40B |
自身(40B) + 所有独家支配的子对象总和 = 96B |
|
String ( |
固定结构:对象头+字段(hash,value引用,coder等)=24B |
自身(24B) + 其独家支配的 |
|
|
对象头(12B)+长度(4B)+数据(3B)+填充(5B)=24B |
无支配对象,Retained=Shallow=24B |
|
Integer |
对象头(12B) + |
无支配对象,Retained=Shallow=16B |
|
String ( |
固定结构:24B |
自身(24B) + 其独家支配的 |
关键启示:
-
Retained Size 才是关键:它揭示了对象的真实内存成本。虽然两个IP字符串自身都是24B,但每个都实际占用了56B内存。
-
警惕重复创建:
deviceIp和ip字段内容相同,但却可能是两个独立的对象,占用了56B * 2 = 112B的内存。如果业务允许,考虑字符串驻留或共享对象来优化。 -
分析工具是必备技能:读懂这样的视图,能帮你快速定位内存消耗的热点,比如是这个
LimpSearchResult对象本身,还是它内部的某个字段。
二、为什么retained是0?
核心结论
Retained Size 为 0 B,并不表示这个对象不占内存(它的 Shallow Size 是 24B),而是表示:在当前的内存快照中,这个对象不被任何其他对象“独家支配”。它是一个“共享对象”,回收它并不会导致其他任何对象被释放。
详细解释
让我们通过一个简单示例来理解这个机制。假设程序中有两个DiskInfoDTO对象:
dto1的diskSize字段值为"4599.03"
dto2的diskSize字段值同样为"4599.03"
总结:
Shallow Size(24B):准确反映String对象本身的内存占用 Retained Size(0B):表明该String是多个父对象共享的资源,没有单一支配者 包含对象自身及仅通过该对象才能访问到的所有对象 对于这个共享String:
- 不会被计入dto1或dto2的Retained Size
- 其Retained Size会归属到更高层的支配对象
- 如静态常量对应的Class对象
- 若无明确支配者,分析工具可能显示为0
Retained Size的计算规则:
由于JVM的字符串池(String Interning)机制或代码逻辑,这两个DTO的diskSize字段可能指向内存中的同一个String对象。
分析这个共享的String对象"4599.03":
Shallow Size:24B
- 表示对象本身在堆中的固定占用空间
- 这个值始终保持不变
Retained Size:0B
- 这是基于支配树(Dominator Tree)的计算结果
- 由于该String对象同时被dto1和dto2引用:
- 仅回收dto1不会释放该String,因为dto2仍在引用
- 仅回收dto2同理
- 因此,两个DTO都无法单独支配这个String对象
图片信息的再解读
图片显示的是 IDEA 调试器的"内存快照"视图,并非专业堆转储分析工具(如 Eclipse MAT)。该视图功能较为基础:
关键发现:字符串值"4599.03"在应用中被重复使用。这种情况通常由以下原因导致:
- 多次查询磁盘信息返回相同结果,虽然创建了多个DiskInfoDTO对象,但其diskSize字段都指向JVM字符串池中的同一个String实例
- 该字符串可能被定义为常量并被静态引用
这对程序意味着:
• 这是正常现象:字符串共享是JVM的常见优化机制,能有效减少内存冗余
• 实际节省了内存:共享机制确保只存储一份"4599.03"数据,而非多份副本
验证方法: 如需确认,可使用专业内存分析工具(如Eclipse MAT)分析完整堆转储文件。在MAT中可执行以下操作:
- 定位该String对象
- 右键选择"Merge Shortest Paths to GC Roots"或"List objects with incoming references"
- 观察结果会显示多个引用路径指向该String对象,证实其为共享对象
结论: 无需担心。Retained Size显示为0B明确表明:这些字符串属于共享资源,回收单个DiskInfoDTO实例不会对其产生影响。
更多推荐


所有评论(0)