https://www.modb.pro/db/557714

从前面几篇文章,我们了解了 NMT 的基础知识以及 NMT 追踪区域分析的相关内容,本篇文章将为大家介绍一下使用 NMT 协助排查内存问题的案例。

6.使用 NMT 协助排查内存问题案例

我们在搞清楚 NMT 追踪的 JVM 各部分的内存分配之后,就可以比较轻松的协助排查定位内存问题或者调整合适的参数。

可以在 JVM 运行时使用 jcmd <pid> VM.native_memory baseline
创建基线,经过一段时间的运行后再使用 jcmd <pid> VM.native_memory summary.diff/detail.diff
命令,就可以很直观地观察出这段时间 JVM 进程使用的内存一共增长了多少,各部分使用的内存分别增长了多少,可以很方便的将问题定位到某一具体的区域。

比如我们看到 MetaSpace 的内存增长异常,可以结合 MAT 等工具查看是否类加载器数量异常、是否类重复加载、reflect 的 inflation 参数设置是否合理;如果 Symbol 内存增长异常,可以查看项目 String.intern 是否使用正常;如果 Thread 使用内存过多,考虑是否可以适当调整线程堆栈大小等等。

案例一:虚高的 VIRT 内存

我们还记得前文(NMT 内存 & OS 内存概念差异性章节)中使用 top 命令查看启动的 JVM 进程,仔细观察会发现一个比较虚高的 VIRT 内存(10.7g),我们使用 NMT 追踪的 Total: reserved 才 2813709KB(2.7g),这多出来的这么多虚拟内存是从何而来呢?

top

   PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
 27420 douyiwa+    20  0   10.7g   697560  17596 S 100.0  0.3      0:18.79 java

Native Memory Tracking:

  Total: reserved=2813077KB, committed=1496981KB

使用 pmap -X <pid>
 观察内存情况:

27420:   java -Xmx1G -Xms1G -XX:+UseG1GC -XX:MaxMetaspaceSize=256M -XX:MaxDirectMemorySize=256M -XX:ReservedCodeCacheSize=256M -XX:NativeMemoryTracking=detail -jar nmtTest.jar
     Address Perm   Offset Device     Inode     Size    Rss    Pss Referenced Anonymous LazyFree ShmemPmdMapped Shared_Hugetlb Private_Hugetlb Swap SwapPss Locked Mapping
    c0000000 rw-p 00000000  00:00         0  1049088 637236 637236     637236    637236        0              0              0               0    0       0      0 
   100080000 ---p 00000000  00:00         0  1048064      0      0          0         0        0              0              0               0    0       0      0 
aaaaea835000 r-xp 00000000  fd:02  45613083        4      4      4          4         0        0              0              0               0    0       0      0 java
aaaaea854000 r--p 0000f000  fd:02  45613083        4      4      4          4         4        0              0              0               0    0       0      0 java
aaaaea855000 rw-p 00010000  fd:02  45613083        4      4      4          4         4        0              0              0               0    0       0      0 java
aaab071af000 rw-p 00000000  00:00         0      304    108    108        108       108        0              0              0               0    0       0      0 [heap]
fffd60000000 rw-p 00000000  00:00         0      132      4      4          4         4        0              0              0               0    0       0      0 
fffd60021000 ---p 00000000  00:00         0    65404      0      0          0         0        0              0              0               0    0       0      0 
fffd68000000 rw-p 00000000  00:00         0      132      8      8          8         8        0              0              0               0    0       0      0 
fffd68021000 ---p 00000000  00:00         0    65404      0      0          0         0        0              0              0               0    0       0      0 
fffd6c000000 rw-p 00000000  00:00         0      132      4      4          4         4        0              0              0               0    0       0      0 
fffd6c021000 ---p 00000000  00:00         0    65404      0      0          0         0        0              0              0               0    0       0      0 
fffd70000000 rw-p 00000000  00:00         0      132     40     40         40        40        0              0              0               0    0       0      0 
fffd70021000 ---p 00000000  00:00         0    65404      0      0          0         0        0       
......

可以发现多了很多 65404 KB 的内存块(大约 120 个),使用 /proc/<pid>/smaps
观察内存地址:

......
fffd60021000-fffd64000000 ---p 00000000 00:00 0 
Size:              65404 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Rss:                   0 kB
Pss:                   0 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Referenced:            0 kB
Anonymous:             0 kB
LazyFree:              0 kB
AnonHugePages:         0 kB
ShmemPmdMapped:        0 kB
Shared_Hugetlb:        0 kB
Private_Hugetlb:       0 kB
Swap:                  0 kB
SwapPss:               0 kB
Locked:                0 kB
VmFlags: mr mw me nr
......

对照 NMT 的情况,我们发现如 fffd60021000-fffd64000000 这种 65404 KB 的内存是并没有被 NMT 追踪到的。这是因为在 JVM 进程中,除了 JVM 进程自己 mmap 的内存(如 Java Heap,和用户进程空间的 Heap 并不是一个概念)外,JVM 还直接使用了类库的函数来分配一些数据,如使用 Glibc 的 malloc/free (也是通过 brk/mmap 的方式):

