GZIPInputStream 流未关闭引起的内存泄漏问题

Stella981
• 阅读 686

近日线上一个项目总是时隔1周发生OOM自动重启,问题很明显内存泄漏了。。。

使用jmap查看一下线上服务堆使用情况,实例最多的前10个类

110125 instances of class [C 
108705 instances of class java.lang.String 
88066 instances of class 
java.util.concurrent.ConcurrentHashMap$Node 
79224 instances of class java.lang.Object 
52984 instances of class [B 
48482 instances of class java.lang.ref.Finalizer 
39684 instances of class java.util.zip.Inflater  <---罪魁祸手
39684 instances of class java.util.zip.ZStreamRef 
28168 instances of class [Ljava.lang.Object; 
26576 instances of class java.util.HashMap$Node 

看到这个类排名第一反应就是GZIP相关的操作可能有问题,那么我们目光聚集到代码上吧

public static String unZip(String str) throws IOException {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    byte[] bytes = Base64.getDecoder().decode(str);
    ByteArrayInputStream in = new ByteArrayInputStream(bytes);
    GZIPInputStream gzip = new GZIPInputStream(in);
    byte[] buffer = new byte[256];
    int n = 0;
    while ((n = gzip.read(buffer)) >= 0) {
        out.write(buffer, 0, n);
    }
    return out.toString(CODE);
}

这段代码是当时想要使用GZIP做解压缩从网上抄来了,当时只是用单测验证了一下这段代码的正确性,就上线了。

出现了内存泄漏问题之后,回过头来反思这段代码发现这里使用了3个流ByteArrayOutputStream ,ByteArrayInputStream ,GZIPInputStream

重点是这三个流在代码结束之后都没有关闭!!! 依次点开三个流的close()方法看了下 ByteArrayOutputStream ,ByteArrayInputStream 这两个流的close()方法其实是空的,说明这两个流其实关闭与否都没有关系。

GZIPInputStream 的close()方法

public void close() throws IOException {
    if (!closed) {
        super.close();
        eos = true;
        closed = true;
    }
}

看到这个方法后,具体怎么关闭的其实不那么重要了,重要的是说明了这个流是需要关闭的

现在我们再看看内存泄漏的具体原因是什么吧,我们依次点开GZIPInputStream 的构造方法

public GZIPInputStream(InputStream in, int size) throws IOException {
    super(in, new Inflater(true), size); //看到堆内大量实例Inflater了
    usesDefaultInflater = true;
    readHeader(in);
}

点开Inflater的构造方法

public Inflater(boolean nowrap) {
    zsRef = new ZStreamRef(init(nowrap)); //c++方法init
}

这个init方法使用了C++ calloc申请内存,这部分内存是无法被Java GC回收的,这就导致我们的服务可用堆内存越来越小,最后程序OOM Crash重启

修改这段代码很简单,所有流使用完毕之后关闭就好了,最终代码如下

private static String unZip(String str) throws IOException {
    try (ByteArrayOutputStream out = new ByteArrayOutputStream();
        ByteArrayInputStream in = new ByteArrayInputStream(Base64.getDecoder().decode(str));
        GZIPInputStream gzip = new GZIPInputStream(in)) {
        byte[] buffer = new byte[256];
        int n = 0;
        while ((n = gzip.read(buffer)) >= 0) {
            out.write(buffer, 0, n);
        }
        return out.toString(CODE);
    }
}

虽然ByteArrayOutputStreamByteArrayInputStream 这两个流的close()方法为空,无需关闭。但是从这个线上问题得出的反思,任何流,使用完毕之后一定要注意养成关闭的好习惯。(除非需要复用流)

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
2年前
Java运行状态分析2:获取线程状态及堆栈信息
Java运行状态分析2:线程状态及堆栈信息基本概念出现内存泄漏或者运行缓慢场景,有时候无法直接从业务日志看出问题时候,需要分析jvm内存和线程堆栈线程堆栈信息主要记录jvm线程在某时刻线程执行情况,分析线程状态可以跟踪到程序出问题的地方​内存堆栈信息主要记录jvm堆中在某时刻对象使用情况,
Wesley13 Wesley13
2年前
Java内存分析工具MAT
MAT是一个强大的内存分析工具,可以快捷、有效地帮助我们找到内存泄露,减少内存消耗分析工具。内存中堆的使用情况是应用性能监测的重点,而对于堆的快照,可以dump出来进一步分析,总的来说,一般我们对于堆dump快照有三种方式:添加启动参数发生OOM时自动dump:java应用的启动参数一般最好都加上XX:HeapDumpOnOutOfMe
Wesley13 Wesley13
2年前
Java知识图谱
1JVM1.内存模型(内存分为几部分?堆溢出、栈溢出原因及实例?线上如何排查?)2.类加载机制3.垃圾回收2Java基础什么是接口?什么是抽象类?区别是什么?什么是序列化?网络通信过程及实践什么是线程?java线程池运行过程及实践(Exec
Stella981 Stella981
2年前
Linux 命令全集
一、开关机sync:把内存中的数据写到磁盘中(关机、重启前都需先执行sync)shutdownrnow或reboot:立刻重启shutdownhnow:立刻关机shutdownh19:00:预定时间关闭系统(晚上7点关机,如果现在超过8点则第二天)shutdownh10:预定时间关闭系统(10分钟后关机)
Stella981 Stella981
2年前
Skynet 小试Debug_console...
  昨天凌晨4点还在写Skynet服务试玩(http://my.oschina.net/CandyMi/blog/846113),结果发现自己写的服务出现内存泄漏了。  今天早上起床吃早餐的时候一直在想:    "如果每个Lua服务处理完客户端关闭的网络连接后,服务是否会自动退出呢?"  答案:不会!    现在,我们来看昨
Wesley13 Wesley13
2年前
JAVA 线上故障排查
线上故障主要会包括CPU、磁盘、内存以及网络问题,而大多数故障可能会包含不止一个层面的问题,所以进行排查时候尽量四个方面依次排查一遍。同时例如jstack、jmap等工具也是不囿于一个方面的问题的,基本上出问题就是df、free、top三连,然后依次jstack、jmap伺候,具体问题具体分析即可。CPU一般来讲我们首先会排查
Stella981 Stella981
2年前
Executors使用不当引起的内存泄漏
线上服务内存溢出这周刚上班突然有一个项目内存溢出了,排查了半天终于找到问题所在,在此记录下,防止后面再次出现类似的情况。先简单说下当出现内存溢出之后,我是如何排查的,首先通过jstack打印出堆栈信息,然后通过分析工具对这些文件进行分析,根据分析结果我们就可以知道大概是由于什么问题引起的。关于jstack如何使用,大家可以先看看这篇文章
Easter79 Easter79
2年前
ThreadLocal 内存泄露的实例分析
前言之前写了一篇深入分析ThreadLocal内存泄漏问题(https://my.oschina.net/thinwonton/blog/1505136)是从理论上分析ThreadLocal的内存泄漏问题,这一篇文章我们来分析一下实际的内存泄漏案例。分析问题的过程比结果更重要,理论结合实际才能彻底分析出内存泄漏的原因。案例与分析
Stella981 Stella981
2年前
JVM内存泄漏导致内存溢出(OOM)的场景
一、概念1\.内存泄漏:对象使用完之后,没有按照预期被GC回收,一直留在内存中2\.内存溢出:大量对象一直留在内存中,导致内存不够用(OOM),影响正常的程序运行二、内存泄漏的场景1\.内存中数据量太大,比如一次性从数据库中取出来太多数据2\.静态集合类中对对象的引用,在使用完后未清空(只把对象设为null,而不是从集合中移除),
Wesley13 Wesley13
2年前
Java内存泄漏解析!
前言:内存管理是Java最重要的优势之一,你只需创建对象,Java垃圾收集器会自动负责分配和释放内存。但是,情况并不那么简单,因为在Java应用程序中经常发生内存泄漏。本章会说明什么是内存泄漏,为什么发生,以及如何防止它们。什么是内存泄漏?内存泄漏的定义:应用程序不再使用的对象,垃圾收集器却无法删除它们,因为它们正在被引用。为了理