You need to enable JavaScript to run this app. 如何通过编程方式无堆转储获取运行中JVM堆内最大对象的实时统计信息
优惠活动
大模型
产品
解决方案
定价
更多

如何通过编程方式无堆转储获取运行中JVM堆内最大对象的实时统计信息

无全量堆转储场景下,Java8 HotSpot(Linux)类维度堆内存实时统计实现方案

先把两个核心疑问的结论摆前面:

  • 纯标准Java API没有办法在不做全堆遍历/转储的前提下,拿到全局堆内类维度的实例数、内存占用统计
  • 主流Profiler的低开销实现完全基于HotSpot VM内置的对象采样能力,走JVM TI原生接口实现,开销极低,精度满足要求

标准Java API的能力边界

梳理JDK8对外暴露的所有相关API,可以明确覆盖范围的硬边界:

  • java.lang.management.MemoryMXBean:仅能输出堆/非堆的整体内存大小、使用率,没有类维度的拆分粒度
  • java.lang.instrument.Instrumentation:
    • 提供的getObjectSize(Object obj)方法需要传入具体的对象引用,才能拿到单个对象的Shallow大小,没有接口能直接枚举堆中所有存活对象的引用
    • 如果要靠这个接口做全局统计,必须从GC Root出发递归遍历整个对象图,这个过程会让JVM进入长时间安全点暂停,和生成一次完整heap dump的开销基本一致,完全达不到实时低开销的要求
    • 仅适合"提前知道要追踪哪些自定义类、主动持有这些类的对象引用"的场景,覆盖不了全局统计的需求
  • 字节码增强的变通路径:通过retransformClasses给所有类的构造方法埋点,累计对象创建的计数。这种方式的硬伤很明显:
    • 只能统计埋点完成后新创建的对象,完全覆盖不了堆中的存量老对象
    • 每个对象构造都要触发Java层的逻辑分支,运行时开销通常超过10%
    • 拿不到对象头、对齐填充带来的实际内存占用,统计值的偏差非常大。

主流Profiler的底层实现逻辑

所有主流性能分析工具在这类场景下,从来不会走全堆遍历的路径,核心都是复用HotSpot内置的TLAB分配采样钩子,实现低开销的类维度内存聚合:

  • HotSpot在对象分配路径上,专门预留了采样触发点:当线程在自己的TLAB缓冲区里完成对象分配时,不会每次都触发回调,而是根据设置的采样间隔,按概率抽样采集当前分配的对象信息
  • 被采样到的对象会直接回调给分析模块,带上所属类的元数据、对象本身的大小,分析模块会根据采样间隔的权重,把采样值放大成全量场景下的估算值
  • 整个过程不需要遍历堆、不需要冻结所有应用线程,运行时开销通常能控制在1%以内,秒级刷新的统计精度和全量heap dump的分析结果偏差通常在5%以内,和日常用Profiler看到的输出效果完全一致
  • 采样间隔可以灵活调整:间隔越小精度越高、开销越大;间隔越大精度越低、开销越小,完全可以根据业务场景动态调整。

Linux+Java8 HotSpot下的原生实现方案(通用化优先)

方案1:基于JVM TI的原生采样代理

这是通用性最强、精度最高的实现方式,完全不依赖特定Profiler的私有能力,适配所有HotSpot VM的版本:
核心实现步骤:

  1. 开发一个C/C++编写的Java代理,目标JVM启动时或者运行时通过attach机制挂载
  2. 在JVM TI初始化阶段申请对应权限,开启对象采样能力,设置自定义的采样间隔
  3. 注册对象采样事件的回调函数,在回调里实时按类维度聚合实例数、累计占用大小

简化的核心代码示例:

// 原生代理初始化阶段核心逻辑(Linux/Java8 HotSpot适配)
jvmtiEnv* jvmti_env;
jvmtiCapabilities jvmti_caps = {0};

// 申请必要的能力
jvmti_env->GetPotentialCapabilities(&jvmti_caps);
jvmti_caps.can_generate_all_class_hook_events = 1;
jvmti_caps.can_tag_objects = 1;
jvmti_env->AddCapabilities(&jvmti_caps);

// 设置采样间隔:每分配1MB大小触发一次采样
jvmti_env->SetObjectAllocationSamplingRate(1 * 1024 * 1024);

// 定义采样回调函数
void JNICALL sample_object_callback(jvmtiEnv* jvmti, JNIEnv* env,
                                    jclass sampled_klass, jlong object_size) {
    // 获取类的全限定名
    char* klass_signature;
    jvmti->GetClassSignature(sampled_klass, &klass_signature, nullptr);
    
    // 本地维护的类维度统计表:key为类签名,value为<累计实例数, 累计估算内存占用>
    class_stat_table[klass_signature].sample_count += 1;
    class_stat_table[klass_signature].estimated_memory += object_size * sampling_weight;
    
    // 释放JVM分配的类名字符串
    jvmti->Deallocate((unsigned char*)klass_signature);
}

// 注册回调、开启事件通知
jvmtiEventCallbacks callbacks = {0};
callbacks.ObjectAllocationSample = sample_object_callback;
jvmti_env->SetEventCallbacks(&callbacks, sizeof(callbacks));
jvmti_env->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_OBJECT_ALLOCATION_SAMPLE, nullptr);

注意:Java8 HotSpot下的采样权重可以根据TLAB的大小动态计算,不需要手动硬编码,最终输出的统计值已经做了权重补偿,和全量堆统计的结果没有明显感知差异。

方案2:读取HotSpot的perf共享内存(零代理侵入)

Linux环境下HotSpot会在/tmp/hsperfdata_<当前用户>/<目标JVM进程PID>路径下创建一块共享内存段,实时写入JVM运行的各类性能指标。Java8版本下,这个共享内存段里已经内置了每个已加载类的TLAB分配累计大小、TLAB外分配大小的统计值,不需要给目标JVM挂载任何代理,只需要按照固定的结构解析这块共享内存,就能拿到类维度的内存占用估算值。
这种方式的侵入性为零,完全不会影响目标JVM的运行,缺点是解析的结构依赖HotSpot的内部实现,版本变更时结构可能调整,通用性比JVM TI方案略差。

常见踩坑提醒

  • 不要尝试靠"枚举所有已加载类、逐类实例计数"的纯Java方案做全局统计:HotSpot没有对外提供"拿到某个类所有存活实例"的标准API,这类操作必须进入安全点遍历堆,和生成heap dump的开销没有区别
  • 不要把采样统计的"估算值"当成绝对精确值:在1MB的采样间隔下,单类的内存占用估算误差通常在5%以内,完全满足日常排查大对象的需求,如果需要100%精确的存量对象统计,再触发全量heap dump就可以
  • 这类实现方式的可移植性很强:只要是Linux下运行的HotSpot VM,不管是Java8还是后续的高版本JDK,核心的采样逻辑不需要做大的修改就能复用。

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.25 08:06:04