使用native_memory监控AARCH64架构下java内存异常增长以及详细分布

一.故障现场

某客户现场发现Java微服务总内存达到30G。这个微服务xmx分配的16G,一般加上其他Class等最多也就20多G一点,这个现场30G内存肯定有问题。

说明:下面的排障方法对所有的芯片架构都是通用的。

二.初步排障

先用arthas分析一下基础的内存分布

从图中可以看出heap内存+nonheap内存是16.3G+1.7G=18G。

发现一点这个JDK版本是海思HiSilicon下的kunpeng-920芯片AARCH64架构

所以呢安装的是毕昇JDK    BiSheng

三.修改JVM启动参数

增加-XX:NativeMemoryTracking=detail

四.监控内存增量

新进程id是39239

#jcmd 39239 VM.native_memory baseline

#jcmd 39239 VM.native_memory summary.diff scale=MB

Native Memory Tracking: end time is 2025-07-14 14:04:16, elapsed time is 7 secs

Total: reserved=29932MB +3MB, committed=28594MB +3MB

-                 Java Heap (reserved=16384MB, committed=16384MB)
                            (mmap: reserved=16384MB, committed=16384MB)

-                     Class (reserved=2108MB, committed=1209MB)
                            (classes #163211)
                            (malloc=26MB #349615 +53)
                            (mmap: reserved=2082MB, committed=1183MB)

-                    Thread (reserved=9325MB +6MB, committed=9325MB +6MB)
                            (thread #4653 +3)
                            (stack: reserved=9304MB +6MB, committed=9304MB +6MB)
                            (malloc=16MB #27912 +18)
                            (arena=5MB #9289 +6)

-                      Code (reserved=782MB, committed=375MB)
                            (malloc=59MB #107139 +53)
                            (mmap: reserved=723MB, committed=316MB)

-                        GC (reserved=730MB, committed=730MB)
                            (malloc=90MB #189800 +97)
                            (mmap: reserved=640MB, committed=640MB)

-                  Compiler (reserved=4MB -5MB, committed=4MB -5MB)
                            (malloc=4MB #8031)
                            (arena=0MB -5 #18 -8)

-                  Internal (reserved=351MB, committed=351MB)
                            (malloc=351MB #297549 +35)

-                    Symbol (reserved=170MB, committed=170MB)
                            (malloc=167MB #1830640)
                            (arena=3MB #1)

-    Native Memory Tracking (reserved=45MB +1MB, committed=45MB +1MB)
                            (malloc=2MB +1MB #26229 +9919)
                            (tracking overhead=43MB)

-               Arena Chunk (reserved=1MB +1MB, committed=1MB +1MB)
                            (malloc=1MB +1MB)

-                   Unknown (reserved=32MB, committed=0MB)
                            (mmap: reserved=32MB, committed=0MB)

我们重点看这里:

-                    Thread (reserved=9325MB +6MB, committed=9325MB +6MB)
                            (thread #4653 +3)
                            (stack: reserved=9304MB +6MB, committed=9304MB +6MB)
                            (malloc=16MB #27912 +18)
                            (arena=5MB #9289 +6)

很明显产生了4653个线程,一共申请9.3G内存。很明显每个线程是2M线程栈大小。

下面对Native Memory Tracking进行详细解释说明:

我们可以逐项分析:

  1. 总体内存 (Total):

    • reserved=29932MB +3MB: JVM 向操作系统申请的虚拟地址空间总量约 29.9GB,比上次报告增加了 3MB。

    • committed=28594MB +3MB: JVM 实际向操作系统申请并保证可用的物理内存(或交换空间)总量约 28.6GB,比上次报告增加了 3MB。

    • 分析: 总保留和提交内存都增加了 3MB。这个增量本身很小,通常不是问题。关键在于这个增长是短暂的波动还是持续增长的趋势。需要持续监控看这个数字是否会稳定下来或继续增长。

  2. Java 堆 (Java Heap):

    • reserved=16384MB, committed=16384MB: 堆最大可保留 16GB,且当前已完全提交(即操作系统已保证这 16GB 物理内存可用)。

    • 分析: 这是完全正常且常见的配置。堆大小(-Xmx)被设置为 16GB,并且 JVM 在启动时或需要时一次性提交了全部堆空间。没有增长迹象。

  3. 类元数据 (Class):

    • reserved=2108MB, committed=1209MB: 为类元数据保留约 2.1GB 地址空间,实际使用约 1.2GB。

    • classes #163211: 已加载了 163,211 个类。

    • malloc=26MB #349615 +53: 通过 malloc 分配了 26MB 内存,分配次数增加了 53 次。

    • 分析: 加载了非常多的类(16万+),这在大型应用(如应用服务器运行多个应用、使用大量库、使用动态字节码生成技术)中是可能的。1.2GB 的元空间使用量对于加载如此多的类来说不算特别异常,但需要监控其增长趋势(+53 次分配表明还在加载新类或元数据)。如果应用已完成主要初始化,元空间使用应该趋于稳定。如果持续增长,需检查是否有类加载器泄漏。

  4. 线程 (Thread):

    • reserved=9325MB +6MB, committed=9325MB +6MB: 为线程栈保留和提交了约 9.3GB 内存,比上次增加了 6MB

    • thread #4653 +3: 当前有 4653 个活跃线程比上次增加了 3 个

    • stack: reserved=9304MB +6MB, committed=9304MB +6MB: 线程栈本身占用了增加的那 6MB。

    • malloc=16MB #27912 +18: 线程相关结构通过 malloc 分配了 16MB,分配次数增加了 18 次。

    • 分析:这是报告中最值得关注的点!

      • 4653 个线程是一个非常巨大的数量。虽然现代操作系统和 JVM 能处理很多线程,但这会带来显著的调度开销、内存开销(每个线程栈默认通常是 1MB,这里总栈空间 9.3GB 也印证了平均栈大小在 2MB 左右)和上下文切换成本。

      • 线程数还在增加+3)。如果这是持续性的增长(例如,线程池未正确配置导致线程泄漏),这就是一个严重问题,最终会导致资源耗尽或性能急剧下降。

      • 即使线程数稳定,4653 个线程对于大多数应用来说也极不寻常,可能表明:

        • 应用设计问题(过度依赖线程而非异步/NIO)。

        • 线程池配置严重错误(核心/最大线程数设置过高)。

        • 存在线程泄漏(线程创建后未正确结束/回收)。

  5. 代码缓存 (Code):

    • reserved=782MB, committed=375MB: 为 JIT 编译的代码保留 782MB,实际使用 375MB。

    • malloc=59MB #107139 +53: 通过 malloc 分配了 59MB,分配次数增加了 53 次。

    • 分析: 375MB 的代码缓存使用量对于大型、长时间运行、执行大量 JIT 编译的应用来说是可能的。+53 次分配表明 JIT 活动仍在进行(编译新方法或去优化/重新编译)。需要监控其是否最终趋于稳定。如果持续增长,可能表明有大量的动态代码生成或频繁的重新编译。

  6. 垃圾收集 (GC):

    • reserved=730MB, committed=730MB: GC 算法和数据结构使用 730MB。

    • malloc=90MB #189800 +97: 通过 malloc 分配了 90MB,分配次数增加了 97 次。

    • 分析: GC 本身需要内存来管理堆和元空间等。730MB 对于管理一个 16GB 堆和 1.2GB 元空间的应用来说在合理范围内+97 次分配表明 GC 活动期间有内部结构的变化或调整。只要总量稳定,通常不是问题。

  7. 编译器 (Compiler):

    • reserved=4MB -5MB, committed=4MB -5MB: 保留和提交内存都减少了 5MB(现在是 4MB)。

    • malloc=4MB #8031: 通过 malloc 分配了 4MB。

    • arena=0MB -5 #18 -8: Arena 分配减少了 5MB(现在是 0MB),分配次数减少了 8 次(现在是 18 次)。

    • 分析: 编译器使用的内存减少了。这通常是良性的,可能是编译器临时工作区释放了内存。没有明显问题。

  8. 内部使用 (Internal):

    • reserved=351MB, committed=351MB: JVM 内部操作使用 351MB。

    • malloc=351MB #297549 +35: 全部通过 malloc 分配,分配次数增加了 35 次。

    • 分析: 这个类别比较宽泛,包含命令行参数、性能数据、JVMTI 代理等。351MB 的大小不算小,但在大型应用中也是可能的。+35 次分配表明内部有活动,但总量没变,通常问题不大。

  9. 符号表 (Symbol):

    • reserved=170MB, committed=170MB: 用于符号(如字段名、方法名、类名等)的内存。

    • malloc=167MB #1830640: 大部分通过 malloc 分配。

    • 分析: 170MB 与加载的巨量类(163211)是相符的。没有增长迹象。稳定。

  10. Native Memory Tracking (NMT):

    • reserved=45MB +1MB, committed=45MB +1MB: NMT 自身开销约 45MB,增加了 1MB。

    • malloc=2MB +1MB #26229 +9919: 通过 malloc 分配了 2MB(增加了 1MB),分配次数大幅增加(+9919 次)。

    • tracking overhead=43MB: 跟踪开销本身占 43MB。

    • 分析: NMT 本身有显著开销(43MB + 2MB malloc),这是开启 NMT 的代价。+1MB 和 +9919 次分配表明在此期间 NMT 记录了大量的内存活动。这是开启 NMT 监控的正常现象。

  11. Arena Chunk:

    • reserved=1MB +1MB, committed=1MB +1MB: 通过 malloc 分配了 1MB(增加了 1MB)。

    • 分析: Arenas 是 JVM 内部使用的一种高效的内存分配方式。1MB 的增量很小,通常不是问题。可能是临时工作区。

  12. 未知 (Unknown):

    • reserved=32MB, committed=0MB: 保留了 32MB 地址空间但未提交任何物理内存。

    • 分析: 这是 JVM 预留但尚未使用的地址空间。committed=0MB 意味着它没有消耗实际的物理内存或交换空间。完全正常。可能是为了未来可能的对齐或分配预留的空间。

结论与建议:

  1. 最关键的警示信号是线程数 (4653 个且还在增加 +3): 这是极不寻常且潜在高风险的。你需要立即调查

    • 为什么会有这么多线程?检查应用代码和配置的线程池(核心线程数、最大线程数、队列大小、拒绝策略)。

    • 是否存在线程泄漏?使用 jstack <pid> 或可视化工具(VisualVM, YourKit, JMC)分析线程堆栈,查看线程名称和状态,找出大量重复的或僵死的线程。

    • 应用的架构是否合理?是否过度依赖阻塞式 I/O 和同步线程?考虑使用异步/NIO(如 Netty, Vert.x)来减少线程需求。

    • 高线程数会消耗大量内存(栈)、增加 CPU 调度开销、并可能导致不稳定。这是需要优先处理的问题。

  2. 类加载数量巨大 (163211) 和元空间使用量 (1.2GB): 虽然对于特定的大型应用可能“正常”,但仍需监控其增长趋势(NMT 报告显示 +53 次分配)。如果应用已稳定运行,元空间使用量应趋于平稳。持续增长可能指向类加载器泄漏。

  3. 代码缓存 (375MB) 和 GC 开销 (730MB): 大小在管理 16GB 堆和大量类的上下文中属于合理范围上限。需要关注其长期稳定性。代码缓存的增长(+53 次分配)表明 JIT 仍在活动。

  4. 总内存小幅增长 (+3MB): 单独看这个小幅增长不是问题,但需要结合线程增长和其他区域的增长趋势来看。如果这是持续监控中的一次快照,关键是看长期趋势

建议行动:

  1. 立即分析线程情况: 使用 jstack 或 Profiler 工具抓取线程转储,分析线程名称、状态和堆栈,找出线程数爆炸的原因。

  2. 持续监控 NMT: 定期(例如每分钟)或在关键操作前后生成 NMT 报告 (jcmd <pid> VM.native_memory [detail|summary|baseline|diff])。关注:

    • Total reserved/committed 的长期趋势。

    • Thread 的数量和内存变化趋势。

    • Class 的 committed 内存和加载类数的变化趋势。

    • Code 的 committed 内存变化趋势。

  3. 监控应用性能: 关注 CPU 使用率(尤其是系统 CPU sy)、GC 暂停时间、吞吐量等指标,高线程数通常会导致这些指标恶化。

  4. 调整(如果确认是问题):

    • 修复线程泄漏或优化线程池配置。

    • 如果元空间持续增长,检查并修复类加载器泄漏。

    • 根据应用行为,考虑调整 -XX:ReservedCodeCacheSize(如果代码缓存持续增长并达到上限)。

    • 评估是否真的需要 16GB 的固定堆(-Xms=-Xmx)。如果堆使用率长期远低于 16GB,适当降低 -Xmx 可以节省大量内存。但如果使用率接近 16GB,则保持是合理的。

总结: 这份 NMT 报告显示超高且仍在增长的线程数是最大的异常点和风险点。类加载数量和部分区域的内存使用量偏高,但需要结合应用类型判断是否在预期范围内。必须优先调查并解决线程数量异常的问题,并持续监控内存使用趋势。

五.查看详细的VM.native_memory detail

我们可以使用下面的命令查看详细的内存申请分布数据

#jcmd 39239 VM.native_memory detail scale=MB

我们只取其中线程栈分配相关内容

[0x0000fff953a00000 - 0x0000fff953c00000] reserved and committed 2MB for Thread Stack from
    [0x0000ffff94cddb44] java_start(Thread*)+0xfc
    [0x0000ffff952882a8]
 
[0x0000fff953c00000 - 0x0000fff953e00000] reserved and committed 2MB for Thread Stack from
    [0x0000ffff94cddb44] java_start(Thread*)+0xfc
    [0x0000ffff952882a8]
 
[0x0000fff953e00000 - 0x0000fff954000000] reserved and committed 2MB for Thread Stack from
    [0x0000ffff94cddb44] java_start(Thread*)+0xfc
    [0x0000ffff952882a8]
 
[0x0000fff954000000 - 0x0000fff954200000] reserved 2MB for Class from
    [0x0000ffff94ea43d0] ReservedSpace::ReservedSpace(unsigned long, unsigned long, bool, char*, unsigned long)+0x218
    [0x0000ffff94c48c24] VirtualSpaceNode::VirtualSpaceNode(bool, unsigned long)+0x1b4
    [0x0000ffff94c49a14] VirtualSpaceList::create_new_virtual_space(unsigned long) [clone .part.96]+0x3c
    [0x0000ffff94c4a548] VirtualSpaceList::get_new_chunk(unsigned long, unsigned long)+0x578

	[0x0000fff954100000 - 0x0000fff954180000] committed 1MB from
            [0x0000ffff94ea5d20] VirtualSpace::expand_by(unsigned long, bool)+0x1c0
            [0x0000ffff94c4a364] VirtualSpaceList::get_new_chunk(unsigned long, unsigned long)+0x394
            [0x0000ffff94c4ab94] SpaceManager::grow_and_allocate(unsigned long)+0x1b4
            [0x0000ffff94c4b004] SpaceManager::allocate(unsigned long)+0x16c

	[0x0000fff954040000 - 0x0000fff9540c0000] committed 1MB from
            [0x0000ffff94ea5d20] VirtualSpace::expand_by(unsigned long, bool)+0x1c0
            [0x0000ffff94c4a364] VirtualSpaceList::get_new_chunk(unsigned long, unsigned long)+0x394
            [0x0000ffff94c4ab94] SpaceManager::grow_and_allocate(unsigned long)+0x1b4
            [0x0000ffff94c4b004] SpaceManager::allocate(unsigned long)+0x16c
 
[0x0000fff954200000 - 0x0000fff954400000] reserved and committed 2MB for Thread Stack from
    [0x0000ffff94cddb44] java_start(Thread*)+0xfc
    [0x0000ffff952882a8]
 
[0x0000fff954400000 - 0x0000fff954600000] reserved and committed 2MB for Thread Stack from
    [0x0000ffff94cddb44] java_start(Thread*)+0xfc
    [0x0000ffff952882a8]

很明显从提供的 Native Memory Tracking (NMT) 详细输出中可以看出,每个线程栈的大小为 2MB  ,reserved and committed 2MB for Thread Stack from

六.查询配置 JVM 线程栈大小的位置

1. 查看当前线程栈大小的命令

# 使用 jinfo 查看进程参数(需替换 <pid> 为实际进程 ID)
jinfo -flags <pid> | grep -E '\-Xss|\-XX:ThreadStackSize'

jcmd <pid>VM.flags | grep ThreadStackSize

  • 如果输出中包含 -Xss 或 -XX:ThreadStackSize,则显示当前配置值(如 -Xss2m 表示 2MB)。

  • 若无输出,表示使用 JVM 默认值(Linux x64 默认通常为 1MB,但我们的场景显示为 2MB)。

 我们在服务器上用上述命令没有得到输出。

没有显式配置线程栈大小参数(-Xss 或 -XX:ThreadStackSize。这意味着 JVM 使用了默认的线程栈大小

结论:

  1. 当前线程栈大小是 JVM 默认值(未通过 -Xss 或 -XX:ThreadStackSize 显式配置)。

  2. 根据 NMT 报告(每个线程栈分配 2MB)

为什么 NMT 显示 2MB?

虽然标准 Linux x64 JDK 默认栈大小是 1MB,但您的环境可能是以下情况:

  1. JDK 变体差异

    • 如果使用 OpenJDK 的服务端优化版本(如 Shenandoah JDK),默认栈大小可能为 2MB。

  2. 容器环境

    • 在 Kubernetes/Docker 中,某些基础镜像会修改 JVM 默认参数。

  3. JVM 版本更新

    • 较新的 JDK 版本(如 JDK 17+)在某些场景下会调整默认值。

 由于我们使用的是毕昇JDK,所以是OpenJDK的差异导致。

 七.显式配置线程栈大小

在 JVM 启动参数中添加: 

# 设置为 1MB(常用值)
-Xss1m

# 或明确指定 KB 单位
-XX:ThreadStackSize=1024

注意:当前线程数 4653 严重过高(即使栈设为 1MB,总栈内存仍高达 4.6GB)。

所以我们使用这两步对线程进行控制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值