从前面几篇文章,我们了解了 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亿