近日线上一个项目总是时隔1周发生OOM自动重启,问题很明显内存泄漏了。。。
使用jmap查看一下线上服务堆使用情况,实例最多的前10个类
1110125 instances of class [C 2108705 instances of class java.lang.String 388066 instances of class 4java.util.concurrent.ConcurrentHashMap$Node 579224 instances of class java.lang.Object 652984 instances of class [B 748482 instances of class java.lang.ref.Finalizer 839684 instances of class java.util.zip.Inflater <---罪魁祸手 939684 instances of class java.util.zip.ZStreamRef 1028168 instances of class [Ljava.lang.Object; 1126576 instances of class java.util.HashMap$Node
看到这个类排名第一反应就是GZIP相关的操作可能有问题,那么我们目光聚集到代码上吧
1public static String unZip(String str) throws IOException { 2 ByteArrayOutputStream out = new ByteArrayOutputStream(); 3 byte[] bytes = Base64.getDecoder().decode(str); 4 ByteArrayInputStream in = new ByteArrayInputStream(bytes); 5 GZIPInputStream gzip = new GZIPInputStream(in); 6 byte[] buffer = new byte[256]; 7 int n = 0; 8 while ((n = gzip.read(buffer)) >= 0) { 9 out.write(buffer, 0, n); 10 } 11 return out.toString(CODE); 12}
这段代码是当时想要使用GZIP做解压缩从网上抄来了,当时只是用单测验证了一下这段代码的正确性,就上线了。
出现了内存泄漏问题之后,回过头来反思这段代码发现这里使用了3个流ByteArrayOutputStream ,ByteArrayInputStream ,GZIPInputStream 。
重点是这三个流在代码结束之后都没有关闭!!! 依次点开三个流的close()方法看了下 ByteArrayOutputStream ,ByteArrayInputStream 这两个流的close()方法其实是空的,说明这两个流其实关闭与否都没有关系。
GZIPInputStream 的close()方法
1public void close() throws IOException { 2 if (!closed) { 3 super.close(); 4 eos = true; 5 closed = true; 6 } 7}
看到这个方法后,具体怎么关闭的其实不那么重要了,重要的是说明了这个流是需要关闭的
现在我们再看看内存泄漏的具体原因是什么吧,我们依次点开GZIPInputStream 的构造方法
1public GZIPInputStream(InputStream in, int size) throws IOException { 2 super(in, new Inflater(true), size); //看到堆内大量实例Inflater了 3 usesDefaultInflater = true; 4 readHeader(in); 5}
点开Inflater的构造方法
1public Inflater(boolean nowrap) { 2 zsRef = new ZStreamRef(init(nowrap)); //c++方法init 3}
这个init方法使用了C++ calloc申请内存,这部分内存是无法被Java GC回收的,这就导致我们的服务可用堆内存越来越小,最后程序OOM Crash重启
修改这段代码很简单,所有流使用完毕之后关闭就好了,最终代码如下
1private static String unZip(String str) throws IOException { 2 try (ByteArrayOutputStream out = new ByteArrayOutputStream(); 3 ByteArrayInputStream in = new ByteArrayInputStream(Base64.getDecoder().decode(str)); 4 GZIPInputStream gzip = new GZIPInputStream(in)) { 5 byte[] buffer = new byte[256]; 6 int n = 0; 7 while ((n = gzip.read(buffer)) >= 0) { 8 out.write(buffer, 0, n); 9 } 10 return out.toString(CODE); 11 } 12}
虽然ByteArrayOutputStream ,ByteArrayInputStream 这两个流的close()方法为空,无需关闭。但是从这个线上问题得出的反思,任何流,使用完毕之后一定要注意养成关闭的好习惯。(除非需要复用流)