一、布景

1 前言

遇到过几回JVM堆外内存走漏的问题,每次问题的排查、修正都耗费了不少时刻,问题持续几月、甚至一两年

咱们将这些排查的思路梳理成一套体系的办法,希望能给对JVM内存散布内存走漏问题有更明晰的理解。

2 这篇文章能带给你什么

1.了解JVM的内存散布.

2.更合理地去设置JVM参数。

3.能大大进步排查JVM内存问题的功率。

3 本文的限制范围

JDK版别

JDK8,其他JDK版别或许有所差异。

要点解说堆外内存

堆内的内存问题文章比较多,一般是dump堆内存,然后剖析即可。

4 文章解说的次序

1.解说JVM内存散布,了解有哪些内存区域、JVM参数等。堆内相关的文章比较多,堆外的比较少,所以要点解说堆外的。

2.解说排查JVM内存问题的思路。

二、JVM内存散布

1 JVM内存散布

【要点中的要点】JVM内存散布图

总体分为堆内内存、堆外内存。

【JVM内存】系统性排查JVM内存问题的思路

三、【要点】Heap Space(堆内内存)

要点关注新生代老年代

1 Young Generation新生代

用于存放新创立的目标,分为一个Eden区两个Survivor区

Young GC产生时会收回该块内存。

2 老年代(Old Generation)

2.1 作用

首要用于存放生命周期较长的目标。

2.2 何时收回

Old GC产生时会收回该块内存,一般触发Old GC时会伴随着一次Young GC

2.3 参数

-Xmn: 新生代的内存巨细

-Xms: Heap的初始巨细

-Xmx: Heap的最大巨细

2.4 问答

装备了Xms,那是不是JVM一发动就运用了这么多的物理内存来划分给Heap?

分状况而定:

(1) 假如未装备了-XX:AlwaysPreTouch,则实践是运用的是虚拟内存,给了一张空头支票,只在初次拜访时,例如存放一批新的Java目标数据,但本来请求的内存不行用了,需求新的内存来,这时才需求分配物理内存,也便是经过缺页反常进入内核中,再由内核来分配内存,再交给JVM进程运用。

一般状况,不会装备-XX:AlwaysPreTouch。

(2) 假如装备了-XX:AlwaysPreTouch,则JVM发动时,则不只分配Xms的巨细的虚拟内存,还会运用物理内存、填充整个堆。

装备-XX:AlwaysPreTouch能够提早请求好物理内存,削减程序运转进程中产生的物理内存分配带来的延迟,能够进步功能。例如布置elasticsearch节点时,能够指定该参数,进步功能。

【JVM内存】系统性排查JVM内存问题的思路

XMX设置多大适宜?

一般的运用,XMX能够设置为物理内存的1/2到2/3,较充分地去利用内存。

需求较多地运用Heap外内存运用,物理内存不要超越1/2,例如ElasticSearch、RocketMQ-broker、Kafka等中间件,需求许多读写文件,操作体系需求许多的Page Cache,才干有满意的缓存进步功能,所以JVM Heap不要过大,以预留给非Heap的其他内存。

四、【要点】Non-Heap Space(非堆内存、堆外内存)

1 什么是堆外内存

Non-Heap Space 翻译为非堆内存,也被称为Off-Heap(堆外内存),我们习惯于叫这部本分存为堆外内存。检查了许多国内外文章,关于这块内存,没有很一致的界说

较可信的是分为下面两种界说:

(1) 广义上的Non-Heap

除开Heap以外的所有内存,包含MetaSpace、NativeMemory(JNI Memory、Direct Memory等)、Stack、Code Cache等。

下面解说的Non-Heap是针关于广义的界说

(2) 狭义上的Non-Heap

只包含Metaspace、code_cache。

留心:

监控体系里会有Non-Heap的监控,例如SkyWalking、Arthas的Non-Heap目标,都是经过JDK自带的MemoryMXBean办法获取的。

所以一般监控体系收集的Non-Heap只是Heap以外的一部本分存!还需求留心NativeMemory等等内存。

监控数据示例;

【JVM内存】系统性排查JVM内存问题的思路

对应的代码:

@Override
public long getNonHeapMemoryMax() {
  return memoryMXBean.getNonHeapMemoryUsage().getMax();
}
@Override
public long getNonHeapMemoryUsed() {
  return memoryMXBean.getNonHeapMemoryUsage().getUsed();
}

