Java压缩流GZIPStream导致的内存泄露
date
Nov 16, 2017
slug
things-about-java-gzipstream
status
Published
tags
Dev
summary
我们来聊聊
GZIPOutputStream
和 GZIPInputStream
, 如果不关闭流会引起的内存泄露问题,以及GZIPStream
申请和释放堆外内存的流程, Let's do it!type
Post
我们来聊聊
GZIPOutputStream
和 GZIPInputStream
, 如果不关闭流会引起的内存泄露问题,以及GZIPStream
申请和释放堆外内存的流程, Let’s do it!引子
在我的工程里面又一个工具类
ZipHelper
用来压缩和解压 String
最近服务出现了占用swap空间的问题,初步定位为内存泄漏,最后通过分析定位到是 Native 方法
Java_java_util_zip_Inflater_init
一直在申请内存(关于分析方法可以查阅这篇博客内存泄露分析实战)但是没有释放,很有可能就是流没有关闭造成的,而这部分代码最大的问题就是没有在finally
里面去关闭流,于是乎我打算改造这部分代码,利用 try-with-resource 语法糖,然后代码就被修改成了这样:是不是顺眼多了呐,可是这样的代码可以压缩的,在解压的时候会报错。一开始我以为是解压的代码出现了问题,最后才发现是因为压缩的时候没有成功压缩,导致解压的时候无法解压。报以下错误
好好的代码怎么会突然压缩失败,后来发现的问题是在
GZIPOutputStream
中,在close()
方法中会主动调用finish()
方法。在下面的方法中才会将压缩后的数据输出到输入流,由于原来的代码会调用
close()
方法,从而间接调用了 finish()
方法。那我我们的try-with-resource
到底出了什么问题,其实问题就在于执行close()
的时间。try-with-resource
执行时机和条件
try-with-resource
是在 JDK7 中新增加的语法糖(其实就是抄的C#),用来自动执行流的关闭操作,只要该类实现了AutoCloseable
的close()
方法。实现了这个接口之后,我们可以将会在
try
代码块执行结束之后自动关闭流由于在
GZIPOutputStream
执行了finish()
方法或者close()
方法之后才会真正的将压缩后的数据写入流,在上文我改造的代码中并没有首先执行finish()
方法,而是直接在try
代码块执行完之后关闭了流 GZIPOutputStream
, 由于close()
方法执行在out.toString("ISO-8859-1")
之后,因此压缩并没有真正的被执行,然而对于ZipHelper.compress()
方法并没有感知,而是返回了没有压缩成功的字符串,从而造成在解压的时候报错。为什么会引起的堆外内存泄漏
通过最开始的代码我们可以看出,在没有发生异常的情况下,
compress()
方法是可以正常的关闭流的,所以内存泄露的根源应该是在uncompress()
方法,通过跟踪GZIPInputStream
的构造函数和close()
应该很快就能找到答案。下面是申请堆外内存和释放堆外内存的过程调用图,可以对比代码参考

由于篇幅的原因就不将JDK源码注释一同贴上来了,感兴趣的同学可以按图索骥,找到对应的注释。
openJDK 中 JVM 关于这个本地方法的实现