千家信息网

导致JVM物理内存消耗大的Bug是怎么样的

发表于:2024-11-21 作者:千家信息网编辑
千家信息网最后更新 2024年11月21日,导致JVM物理内存消耗大的Bug是怎么样的,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。概述最近我们公司在帮一个客户查一
千家信息网最后更新 2024年11月21日导致JVM物理内存消耗大的Bug是怎么样的

导致JVM物理内存消耗大的Bug是怎么样的,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。

概述

最近我们公司在帮一个客户查一个JVM的问题(JDK1.8.0_191-b12),发现一个系统老是被OS Kill掉,是内存泄露导致的。在查的过程中,阴差阳错地发现了JVM另外的一个Bug。这个Bug可能会导致大量物理内存被使用,我们已经反馈给了社区,并得到快速反馈,预计在OpenJDK8最新版中发布(JDK11中也存在这个问题)。

PS:用户的那个问题最终也解决了,定位下来算是C2的一个设计缺陷导致大量内存被使用,安全性上没有得到保障。

找出消耗大内存的线程

接下来主要分享下这个BUG的发现过程,先要客户实时跟踪进程的情况,当内存使用明显上升的时候,通过/proc//smaps,看到了不少64MB的内存分配,Rss也基本消耗完了。

7fd690000000-7fd693f23000 rw-p 00000000 00:00 0 Size:              64652 kBRss:               64652 kBPss:               64652 kBShared_Clean:          0 kBShared_Dirty:          0 kBPrivate_Clean:         0 kBPrivate_Dirty:     64652 kBReferenced:        64652 kBAnonymous:         64652 kBAnonHugePages:         0 kBSwap:                  0 kBKernelPageSize:        4 kBMMUPageSize:           4 kBLocked:                0 kBVmFlags: rd wr mr mw me nr sd 7fd693f23000-7fd694000000 ---p 00000000 00:00 0 Size:                884 kBRss:                   0 kBPss:                   0 kBShared_Clean:          0 kBShared_Dirty:          0 kBPrivate_Clean:         0 kBPrivate_Dirty:         0 kBReferenced:            0 kBAnonymous:             0 kBAnonHugePages:         0 kBSwap:                  0 kBKernelPageSize:        4 kBMMUPageSize:           4 kBLocked:                0 kBVmFlags: 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线程消耗太大,最主要的原因是存在非常大的方法需要编译,而这个编译的过程是需要大量的内存消耗的,正因为如此,才会导致内存突然暴增,所以给大家一个建议,方法不要写太大啦,如果这个方法调用还很频繁,那真的会很悲剧的。

看完上述内容是否对您有帮助呢?如果还想对相关知识有进一步的了解或阅读更多相关文章,请关注行业资讯频道,感谢您对的支持。

内存 线程 分配 问题 消耗 时候 代码 位置 方法 机制 系统 复用 物理 客户 社区 过程 面的 跟踪 频繁 接下来 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 备案服务器怎么购买 公司oa 服务器中勒索病毒 图书借阅的数据库怎么搞 千叶互联网科技短视频变现 炒股软件开发哪个好 数据库可用空间为0怎么增加 魔兽世界怀旧服不同服务器的公会 我的世界论坛好玩的服务器 激战2捏脸数据库 贵州华为云软件开发在哪里 网站数据库开发需要哪些技术 lol显示服务器连接异常即将退出 域服务器地址cal 通信网络技术基础培训 信息网络技术做什么的 提交前后对数据库有影响吗 珠海管理冷库软件开发 集中式与分布式数据库 网络安全周素材 保定行为管理服务器公司 堡垒机支持国产应用发布服务器 深圳物流软件开发团队 成都蓉翼众创网络技术公司 从表中提取数据库 邮箱下载附件显示服务器错误 软件开发怎么编造工作经验 万能钥匙要打开数据库 服务器怎么看国标程序端口 云服务器管理权 有限元自主软件开发
0