前言
这个话题已经是老生常谈了, 之所以又被我拎出来, 是因为博主隔壁的一个童鞋最近写了一篇叫做ThreadLocal 内存泄露的文章, 我就不上链接了, 因为写的实在是..(省略一万字)
重点是写完后, 还被我问懵了. 出于人道主义关怀, 博主很不要脸的再写一篇.
正文
定义
首先, 我们要先谈一下定义, 因为一堆人搞不懂内存溢出和内存泄露的区别.
内存溢出(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 上, 有一个问题, 如下所示
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.
大致就是, 因为面试需要手写一段内存泄露的程序, 然后提问的人突然懵逼了, 于是很多大佬纷纷给出回答.
案例一
此例子出自算法(第四版)一书, 我简化了一下
- 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 空了, 这些元素占的内存也不会被回收的. 解决方案就是
- 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 使用了线程池, 线程是可能存在复用情况.
来源: https://juejin.im/entry/5b6cfd42e51d45348a302c04