Java 堆内存溢出原因分析

前言

任何使用过基于 Java 的企业级后端应用的软件开发者都会遇到过这种低劣、奇怪的报错,这些报错来自于用户或是测试工程师: java.lang.OutOfMemoryError:Java heap space。

为了弄清楚问题,我们必须返回到算法复杂性的计算机科学基础,尤其是“空间”复杂性。如果我们回忆,每一个应用都有一个最坏情况特征。具体来说,在存储维度方面,超过推荐的存储将会被分配到应用程序上,这是不可预测但尖锐的问题。这导致了堆内存的过度使用,因此出现了"内存不够"的情况。

这种特定情况最糟糕的部分是应用程序不能修复,并且将崩溃。任何重启应用的尝试 - 甚至使用最大内存(-Xmx option)- 都不是长久之计。如果不明白什么导致了堆使用的膨胀或突出,内存使用稳定性(即应用稳定性)就不能保障。于是,什么才是更有效的理解关于内存的编程问题的途径?当内存溢出时,明白应用程序的内存堆和分布情况才能回答这个问题。

在这一前提下,我们将聚焦以下方面:

  • 当内存溢出时,获取到 Java 进程中的堆转储。
  • 明白应用程序正在遭遇的内存问题的类型。
  • 使用一个堆分析器,可以使用 Eclipse MAT 这个优秀的开源项目来分析内存溢出的问题。

配置应用,为堆分析做准备

任何像内存溢出这种非确定性的、时有时无的问题对于事后的分析都是一个挑战。所以,最好的处理内存溢出的方法是让 JVM 虚拟机转储一份 JVM 虚拟机内存状态的堆文件。

Sun HotSpot JVM 有一种方法可以引导 JVM 转储内存溢出时的堆状态到一个文件中。其标准格式为 .hprof 。所以,为了实现这种操作,向 JVM 启动项中添加 XX:+HeapDumpOnOutOfMemoryError 。因为内存溢出可能经过很长一段时间才会发生,向生产系统增加这一选项也是必须的。

如果堆转储 .hprof 文件必须被写在一个特定的文件系统位置,那么就添加目录途径到 XX:HeapDumpPath 。只需确保该应用对于指定目录途径始终拥有写入权限。

原因分析

101:了解内存溢出错误的本质

当尝试去评估和了解一个内存溢出错误时,最先做的事情应该是观察内存增长特征。根据情况做出可能性的评估:

  • 尖峰状:这种类型的内存溢出在某种类型的加载上会是比较激烈的。当 JVM 分配内存给 20 个用户时,应用程序可以正常运行。但是,如果到第 100 个用户时可能会遭遇到内存峰值,从而导致内存溢出。有两种可能的办法去解决这个问题。
  • 泄露:由于某些编程问题,内存使用随着时间的推移逐渐增加。

拥有良性垃圾回收机制的健康图表

健康一段时间后,随时间推移而泄露的图表

引起内存使用凸起、导致内存溢出的内存图表

在我们了解导致使用率激增的内存问题的本质之后,基于从对分析中得到的推断,下面的这些方法或许可以用来避免遭遇内存溢出的错误。

解决内存问题

1.修复引起内存溢出的代码:由于应用在某段时间内增量添加了一个对象而没有清除其引用(来自正在运行的应用程序的对象引用),导致不得不修复程序错误。例如,这一错误可能是插入了一个哈希表, 其中的业务对象会逐渐增加,然而业务逻辑和事务在完成之后并没有删除这些对象。

2.增加内存最大值作为一种修复方法。在了解了运行内存特征和堆之后,可能必须增加分配的最大堆内存来避免再次发生内存溢出,因为推荐的最大内存值不能够满足应用程序的稳定性。所以,应用程序可能不得不基于堆分析器的评估,将 Java -Xmx 的 flag 信息更新成一个更高值后再来运行。

堆分析

下面我们将详细分析如何使用一个堆分析工具来分析堆转储。在示例中,将使用到 Eclipse 基金会的开源工具 MAT 。

使用 MAT 进行堆分析

是时候进行深入探讨了。我们将通过一系列的步骤,帮助探索在 MAT 中的不同表现和视图,以获取一个堆内存溢出的示例并思考分析。

1. 打开内存溢出错误发生时产生的 .hprof 堆文件。确保复制转储文件到一个专门的文件夹下,因为 MAT 会创建许多索引文件:文件 -> 打开

2. 打开转储文件,有内存泄漏嫌疑报告和组件报告的选项。选择运行泄漏嫌疑报告。

