You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Java堆dump中ConcurrentHashMap保留堆内存低于预期的原因排查

理解ConcurrentHashMap堆转储中保留堆的差异

首先得明确保留堆(Retained Heap)的核心定义:它代表的是当某个对象被垃圾回收时,能够被释放的内存总量——这其中不包含被其他存活对象引用的部分。这就是你看到两种不同Node条目差异的根本原因。

为什么会出现两种不同的情况?

1. 条目符合预期的情况(Node保留堆=键+值+自身浅堆)

像你给出的第二个例子:

[9706] java.util.concurrent.ConcurrentHashMap$Node @ 0x6c6a1ab98 | 32 | 3,160
|- key java.lang.String @ 0x6c6a1abb8 | 24 | 128
|- val org.apache.zookeeper.server.DataNode @ 0x6c6a1ac38 | 32 | 3,000

这里的String键和DataNode只被这个ConcurrentHashMap的Node引用,没有其他存活对象持有它们的引用。所以当这个Node被GC时,键、值和Node自身的内存都会被释放,保留堆自然就是三者的总和:32+128+3000=3160,和工具显示的一致。

2. 条目不符合预期的情况(Node保留堆远小于键+值)

再看第一个例子:

[5490] java.util.concurrent.ConcurrentHashMap$Node @ 0x6c6a23910 | 32 | 136
|- key java.lang.String @ 0x6c6a23930 | 24 | 104
|- val org.apache.zookeeper.server.DataNode @ 0x6c6a23998 | 32 | 3,304

这里的关键是:键或值被其他对象引用了,所以它们的内存不会被算入当前Node的保留堆:

  • 对于DataNode:ZooKeeper的DataNode通常会被其他结构引用(比如父节点的子节点列表、Watcher关联的对象、或者ZooKeeper内存数据库中的其他条目)。这些外部引用意味着即使当前Node被GC,这个DataNode依然会存活,所以它的3304字节内存不会被算入Node的保留堆。
  • 对于String键:如果这个路径字符串被其他地方复用(比如ZooKeeper的其他节点路径、配置中的常量),它也会被外部引用,所以只有String对象自身的小部分内存(或者完全没有)会被算入Node的保留堆。

最终Node的保留堆136,其实只是Node自身(32)加上键和值中仅被当前Node引用的那一小部分内存

为什么总保留堆只有预期的一半(24MB vs 48MB)

你预期的48MB是16000个3000字节byte[]的总和,但实际ConcurrentHashMap的总保留堆只有24MB,说明大约一半的DataNode(或其内部的byte[])被外部引用持有。这些被共享的对象内存不会被算入HashMap的保留堆,因为即使HashMap被GC,它们依然会存活。

验证方法

你可以用堆分析工具(比如Eclipse MAT或VisualVM)做以下操作:

  • 选中异常条目的DataNodeString,查看传入引用(Incoming References),就能找到哪些其他对象在引用它们。
  • 查看HashMap的支配树(Dominator Tree),支配树会显示HashMap作为唯一支配者的对象——这些对象的内存总和才是HashMap真正“独占”的内存,也就是它的保留堆的准确值。

内容的提问来源于stack exchange,提问作者weima

火山引擎 最新活动