一、每个字段后面的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)

    • ​实例数据​​: 该对象有多个字段(snmodelIdstatusdeviceIpkeyip等),每个字段是一个引用,占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 (sn)​

固定结构:对象头+字段(hash,value引用,coder等)=24B

自身(24B) + 其独家支配的byte[3](24B) = 48B

byte[3]

对象头(12B)+长度(4B)+数据(3B)+填充(5B)=24B

无支配对象,Retained=Shallow=24B

​Integer​

对象头(12B) + int value(4B) = 16B

无支配对象,Retained=Shallow=16B

​String (ip)​

固定结构:24B

自身(24B) + 其独家支配的byte[14](32B) = 56B

​关键启示:​

  1. ​Retained Size 才是关键​​:它揭示了对象的真实内存成本。虽然两个IP字符串自身都是24B,但每个都实际占用了56B内存。

  2. ​警惕重复创建​​:deviceIp和 ip字段内容相同,但却可能是两个独立的对象,占用了 56B * 2 = 112B的内存。如果业务允许,考虑​​字符串驻留​​或共享对象来优化。

  3. ​分析工具是必备技能​​:读懂这样的视图,能帮你快速定位内存消耗的热点,比如是这个 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"在应用中被重复使用。这种情况通常由以下原因导致:

  1. 多次查询磁盘信息返回相同结果,虽然创建了多个DiskInfoDTO对象,但其diskSize字段都指向JVM字符串池中的同一个String实例
  2. 该字符串可能被定义为常量并被静态引用

这对程序意味着:

• 这是正常现象:字符串共享是JVM的常见优化机制,能有效减少内存冗余

• 实际节省了内存:共享机制确保只存储一份"4599.03"数据,而非多份副本

验证方法: 如需确认,可使用专业内存分析工具(如Eclipse MAT)分析完整堆转储文件。在MAT中可执行以下操作:

  1. 定位该String对象
  2. 右键选择"Merge Shortest Paths to GC Roots"或"List objects with incoming references"
  3. 观察结果会显示多个引用路径指向该String对象,证实其为共享对象

结论: 无需担心。Retained Size显示为0B明确表明:这些字符串属于共享资源,回收单个DiskInfoDTO实例不会对其产生影响。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