配置说明
- 系统:Windows10
- 项目:KeyboardPiano V1.7
- 对象:音频播放类
com.sun.media.sound.DirectAudioDevice$DirectClip
- 原因:sun 的老旧框架,
Clip.close()
,音频数据audioData[]
无法释放,从而导致堆内存泄露 - 工具:JConsole、Memory Analyzer、Eclipse
项目简介,KeyboardPiano 是基于
java
实现的键盘钢琴,其原理是按一个键播放一段音频
排查之路
视频教程
- Memory Analyzer 内存泄露排查过程 其他细节请见 图文教程
注意:请读者务必安装 Memory Analyzer,才能进行相应操作
图文教程
-
运行项目,经过大量按键后,查看任务管理器,出现内存猛增的情况,且有增无减(img1)
-
img1 任务管理器内存情况
由于资源管理器并不能显示更多的内存消息,所以借助 JConsole 查看内存的泄露类型(堆内img2/堆外img3)
-
img2 堆内存
-
img3 非堆内存
有上图可知,此处发生的是堆内存泄露,非堆内存处于可接受范围内,为了查看堆内存细节,这里使用 Memory Analyzer 做内存分析
- Memory Analyzer 安装使用请参考这两篇教程
参照以上教程,排查内存泄露触发点
整体流程:限制内存大小,制造溢出,查找溢出
-
鼠标右键点击主类
KeyboardPiano
=> Run As => Run Configurations -
设置以下参数
-Xms50m
JVM初始分配的堆内存,设置最小堆内存为 50 M-Xmx50m
JVM最大允许分配的堆内存,设置最大堆内存为 50 M-XX:+HeapDumpOnOutOfMemoryError
当出现 OOM 时进行 HeapDump-XX:HeapDumpPath
设置 dump 文件输出路径 (请读者务必修改成自己的路径)
-Xms50m -Xmx50m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=E:\Java_code\piano_tmp\memory_test
-
img4 图里面参数仅作参考,以上面代码为准
-
在一系列猛如虎的按键操作后,JVM 承受不住外界的压力,果断溢出报错
-
img5 溢出报错
-
接着在 eclipse 打开 *.hprof 文件,选择 Leak Suspects Report, 点击 Finish
-
分析查看可疑点,博主在 Problem Suspect 2 发现异常,如下图
-
img6 Problem Suspect 2
-
说明
DirectClip
有问题,切换到 Overview 窗口,点击查看 Dominator Tree -
按照 Retained Heap 排序,可以看到一堆的
DirectClip
-
随便找一个展开,发现
byte[]
占据了绝大多数的内存,列为重点怀疑对象 -
img7 Dominator Tree
-
接着鼠标右键点开其中一个
DirectClip
对象,选择 List objects => with outgoing references 查看外部引用 -
展开后发现,
byte[] audioData
就是罪魁祸首,从名字可以看出,音频的数据就保存在该byte
数组 -
img8
DirectClip
内存分配情况
-
最后,根据博主测试得出,
DirectClip
即使关闭了,audioData
也得不到释放,才导致了堆内存泄露。所以可以说,这是 sun 老旧框架的 BUG,并非自身程序的问题 -
那泄露的关键点已经找到了,如何修正 BUG 呢?详细见 KeyboardPianoV1.7.2 Debug(音频优化) 关键在于更换播放方式
数据表格
- JConsole 内存分析数据表(主要参考数据为高亮部分),内存分类以及相关拓展资料请见 相关链接
Memories | initialized | increasing | total |
---|---|---|---|
1. Heap Memory Usage | 100 | 250 | 350 |
2. Non-Heap Memory Usage | 22 | 10 | 32 |
3. Memory Pool "PS Old Gen" | 0 | 220 | 220 |
4. Memory Pool "PS Eden Space" | 100 | 0 | 100 |
5. Memory Pool "PS Survivor Space" | 0 | 22 | 22 |
6. Memory Pool “Metaspace” | 16 | 1 | 17 |
7. Memory Pool “Code Cache” | 5 | 7 | 12 |
8. Memory Pool “Compressed Class Space” | 2 | 0 | 2 |
相关链接
-
内存管理机制 ← 以下说明摘抄于该教程
通常,会认为在堆上分配对象的代价比较大,但是GC却优化了这一操作:
C++中,在堆上分配一块内存,会查找一块适用的内存加以分配,如果对象销毁,这块内存就可以重用;
而Java中,就想一条长的带子,每分配一个新的对象,Java的“堆指针”就向后移动到尚未分配的区域
但是这种工作方式有一个问题:如果频繁的申请内存,资源将会耗尽。这时GC就介入了进来,它会回收空间,并使堆中的对象排列更紧凑。这样,就始终会有足够大的内存空间可以分配。 -
内存分类 深入浅出的典例
- 伊甸园空间(堆):大多数对象最初分配内存的池。
- 生存空间(堆):包含伊甸园空间垃圾收集后生存的对象。
- 年老代(堆):池包含已经存在一段时间的对象。
- 永久代(非堆):池包含的所有虚拟机本身的反射的数据,如类和方法的对象。 Java虚拟机,使用类数据共享,这一代分为只读和读写区域。
- 代码缓存(非堆):HotSpot Java虚拟机的还包括一个代码缓存,包含内存,使用本机代码的编译和存储。
-
缓存机制详解
-
Linux 堆外内存的排查参考
后记
内存泄露是纯代码层面的问题, 而内存泄露处理则是为了提高程序的健壮性