当前位置: 主页 > JAVA语言

java如何排查内存泄露-讲解排查JVM内存问题的思路及思路梳理

发布时间:2023-06-22 16:04   浏览次数:次   作者:佚名

内存泄漏排查_java如何排查内存泄露_qt内存泄漏排查方法

一、背景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内存分布图

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

java如何排查内存泄露_内存泄漏排查_qt内存泄漏排查方法

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

重点关注新生代、老年代。

1 Young Generation新生代

用于存放新创建的对象,分为一个Eden区和两个Survivor区。

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

2 老年代(Old Generation)2.1作用

主要用于存放生命周期较长的对象。

2.2何时回收

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

2.3参数

-Xmx: 新生代的内存大小

-Xms: Heap的初始大小

-Xmx: Heap的最大大小

java如何排查内存泄露_内存泄漏排查_qt内存泄漏排查方法

2.4 问答配置了Xms,那是不是JVM一启动就使用了这么多的物理内存来划分给Heap?

分情况而定:

(1) 如果未配置了-XX:AlwaysPreTouch,则实际是使用的是虚拟内存,给了一张空头支票,只在首次访问时,例如存放一批新的Java对象数据,但原来申请的内存不够用了,需要新的内存来,这时才需要分配物理内存,也就是通过缺页异常进入内核中,再由内核来分配内存,再交给JVM进程使用。

一般情况,不会配置-XX:AlwaysPreTouch。

(2) 如果配置了-XX:AlwaysPreTouch,则JVM启动时,则不仅分配Xms的大小的虚拟内存,还会使用物理内存、填充整个堆。

配置-XX:AlwaysPreTouch可以提前申请好物理内存,减少程序运行过程中发生的物理内存分配带来的延迟,可以提升性能。例如部署elasticsearch节点时,可以指定该参数,提升性能。

内存泄漏排查_qt内存泄漏排查方法_java如何排查内存泄露

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(堆外内存)java如何排查内存泄露,大家习惯于叫这部分内存为堆外内存。查看了很多国内外文章java如何排查内存泄露,对于这块内存,没有很统一的定义。

较可信的是分为下面两种定义:

(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等等内存。

监控数据示例;

内存泄漏排查_java如何排查内存泄露_qt内存泄漏排查方法

对应的代码:

java复制代码@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=

-XX:MaxMetaspaceSize=

-XX:MetaspaceSize 参数设置了元空间的初始大小,在 JDK 8 中,-XX:MetaspaceSize 参数的默认值为 21 MB。。当元空间使用量达到这个值时,JVM 将触发 Full GC(也会附带younggc) 来尝试回收不再需要的类元数据以及相关资源。

如果回收后元空间仍然无法满足需求,那么 JVM 将尝试扩展元空间的大小。

问答:很多同学奇怪,我们有时看到某些应用启动一段时候,堆内存使用量不高,为何会发生一次FULL GC?

qt内存泄漏排查方法_java如何排查内存泄露_内存泄漏排查

这很可能是因为应用的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性能。

java如何排查内存泄露_qt内存泄漏排查方法_内存泄漏排查

创建 Direct Buffer 的方法

ByteBuffer.allocateDirect()

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

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

相关参数

-XX:MaxDirectMemorySize=

如果未设置-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++代码中实现相关的本地方法。

示例:

java复制代码private native int inflateBytes(long addr, byte[] b, int off, int len);

该native方法内部也会申请内存用以存储数据,这部分内存属于JNI内存的一部分。

参数

qt内存泄漏排查方法_java如何排查内存泄露_内存泄漏排查

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

注意:与-XX:MaxDirectMemorySize=无关。

JNI内存分配过程

内存泄漏排查_qt内存泄漏排查方法_java如何排查内存泄露

4 Stack(栈内存)4.1 Stack介绍用于存储线程执行过程中的局部变量、方法调用、操作数栈等。栈内存由JVM自动管理,每个线程都有一个独立的栈。栈内存与堆内存相互独立,它们之间不共享数据。分为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,将文件或文件的一部分映射到内存中的技术,通过内存映射文件可以实现高效的文件读写操作。

使用方式

java复制代码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的内存是由操作系统控制的,不算在进程占用。

内存分配过程

内存泄漏排查_java如何排查内存泄露_qt内存泄漏排查方法

五、【重点】内存排查工具1 堆内内存相关工具

整理了堆内内存相关的工具。

建议从上往下逐一执行命令,从整体到局部,逐步排查出具体的问题。

java如何排查内存泄露_内存泄漏排查_qt内存泄漏排查方法

2 堆外内存相关工具

内存泄漏排查_qt内存泄漏排查方法_java如何排查内存泄露

不同的内存区域可以使用不同的命令进行排查,同时也留意合理设置对应内存区域的参数。

qt内存泄漏排查方法_java如何排查内存泄露_内存泄漏排查

六、JVM内存使用量过大问题排查思路1 整体的排查思路

java如何排查内存泄露_内存泄漏排查_qt内存泄漏排查方法

使用量大原因一般分为

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等工具进行分析。

复现问题

在没有了解问题原因、内存增长规律的情况下,想要复现问题,有时是很困难的!可能要花费很长时间、且需要些运气。

qt内存泄漏排查方法_内存泄漏排查_java如何排查内存泄露

所以我们尽量保留问题现场,方便找出规律。

内存泄漏按发生方式来分类:

按发生方式来分类

说明和示例

复现难度

周期性增长

例如有的可能是定时任务触发才发生,但定时任务可能一周才跑一次

周期越长,排查难度越大。

常发性内存泄漏

发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏

容易重现

偶发性

发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。例如:只有某个执行步骤中执行到某个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杀掉

案例需要比较长的文章来说明,这些后续再另外写文章补充吧。

八、总结

java如何排查内存泄露_内存泄漏排查_qt内存泄漏排查方法