1. 创建springboot工程
- 只有一个controller
package cn.enjoyedu.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/jvm")
public class MatController {
@RequestMapping("/mat")
public String mat(){
ThreadLocal<Byte[]> localVariable = new ThreadLocal<>();
localVariable.set(new Byte[4096*1024]);// 为线程添加变量
return "success";
}
@RequestMapping("/mat1")
public String mat1(){
ThreadLocal<Byte[]> localVariable = new ThreadLocal<>();
localVariable.set(new Byte[4096*1024]);// 为线程添加变量
localVariable.remove();
return "success";
}
}
2. 打成jar包,上传至服务器
- java -jar -XX:+HeapDumpOnOutOfMemoryError jvm-1.0-SNAPSHOT.jar,启动项目。
HeapDumpOnOutOfMemoryError 表示在发生OOM之前导出了 dump 日志
3. 使用ab进行压测
- ab -c 10 -n 1000 http://127.0.0.1:8080/jvm/mat
4. 查看日志
- 发生OOM
5. 查看dump日志文件
6. 导入MAT
- 点击显示柱状图,点击Retained Heap排序
- 查看Retained Heap最大的类的incoming references
- 查看GC Roots
- 这里可以很明显地查看到是 ThreadLocal 这块的代码出现了问题
7. 原因分析
- ThreadLocal 是基于 ThreadLocalMap 实现的,这个 Map 的 Entry 继承了 WeakReference,而 Entry 对象中的 key 使用了 WeakReference 封装,也就是说 Entry 中的 key 是一个弱引用类型,而弱引用类型只能存活在下次 GC 之前。
- 当发生一次垃圾回收,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的 话(肯定不会结束),这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而这块 value 永远不会被访问到了,所以存在着内存泄露。如下图:
- 只有当前 thread 结束以后,current thread 就不会存在栈中,强引用断开,Current Thread、Map value 将全部被 GC 回收(但是这 种情况很难)。最好的做法是不在需要使用 ThreadLocal 变量后,都调用它的 remove()方法,清除数据。