zipinputstream未关闭会导致堆外内存和文件描述符泄漏;其内部inflater通过jni分配native内存且依赖close()释放,同时包装fileinputstream时还会占用os文件描述符,最终引发服务假死或oom-kill。

ZipInputStream 未关闭确实会引发本地内存句柄耗尽,这不是堆内存泄漏,而是堆外资源泄漏——它不体现在堆 dump 中,却会持续占用操作系统级的 native 内存和文件描述符(fd),最终导致服务假死、连接拒绝或 OOM-kill。
为什么 ZipInputStream 不关会吃掉本地内存?
ZipInputStream 内部持有一个 Inflater 实例,而 Inflater 又通过 JNI 调用 native 层的 zlib 库,分配了一块不可被 JVM GC 管理的堆外内存(通常几 KB 到几十 KB 不等)。这个 native 内存只有在调用 close() 时才会由 JVM 触发 ZStreamRef.clean() 回收。若忘记 close,每次解压都会新增一个未释放的 Inflater,积少成多就会撑爆 native heap。
同时,ZipInputStream 通常包装 FileInputStream 或 ByteArrayInputStream,前者还会额外占用一个 OS 文件描述符(fd)。fd 耗尽后,新 open()、socket 连接、日志写入等系统调用会直接失败,报错如 java.io.IOException: Too many open files。
如何快速确认是 ZipInputStream 引起的?
不用等 OOM,从几个典型信号入手:
-
jmap -histo 显示大量
java.util.zip.Inflater和java.util.zip.ZStreamRef实例(常见于泄漏初期) - lsof -p [pid] | wc -l 查看 fd 数量是否持续上涨(尤其 > 65535)
- pmap -x [pid] 观察是否存在大量小块(如 64KB/128KB)匿名映射内存,且地址不归属 JVM NMT 统计范围
-
top 中 RES 持续增长,但
jcmd [pid] VM.native_memory summary显示 committed 增长缓慢 → 表明泄漏发生在 JVM 外部 native 层
修复代码的关键姿势
必须确保 ZipInputStream(及其底层 InputStream)被显式关闭,推荐使用 try-with-resources:
错误写法:
ZipInputStream zis = new ZipInputStream(new FileInputStream(file));
while ((entry = zis.getNextEntry()) != null) {
// ... 处理 entry
}
// ❌ 忘记 zis.close()
正确写法:
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(file))) {
while ((entry = zis.getNextEntry()) != null) {
// ... 处理 entry
}
// ✅ 自动 close,触发 Inflater 释放
} catch (IOException e) {
// ...
}
注意:如果 ZipInputStream 包装的是 ByteArrayInputStream,虽不占 fd,但 Inflater 仍需释放,所以依然要 close。
上线前的防御性检查建议
光靠人工 review 容易遗漏,可结合以下手段提前拦截:
- 静态扫描:用 SonarQube 或 Alibaba Java Coding Guidelines 插件检测未关闭的 Closeable 资源(规则名如
java:S2095) - 运行时监控:在测试环境开启
-XX:NativeMemoryTracking=detail,对比压测前后memory/zip类别内存增长 - 容器层防护:在 Docker/K8s 中设置
ulimit -n并配置 liveness probe 检查 fd 使用率(如lsof -p $PID | wc -l> 50000 就重启)











