先想清楚你要“看结构”还是“拿实例”,前者用类元数据与反射,后者必须走堆快照或JVMTI,别无他路。
很多人第一次做对象遍历,思路都是“把堆里所有对象挨个摸一遍”,这个念头在开发环境里无所谓,放到线上就是事故,业内专家指出,JVM并没有开放一个“列出所有实例”的公共API,你能用的入口其实就那么几个,选错了不仅慢,还可能把服务拖进Full GC。
三个入口,对应三种完全不同的场景
jmap / jcmd:快照式遍历,离线看全貌
这是最常被搜到的路径,在终端里敲:
jcmd <pid> GC.class_histogram
jcmd <pid> GC.heap_dump /tmp/dump.hprof
前者只给类直方图,告诉你哪个类有多少个实例、占多少字节,后者把整堆落盘成hprof,再用Eclipse MAT或VisualVM打开做对象图遍历。
它的好处是“全”,坏处是重:一次heap dump在几GB堆上会让应用暂停可感知的一段时间,dump文件动辄几百MB,所以它适合事后分析,不适合在线实时迭代,真要在线看直方图,jmap -histo:live会先触发一次Full GC,这一点务必记牢。
JVMTI:在线遍历的正统路径
JVM Tool Interface里有几个专门的函数:
IterateThroughHeap:按类或标签遍历堆内实例。FollowReferences:从指定根对象出发,沿引用链做可达性遍历。GetObjectsWithTags:按标签批量取对象,适合自己打标后再筛选。
它们必须通过Agent方式加载,通常写成C/C++的JVMTI Agent,或者在Java侧通过JPLIS Agent配合native桥接,这条路能拿到活对象的引用和字段值,不落盘、不重启,代价是编写门槛高,而且遍历期间要处理对象移动、并发修改等问题。
Instrumentation + 反射:只适合静态结构和少量采样
Instrumentation.getAllLoadedClasses()能列出所有已加载的类,配合反射可以遍历字段、方法、注解,但注意,它拿不到“实例”,想拿实例,还是得回到JVMTI。
反射遍历字段时有两个坑:JDK 9以后模块系统会拦setAccessible,需要--add-opens;另外Field.get会产生装箱和临时对象,遍历十万级实例就可能明显影响GC频率。
JVMTI遍历对象和反射遍历到底差在哪?性能账要算清楚
这两者的差别不是“快慢”,而是“能看见什么”。
| 维度 | JVMTI遍历 | 反射遍历 |
|---|---|---|
| 能否拿到实例 | 能,按类或标签筛选 | 不能,只能拿类结构 |
| 是否影响应用停顿 | 可控,可分段遍历 | 单次调用短,但循环总量大 |
| 开发成本 | 高,需要Agent | 低,纯Java代码 |
| 引用类型处理 | 原生支持弱、软、虚引用区分 | 反射拿不到引用强度信息 |
| 典型工具 | Arthas vmtool、async-profiler | 自研诊断代码、JOL |
以Arthas为例,vmtool --action getInstances --className com.example.Order --limit 10这条命令就是走Attach机制注入Agent后完成的实例遍历,属于JVMTI体系,它比你自己写反射循环省事得多,也更安全。
行业共识认为,需要“按条件捞对象”时优先用现成工具,别自己造轮子,自研遍历代码最大的风险不是性能,而是改变了对象的可达性你在遍历时用强引用持有了一批对象,GC就动不了它们,观测行为本身污染了观测结果。
线上服务如何排查堆外对象泄漏?场景化遍历姿势
堆外泄漏是最典型的“遍历目标不在堆里”的场景,DirectByteBuffer、Netty的PooledByteBuf、JNI分配的native内存,都不在常规堆遍历范围内。
操作路径大致这样:
- 先用
jcmd <pid> VM.native_memory summary看NMT是否开启,确认native内存趋势。 - 用
jcmd <pid> GC.class_histogram看java.nio.DirectByteBuffer的实例数和大小,它通常是堆外内存的“账本”。 - 打开堆转储,在MAT里用OQL查
select from java.nio.DirectByteBuffer,顺着cleaner字段看释放链是否断裂。 - 如果怀疑是Netty的池化内存,用Arthas的
vmtool按类名捞PooledByteBuf相关实例,结合引用链判断是否被业务代码长期持有。
这里的关键判断是:堆外对象的Java侧引用往往只有一个“壳”,真正占内存的是native指针,遍历时要盯住壳对象的生命周期,而不是壳对象自身的大小。
遍历对象图的算法细节,决定你跑不跑得动
真正做可达性遍历时,两个基础选择会直接决定成败。
- 广度优先还是深度优先:BFS用队列,能快速定位到浅层的大对象,但队列会随对象数线性膨胀;DFS用显式栈,内存占用更平稳,适合深引用链,生产环境更推荐带深度上限的DFS。
- 如何去重:用
IdentityHashMap或按对象地址建HashSet,避免环形引用导致死循环,别用equals,业务重写过的equals会让两个不同实例被判成同一个。 - 引用强度要分开处理:软引用和弱引用在GC时会被清理,遍历时如果把它们当作强引用继续往下走,会得到根本不存在的引用链,JVMTI的
FollowReferences提供了引用类型过滤参数,务必用上。
还有一条实操建议:遍历前先做类过滤,整堆几百万对象逐个访问,耗时以分钟计;如果只遍历某个包下的3个类,往往几十毫秒就结束。IterateThroughHeap支持按类名过滤,反射方案里也应该先用getAllLoadedClasses筛出目标类再动手。
据OpenJDK社区公开的文档说明,JVMTI的堆遍历接口本身不保证强一致性遍历过程中对象可能被移动或回收,所以工具层通常要配合suspend/resume或分段快照来保证结果可用,这也意味着,追求“绝对精确”的全量在线遍历,代价往往高到不值得。
一个常被忽略的成本维度
商业APM产品里带的对象引用分析功能,授权费用按节点算,中小团队往往承担不起,这也是自研遍历脚本长期存在的原因,但自研代码的隐性成本在维护:JDK每升级一次大版本,Unsafe和反射的兼容性都要重新验证一遍,选工具时把这块算进去,比只看单次遍历快慢更实际。
Java遍历虚拟机对象Q&A:迭代访问的三个高频疑问
Q:能不能不落堆转储,直接在线拿到某个类的所有实例?
可以,走JVMTI的IterateThroughHeap或直接用Arthas的vmtool getInstances,前提是目标JVM允许Attach,且你能接受遍历期间短暂的对象访问开销,JDK 21以后Attach机制默认更严格,需要在启动参数里显式放开。
Q:遍历出来的对象数量比直方图少很多,是什么原因?
常见三种情况:一是遍历时用了类过滤器,只扫了部分类;二是对象在遍历期间被GC回收了,没做同步;三是软引用、弱引用被跳过,对照GC.class_histogram的结果逐项排除即可。
Q:自己写反射遍历字段,哪些写法最容易踩坑?Field.get在基本类型上会装箱,大量调用直接推高GC压力;setAccessible(true)在模块化环境下需要--add-opens java.base/目标包=ALL-UNNAMED;遍历静态字段时注意区分static与非static,前者属于类而非实例,更稳妥的做法是用MethodHandles的VarHandle替代反复反射,减少元数据查找开销。
把入口选对、把过滤做在前面、把引用强度分清,遍历虚拟机对象这件事就不会变成一场性能赌博,多数情况下,你需要的不是“遍历所有对象”,而是“遍历符合条件的那一小撮”。
图片来源于AI模型,如侵权请联系管理员。作者:酷小编,如若转载,请注明出处:https://www.kufanyun.com/ask/928660.html


评论列表(4条)
这篇文章的内容非常有价值,我从中学习到了很多新的知识和观点。作者的写作风格简洁明了,却又不失深度,让人读起来很舒服。特别是需要部分,给了我很多新的思路。感谢分享这么好的内容!
这篇文章的内容非常有价值,我从中学习到了很多新的知识和观点。作者的写作风格简洁明了,却又不失深度,让人读起来很舒服。特别是需要部分,给了我很多新的思路。感谢分享这么好的内容!
这篇文章写得非常好,内容丰富,观点清晰,让我受益匪浅。特别是关于需要的部分,分析得很到位,给了我很多新的启发和思考。感谢作者的精心创作和分享,期待看到更多这样高质量的内容!
@lucky506man:这篇文章写得非常好,内容丰富,观点清晰,让我受益匪浅。特别是关于需要的部分,分析得很到位,给了我很多新的启发和思考。感谢作者的精心创作和分享,期待看到更多这样高质量的内容!