这个话题已是老生常谈了,之因此又被我拎出来,是由于博主隔壁的一个童鞋最近写了一篇叫作《ThreadLocal内存泄露》的文章,我就不上连接了,由于写的实在是。。(省略一万字)
重点是写完后,还被我问懵了。出于人道主义关怀,博主很不要脸的再写一篇。java
首先,咱们要先谈一下定义,由于一堆人搞不懂内存溢出和内存泄露的区别。
内存溢出(OutOfMemory):你只有十块钱,我却找你要了一百块。对不起啊,我没有这么多钱。(给不起)
内存泄露(MemoryLeak):你有十块钱,我找你要一块。可是无耻的博主,不把钱还你了。(没退还)
关系:屡次的内存泄露,会致使内存溢出。(博主不要脸的找你多要几回钱,你就没钱了,就是这个道理。)web
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
文件,借助工具分析是哪一个对象太多了。基本能定位到问题在哪。apache
在stackoverflow上,有一个问题,以下所示数组
I just had an interview, and I was asked to create a memory leak with Java. Needless to say I felt pretty dumb having no clue on how to even start creating one.tomcat
大体就是,由于面试须要手写一段内存泄露的程序,而后提问的人忽然懵逼了,因而不少大佬纷纷给出回答。
案例一
此例子出自《算法》(第四版)一书,我简化了一下app
class stack{ Object data[1000]; int top = 0; public void push(Object o){ data[top++] = o; } public Object pop(Object o){ return data[--top]; } }
当数据从栈里面弹出来以后,data数组还一直保留着指向元素的指针。那么就算你把栈pop空了,这些元素占的内存也不会被回收的。
解决方案就是less
public Object pop(Object o){ Object result = data[--top]; data[top] = null; return result; }
案例二
这个实际上是一堆例子,这些例子形成内存泄露的缘由都是相似的,就是不关闭流,具体的,能够是文件流,socket流,数据库链接流,等等
具体以下,没关文件流
try { BufferedReader br = new BufferedReader(new FileReader(inputFile)); ... ... } catch (Exception e) { e.printStacktrace(); }
再好比,没关闭链接
try { Connection conn = ConnectionFactory.getConnection(); ... ... } catch (Exception e) { e.printStacktrace(); }
解决方案就是。。。嗯,你们应该都会。。你敢说你不会调close()
方法。
案例三
讲这个例子前,你们对ThreadLocal
在Tomcat
中引发内存泄露有了解么。不过,我要说一下,这个泄露问题,和ThreadLocal自己关系不大,我看了一下官网给的例子,基本都是属于使用不当引发的。
在Tomcat的官网上,记录了这个问题。地址是:https://wiki.apache.org/tomcat/MemoryLeakProtection
不过,官网的这个例子,可能很差理解,咱们略做改动。
public class HelloServlet extends HttpServlet{ private static final long serialVersionUID = 1L; static class LocalVariable { private Long[] a = new Long[1024 * 1024 * 100]; } final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>(); @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { localVariable.set(new LocalVariable()); } }
再来看下conf下sever.xml配置
<!--The connectors can use a shared executor, you can define one or more named thread pools--> <Executor name="tomcatThreadPool" namePrefix="catalina-exec-" 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
使用了线程池,线程是可能存在复用状况。