ThreadLocal会造成内存泄漏的是一个很常见的面试题的,但是究竟为什么ThreadLocal会造成内存泄漏,网上的文章基本上就是说使用了弱引用导致的内存泄漏,但是为什么使用弱引用会导致内存泄漏那?
弱引用
弱引用是对一个对象(称为referent
)的引用的持有者。使用弱引用后,可以维持对referent
的引用,而不会阻止它被垃圾收集。当垃圾收集器跟踪堆的时候,如果对一个对象的引用只有弱引用,那么这个referent就会成为垃圾收集的候选对象,就像没有任何剩余的引用一样,而且所有剩余的弱引用都被清除。(只有弱引用的对象称为弱可及(weakly reachable)。
来看一个例子:
public class Car {
private String name;
public Car(String name) {
this.name = name;
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("car gc");
}
public static void main(String[] args) {
Car car = new Car("baoma");
WeakReference<Car> carWeakReference = new WeakReference<Car>(car);
System.out.println(car);
System.out.println(carWeakReference.get());
car = null;
System.gc();
System.out.println(carWeakReference.get());
}
}
在上述例子中car
是Car对象
(referent)的强引用,并且是GC Root。而carWeakReference
是持有referent的引用car
的弱引用。当car=null
后,弱引用并不会阻止Car对象
被GC回收。
com.xiaowei.Car@5cad8086
com.xiaowei.Car@5cad8086
null
car gc // Car对象已经被回收
全局Map导致内存泄漏
public class SocketManager {
private Map<Socket, User> m = new HashMap<Socket, User>();
public void setUser(Socket s, User u) {
m.put(s, u);
}
public User getUser(Socket s) {
return m.get(s);
}
public void removeUser(Socket s) {
m.remove(s);
}
public boolean isEmpty() {
return m.isEmpty();
}
}
一个常见场景就是客户端的Socket需要和User进行绑定,在这种场景下我们期望的是Socket关闭后,相应的映射也从Map中删除,但是除非我们手动删除这个映射,否则会导致Socket和User对象永远无法被GC回收。
public static void main(String[] args) {
SocketManager socketManager = new SocketManager();
int i = 0;
while (i < 5000) {
Socket s = new Socket();
User u = new User();
socketManager.setUser(s, u);
s = null;
u = null;
System.gc();
}
}
可以看到,即使引用已经为null,但是由于Map的Node的引用关系,导致Socket和User依旧无法被GC回收。
public static void main(String[] args) throws InterruptedException {
SocketManager socketManager = new SocketManager();
int i = 0;
while (i < 5000) {
Socket s = new Socket();
User u = new User();
socketManager.setUser(s, u);
// 移除映射
socketManager.removeUser(s);
s = null;
u = null;
System.gc();
}
}
移除Map的映射后,正常进行GC。
通常情况下我们应该是HashMap,但是有没有一种方式能在Socket关闭之后自动删除Map中的映射那?答案是使用弱引用。
private static ReferenceQueue<Socket> rq = new ReferenceQueue<Socket>();
public static void main(String[] args) throws InterruptedException {
SocketManager socketManager = new SocketManager();
Thread thread = new Thread(() -> {
try {
int cnt = 0;
WeakReference<Socket> k;
while ((k = (WeakReference) rq.remove()) != null) {
System.out.println((cnt++) + " 回收了:" + k);
// 反向操作
socketManager.removeUser(k);
}
} catch (InterruptedException e) {
//结束循环
}
});
thread.setDaemon(true);
thread.start();
Socket s = new Socket();
User u = new User();
WeakReference<Socket> weakReference = new WeakReference<Socket>(s, rq);
socketManager.setUser(weakReference, u);
// help gc
s = null;
System.gc();
TimeUnit.SECONDS.sleep(1);
System.out.println("map.size->" + socketManager.size());
}
输出:
0 回收了:java.lang.ref.WeakReference@5fd0d5ae
map.size->0
使用WeakHashMap
WeakHashMap用弱引用承载映射键
,这使得应用程序不再使用键对象时它们可以被垃圾收集,get()实现可以根据 WeakReference.get()是否返回null来区分死的映射和活的映射。但是这只是防止Map内存消耗在应用程序的生命周期中不断增加所需要做的工作的一半,还需要做一些工作以便在键对象被收集后从Map中删除死项
。否则,Map会充满对应于死键的项。虽然这对于应用程序是不可见的,但是它仍然会造成应用程序耗尽内存,因为即使键被收集了,Map.Entry和值对象也不会被收集。
WeakHashMap有一个名为expungeStaleEntries()
的私有方法,大多数Map操作中会调用它,它去掉引用队列中所有失效的引用,并删除关联的映射Map.Entry。
private Map<Socket, User> m = new WeakHashMap<Socket, User>();
public static void main(String[] args) throws InterruptedException {
SocketManager socketManager = new SocketManager();
Socket s = new Socket();
User u = new User();
socketManager.setUser(s, u);
// help gc
s = null;
System.gc();
TimeUnit.SECONDS.sleep(2);
// 会调用expungeStaleEntries 删除Map.Entry
System.out.println(socketManager.isEmpty());
}
输出:
Socket GC
true
为什么ThreadLocal会内存泄漏
TheadLocal的原理是每个Thread内维护一个ThreadLocalMap,ThreadLocal对象是ThreadLocalMap的key。
static class ThreadLocalMap {
/**
*这个哈希映射中的Entry扩展了WeakReference,使用它的主ref字段作为键(它总是一个*ThreadLocal对象)。请注意,空键(即entry.get() == null)意味着键不再被引用,
*所以条目可以从表中删除。在下面的代码中,这样的条目被称为“陈旧条目(stale entries)”。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
所以当ThreadLocal对象的强引用为null时,ThreadLocalMap.Entry
和ThreadLocal
对象会被GC,而value无法被GC。
参考:
[^]:用弱引用堵住内存泄漏