3. 泄漏嫌疑表打开后,在预览窗口的饼状图会展示在每个对象基础上保留内存的分布情况。它显示了内存中的最大对象(拥有最高保留内存的对象 —— 累积的内存和引用的对象)。

4. 上面的饼图通过聚合拥有最高内存引用(本身内存和总内存)的对象来展示 3 个问题嫌疑人。

让我们逐一分情况查看,评估它是否是内存溢出错误的根本原因。

可疑点 1

由 “<system class loader>” 加载的 454,570 个 “java.lang.ref.Finalizer” 实例占用了 790,205,576(47.96%)个字节。

这就是告诉我们有 454,570 个 JVM finalizer(终结器)实例占据了分配的应用内存的近 50 %。

假设读者知道 Java Finalizer是做什么的,上面的信息会让我们明白什么呢?

本质上,开发者编写了一些定制化的终结器去释放一个实例的资源。这些由终结器收集的实例不在 JVM 使用单独队列的垃圾回收算法的范围之内。实际上,这种途径比起垃圾回收机制的清理路径更长。所以现在我们应该努力搞清楚这些终结器到底终结了什么?

也或许是可疑点 2 ,占据了 20% 的 sun.security.ssl.SSLSocketImpl 。我们能确认是否这些就是要被终结器终结的实例吗?

可疑点 2

现在,让我们打开在 MAT 顶部的工具按钮下面的 Dominator 视图。我们会看到所有的列出的类实例,经由 MAT 解析展示出有效的堆存储。

下一步,在 Dominator 视图,我们尝试理解 java.lang.Finalizer 和 sun.security.ssl.SSLSocketImpl 之间的关系。我们右键点击 sun.security.ssl.SSLSocketImpl 这一列,打开 GC Roots -> exclude soft/weak references。

现在,MAT 将会开始绘制内存的图表来显示 GC root 的路径以及它所对应的实例引用。这会被显示在另外一个页面上,显示的引用如下:

如上面引用链显示,实例 SSLSocketImpl 来自于 java.lang.ref.Finalizer,整个 SSLSocketImpl 实例大约占用了 88k。我们还注意到 finalizer 链是一个针链表数据结构它指向下一个实例。

推论:在这一点上,我们有一个明确的感觉,Java finalizer 试图在收集 SSLSocketImpl 对象。为了解释为什么还有很多信息没有被收集到,我开始检查代码。

检查代码

代码检查需要查看是不是由 socket 套接字被关闭导致的。在这种情况下,它显示与 I/O 相关的所有流,需要被正确地关闭。在一点上,我们怀疑 JVM 是始作俑者。实际上,在 Open JDK 6.0.XX 的 GC(垃圾收集器)上的代码中有一个 BUG。

我希望这篇文章给你一个模式来分析 Java 应用中的错误是由堆存储还是内部问题导致的。希望你使用堆分析愉快!

Shallow heap (浅堆) vs. Retained Heap (保留堆)

浅堆是一个对象消耗的内存。根据情况,一个对象需要 32 位或 64 位(取决于其操作系统架构),对于整型为 4 字节,对于 Long 型为 8 字节等等。依据堆转储格式,其内存大小(比如,向 8 对齐)或许适应于更好地塑造虚拟机的真实消耗。

X 的保留集合是当 X 被垃圾回收时,那些将要被移除的对象集合。

X 的保留堆是在 X 的保留集合中所有对象的浅堆之和,也就是 X 存留的内存。

总体讲,一个对象的浅堆就是其在堆中的大小。同一个对象的保留大小就是当对象被垃圾回收时堆内存的总量。

