java 堆内存泄露排查(例子)


配置说明

  • 系统:Windows10
  • 项目:KeyboardPiano V1.7
  • 对象:音频播放类 com.sun.media.sound.DirectAudioDevice$DirectClip
  • 原因:sun 的老旧框架,Clip.close(),音频数据 audioData[] 无法释放,从而导致堆内存泄露
  • 工具:JConsole、Memory Analyzer、Eclipse

项目简介,KeyboardPiano 是基于 java 实现的键盘钢琴,其原理是按一个键播放一段音频


排查之路

视频教程

注意:请读者务必安装 Memory Analyzer,才能进行相应操作

图文教程

  • 运行项目,经过大量按键后,查看任务管理器,出现内存猛增的情况,且有增无减(img1

  • img1 任务管理器内存情况
    img1

由于资源管理器并不能显示更多的内存消息,所以借助 JConsole 查看内存的泄露类型(堆内img2/堆外img3

  • img2 堆内存
    img2

  • img3 非堆内存
    img3

有上图可知,此处发生的是堆内存泄露,非堆内存处于可接受范围内,为了查看堆内存细节,这里使用 Memory Analyzer 做内存分析

参照以上教程,排查内存泄露触发点

整体流程:限制内存大小,制造溢出,查找溢出

  • 鼠标右键点击主类 KeyboardPiano => Run As => Run Configurations

  • 设置以下参数

    1. -Xms50m JVM初始分配的堆内存,设置最小堆内存为 50 M
    2. -Xmx50m JVM最大允许分配的堆内存,设置最大堆内存为 50 M
    3. -XX:+HeapDumpOnOutOfMemoryError 当出现 OOM 时进行 HeapDump
    4. -XX:HeapDumpPath 设置 dump 文件输出路径 (请读者务必修改成自己的路径)
-Xms50m -Xmx50m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=E:\Java_code\piano_tmp\memory_test
  • img4 图里面参数仅作参考,以上面代码为准
    img4

  • 在一系列猛如虎的按键操作后,JVM 承受不住外界的压力,果断溢出报错

  • img5 溢出报错
    img6

  • 接着在 eclipse 打开 *.hprof 文件,选择 Leak Suspects Report, 点击 Finish

  • 分析查看可疑点,博主在 Problem Suspect 2 发现异常,如下图

  • img6 Problem Suspect 2
    img6

  • 说明 DirectClip 有问题,切换到 Overview 窗口,点击查看 Dominator Tree

  • 按照 Retained Heap 排序,可以看到一堆的 DirectClip

  • 随便找一个展开,发现 byte[] 占据了绝大多数的内存,列为重点怀疑对象

  • img7 Dominator Tree
    img7

  • 接着鼠标右键点开其中一个 DirectClip 对象,选择 List objects => with outgoing references 查看外部引用

  • 展开后发现,byte[] audioData 就是罪魁祸首,从名字可以看出,音频的数据就保存在该 byte 数组

  • img8 DirectClip 内存分配情况
    img8

  • 最后,根据博主测试得出,DirectClip 即使关闭了,audioData 也得不到释放,才导致了堆内存泄露。所以可以说,这是 sun 老旧框架的 BUG,并非自身程序的问题

  • 那泄露的关键点已经找到了,如何修正 BUG 呢?详细见 KeyboardPianoV1.7.2 Debug(音频优化) 关键在于更换播放方式


数据表格

  • JConsole 内存分析数据表(主要参考数据为高亮部分),内存分类以及相关拓展资料请见 相关链接
Memoriesinitializedincreasingtotal
1. Heap Memory Usage100250350
2. Non-Heap Memory Usage221032
3. Memory Pool "PS Old Gen"0220220
4. Memory Pool "PS Eden Space"1000100
5. Memory Pool "PS Survivor Space"02222
6. Memory Pool “Metaspace”16117
7. Memory Pool “Code Cache”5712
8. Memory Pool “Compressed Class Space”202

相关链接

  • 内存管理机制 ← 以下说明摘抄于该教程

    通常,会认为在堆上分配对象的代价比较大,但是GC却优化了这一操作:
    C++中,在堆上分配一块内存,会查找一块适用的内存加以分配,如果对象销毁,这块内存就可以重用;
    而Java中,就想一条长的带子,每分配一个新的对象,Java的“堆指针”就向后移动到尚未分配的区域

    但是这种工作方式有一个问题:如果频繁的申请内存,资源将会耗尽。这时GC就介入了进来,它会回收空间,并使堆中的对象排列更紧凑。这样,就始终会有足够大的内存空间可以分配。

  • 内存分类 深入浅出的典例

    1. 伊甸园空间(堆):大多数对象最初分配内存的池
    2. 生存空间(堆):包含伊甸园空间垃圾收集后生存的对象。
    3. 年老代(堆):池包含已经存在一段时间的对象
    4. 永久代(非堆):池包含的所有虚拟机本身的反射的数据,如类和方法的对象。 Java虚拟机,使用类数据共享,这一代分为只读和读写区域。
    5. 代码缓存(非堆):HotSpot Java虚拟机的还包括一个代码缓存,包含内存,使用本机代码的编译和存储。
  • 缓存机制详解

    1. JAVA面试——缓存
    2. 用Java实现多种缓存机制
  • Linux 堆外内存的排查参考

    1. DirectByteBuffer堆外内存溢出问题排查
    2. 记一次JVM堆外内存泄露Bug的查找
    3. 使用google perf工具来排查堆外内存占用

后记

内存泄露是纯代码层面的问题, 而内存泄露处理则是为了提高程序的健壮性

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值