2 【要点】MetaSpace(元数据空间)

用于存储类元数据(如类界说和办法界说)的内存区域。Metaspace 在 JDK 8 中取代了永久代(PermGen)。

2.1 相关参数

-XX:MetaspaceSize=<size>

-XX:MaxMetaspaceSize=<size>

-XX:MetaspaceSize 参数设置了元空间的初始巨细,在 JDK 8 中,-XX:MetaspaceSize 参数的默许值为 21 MB。。当元空间运用量抵达这个值时,JVM 将触发 Full GC(也会顺便younggc) 来测验收回不再需求的类元数据以及相关资源。

假如收回后元空间依然无法满意需求,那么 JVM 将测验扩展元空间的巨细。

问答:许多同学古怪,咱们有时看到某些运用发动一段时分,堆内存运用量不高,为何会产生一次FULL GC?

这很或许是因为运用的JVM参数里没有设置-XX:MetaspaceSize,或许-XX:MetaspaceSize设置的比较小。

-XX:MaxMetaspaceSize 参数设置了元空间的最大巨细。元空间会依据需求动态扩展,但不会超越这个设置的最大值。当元空间运用量超越这个值时,JVM 将触发 Full GC(也会顺便younggc),测验收回不再需求的类元数据以及相关资源。假如收回后元空间依然无法满意需求,那么 JVM 将抛出java.lang.OutOfMemoryError: Metaspace过错。因而,这个参数既与 Full GC 相关,也与 OOM 相关。

2.2 问答

怎么合理设置-XX:MaxMetaspaceSize参数?

主张JVM发动参数指定-XX:MaxMetaspaceSize,一般巨细256M满意,因为默许值无限大,假如出现频繁加载class等状况,容易出现OOM。

2.2 OOM反常

OOM报错: java.lang.OutOfMemoryError: Metaspace

3 Native Memory(本地内存)

3.1 Direct Memory(直接内存)

是Java NIO 结构引入的一种内存分配机制,答应在堆外分配内存以便更高效地履行 I/O 操作,通常用于NIO网络编程,JVM运用该内存作为缓冲区,进步I/O功能。

【JVM内存】系统性排查JVM内存问题的思路

创立 Direct Buffer 的办法

ByteBuffer.allocateDirect()

该办法分配内存:内部用的是unsafe.allocateMemory(size)办法,但不归于Java NIO库的一部分,

且jdk官方不引荐直接运用unsafe.allocateMemory(size)办法,该办法不受-XX:MaxDirectMemorySize参数操控,容易导致内存被无节制地运用,所以引荐ByteBuffer.allocateDirect()办法分配内存。

相关参数

-XX:MaxDirectMemorySize=<size>

假如未设置-XX:MaxDirectMemorySize,默许值等于Xmx。

可指定最大直接内存巨细,DirectMemory会超越MaxDirectMemorySize前,触发FULL GC(也会顺便Young GC),堆内DirectByteBuffer等会目标收回时,会触发目标的clean逻辑,开释该目标相关的DirectMemory,当gc后还是不行,就会OOM。

问答:怎么合理设置-XX:MaxDirectMemorySize参数?

因为默许值等于Xmx,所以主张指定一下MaxDirectMemorySize,Netty等结构会用到DirectMemory,且一般设置1G满意

结构和中间件

Netty(底层运用Java NIO技术)、Java NIO库(Java NIO库自身运用直接缓冲区进行高功能网络和文件I/O操作)等。

当请求堆外内存时,NIO 和 Netty 会比较计数器字段和最大值的巨细,假如计数器的值超越了最大值的限制,会抛出 OOM 的反常。

OOM结果

NIO 中是:OutOfMemoryError: Direct buffer memory。

Netty 中是:OutOfDirectMemoryError: failed to allocate capacity byte(s) of direct memory (used: usedMemory , max: DIRECT_MEMORY_LIMIT )

3.2 JNI Memory(JNI内存)

JNI (Java Native Interface) memory是指Java运用程序与本地代码交互时运用的内存。Java Native Interface (JNI) 是 Java 与本地(如 C 或 C++)代码进行交互的桥梁。

JNI办法

运用办法:在Java中运用native关键字界说办法,并在C/C++代码中完成相关的本地办法。

示例:

