zhaojishun 的博客

记录精彩的程序人生

生命不息,折腾不止!
  menu
43 文章
176023 浏览
0 当前访客
ღゝ◡╹)ノ❤️

记一次内存溢出OutOfMemoryError: Java heap space

记一次内存溢出OutOfMemoryError: Java heap space

起因是有一个凌晨两点的定时服务在运行是内存溢出了,出现概率不是很高,在本地运行后发现定时任务占用内存并不高,怀疑其他代码有内存泄露问题,

image-20200915193355789

这是程序没运行、运行后3分钟、运行后8小时后的内存 占用情况,通过top命令确定是java程序占用的内存,程序占用内存越来越高,怀疑有内存泄露问题

接下来我们设置最大堆内存使用大小,设置出现堆内存溢出时将快照文件保存到指定目录

nohup java -jar -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/root/dump_OOME.hprof FEBS-Shiro-2.0.jar &
-XX:+HeapDumpOnOutOfMemoryError    //虚拟机在堆异常时生成对存储快照,后缀一般是.hprof
-Xms    //Java堆的最小值,例如-Xms20m,将Java堆的最小值设置为20MB
-Xmx    //Java堆的最大值,例如-Xms40m,将Java堆的最大值设置为40MB

等待程序出现OOM错误,收集快照文件。由于程序等了一天没有OOM错,且程序一直占用大量内存,于是我们手动保存快照文件

使用jmap 工具

将jvm 内存快照保存到本地

由于我服务器使用的是openjdk 默认安装并没有jmap 工具 需要下载

  1. 查看jdk 版本
[root@iZ2ze9uwnp0zegoakytcyuZ jvm]# java -version
openjdk version "1.8.0_262"
OpenJDK Runtime Environment (build 1.8.0_262-b10)
OpenJDK 64-Bit Server VM (build 25.262-b10, mixed mode)
  1. 查看openJDK有jmap的yum源
[root@iZ2ze9uwnp0zegoakytcyuZ jvm]# yum whatprovides '*/jmap' | grep 1.8.0
1:java-1.8.0-openjdk-devel-1.8.0.242.b08-1.el7.i686 : OpenJDK Development
Filename    : /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.242.b08-1.el7.i386/bin/jmap
1:java-1.8.0-openjdk-devel-1.8.0.242.b08-1.el7.x86_64 : OpenJDK Development
Filename    : /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.242.b08-1.el7.x86_64/bin/jmap
1:java-1.8.0-openjdk-devel-1.8.0.252.b09-2.el7_8.i686 : OpenJDK Development
Filename    : /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.i386/bin/jmap
1:java-1.8.0-openjdk-devel-1.8.0.252.b09-2.el7_8.x86_64 : OpenJDK Development
Filename    : /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/bin/jmap
1:java-1.8.0-openjdk-devel-1.8.0.262.b10-0.el7_8.i686 : OpenJDK Development
Filename    : /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.262.b10-0.el7_8.i386/bin/jmap
1:java-1.8.0-openjdk-devel-1.8.0.262.b10-0.el7_8.x86_64 : OpenJDK Development
Filename    : /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.262.b10-0.el7_8.x86_64/bin/jmap
  1. 选择安装对应的版本,我这里安装java-1.8.0-openjdk-devel-1.8.0.262.b10-0.el7_8.x86_64这个版本,注意要对应本地的版本
yum install java-1.8.0-openjdk-devel-1.8.0.262.b10-0.el7_8.x86_64
  1. 测试 安装是否成功
[root@iZ2ze9uwnp0zegoakytcyuZ jvm]# jmap
Usage:
    jmap [option] <pid>
        (to connect to running process)
    jmap [option] <executable <core>
        (to connect to a core file)
    jmap [option] [server_id@]<remote server IP or hostname>
        (to connect to remote debug server)

where <option> is one of:
    <none>               to print same info as Solaris pmap
    -heap                to print java heap summary
    -histo[:live]        to print histogram of java object heap; if the "live"
                         suboption is specified, only count live objects
    -clstats             to print class loader statistics
    -finalizerinfo       to print information on objects awaiting finalization
    -dump:<dump-options> to dump java heap in hprof binary format
                         dump-options:
                           live         dump only live objects; if not specified,
                                        all objects in the heap are dumped.
                           format=b     binary format
                           file=<file>  dump heap to <file>
                         Example: jmap -dump:live,format=b,file=heap.bin <pid>
    -F                   force. Use with -dump:<dump-options> <pid> or -histo
                         to force a heap dump or histogram when <pid> does not
                         respond. The "live" suboption is not supported
                         in this mode.
    -h | -help           to print this help message
    -J<flag>             to pass <flag> directly to the runtime system
  1. 先通过 jmap -histo pid的方式将对象使用情况导出到文件
jmap -histo 2596 > me.txt

