每日一博 | 一个导致 JVM 物理内存消耗大的 Bug
本文来自: PerfMa技术社区
概述
最近我们公司在帮一个客户查一个JVM的问题(JDK1.8.0_191-b12),发现一个系统老是被OS Kill掉,是内存泄露导致的。在查的过程中,阴差阳错地发现了JVM另外的一个Bug。这个Bug可能会导致大量物理内存被使用,我们已经反馈给了社区,并得到快速反馈,预计在OpenJDK8最新版中发布(JDK11中也存在这个问题)。
PS:用户的那个问题最终也解决了,定位下来算是C2的一个设计缺陷导致大量内存被使用,安全性上没有得到保障。
找出消耗大内存的线程
接下来主要分享下这个BUG的发现过程,先要客户实时跟踪进程的情况,当内存使用明显上升的时候,通过/proc/<pid>/smaps,看到了不少64MB的内存分配,Rss也基本消耗完了。
7fd690000000-7fd693f23000 rw-p 00000000 00:00 0 Size: 64652 kB Rss: 64652 kB Pss: 64652 kB Shared_Clean: 0 kB Shared_Dirty: 0 kB Private_Clean: 0 kB Private_Dirty: 64652 kB Referenced: 64652 kB Anonymous: 64652 kB AnonHugePages: 0 kB Swap: 0 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Locked: 0 kB VmFlags: rd wr mr mw me nr sd 7fd693f23000-7fd694000000 ---p 00000000 00:00 0 Size: 884 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 AnonHugePages: 0 kB Swap: 0 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Locked: 0 kB VmFlags: mr mw me nr sd
再通过strace命令跟踪了下系统调用,再回到上面的虚拟地址,我们找到了相关的mmap系统调用
[pid 71] 13:34:41.982589 mmap(0x7fd690000000, 67108864, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = 0x7fd690000000 <0.000107>
执行mmap的线程是71号线程,接着通过jstack把线程dump出来,找到了对应的线程其实是C2 CompilerThread0
"C2 CompilerThread0" #39 daemon prio=9 os_prio=0 tid=0x00007fd8acebb000 nid=0x47 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE
最后再grep了一下strace的输出,果然看到这个线程在大量的进行内存分配,总共有2G多。
经典的64M问题
对于64M的问题,是一个非常经典的问题,在JVM中并没有这种大量分配64M大小的逻辑,因此可以排除JVM特定意义的分配。这其实是glibc里针对malloc函数分配内存的一种机制,glibc从2.10开始提供的一种机制,为了分配内存更加高效,glibc提供了arena的机制,默认情况下在64位下每一个arena的大小是64M,下面是64M的计算逻辑,其中sizeof(long)为8
define DEFAULT_MMAP_THRESHOLD_MAX (4 * 1024 * 1024 * sizeof(long)) define HEAP_MAX_SIZE (2 * DEFAULT_MMAP_THRESHOLD_MAX) p2 = (char *) MMAP (aligned_heap_area, HEAP_MAX_SIZE, PROT_NONE, MAP_NORESERVE);
一个进程最多能分配的arena个数在64位下是8 * core,32位下是2 * core个
#define NARENAS_FROM_NCORES(n) ((n) * (sizeof (long) == 4 ? 2 : 8)) { int n = __get_nprocs (); if (n >= 1) narenas_limit = NARENAS_FROM_NCORES (n); else /* We have no information about the system. Assume two cores. */ narenas_limit = NARENAS_FROM_NCORES (2); }
这种分配机制的好处,主要是应对多线程的环境,为每个核留有几个64M的缓存块,这样线程在分配内存的时候因为没有锁而变得更高效,如果达到上限了就会去慢速的main_arena里分配了。
可以通过设置环境变量MALLOC_ARENA_MAX来设置64M块的个数,当我们设置为1的时候就会发现这些64M的内存块都没有了,然后都集中分配到一个大区域了,也就是main_arena,说明这个参数生效了。
无意的发现
再回过来思考为什么C2线程会出现大于2G的内存消耗的时候,无意中跟踪C2这块代码发现了如下代码可能会导致大量内存消耗,这个代码的位置是nmethod.cpp的nmethod::metadata_do方法,不过这块如果真的发生的话,肯定不是看到C2的线程大量分配,而是看到VMThread这个线程,因为下面这块代码主要是它执行的。
void nmethod::metadata_do(void f(Metadata*)) { address low_boundary = verified_entry_point(); if (is_not_entrant()) { low_boundary += NativeJump::instruction_size; // %%% Note: On SPARC we patch only a 4-byte trap, not a full NativeJump. // (See comment above.) } { // Visit all immediate references that are embedded in the instruction stream. RelocIterator iter(this, low_boundary); while (iter.next()) { if (iter.type() == relocInfo::metadata_type ) { metadata_Relocation* r = iter.metadata_reloc(); // In this metadata, we must only follow those metadatas directly embedded in // the code. Other metadatas (oop_index>0) are seen as part of // the metadata section below. assert(1 == (r->metadata_is_immediate()) + (r->metadata_addr() >= metadata_begin() && r->metadata_addr() < metadata_end()), “metadata must be found in exactly one place”); if (r->metadata_is_immediate() && r->metadata_value() != NULL) { Metadata* md = r->metadata_value(); if (md != _method) f(md); } } else if (iter.type() == relocInfo::virtual_call_type) { // Check compiledIC holders associated with this nmethod CompiledIC *ic = CompiledIC_at(&iter); if (ic->is_icholder_call()) { CompiledICHolder* cichk = ic->cached_icholder(); f(cichk->holder_metadata()); f(cichk->holder_klass()); } else { Metadata* ic_oop = ic->cached_metadata(); if (ic_oop != NULL) { f(ic_oop); } } } } } inline CompiledIC* CompiledIC_at(RelocIterator* reloc_iter) { assert(reloc_iter->type() == relocInfo::virtual_call_type || reloc_iter->type() == relocInfo::opt_virtual_call_type, "wrong reloc. info"); CompiledIC* c_ic = new CompiledIC(reloc_iter); c_ic->verify(); return c_ic; }
注意上面的CompiledIC *ic = CompiledIC_at(&iter);这段代码,因为CompiledIC是一个ResourceObj,这种资源会在c heap里分配(malloc),不过他们是和线程进行关联的,假如我们在某处代码声明了ResourceMark,那当执行到这里的时候会标记当前的位置,再接下来线程要分配内存的时候如果线程关联的内存不够用,就会malloc一块插进去并被管理起来,否则会实现内存的复用。当ResourceMark析构函数执行的时候,会将之前的位置还原,后面这个线程如果要分配内存又会从这个位置开始复用内存块。注意这里说的内存块和上面的64M内存块不是一个概念。
因为这段代码在while循环里,因此存在非常多次数的重复调用,这样明明在执行完一次之后可以复用内存的地方并不能复用,而可能会导致大量的内存被不断分配。表现起来可能就是物理内存消耗很大,远大于Xmx。
这个修复办法也很简单,就是在CompiledIC *ic = CompiledIC_at(&iter);前加上ResourceMark rm;即可。
这个问题主要发生的场景是针对频繁大量做Class Retransform或者Class Redefine的场景。所以如果系统里有这种agent的时候还是要稍微注意下这个问题。
这个问题发现后我们给社区提了patch,不过后面发现再JDK12中其实已经修复了,但是在之前的版本里的都没有修复,这个问题提交给社区后,有人很快响应了,并可能在OpenJDK1.8.0-212中被fix。
最后在这里也简单提下客户那边的那个问题,之所以C2线程消耗太大,最主要的原因是存在非常大的方法需要编译,而这个编译的过程是需要大量的内存消耗的,正因为如此,才会导致内存突然暴增,所以给大家一个建议,方法不要写太大啦,如果这个方法调用还很频繁,那真的会很悲剧的。
推荐阅读:

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
主流聊天软件中的好友备注和群成员备注是如何实现的?
想了解下大厂对于聊天中的好友备注和群成员备注是如何设计和实现的? 希望有了解的大牛们能帮我简单介绍下 1、好友备注和群成员备注持久层是怎么设计存储的?尤其是群成员备注,比如qq,如果有好友备注那群成员备注就会使用好友的备注,那么这个群成员的数据拉取就很难做缓存,实时拉取的性能又比较差。 2、基于什么样的设计和机制下,实现用户在修改了备注后,单聊和群聊的实时记录和聊天记录中都会同步修改?一般而言消息都是本地化存储,那么这个修改难道要通知app去修改? 希望有了解这块技术细节的大牛,能帮忙解答下这块的疑惑。 主要涉及数据建模,名单拉取性能,修改后的同步问题,历史聊天记录中的备注追溯等
- 下一篇
曾用 AI 算法“智能”涨价的 Uber,疫情重压下关掉“AI 实验室”
5月18日,外媒报道,Uber 宣布因冠状病毒影响了其打车业务,将裁员3000人,并取消多余的项目、关闭数十个孵化器和实验室,其中就包括“AI实验室”。这也意味着,Uber 放弃了四年前发展无人驾驶的计划。对于这些,彭博社称 Uber 是“Narrower Ambitions(没有梦想)”。 “实验室往往是公司出现财务问题时第一个被关掉的。” Uber 首席执行官 Dara Khosrowshahi 在内部信中称,Uber 必须做出这个决定,因为要保证Uber 是一个依靠自身,而不是依赖投资保持增长和创新的公司。简而言之,就是 Uber 现在需要的是能做到盈收的业务,很显然 AI 实验室办不到。据华尔街日报报道,裁掉这些员工和实验室,将为 Uber 节省超过10亿美元的固定成本。 一位来自佐治亚理工学院、研究 AI 的副教授 Mark Riedl 就此事发表评论,“实验室往往是公司出现财务问题时第一个被关掉的。” 实际上,在 AI 实验室被关掉之前,它就被指已经“名存实亡”。2016年,Uber 时任创始人Travis Kalanick 重金聘任人工智能圈的泰斗级人物、纽约大学的教授...
相关文章
文章评论
共有0条评论来说两句吧...