private native int inflateBytes(long addr, byte[] b, int off, int len);

该native办法内部也会请求内存用以存储数据,这部本分存归于JNI内存的一部分。

参数

无特定的 JVM 参数,但需求在本地代码中办理内存分配和开释。

留心:与-XX:MaxDirectMemorySize=无关。

JNI内存分配进程

【JVM内存】系统性排查JVM内存问题的思路

4 Stack(栈内存)

4.1 Stack介绍

  1. 用于存储线程履行进程中的部分变量、办法调用、操作数栈等。
  2. 栈内存由JVM自动办理,每个线程都有一个独立的栈
  3. 栈内存与堆内存相互独立,它们之间不共享数据。
  4. 分为VM Stack(Java虚拟机栈)、Native Stack(本地办法栈)

4.2 分类

(1) VM Stack(Java虚拟机栈)

用于存储线程履行Java办法时所需的信息。

当一个办法履行完成后,其对应的栈帧会从栈中弹出,开释该办法所占用的内存空间

每个线程对应一个Java线程栈,巨细由-Xss参数操控,默许是1M,当超越1M会报错StackOverFlowError

(2) Native Stack(本地办法栈)

用于存储本地办法(经过Java Native Interface,JNI调用的办法)的信息。

本地办法栈与Java虚拟机栈的首要差异在于,它是为本地办法提供内存空间,而不是Java办法。

5 特别内存

5.1 MMap

介绍

底层用的操作体系的mmap,将文件或文件的一部分映射到内存中的技术,经过内存映射文件能够完成高效的文件读写操作。

运用办法

FileChannel fileChannel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ, StandardOpenOption.WRITE); // 以读写的办法打开文件通道
MappedByteBuffer buf = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size()); // 将整个文件映射到内存

参数

无特定的 JVM 参数。

留心:与-XX:MaxDirectMemorySize=无关。

结构和中间件

Lucene、RocketMQ、Kafka等。

留心

mmap不归于JVM进程占用的内存!

当运用java.nio.channels.FileChannel#map办法时,分配的内存实践上是由操作体系办理的,并不是由JVM办理。这部本分存是映射到文件的内存区域,又称为内存映射文件(Memory-Mapped File)。在操作体系中,这部本分存被分类为文件缓存,而非Java进程的私有内存。

内存映射文件答应将文件或文件的一部分映射到进程的地址空间。一旦建立了映射,进程能够像拜访惯例内存一样拜访文件。操作体系会负责将对映射内存的更改写回磁盘。

因而,当你运用一些指令(如ps、top)检查Java进程的内存运用时,这部本分存映射文件的运用量并不会直接计算到进程的私有内存中。这部本分存运用在某种程度上是透明的,但依然受操作体系的文件缓存办理。

在Linux体系中,能够经过检查**/proc/meminfo**文件来获取关于内存映射文件的信息。

该结论依据试验:运用mmap办法写入2G文件,用arthas的memory指令检查JVM进程对应mmap运用量,已经是2G,但实践JVM的内存占有量,只要703M,这是因为mmap的内存是由操作体系操控的,不算在进程占用。

内存分配进程

【JVM内存】系统性排查JVM内存问题的思路

五、【要点】内存排查东西

1 堆内内存相关东西

整理了堆内内存相关的东西。

主张从上往下逐一履行指令,从全体到部分,逐渐排查出详细的问题。

【JVM内存】系统性排查JVM内存问题的思路

2 堆外内存相关东西

不同的内存区域能够运用不同的指令进行排查,一起也留心合理设置对应内存区域的参数。

【JVM内存】系统性排查JVM内存问题的思路

六、JVM内存运用量过大问题排查思路

1 全体的排查思路

【JVM内存】系统性排查JVM内存问题的思路

运用量大原因一般分为

1.数据量大,天然运用量大

2.JVM内存走漏,导致能够开释的内存未开释

JVM内存走漏:

在JVM运转进程中,因为(1)未正确开释不再运用的内存 (2)或许履行内存开释步骤后内存却未收回,导致内存占用持续增长,甚至终究耗尽导致OOM(内存溢出)的现象

发现问题、提早预知问题

依赖于监控告警:falcon、prometheus、troy等,首要是内存、GC相关

发现问题、提早预知问题

先止损,一般处理办法是经过重启,或许手动触发fullgc。