既然 JVM 使用了 Glibc 的 malloc/free,就不得不提及 malloc 的机制,早期版本的 malloc 只有一个 arena(分配区),每次分配时都要对分配区加锁,分配完成之后再释放,这就导致了多线程的情况下竞争比较激烈。所以 malloc 改动了其分配机制,甚至有了 arena per-thread 的模式,即如果在一个线程中首次调用 malloc,则创建一个新的 arena,而不是去查看前面的锁是否会发生竞争,对于一定数量的线程可以避免竞争在自己的 arena 上工作。

arena 的数量限制在 32 位系统上是 2 * CPU 核心数,64 位系统上是 8 * CPU 核心数,当然我们也可以使用 MALLOC_ARENA_MAX (Linux 环境变量,详情可以查看 mallopt(3)[1])来控制。查看发现运行 JVM 进程的环境 CPU 信息(物理 CPU 核数):Core(s) per socket: 64

我们给当前环境设置 MALLOC_ARENA_MAX=2,重启 JVM 进程,查看使用情况:

top

   PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
 36319 douyiwa+  20   0 3108340 690872  17828 S 100.0   0.3   0:07.61 java

虚高的 VIRT 内存已经降下来了,继续查看 pmap/smaps 会发现众多的 65404 KB 的内存空间也消失了(120 * 65404 KB = 7848480 KB 正好对应了 10.7g - 3108340 KB 的内存,即 VIRT 降低的内存)。为什么我们的 JVM 进程会使用如此多的 arena 呢?因为我们在启动 JVM 进程的时候,并没有手动去设置一些进程的数目,如:CICompilerCount(编译线程数)、ConcGCThreads/ParallelGCThreads(并发 GC 线程数)、G1ConcRefinementThreads(G1 Refine 线程数)等等。这些参数大多数根据当前机器的 CPU 核数去计算默认值,使用 jinfo -flags <pid>
查看机器参数发现:

-XX:CICompilerCount=18 
-XX:ConcGCThreads=11 
-XX:G1ConcRefinementThreads=43

这些线程数目都是比较大的,我们也可以不修改 MALLOC_ARENA_MAX 的数量,而通过参数减小线程的数量来减少 arena 的数量。

Glibc 的 malloc 有时会出现碎片问题,可以使用 jemalloc/tcmalloc 等替代 Glibc。

案例二:堆外内存的排查

有时候我们会发现,Java 堆、MetaSpace 等区域是比较正常的,但是 JVM 进程整体的内存却在不停的增长,此时我们就可以使用 NMT 的 baseline & diff 功能来观察究竟是哪块区域内存一直增长。

比如在一次案例中发现:

Native Memory Tracking:
Total: reserved=8149334KB +1535794KB, committed=6999194KB +1590490KB
  ......
