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

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

许多新手不太清楚导致JVM消耗大量物理内存的Bug。为了帮助大家解决这个问题,下面小编就为大家详细讲解一下。有这种需求的人可以从中学习,希望你能有所收获。

00-10: 10最近我公司在给客户(JDK1.8.0_191-b12)检查一个JVM问题,发现一个系统总是被OS Kill杀死,是内存泄漏导致的。在检查的过程中,我错误地发现了JVM中的另一个Bug。此错误可能会导致使用大量物理内存。我们已经向社区提供了反馈,并获得了快速反馈。预计会在OpenJDK8的最新版本发布(JDK11也存在这个问题)。导致JVM物理内存消耗大的Bug是怎么样的

PS:用户的问题终于解决了。定位是C2的设计缺陷,导致占用大量内存,安全性得不到保障。

00-1010接下来,我们主要分享一下这个BUG的发现过程。首先,我们要求客户实时跟踪进度。当内存使用量明显增加时,我们通过/proc/PID/smap看到了大量64MB的内存分配,Rss基本被消耗殆尽。

7fd 69000000-7fd 693 f 23000 rw-p 0000000000033600000大小:64652 kbrs :64652 kbps 336064652 kbshared _ clean :0 kbshared _ dirty 33600 kbps rivate _ clean 3: kbps rivate _ dirty :64652 kbps

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

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

内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/44635.html

(0)

相关推荐

  • Oracle执行计划中常见index访问方式有哪些

    技术Oracle执行计划中常见index访问方式有哪些小编给大家分享一下Oracle执行计划中常见index访问方式有哪些,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧![SQL]SELECT COUNT

    攻略 2021年11月20日
  • python光学仿真面向对象光学元件类的实现是什么

    技术python光学仿真面向对象光学元件类的实现是什么python光学仿真面向对象光学元件类的实现是什么,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能

    攻略 2021年10月20日
  • Bytom的P2P网络通讯录结构是什么?

    技术Bytom的P2P网络地址簿结构体是怎样的这篇文章主要讲解了“Bytom的P2P网络地址簿结构体是怎样的”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Bytom的P2

    攻略 2021年12月20日
  • 解决在PDF文档中复制代码报错问题

    技术解决在PDF文档中复制代码报错问题 解决在PDF文档中复制代码报错问题现在我们做学习笔记最好用的就是markdown,笔记做好以后可以导出PDF文档,但是当我们复制PDF文档的代码直接拷贝到 IDE

    礼包 2021年12月10日
  • k8s上的火花与k8s操作器上的火花的对比是怎样的

    技术spark on k8s与spark on k8s operator的对比是怎样的这期内容当中小编将会给大家带来有关spark on k8s与spark on k8s operator的对比是怎样的,文章内容丰富且以

    攻略 2021年12月15日
  • 军号声,上有井冈山下有东固山的背景

    技术军号声,上有井冈山下有东固山的背景井冈山,天下闻名,而“东井冈”,知道的人却不多军号声。她就是位于井冈山东侧的东固畲族乡山区,现属吉安市青原区。1929年1月,井冈山失守。红四军主力3600余人在毛泽东、朱德、陈毅率

    生活 2021年10月20日