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


    近日线上一个项目总是时隔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);
        }
    }
    

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

  • 相关阅读:
    史上最全的maven pom.xml文件教程详解
    Java中的断言Assert
    Log4j详细配置
    (实战篇)SSM三大框架整合详细教程(Spring+SpringMVC+MyBatis)
    Java的反射机制
    mysql数据库常用SQL语句
    响应式布局——@media详解
    CXF使用教程(三)——基于Spring的webService开发
    CXF使用教程(二)——webService自动生成客户端代码
    Spring事务管理
  • 原文地址:https://www.cnblogs.com/migoo/p/11752667.html
Copyright © 2020-2023  润新知