介绍一下使用NMT协助排查内存问题的案例

从前面几篇文章,我们了解了 nmt 的基础知识以及 nmt 追踪区域分析的相关内容,本篇文章将为大家介绍一下使用 nmt 协助排查内存问题的案例。
6.使用 nmt 协助排查内存问题案例
我们在搞清楚 nmt 追踪的 jvm 各部分的内存分配之后,就可以比较轻松的协助排查定位内存问题或者调整合适的参数。
可以在 jvm 运行时使用 jcmd vm.native_memory baseline 创建基线,经过一段时间的运行后再使用 jcmd 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 javanative memory tracking:  total: reserved=2813077kb, committed=1496981kb  
使用 pmap -x  观察内存情况:
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 javaaaaaea854000 r--p 0000f000  fd:02  45613083        4      4      4          4         4        0              0              0               0    0       0      0 javaaaaaea855000 rw-p 00010000  fd:02  45613083        4      4      4          4         4        0              0              0               0    0       0      0 javaaaab071af000 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//smaps 观察内存地址:
......fffd60021000-fffd64000000 ---p 00000000 00:00 0 size:              65404 kbkernelpagesize:        4 kbmmupagesize:           4 kbrss:                   0 kbpss:                   0 kbshared_clean:          0 kbshared_dirty:          0 kbprivate_clean:         0 kbprivate_dirty:         0 kbreferenced:            0 kbanonymous:             0 kblazyfree:              0 kbanonhugepages:         0 kbshmempmdmapped:        0 kbshared_hugetlb:        0 kbprivate_hugetlb:       0 kbswap:                  0 kbswappss:               0 kblocked:                0 kbvmflags: 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 查看机器参数发现:
-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 频率皆恢复正常。


华为P20 Plus什么时候出?或配结构光人脸识别 亮点是类似iPhone X的刘海屏设计
智能家居生态哪一部分比较重要
锰基正极材料取得重要进展 钠离子电池有望取代锂离子电池
DP7361 是一款立体声六通道线性输出的数模转换器-兼容CS4361
华为云5G+云+AI技术将赋能大型政企智能化升级
介绍一下使用NMT协助排查内存问题的案例
威联通推新款PCIe无线扩展卡,Wi-Fi最大速度是Wi-Fi5的2.8倍
华为正式进军韩国智能手机市场 将推出荣耀6
苹果担忧成真 FBI的iOS破解工具遭泄露
研华许杰弘:双向奔赴,Edge AI正在与产业深度融合
剖析电阻可靠性相关的参数2
机器人对你来说是一个什么样的存在?不会被机器人淘汰吗?
燃气管道泄漏监测报警系统的功能及原理说明
凸轮控制器操作手柄控制电动机的原理解析
“共享开发板”活动上线!
三极管组成的简易RS-232C接口电路
阿斯顿·马丁DBX,2019年将量产,一台高低盘的跨界车,极具科技感,史上最贵suv!
自动化仪器仪表的功能特点和具体应用
基于在线分析仪器的测试方法与测试技术
虽然小米最近销售平淡,但雷军说智能硬件年收入会超150亿