一些对象的主要集合,比如某一特定类的所有对象、或是由某一特定类加载器加载的所有类的所有对象、或仅仅是一些任意的对象,它们的保留集是如果那些主要集的所有对象变得不可接近时所释放的对象集。保留集包括这些对象和仅通过这些对象才能获取的其它对象。保留集的大小是包含在保留集中的所有对象的堆的大小。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Java常见内存溢出异常分析与解决

    Java虚拟机规范规定JVM的内存分为了好几块,比如堆,栈,程序计数器,方法区等,而Hotspot jvm的实现中,将堆内存分为了三部分,新生代,老年代,持久带,其中持久带实现了规范中规定的方法区,而内存模型中不同的部分都会出现相应的OutOfMemoryError错误,接下来我们就分开来讨论一下.java.lang.OutOfMemoryError这个错误我相信大部分开发人员都有遇到过,产生该错误的原因大都出于以下原因: JVM内存过小.程序不严密,产生了过多的垃圾. 导致OutOfMemor

  • 浅谈java内存管理与内存溢出异常

    说到内存管理,笔者这里想先比较一下java与C.C++之间的区别: 在C.C++中,内存管理是由程序员负责的,也就是说程序员既要完成繁重的代码编写工作又要时常考虑到系统内存的维护 在java中,程序员无需考虑内存的控制和维护,而是交由JVM自动管理,这样就不容易出现内存泄漏和溢出的问题.然而,一旦出现内存泄漏和溢出方面的问题,如果不了解JVM的内存管理机制就很难找到错误所在. 1.JVM运行时数据区 JVM在运行java程序的时候会将它所管理的内存划分为若干个不同的区域,这些区域不仅有自己的用途

  • Java排序算法之堆排思想及代码实现

    在介绍堆排序前,我们需要了解一下一种数据结构 -- 顶堆. 什么是顶堆? 它是一颗完全二叉树,顶堆有大顶堆和小顶堆两种.所谓大顶堆就是在这颗完全二叉树中,任何一颗子树都满足:父结点的值 > 孩子结点的值:小顶堆则相反. 如图: 什么是堆排序(Heapsort)? 利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种.可以利用数组的特点快速定位指定索引的元素. 现在给我们一个无序数组,我们将其从小到大排序,使用堆排序的实现步骤和思想如下: 1.让这个数组变成一个大根堆 2.将最后一

  • java堆排序原理与实现方法分析

    本文实例讲述了java堆排序原理与实现方法.分享给大家供大家参考,具体如下: 堆是一个数组,被看成一个近似完全二叉树. 举例说明: 堆的性质: 1.已知元素在数组中的序号为i 其父节点的序号为 i/2的整数 其左孩子节点的序号为2*i 其右孩子节点的序号为2*i+1 2.堆分为最大堆和最小堆 在最大堆中,要保证父节点的值大于等于其孩子节点的值 在最小堆中,要保证父节点的值小于等于其孩子节点的值 java实现堆排序 public class MyHeapSort { public void Hea

  • 完美解决java读取大文件内存溢出的问题

    1. 传统方式:在内存中读取文件内容 读取文件行的标准方式是在内存中读取,Guava 和Apache Commons IO都提供了如下所示快速读取文件行的方法: Files.readLines(new File(path), Charsets.UTF_8); FileUtils.readLines(new File(path)); 实际上是使用BufferedReader或者其子类LineNumberReader来读取的. 传统方式的问题: 是文件的所有行都被存放在内存中,当文件足够大时很快就会

  • Java编程常见内存溢出异常与代码示例

    Java 堆是用来存储对象实例的, 因此如果我们不断地创建对象, 并且保证 GC Root 和创建的对象之间有可达路径以免对象被垃圾回收, 那么当创建的对象过多时, 会导致 heap 内存不足, 进而引发 OutOfMemoryError 异常. /** * @author xiongyongshun * VM Args: java -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError */ public class OutOfMemoryErrorTe

  • Java内存溢出和内存泄露

    虽然jvm可以通过GC自动回收无用的内存,但是代码不好的话仍然存在内存溢出的风险. 一.为什么要了解内存泄露和内存溢出? 1.内存泄露一般是代码设计存在缺陷导致的,通过了解内存泄露的场景,可以避免不必要的内存溢出和提高自己的代码编写水平: 2.通过了解内存溢出的几种常见情况,可以在出现内存溢出的时候快速的定位问题的位置,缩短解决故障的时间.  二.基本概念  理解这两个概念非常重要. 内存泄露:指程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存.即被分配的对象可达但已无

  • Java内存区域与内存溢出异常详解

    Java内存区域与内存溢出异常 概述 对于 C 和 C++程序开发的开发人员来说,在内存管理领域,程序员对内存拥有绝对的使用权,但是也要主要到正确的使用和清理内存,这就要求程序员有较高的水平. 而对于 Java 程序员来说,在虚拟机的自动内存管理机制的帮助下,不再需要为每一个 new 操作去写配对的 delete/free 代码,而且不容易出现内存泄漏和内存溢出问题,看起来由虚拟机管理内存一切都很美好.不过,也正是因为 Java 程序员把内存控制的权力交给了 Java 虚拟机,一旦出现内存泄漏和

  • Java堆内存又溢出了!教你一招必杀技(推荐)

    JAVA堆内存管理是影响性能主要因素之一. 堆内存溢出是JAVA项目非常常见的故障,在解决该问题之前,必须先了解下JAVA堆内存是怎么工作的. 先看下JAVA堆内存是如何划分的,如图: 1.JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation).老年代(Old Generation),非堆内存就一个永久代(Permanent Generation). 2.年轻代又分为Eden和Survivor区.Survivor区由FromSpace和ToSpace组成.Eden

  • Java 堆内存溢出原因分析

    前言 任何使用过基于 Java 的企业级后端应用的软件开发者都会遇到过这种低劣.奇怪的报错,这些报错来自于用户或是测试工程师: java.lang.OutOfMemoryError:Java heap space. 为了弄清楚问题,我们必须返回到算法复杂性的计算机科学基础,尤其是"空间"复杂性.如果我们回忆,每一个应用都有一个最坏情况特征.具体来说,在存储维度方面,超过推荐的存储将会被分配到应用程序上,这是不可预测但尖锐的问题.这导致了堆内存的过度使用,因此出现了"内存不够&

  • java OOM内存泄漏原因及解决方法

    前言 这篇文章主要介绍了java OOM内存泄漏原因及解决方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.什么是OOM OOM,全称"Out Of Memory",翻译成中文就是"内存用完了",当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error 二.为什么会OOM.出现的原因是什么 为什么会没有内存了呢?原因不外乎有两点: ① 分配的少了:比如虚拟机本身可

  • JAVA程序内存溢出问题原因分析

    本文较为详细的分析了JAVA程序内存溢出问题原因.分享给大家供大家参考.具体如下: 遇到一个线上系统报 java.lang.OutOfMemoryError: PermGen space 错误,需要定位一下问题.很久之前到时弄过这个,现在还真有点不记得了,但这个真的是一个非常有意思的问题,值得好好研究一下.首先第一反应当然是加上-XX:+PrintGCDetails参数来看具体的GC日志,但是由于程序是tomcat启动的,担心里面封装的东西太多不好定位,既然在windows下面,所以还是借助可视

  • 教你用MAT工具分析Java堆内存泄漏问题的解决方法

    一.MAT概述与安装 MAT,全称Memory Analysis Tools,是一款分析Java堆内存的工具,可以快速定位到堆内泄漏问题.该工具提供了两种使用方式,一种是插件版,可以安装到Eclipse使用,另一种是独立版,可以直接解压使用. 我把独立版MAT安装包放到了网盘上,方便直接下载 链接: https://pan.baidu.com/s/1DVHlHuSfi_4TVl2ei5YuLA 提取码: 42qt 独立版解压后,其内部文件是这样的-- 这里有一个MemoryAnalyzer.in

  • 一文教会你使用jmap和MAT进行堆内存溢出分析

    jmap:Java内存映像工具 jmap(Memory Map for Java)命令用于生成堆转储快照(一般称为heapdump或dump文件).另外,还有几种方式获取dump文件:使用JVM参数选项-XX:+HeapDumpOnOutOfMemoryError参数,可以让虚拟机在OOM异常出现之后自动生成dump文件,通过-XX:HeapDumpPath=path 设置dump文件路径(有时候dump文件比较大的时候可能无法自动导出,这时候就需要使用jmap -dump手动导出了):通过-X

  • Java基础之堆内存溢出的解决

    一.实战-内存溢出 堆内存溢出 栈内存溢出 方法区溢出 直接内存溢出 二.实战-堆内存溢出 演示堆内存溢出代码,并且定位问题 总结堆内存溢出的场景与解决方案 分析商城项目中可能存在堆内存溢出的代码并且解决 三.堆内存溢出演示代码 public class HeapOOMTest { private List<String> oomList = new ArrayList<>(); public static void main(String[] args) { HeapOOMTes

  • 基于Java堆内存的10个要点的总结分析

    Java堆内存的10个要点 .javaoutofmemoryerrorgenerationjvmprofiler编程当我开始学习Java编程时,我不知道什么是堆内存或堆空间,我甚至不知道当对象创建时,它们被放在了哪里.当我开始正式写一些程序后,我会经常遇到java.lang.outOfMemoryError的报错,之后我才开始关注什么是堆内存或者说堆空间(heap space).对大多数程序员都经历过这样的过程,因为学习一种语言是非常容易来的,但是学习基础是非常难的,因为没有什么特定的流程让你学

  • 浅谈JVM内存溢出原因和解决思路

    目录 栈溢出(虚拟机栈和本地方法栈) 产生原因 解决思路 堆溢出 产生原因 解决思路 方法区和运行时常量池溢出 产生原因 解决思路 本机直接内存溢出 产生原因 解决思路 栈溢出(虚拟机栈和本地方法栈) 产生原因 在HotSpot中,只能由-Xss参数来设定.因为在HotSpot中不区分虚拟机栈和本地方法栈的. 栈溢出时会出现两种异常:StackOverflowError异常和OutOfMemoryError异常. StackOverflowError异常因为线程请求的栈深度大于虚拟机允许的最大深

随机推荐