查看后发现 [Ljava.util.HashMap$Node; 占用较大内存

image-20200915115851515

#对象说明
B  byte
C  char
D  double
F	float
I   int
J  long
Z	boolean
[	数组,如[I表示int[] 
[L+类名 其他对象
  1. 接着导出快照文件
[root@iZ2ze9uwnp0zegoakytcyuZ jvm]# ps -ef | grep java
root     25185 17755  0 15:46 pts/0    00:00:00 grep --color=auto java
root     26843     1  0 09:51 ?        00:02:55 java -jar -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/root/dump_OOME.hprof FEBS-Shiro-2.0.jar

[root@iZ2ze9uwnp0zegoakytcyuZ ~]# jmap -dump:live,format=b,file=m.hprof 26843
Dumping heap to /root/m.hprof ...
Heap dump file created

一个快照400多M 离谱

image-20200914155054611

下载到本地

通过MAT工具对dump文件进行分析

mat工具介绍

image-20200915155611388

Histogram 可以列出内存中每个对象的名字、数量以及大小。

Top Consumers 直接展示出我们内存中的大内存

Dominator Tree 会将所有内存中的对象按大小进行排序,并且我们可以分析对象之间的引用结构。

Leak Suspects,直接给出分析,分析出我们代码中可能会出现内存泄漏的地方

其余action不常用

Histogram 视图

Histogram 视图 列出每个class产生了多少个实例,以及占有多大内存,所占百分比,可以很容易找出占用内存最多的几个类,根据Retained Heap排序,找出前几个。

可以分不同的维度来查看类的Histogram视图,Group by class、Group by superclass、Group by class loader、Group by package 只要有溢出,时间久了,溢出类的实例数量或者其占有的内存会越来越多,排名也就越来越前,通过多次对比不同时间点下的Histogram图对比就能很容易把溢出类找出来。

image-20200916080912602

Histogram可以展示某个特定类的对象个数和每个对象使用的内存。当然char[],String和Object[]都不太会导致内存问题。为了更好的组织这个视图,你可以通过classloader或者package来分组。这会让你更好的专注在你自己的对象上。

image-20200915195441179

通过这个视图我们可以看见HashMap$Node[] 这个对象占据非常高的内存,非常可疑。我们也可以看见每一个对象正在占用的内存数量。这里有两个数值,Shallow HeapRetained Heap。Shallow heap是一个对象消费的内存数量每个对象的引用需要32(或者64 bits,基于你的CPU架构)。基本数据类型例如整形和长整形需要4或者8 bytes以及其他的。其实更有用的参数是Retained Heap.

Top Consumers 内存中的大内存

image-20200916183010647

Leak Suspects分析出我们代码中可能会出现内存泄漏的地方

image-20200916183308877

Dominator Tree 支配树

列出每个对象(Object instance)与其引用关系的树状结构,还包含了占有多大内存,所占百分比 可以很容易的找出占用内存最多的几个对象,根据Percentage(百分比)来排序。 可以分不同维度来查看对象的Dominator Tree视图,Group by class、Group by class loader、Group by package 和Histogram类似,时间久了,通过多次对比也可以把溢出对象找出来,Dominator Tree和Histogram的区别是站的角度不一样,Histogram是站在类的角度上去看,Dominator Tree是站的对象实例的角度上看,Dominator Tree可以更方便的看出其引用关系。

另外大家应该可以注意到,在每一行的最左边都有一个文件型的图标,这些图标有的左下角带有一个黄色的点,有的则没有。带有红点的对象就表示是可以被GC Roots访问到的,根据上面的讲解,可以被GC Root访问到的对象都是无法被回收的。那么这就说明所有带红色的对象都是泄漏的对象吗?当然不是,因为有些对象系统需要一直使用,本来就不应该被回收。我们可以注意到,上图当中所有带黄点的对象最右边都有写一个System Class,说明这是一个由系统管理的对象,并不是由我们自己创建并导致内存泄漏的对象。

image-20200915155721077

首先Retained Heap表示这个对象以及它所持有的其它引用(包括直接和间接)所占的总内存,因此从上图中看,前五行的Retained Heap是最大的,我们分析内存泄漏时,内存最大的对象也是最应该去怀疑的。

从上图可以看出头几个对象占用的内存百分比非常低,那么结合上面的疑似泄露点下方的许多sun.security.ssl.SSLSessionContextImpl就变得非常可疑

通过右键 Merge Shortest Paths to GC Roots > whth all references 查看对象的引用

image-20200916080318936

至此泄露点已经找到,接下来就是分析为什么会泄露了。

解决

参考https://blog.csdn.net/fedorafrog/article/details/107015711

  • 修改mysql url useSSL=false
  • 修改 hikari中 闲连接存活最大时间 idle-timeout: 0 空闲连接存活最大时间, 0则表示空闲的连接在连接池中永远不被移除
  • 修改max-lifetime: 28700000连接最大存活时间.不等于0且小于30秒,会被重置为默认值30分钟.设置应该比mysql设置的超时时间短

问题已经解决

程序运行2天后

image-20200916184337588

关键词

  • Objects: 这个类有多少个实例
  • Shallow Heap: 单个实例占用的内存
  • Retained Heap: 单个实例及其引用一共占有多少内存
  • Percentage :百分比
  • list objects -- with outgoing references : 查看这个对象持有的外部对象引用。
  • list objects -- with incoming references : 查看这个对象被哪些外部对象引用。
  • show objects by class -- with outgoing references :查看这个对象类型持有的外部对象引用
  • show objects by class -- with incoming references :查看这个对象类型被哪些外部对象引用

参考

https://www.cnblogs.com/shihaiming/p/11389386.html 使用Eclipse Memory Analyzer进行内存泄漏分析

https://www.jianshu.com/p/759e02c1feee 关于使用Eclipse Memory Analyzer的10点小技巧

https://blog.csdn.net/fedorafrog/article/details/107015711 Druid数据库连接池引起的FullGC问题排查、分析、解决

https://blog.csdn.net/a740169405/article/details/53610689 Shallow Heap 和 Retained Heap的区别

https://blog.csdn.net/fedorafrog/article/details/107015711 Druid数据库连接池引起的FullGC问题排查、分析、解决

https://blog.csdn.net/kusedexingfu/article/details/108347296 MAT内存分析工具介绍

https://www.eclipse.org/mat/downloads.php mat工具安装


标题:记一次内存溢出OutOfMemoryError: Java heap space
作者:zhaojishun
地址:http://blog.zhaojishun.cn/articles/2020/09/16/1600255334410.html