文档章节

一个导致JVM物理内存消耗大的Bug

PerfMa
 PerfMa
发布于 05/22 10:29
字数 1841
阅读 3W
收藏 53

阿里云携手百名商业领袖、技术大咖,带您一探行进中的数字新基建!>>>

本文来自: PerfMa技术社区

PerfMa(笨马网络)官网

概述

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

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线程消耗太大,最主要的原因是存在非常大的方法需要编译,而这个编译的过程是需要大量的内存消耗的,正因为如此,才会导致内存突然暴增,所以给大家一个建议,方法不要写太大啦,如果这个方法调用还很频繁,那真的会很悲剧的。

推荐阅读

PerfMa KO 系列之 JVM 参数【Memory篇】

不改一行代码定位线上性能问题

© 著作权归作者所有

PerfMa
粉丝 62
博文 30
码字总数 59950
作品 0
杭州
私信 提问
加载中

评论(3)

h
heike07
真是可以
丁富贵
厉害了我的哥
淘淘我的小宝宝
淘淘我的小宝宝
webservice客户端调用时动态编译?
升级到JDK9的一个BUG,你了解吗

概述 前几天在一个群里看到一个朋友发了一个demo,说是JDK的bug,昨天在JVM的一个群里又有朋友发了,觉得挺有意思,分享给大家,希望大家升级JDK的版本的时候注意下是否存在这样的代码,如果...

你假笨
2018/06/06
0
0
此次部署在k8s集群中的SpringBoot项目OOMKilled问题汇总

现象1:在执行任务时,在页面上发现任务执行失败了(SprintBoot项目) # kubectl get pod |grep podname  发现有重启的记录 #kubectl describe pod podname  发现Reason:OOMKilled,Exit...

osc_1fuqrqut
2019/04/24
33
0
Java中常见OOM的场景及解决方法

OOM for Heap (java.lang.OutOfMemoryError: Java heap space) 分析 此OOM是由于JVM中heap的最大值不满足需要,将设置heap的最大值调高即可,参数样例为:-Xmx2G JVM堆的设置是指Java程序运行...

ikilun
2018/06/26
0
0
一些长时间GC停顿问题的排查及解决办法

对于许多企业级应用,尤其是OLTP应用来说,长暂停很可能导致服务超时,而对这些运行在JVM上的应用来说,垃圾回收(GC)可能是长暂停最主要的原因。本文将描述一些可能碰到GC长暂停的不同场景...

一看就喷亏的小猿
2019/04/22
76
0
ES 30 - Elasticsearch生产集群的配置建议

[TOC] 在生产环境中, 要保证服务在各种极限情况下的稳定和高可用, 所以在部署ES集群时, 需要考虑服务器的内存、CPU、磁盘, 集群的网络、节点个数, 并且要优化JVM的各项参数. 首先从这些方面着...

osc_nnbkiac5
2019/06/24
15
0

没有更多内容

加载失败,请刷新页面

加载更多

10 个牛逼的后台开源项目,接私活赚钱必备!

作者:SevDot https://www.jianshu.com/p/3bc7404af887 做程序员的,除了本职工作外,不少人会有一份副业,帮人开发网站或小程序等。这些工作肯定是离不开向客户交付管理后台的,那么一套又漂...

Java技术栈
31分钟前
24
0
将视频转为GIF动态图的图文分享

将视频转为GIF?因为GIF是压缩格式的文件,用于减少文件在网络上传递的时间,所以很多自媒体人都会把视频的某一片段转换为GIF,然后发布在公众号平台上。如果你也想通过视频转GIF创作一些有趣...

茹鋽
32分钟前
19
0
怎么将B站内容一键上传到快手?新方法!

随着媒体平台的不断发展,最近这几年,小视频产业真的被大家青睐,其品牌个性凸显、流量总和巨大、销售数据好的优点,不断汇聚了企业、个人,正是为了取得更出众的内容展示总量,他们经常会涵...

易媒助手
33分钟前
17
0
在新标签页(而不是新窗口)中打开URL - Open a URL in a new tab (and not a new window)

问题: I'm trying to open a URL in a new tab, as opposed to a popup window. 我正在尝试在新选项卡(而不是弹出窗口)中打开URL 。 I've seen related questions where the responses wo......

fyin1314
38分钟前
13
0
Romax Nexus—机电一体传动系统设计与仿真平台

经纬恒润作为Romax Nexus工具国内业务的代理商,致力于为电驱动系统、齿轮、轴承行业提供解决方案。英国Romax公司是一家集软件开发与项目咨询为一体的技术服务公司,是汽车、工业、风电及轴承...

Hirain
38分钟前
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部