注:源代码和部分说明、图片来自享学课堂,略有修改,学习之后所做笔记,方便回顾,也给大家一个参考
引发的内存泄漏分析
预备知识
引用
Object o = new Object();
这个o,我们可以称之为对象引用,而new Object()我们可以称之为在内存中产生了一个对象实例。
当写下 o=null时,只是表示o不再指向堆中object的对象实例,不代表这个对象实例不存在了。
强引用
一直活着:类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。
软引用
有一次活的机会:软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。
弱引用
回收就会死亡:被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在JDK 1.2之后,提供了WeakReference类来实现弱引用。
虚引用
也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。
内存泄漏的现象
执行下的ThreadLocalOOM,并将堆内存大小设置为-Xmx256m
public class MyThreadLocalOOM1 {
public static final Integer SIZE = 500;
static ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 1,
TimeUnit.MINUTES, new LinkedBlockingDeque<>());
static class LocalVariable {//总共有5M
private byte[] locla = new byte[1024 * 1024 * 5];
}
public static void main(String[] args) {
try {
for (int i = 0; i < SIZE; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
new LocalVariable();
System.out.println("开始执行");
}
});
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
我们启用一个线程池,大小固定为5个线程执行,在使用中的堆5*5M=25M
在jdk安装木的的/bin 下面,找到jvisualvm.exe,启动;选择正在运行的线程,选择监视,
情况1:使用普通的变量:可以看到使用的堆大小大概在25M左右浮动,堆的总大小是我们设置的256M
备注:在执行线程的时候,一定要加上Thread.sleep(100),如果不加上这个,看到的堆使用情况就是一条水平的直线
情况2:当我们启用了ThreadLocal变量以后:
public class MyThreadLocalOOM1 {
public static final Integer SIZE = 500;
static ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 1,
TimeUnit.MINUTES, new LinkedBlockingDeque<>());
static class LocalVariable {//总共有5M
private byte[] locla = new byte[1024 * 1024 * 5];
}
final static ThreadLocal<LocalVariable> local = new ThreadLocal<>();
public static void main(String[] args) {
try {
for (int i = 0; i < SIZE; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
local.set(new LocalVariable());
//new LocalVariable();
System.out.println("开始执行");
}
});
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行完成后我们可以看见,内存占用变为了100多M
情况3::加入一行代码ThreadLocal.remove(),再执行,看看内存情况:
public class MyThreadLocalOOM1 {
public static final Integer SIZE = 500;
static ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 1,
TimeUnit.MINUTES, new LinkedBlockingDeque<>());
static class LocalVariable {//总共有5M
private byte[] locla = new byte[1024 * 1024 * 5];
}
final static ThreadLocal<LocalVariable> local = new ThreadLocal<>();
public static void main(String[] args) {
try {
for (int i = 0; i < SIZE; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
local.set(new LocalVariable());
//new LocalVariable();
System.out.println("开始执行");
local.remove();
}
});
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
当添加上local.remove();,当我们启用了ThreadLocal以后确实发生了内存泄漏。
分析
根据我们前面对ThreadLocal的分析,我们可以知道每个Thread 维护一个 ThreadLocalMap,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object,也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。仔细观察ThreadLocalMap,这个map是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
因此使用了ThreadLocal后,引用链如图所示
图中的虚线表示弱引用。
这样,当把threadlocal变量置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被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回收。
解决内存泄露的方法:使用remove
最好的做法是不在需要使用ThreadLocal变量后,都调用它的remove()方法,清除数据。
情况2 发生内存泄露的原因分析
情况2中,由于Thread.sleep(100),可以清晰地看到,线程池中的任务执行完了,但是线程池中的五个线程依然会存在一段时间,因为我们没有显示调用remove()方法,所以导致五个线程中的new LocalVariable()没有被释放,发生了内存泄露。
查看ThreadLocal可以知道,在set(),get()有些时候,会调用expungeStaleEntry()来清除Entry中key=null 的值,但是是不及时的,只有remove(),显示地调用了expungeStaleEntry()
既然使用弱引用导致了内存泄露,为什么还要使用弱引用而不是用强引用?
为什么使用弱引用而不是强引用?
下面我们分两种情况讨论:
key 使用强引用:对ThreadLocal对象实例的引用被置为null了,但是ThreadLocalMap还持有这个ThreadLocal对象实例的强引用(就是Entry中的key),如果没有手动删除,ThreadLocal的对象实例不会被回收,导致Entry内存泄漏。
key 使用弱引用:对ThreadLocal对象实例的引用被被置为null了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal的对象实例也会被回收。value在下一次ThreadLocalMap调用set,get,remove都有机会被回收。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread(ThreadLocalMap是Thread的一个变量)一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
总结
- JVM利用设置ThreadLocalMap的Key为弱引用,来避免内存泄露。
- JVM利用调用remove、get、set方法的时候,回收弱引用。
- 当ThreadLocal存储很多Key为null的Entry的时候,而不再去调用remove、get、set方法,那么将导致内存泄漏。
- 使用线程池+ ThreadLocal时要小心,因为这种情况下,线程是一直在不断的重复运行的,从而也就造成了value可能造成累积的情况。