一个导致JVM物理内存消耗大的Bug
概述
最近我们公司在帮一个客户查一个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线程消耗太大,最主要的原因是存在非常大的方法需要编译,而这个编译的过程是需要大量的内存消耗的,正因为如此,才会导致内存突然暴增,所以给大家一个建议,方法不要写太大啦,如果这个方法调用还很频繁,那真的会很悲剧的。
推荐阅读:
一个导致JVM物理内存消耗大的Bug的更多相关文章
- 移动端H5页面开发,碰到一个字体变大的BUG
移动端H5页面开发,碰到一个字体变大的BUG webkit内核下,对不定高宽的元素可能会放大其字体.那么,就可以设置一个max-width:或者使用-webkit-text-size-adjust: ...
- 解Bug之路-记一次JVM堆外内存泄露Bug的查找
解Bug之路-记一次JVM堆外内存泄露Bug的查找 前言 JVM的堆外内存泄露的定位一直是个比较棘手的问题.此次的Bug查找从堆内内存的泄露反推出堆外内存,同时对物理内存的使用做了定量的分析,从而实锤 ...
- 线程崩溃为什么不会导致 JVM 崩溃
大家好,我是坤哥 网上看到一个很有意思的据说是美团的面试题:为什么线程崩溃崩溃不会导致 JVM 崩溃,这个问题我看了不少回答,但都没答到根本原因,所以决定答一答,相信大家看完肯定会有收获,本文分以下几 ...
- JVM OOM异常会导致JVM退出吗?
出处: https://mp.weixin.qq.com/s/8j8YTcr2qhVActLGzOqe7Q https://blog.csdn.net/h2604396739/article/de ...
- 记一个关于std::unordered_map并发访问的BUG
前言 刷题刷得头疼,水篇blog.这个BUG是我大约一个月前,在做15445实现lock_manager的时候遇到的一个很恶劣但很愚蠢的BUG,排查 + 摸鱼大概花了我三天的时间,根本原因是我在使用s ...
- JVM 输出 GC 日志导致 JVM 卡住,我 TM 人傻了
本系列是 我TM人傻了 系列第七期[捂脸],往期精彩回顾: 升级到Spring 5.3.x之后,GC次数急剧增加,我TM人傻了:https://zhuanlan.zhihu.com/p/3970425 ...
- java环境中基于jvm的两大语言:scala,groovy
一.java环境中基于jvm的两大语言:scala,groovy 可以在java项目里混编这两种语言: scala:静态语言,多范式语言,糅合了面向对象.面向过程:可以与java和net互操作:融汇了 ...
- Response.Write()方法响应导致页面字体变大的解决办法
关于ASP.NET中用Response.Write()方法响应导致页面字体变大的解决办法 最近研究了ASP.NET,发现一个问题,比方说在页面里面有个Button,要点击以后要打开新窗口,而且 ...
- 面试官问,说一个你在工作非常有价值的bug
如果你去参考面试,做足了准备,面对面试官员从容不迫,吐沫横飞的大谈自己的工作经历.突然,面试官横插一句:说一个你在工作非常有价值的bug.顿时,整个空气都仿佛都凝固了!“What?”... 我想没几个 ...
随机推荐
- GDI+ 绘制砂岩含量图版
图版是在工作中经常会使用到的工具,它能够大大提高我们日常工作效率.地质图版在地质工作中具有举足轻重的作用,不仅可以轻松判断岩性,也可以依据经验图版直接得到结果,十分方便. 本程序目的绘制出一个地质常用 ...
- Maven Wrapper简介
文章目录 简介 Maven Wrapper的结构 下载Maven Wrapper 使用 Maven Wrapper简介 简介 开发java项目少不了要用到maven或者gradle,对比gradle而 ...
- 华硕笔记本无法U盘启动,快捷键识别不了
http://www.udaxia.com/upqd/8254.html 转载于:https://www.cnblogs.com/wanglinjie/p/10507888.html
- Flask中可以利用Flask-SQLAlchemy
为什么80%的码农都做不了架构师?>>> 官方文档:http://flask-sqlalchemy.pocoo.org/2.3/ 1.安装(进入虚拟环境)--利用镜像安装PyMy ...
- Linux 搭建nginx的PID
pid logs/nginx.pid 安装的时候就是没有,其实在启动 nginx 时自动生成的 里面存放的是 当前 nginx 住进程的 ID 号:所以在配置文件中指定pidpid /usr/loca ...
- Jenkins 邮件收发(qq 邮箱)
一.配置 Jenkins 邮箱的全局配置 检查是否已安装插件 Email Extension Plugin 获取 qq 邮箱 授权码 进入 qq 邮箱 ---> 设置 ---> 账户 配置 ...
- java基础知识备忘
1.java内存分配 a.寄存器cup -- 暂不涉及 b.本地方法栈 -- 虚拟机调用windows功能用的,比如创建文件夹 c.方法区 -- 存放 .class文件,负责存放方法 d.栈 -- ...
- CSS颜色及文本字体
CSS颜色及文本字体 CSS颜色表示法 CSS文本设置 CSS边框属性 背景属性 元素溢出 CSS颜色及文本字体 CSS颜色表示法 颜色名表示,比如:red 红色,yellow黄色,pick粉色 16 ...
- 【HBase】Java实现过滤器查询
目录 概述 代码实现 rowKey过滤器RowFilter 列族过滤器FamilyFilter 列过滤器QualifierFilter 列值过滤器ValueFilter 专用过滤器 单列值过滤器 Si ...
- 【Kafka】消息队列相关知识
目录 概述 常用消息队列 常用消息队列对比 应用场景 消息队列的两种模式 概述 消息(Message) 是指在应用系统之间传递的数据.消息可以非常简单,比如只包含文本字符串,也可以更复杂,可能包含嵌入 ...