浅谈内存泄露

  1. 云栖社区>
  2. 博客>
  3. 正文

浅谈内存泄露

java填坑路 2018-08-08 15:19:00 浏览476
展开阅读全文

正文


定义

首先,我们要先谈一下定义,因为一堆人搞不懂内存溢出和内存泄露的区别。

内存溢出(OutOfMemory):你只有十块钱,我却找你要了一百块。对不起啊,我没有这么多钱。(给不起)

内存泄露(MemoryLeak):你有十块钱,我找你要一块。但是无耻的博主,不把钱还你了。(没退还)

关系:多次的内存泄露,会导致内存溢出。(博主不要脸的找你多要几次钱,你就没钱了,就是这个道理。)



危害

ok,大家在项目中有没遇到过java程序越来越卡的情况。

因为内存泄露,会导致频繁的Full GC,而Full GC 又会造成程序停顿,最后Crash了。因此,你会感觉到你的程序越来越卡,越来越卡,然后你就被产品经理鄙视了。顺便提一下,我们之所以JVM调优,就是为了减少Full GC的出现。

我记得,我曾经有一次,就遇到项目刚上线的时候好好的。结果随着时间的堆积,报了OutOfMemoryError: PermGen space。

说到这个PermGen space,突然间,一阵洪荒之力,从博主体内喷涌而出,一定要介绍一下这个方法区,不过点到为止,毕竟这不是在讲《jvm从入门到放弃》。

方法区:出自java虚拟机规范, 可供各条线程共享运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。

上面讲的是规范,在不同虚拟机里头实现是不一样的,最典型的就是永久代(PermGen space)元空间(Metaspace)

jdk1.8以前:实现方法区的叫永久代。因为在很久远以前,java觉得类几乎是静态的,并且很少被卸载和回收,所以给了一个永久代的雅称。因此,如果你在项目中,发现堆和永久代一直在不断增长,没有下降趋势,回收的速度根本赶不上增长的速度,不用说了,这种情况基本可以确定是内存泄露。

jdk1.8以后:实现方法区的叫元空间。Java觉得对永久代进行调优是很困难的。永久代中的元数据可能会随着每一次Full GC发生而进行移动。并且为永久代设置空间大小也是很难确定的。因此,java决定将类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。这样,我们就避开了设置永久代大小的问题。但是,这种情况下,一旦发生内存泄露,会占用你的大量本地内存。如果你发现,你的项目中本地内存占用率异常高。嗯,这就是内存泄露了。


如何排查

(1)通过jps查找java进程id。

(2)通过top -p [pid]发现内存占用达到了最大值

(3)jstat -gccause pid 20000 每隔20秒输出Full GC结果

(4)发现Full GC次数太多,基本就是内存泄露了。生成dump文件,借助工具分析是哪个对象太多了。基本能定位到问题在哪。


实例

在stackoverflow上,有一个问题,如下所示


img_5df4cdb1af5d7bac7683021f21b8371a.png

大致就是,因为面试需要手写一段内存泄露的程序,然后提问的人突然懵逼了,于是很多大佬纷纷给出回答。

案例一

此例子出自《算法》(第四版)一书,我简化了一下


img_0e7b537b076d9baa0923717030494648.png

当数据从栈里面弹出来之后,data数组还一直保留着指向元素的指针。那么就算你把栈pop空了,这些元素占的内存也不会被回收的。

解决方案就是


img_31080ead117ef4408cf9ef61d252586c.png

案例二

这个其实是一堆例子,这些例子造成内存泄露的原因都是类似的,就是不关闭流,具体的,可以是文件流,socket流,数据库连接流,等等

具体如下,没关文件流


img_03af28d14c12ac18d667c3c6d2d13a7a.png

再比如,没关闭连接


img_0e4fd08bb9558f1e0ae399bfe41f2d4f.png

解决方案就是。。。嗯,大家应该都会。。你敢说你不会调close()方法。

案例三

讲这个例子前,大家对ThreadLocal在Tomcat中引起内存泄露有了解么。不过,我要说一下,这个泄露问题,和ThreadLocal本身关系不大,我看了一下官网给的例子,基本都是属于使用不当引起的。

在Tomcat的官网上,记录了这个问题。地址是:https://wiki.apache.org/tomcat/MemoryLeakProtection

不过,官网的这个例子,可能不好理解,我们略作改动。

public classHelloServletextendsHttpServlet{

    private static final long serialVersionUID = 1L;

    static classLocalVariable{

        private Long[] a = new Long[1024 * 1024 * 100];

    }

    final static ThreadLocal localVariable = new ThreadLocal();

    @Override

    publicvoiddoGet(HttpServletRequest request, HttpServletResponse response)throwsIOException, ServletException{

        localVariable.set(new LocalVariable());

    }

}

再来看下conf下sever.xml配置


maxThreads="150"minSpareThreads="4"/>

线程池最大线程为150个,最小线程为4个

Tomcat中Connector组件负责接受并处理请求,每来一个请求,就会去线程池中取一个线程。

在访问该servlet时,ThreadLocal变量里面被添加了new LocalVariable()实例,但是没有被remove,这样该变量就随着线程回到了线程池中。另外多次访问该servlet可能用的不是工作线程池里面的同一个线程,这会导致工作线程池里面多个线程都会存在内存泄露。

另外,servlet的doGet方法里面创建new LocalVariable()的时候使用的是webappclassloader。

那么

LocalVariable对象没有释放 -> LocalVariable.class没有释放 -> webappclassloader没有释放 -> webappclassloader加载的所有类也没有被释放,也造成了内存泄露。

除此之外,你在eclipse中,做一个reload操作,工作线程池里面的线程还是一直存在的,并且线程里面的threadLocal变量并没有被清理。而reload的时候,又会新构建一个webappclassloader,重复上述步骤。多reload几次,就内存溢出。

不过Tomcat7.0以后,你每做一次reload,会清理工作线程池中线程的threadLocals变量。因此,这个问题在tomcat7.0后,不会存在。

ps:ThreadLocal的使用在Tomcat的服务环境下要注意,并非每次web请求时候程序运行的ThreadLocal都是唯一的。ThreadLocal的什么生命周期不等于一次Request的生命周期。ThreadLocal与线程对象紧密绑定的,由于Tomcat使用了线程池,线程是可能存在复用情况。


欢迎工作一到五年的Java工程师朋友们加入Java架构开发:860113481

群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

网友评论

登录后评论
0/500
评论
java填坑路
+ 关注