保存现场

假如条件答应必定不要直接操作重启、回滚等动作恢复,优先经过摘掉流量的办法来恢复,例如:经过dubbo操控台将某个provider实例禁止拜访。

然后将堆(手艺dump、或许指定-XX:+HeapDumpOnOutOfMemoryError)、栈(jstack指令导出)、GC 日志等关键信息保存下来,不然错过了定位根因的时机,后续想要复现、处理的难度将大大增加。

确定是那个进程的问题

当出现内存问题时,需求确认是那个出场的问题。

当产生进程A被操作体系的OOM-killer杀掉时,或许不是A的问题,或许是进程B占用内存过多,导致体系内存不行用,

然后触发OOM-killer计算出oom分数(依据内存、进程运转时刻等打分,参考文档),挑选杀掉了进程A。

剖析日志

剖析运用日志是否有outofmemory等关键字;

剖析体系日志/var/log/messages或许dmesg观察outofmemory的状况、进程运转的记载;

剖析运用GC日志;

查找不同内存区域占比、判断可疑的内存

依据指令、监控渠道,逐一剖析内存区域大户:Heap、MetaSpace、DirectMemory、JNI Memory。

剖析可疑内存数据内容

剖析内存占用大的区域中的数据,也能够辅助定位对应源码。

剖析可疑内存调用栈

关于java而言,引荐运用arthas的trace和stack指令,但是arthas无法对native办法进行阻拦,此刻能够借助jstack或许arthas阻拦或许调用native办法的上层办法。

关于JNI Memory,这块内存是C、C++等native办法相关的,需求用gperftools、gdb等东西进行剖析。

复现问题

在没有了解问题原因、内存增长规则的状况下,想要复现问题,有时是很困难的!或许要花费很长时刻、且需求些命运。

所以咱们尽量保存问题现场,方便找出规则。

内存走漏按产生办法来分类:

按产生办法来分类 阐明和示例 复现难度
周期性增长 例如有的或许是守时使命触发才产生,但守时使命或许一周才跑一次 周期越长,排查难度越大。
常发性内存走漏 产生内存走漏的代码会被多次履行到,每次被履行的时分都会导致一块内存走漏 容易重现
偶发性 产生内存走漏的代码只要在某些特定环境或操作进程下才会产生。例如:只要某个履行步骤中履行到某个if段才会产生 一般难度较大
一次性内存走漏 产生内存走漏的代码只会被履行一次。例如,只要运用发动进程中,或许某个类初始化时才会产生 一般难度较大
隐式内存走漏 程序在运转进程中不断的分配内存,只要在特别状况下才会收回。(1) 需求抵达一个极限才会进行收回,例如:假如没有设置metaspace最大巨细,但是一向加载class,当触发fullgc可收回metaspace,但是直到内存不行也未能触发过fullgc。(2) 存在内存碎片,尽管履行了开释内存的步骤,但是实践并未是否内存。例如ptmalloc内存分配库导致的内存走漏问题。 一般难度较大

修正问题

JVM内存问题一般是代码问题、JVM参数问题、malloc内存分配库等,针对不同类型的问题进行修正。

七、事例

事例遇到比较多:

1.(1) 不合理地运用fastjson,导致频繁地在创立、加载class (2)未设置-XX:MaxMetaspaceSize 导致了内存一向增长,直到OOM

2.JNI Memory内存走漏

3.JVM参数-XX:SoftRefLRUPolicyMSPerMB和metaspace导致的fullgc

4.vim指令修改文件导致的事务运用的进程被oom-killer杀掉

事例需求比较长的文章来阐明,这些后续再别的写文章补充吧。

八、总结

  • 首先是看这张图,了解JVM内存的散布。

【JVM内存】系统性排查JVM内存问题的思路

  • 遇到内存问题,先依据通用的排查思路一遍内存的运用状况。

  • 有许多JDK、Linux内存相关的指令,我们能够去测验一下,先查大范围的内存占用,再逐渐定位到详细的内存区域、代码、参数等。

  • 重启程序、体系能临时处理许多内存问题,但是,主张去深究一下,会学到许多JVM内存办理和Linux内存办理的常识,还是很风趣的。

  • 此外,把握了JVM内存办理的设计后,发现许多程序的内存是比较糟蹋的,能够对JVM参数做针对性优化,能削减许多机器资源。