-                  Internal (reserved=1723321KB +1472458KB, committed=1723321KB +1472458KB)
                            (malloc=1723289KB +1472458KB #109094 +47573)
                            (mmap: reserved=32KB, committed=32KB)
  ......
[0x00007fceb806607a] Unsafe_AllocateMemory+0x17a
[0x00007fcea1d24e68]
                             (malloc=1485579KB type=Internal +1455929KB #2511 +2277)
  ......

我们可以确认内存 1590490KB 的增长,基本上都是由 Internal 的 Unsafe_AllocateMemory 所分配的,此时可以优先考虑 NIO 中 ByteBuffer.allocateDirect DirectByteBuffer FileChannel.map 等使用方式是不是出现了泄漏,可以使用 MAT 查看 DirectByteBuffer 对象的数量是否异常,并可以使用 -XX:MaxDirectMemorySize 来限制 Direct 的大小。设置 -XX:MaxDirectMemorySize 之后,进程异常的内存增长停止,但是 GC 频率变高,查看 GC 日志发现:.887+0800: 238210.127: [Full GC (System.gc()) 1175M->255M(3878),0.8370418 secs]
。FullGC 的频率大大增加,并且基本上都是由 System.gc() 显式调用引起的(HotSpot中的System.gc()为 FulGC),查看 DirectByteBuffer 相关逻辑:

# DirectByteBuffer.java
  
DirectByteBuffer(int cap) {                   // package-private
        ......
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }
  
# Bits.java
  
  static void reserveMemory(long size, int cap) {
        ......
        System.gc();
        ......
    }

DirectByteBuffer 在 unsafe.allocateMemory(size) 之前会先去做一个 Bits.reserveMemory(size, cap) 的操作,Bits.reserveMemory 会显式的调用 System.gc() 来尝试回收内存,看到这里基本可以确认为 DirectByteBuffer 的问题,排查业务代码,果然发现一处 ByteBufferStream 使用了 ByteBuffer.allocateDirect 的方式而流一直未关闭释放内存,修正后内存增长与 GC 频率皆恢复正常。

参考

  1. https://man7.org/linux/man-pages/man3/mallopt.3.html

[转帖]Native Memory Tracking 详解(4):使用 NMT 协助排查内存问题案例的更多相关文章

  1. 详解Native Memory Tracking之追踪区域分析

    摘要:本篇图文将介绍追踪区域的内存类型以及 NMT 无法追踪的内存. 本文分享自华为云社区<[技术剖析]17. Native Memory Tracking 详解(3)追踪区域分析(二)> ...

  2. 带你认识JDK8中超nice的Native Memory Tracking

    摘要:从 OpenJDK8 起有了一个很 nice 的虚拟机内部功能: Native Memory Tracking (NMT). 本文分享自华为云社区<Native Memory Tracki ...

  3. 全网最硬核 JVM 内存解析 - 1.从 Native Memory Tracking 说起

    个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判.如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 i ...

  4. C++的性能C#的产能?! - .Net Native 系列《二》:.NET Native开发流程详解

    之前一文<c++的性能, c#的产能?!鱼和熊掌可以兼得,.NET NATIVE初窥> 获得很多朋友支持和鼓励,也更让我坚定做这项技术的推广者,希望能让更多的朋友了解这项技术,于是先从官方 ...

  5. 详解js变量、作用域及内存

    详解js变量.作用域及内存 来源:伯乐在线 作者:trigkit4       原文出处: trigkit4    基本类型值有:undefined,NUll,Boolean,Number和Strin ...

  6. java native本地方法详解(转)

    文章链接出处: 详解native方法的使用 自己实现一个Native方法的调用 JNI 开始本篇的内容之前,首先要讲一下JNI.Java很好,使用的人很多.应用极 广,但是Java不是完美的.Java ...

  7. [转帖]Tomcat目录结构详解

    Tomcat目录结构详解 https://www.cnblogs.com/veggiegfei/p/8474484.html 之前应该是知道一点 但是没有这么系统 感谢原作者的描述. 1.bin: 该 ...

  8. [转帖]Linux chattr 命令详解

    Linux chattr 命令详解 https://www.cnblogs.com/ftl1012/p/chattr.html 常见命令参数 1 2 3 4 5 6 7 8 9 10 11 12 A: ...

  9. [转帖]ECC公钥格式详解

    ECC公钥格式详解 https://www.cnblogs.com/xinzhao/p/8963724.html 本文首先介绍公钥格式相关的若干概念/技术,随后以示例的方式剖析DER格式的ECC公钥, ...

  10. [转帖]【Oracle】详解Oracle中NLS_LANG变量的使用

    [Oracle]详解Oracle中NLS_LANG变量的使用 https://www.cnblogs.com/HDK2016/p/6880560.html NLS_LANG=LANGUAGE_TERR ...

随机推荐

  1. JavaScript异步编程4——Promise错误处理

    目录 1. 概述 2. 详论 3. 参考 1. 概述 在上一篇文章<JavaScript异步编程3--Promise的链式使用>中,通过Promise的链式使用,避免程序中多次嵌套回调(回 ...

  2. MindSpore!这款刚刚开源的深度学习框架我爱了!

    [摘要] 本文主要通过两个实际应用案例:一是基于本地 Jupyter Notebook 的 MNIST 手写数据识别:二是基于华为云服务器的 CIFAR-10 图像分类,对开源框架 MindSpore ...

  3. 业务并发度不够,数仓的CN可以来帮忙

    摘要: CN全称协调节点(Coordinator Node),是和用户关系最密切也是DWS内部非常重要的一个组件,它负责提供外部应用接口.优化全局执行计划.向Datanode分发执行计划,以及汇总.处 ...

  4. 火山引擎A/B测试推出智能流量调优实验,助力汽车行业破局营销困境

    更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 2023年是汽车行业挑战加剧的一年,在这样一个变革时期,多家车企都在进行创新技术和战略调整,实现灵活的科学决策,在 ...

  5. PPT 配色方法

    https://www.bilibili.com/video/BV1ha411g7f5/?p=10 https://dribbble.com/ 问题1:配色太多,主题色混乱 一个主色,两辅色 问题2: ...

  6. Django rest_framework使用自定义异常

    完整代码 https://gitee.com/mom925/django-system 在settings.py中配置 REST_FRAMEWORK = { "EXCEPTION_HANDL ...

  7. 【算法学习笔记】区间DP

    基本的知识点引用自 OI wiki,感谢社区的帮助 什么是区间 DP? 区间类动态规划是线性动态规划的扩展,它在分阶段地划分问题时,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来有很大的关系.令 ...

  8. 消息服务 + Serverless 函数计算如何助力企业降本提效?

    作者 | 柳下 背景介绍 消息队列服务(下文均以 Message Service 命名)作为云计算 PaaS 领域的基础设施之一,其高并发.削峰填谷的特性愈发受到开发者关注.Message Servi ...

  9. 定向减免!函数计算让 ETL 数据加工更简单

    业内较为常见的高频短时 ETL 数据加工场景,即频率高时延短,一般费用大头均在函数调用次数上,推荐方案一般为攒批处理,高额的计算成本往往令用户感到头疼,函数计算推出定向减免方案,让 ETL数据加工更简 ...

  10. 结构体Struct、联合体Union与类Class

    结构体Struct.联合体Union与类Class 1. Struct/Class struct能包含成员函数吗? 能! struct能继承吗? 能!! struct能实现多态吗? 能!!! 1.1 ...