涉及问题:
- WeakHashMap和ThreadLocal的异同,两者分别怎样清理失效的Entry?
- 为什么ThreadLocal会内存泄漏?原理类似的WeakHashMap会内存泄漏吗?
在网上查没看懂,以下是自己看源码后的一些思考,很口语化且不保证一定正确,欢迎纠错
WeakHashMap源码
WeakHashMap.Entry直接继承自WeakReference,是弱引用本身,它持有的referent指向对象A(key)。另外,WeakHashMap.Entry中持有强引用value
对象A不再被外部引用后会被回收,回收时WeakHashMap.Entry对象作为弱引用加入引用队列queue中:
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> {
/**
* The entries in this hash table extend WeakReference, using its main ref
* field as the key.
*/
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
/**
* Creates new entry.
*/
Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
// ...
}
// ...
}
public class WeakReference<T> extends Reference<T> {
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
// ...
}
public abstract class Reference<T> {
private T referent; /* Treated specially by GC */
volatile ReferenceQueue<? super T> queue;
// ...
}
expunge:清除
Stale:不新鲜的
Entries:条目(键值对)
失效条目指这样的条目:Entry(key=null, value=A)
get()、put()、size()等方法都会调用expungeStaleEntries()。expungeStaleEntries()会把队列中的所有引用取出。取出的Object x的实际类型是WeakHashMap.Entry,转换为Entry<K,V>类型后,可以从中取出强引用value,将value置null,并从Map中把该Entry去除(此处即使不让value=null,value指向的值对象应该也能被回收,因为Map不再持有该Entry -> Entry被回收 -> Entry中value曾指向的值对象被回收,只是得经过多次GC才行。所以手动value=null应该是为了使值对象更快回收?):
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> {
// 清除所有失效的条目
private void expungeStaleEntries() {
// 轮询引用队列,移除所有条目
for (Object x; (x = queue.poll()) != null; ) {
// 同步队列以保证线程安全
synchronized (queue) {
@SuppressWarnings("unchecked") // 抑制类型转换警告
Entry<K,V> e = (Entry<K,V>) x; // 将Object转换为Entry类型
int i = indexFor(e.hash, table.length); // 计算哈希值对应的索引位置
Entry<K,V> prev = table[i]; // 当前索引位置的头结点
Entry<K,V> p = prev; // 从头结点开始遍历链表
// 遍历链表直到尾部
while (p != null) {
Entry<K,V> next = p.next;
// 如果当前节点是失效的节点
if (p == e) {
// 如果是链表的第一个节点
if (prev == e) {
table[i] = next;
} else {
prev.next = next;
}
// 此处不能将e.next置为null,因为可能被迭代器使用
e.value = null; // 清除值引用,帮助垃圾回收
size--; // 减少有效条目计数
break;
}
// 移动到下一个节点继续查找
prev = p;
p = next;
}
}
}
}
// ...
}
例子:WeakHashMap用法
需要经过几次GC才能看到值对象被回收(使用JDK8,参数:-XX:+PrintGCDetails -Xmn10m -Xmx10m ):
public class WeakHashMapExample {
public static void main(String[] args) {
WeakHashMap<String, Value> map = new WeakHashMap<>();
String key = new String("key");
// 注意:
// 此处不能使用字面值定义,否则key和字符串常量池中的引用指向同一对象,即使key=null该对象也不能被回收
// 而使用new定义时,key指向的对象和字符串常量池中的对象不是同一对象,key=null时前者可被回收
// String key = "key";
map.put(key, new Value());
System.out.println("weakHashMap: " + map);
key = null; // 这意味着"弱键"key1再没有被其它对象引用,调用gc时会回收WeakHashMap中与key1对应的键值对
// 内存回收,这里会回收WeakHashMap中与"key0"对应的键值对
System.out.println("GC 1:");
System.gc(); // 此次GC回收key字符串对象,同时使弱引用加入referenceQueue
try {
Thread.sleep(100); // 等待GC
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
map.get("1"); // 调用get()方法触发expungeStaleEntries(),从referenceQueue取出弱引用把对应value置为null
System.out.println("GC 2:");
System.gc(); // value=null后,此次GC可以回收Value对象
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("GC 3:");
System.gc(); // Value对象被回收后,此次GC可以回收之前被Value对象持有引用的byte[]数组
// 打印WeakHashMap
System.out.println("weakHashMap: " + map);
}
}
ThreadLocal
原理类似WeakHashMap,只是ThreadLocal使用线性探测法而不是链地址法,另外也没有使用ReferenceQueue,所以它清理失效Entry没有WeakHashMap积极(几乎不会做对所有失效Entry的清理):
public class ThreadLocal<T> {
// 虽然ThreadLocalMap类定义在ThreadLocal里,但ThreadLocalMap对象存在Thread里
static class ThreadLocalMap {
// ThreadLocalMap.Entry继承自弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
private int size = 0;
// ...
}
// ...
}
class Thread implements Runnable {
// ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class.
ThreadLocal.ThreadLocalMap threadLocals = null;
// ...
}
set():
虽然set()中也调用了expungeStaleEntry(int staleSlot),但和WeakHashMap不一样,ThreadLocalMap没有使用引用队列,所以不具体知道哪些Entry需要清除。所以set()中并不会清除所有失效Entry,只会为了腾出空间而做消极的清除,另外把冲突链上的失效Entry顺带清除一下:
public class ThreadLocal<T> {
// 设置当前线程局部变量的值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 获取当前Thread对象的ThreadLocalMap
if (map != null)
map.set(this, value); // 调用ThreadLocalMap的set()方法来存储键值对
else
createMap(t, value); // 如果ThreadLocalMap为空,则创建一个新的Map并存储键值对
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; // 返回与当前线程关联的ThreadLocalMap
}
static class ThreadLocalMap {
// 内部类ThreadLocalMap,用于存储ThreadLocal的键值对
private Entry[] table; // ThreadLocalMap的哈希表,存储键值对
private int size = 0; // 哈希表中条目的数量
// 存储键值对
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table; // ThreadLocalMap的哈希表
int len = tab.length; // 哈希表的长度
int i = key.threadLocalHashCode & (len-1); // 计算key的哈希值,并定位到哈希表的索引
// 使用线性探测法寻找合适位置或冲突链中的key相等的条目
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) { // 如果找到key相等的条目
e.value = value; // 直接更新value
return;
}
if (k == null) { // 如果当前条目中的键已被回收
replaceStaleEntry(key, value, i); // 清除失效条目,并用新键值对替换
return;
}
// 如果当前条目被占用且key不匹配,继续探测到下一个索引
}
// 如果没有找到key相等的条目,并且没有遇到null(即没有冲突链中的空位)
tab[i] = new Entry(key, value); // 在探测到的第一个null位置创建新的条目
int sz = ++size; // 增加条目数量
// 清理一些失效的条目,并检查是否需要进行扩容操作
if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清理并检查扩容阈值
rehash(); // 如果需要,进行扩容操作
}
// 线性探测的辅助方法,获取下一个索引
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
// 清理一些失效的条目
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false; // 标记是否清理了任何条目
Entry[] tab = table; // 当前哈希表
int len = tab.length; // 哈希表长度
// 从当前索引开始,尝试清理失效的条目
do {
i = nextIndex(i, len); // 移动到下一个索引
Entry e = tab[i];
if (e != null && e.get() == null) { // 如果条目存在但键已被回收
n = len; // 重置索引,重新开始清理
removed = true; // 标记清理了条目
i = expungeStaleEntry(i); // 清除失效条目
}
} while ( (n >>>= 1) != 0 );
return removed; // 返回是否清理了条目
}
// 清除索引为staleSlot的失效条目,并重新哈希冲突链上的其他条目
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table; // 当前线程的ThreadLocalMap的哈希表
int len = tab.length; // 哈希表的长度
// 清除索引为staleSlot的条目
tab[staleSlot].value = null; // 设置值对象为null
tab[staleSlot] = null; // 设置条目本身为null
size--; // 减少ThreadLocalMap的大小
// 从staleSlot的下一个索引开始,重新哈希直到遇到null
Entry e; // 当前条目
int i; // 当前索引
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get(); // 获取当前条目的键(ThreadLocal对象)
if (k == null) { // 如果键已经被回收
e.value = null; // 清除值对象
tab[i] = null; // 清除条目本身
size--; // 减少ThreadLocalMap的大小
} else {
// 计算键的哈希值,并与哈希表长度进行模运算,得到新的哈希位置
int h = k.threadLocalHashCode & (len - 1);
if (h != i) { // 如果当前条目的哈希位置与新的哈希位置不一致
tab[i] = null; // 清除当前位置的条目
// 扫描直到找到null位置,以便重新放置当前条目
// 与Knuth算法不同,我们需要扫描到null,因为可能有多个条目已经失效
while (tab[h] != null) {
h = nextIndex(h, len); // 找不到null位置,继续扫描
}
tab[h] = e; // 放置当前条目到新的哈希位置
}
}
}
return i; // 返回当前扫描的索引,此时索引指向null
}
// ...
}
// ...
}
get():
只会把冲突链上的失效Entry顺带清除一下:
public class ThreadLocal<T> {
// 获取当前线程局部变量的值
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 获取当前Thread对象的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // 获取键为当前ThreadLocal的条目
if (e != null) { // 如果条目存在
@SuppressWarnings("unchecked") // 抑制类型转换的警告
T result = (T)e.value; // 强制类型转换并返回值
return result;
}
}
// 如果没有找到条目,则初始化并设置初始值
return setInitialValue();
}
// ...
static class ThreadLocalMap {
// 根据键获取对应的条目
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1); // 计算键的哈希值,并定位到哈希表的索引
Entry e = table[i]; // 获取该索引上的条目
if (e != null && e.get() == key) // 如果该条目存在且键匹配
return e; // 返回匹配的条目
else
return getEntryAfterMiss(key, i, e); // 继续搜索冲突链
}
// 在首次获取条目未命中后,继续搜索冲突链
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table; // 当前哈希表
int len = tab.length; // 哈希表长度
while (e != null) { // 遍历冲突链
ThreadLocal<?> k = e.get();
if (k == key) // 如果键匹配
return e; // 返回匹配的条目
if (k == null) // 如果键已被回收
expungeStaleEntry(i); // 清除失效条目
else {
// 如果键不匹配,继续探测到下一个索引
i = nextIndex(i, len);
e = tab[i];
}
}
return null; // 如果没有找到匹配的条目,返回null
}
// ...
}
}
remove():
把key(ThreadLocal对象)对应的value置为null,以免值对象(本地线程中的数据副本)被一直存活的ThreadLocalMap对象持有,无法被回收。除当前key所在Entry外只会把冲突链上的失效Entry顺带清除一下
public class ThreadLocal<T> {
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
// ...
}
public class ThreadLocal<T> {
static class ThreadLocalMap {
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
// 用于清除索引为staleSlot的失效条目,并重新哈希冲突链上的其他条目
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table; // 获取当前线程的ThreadLocalMap的哈希表
int len = tab.length; // 获取哈希表的长度
// 清除索引为staleSlot的条目
tab[staleSlot].value = null; // 设置值对象为null
tab[staleSlot] = null; // 设置条目本身为null
size--; // 减少ThreadLocalMap的大小
// 从staleSlot的下一个索引开始,重新哈希直到遇到null
Entry e;
int i;
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get(); // 获取当前条目的键(ThreadLocal对象)
if (k == null) {
// 如果键已经被回收,清除当前条目
e.value = null;
tab[i] = null;
size--;
} else {
// 计算键的哈希值,并与哈希表长度进行模运算,得到新的哈希位置
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
// 如果当前条目的哈希位置与新的哈希位置不一致,需要移动条目
tab[i] = null;
// 扫描直到找到null位置,以便重新放置当前条目
// 与Knuth算法不同,我们需要扫描到null,因为可能有多个条目已经失效
while (tab[h] != null) {
h = nextIndex(h, len); // 找不到null位置,继续扫描
}
tab[h] = e; // 放置当前条目到新的哈希位置
}
}
}
return i; // 返回当前扫描的索引,此时索引指向null
}
// ...
}
// ...
}
expungeStaleEntries()
这个方法会遍历整个哈希表来清理所有失效Entry,但只在Re-pack或re-size哈希表时才会被调用(只会在set()中被调用):
public class ThreadLocal<T> {
static class ThreadLocalMap {
private int size = 0; // The number of entries in the table.
private int threshold; // The next size value at which to resize.
private void set(ThreadLocal<?> key, Object value) {
// ...
int sz = ++size;
// size要超过阈值,调整哈希表大小,并重新哈希
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash(); // 只在这里被调用
}
// Re-pack and/or re-size the table. First scan the entire table removing stale entries.
// If this doesn't sufficiently shrink the size of the table, double the table size.
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
// Expunge all stale entries in the table.
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
// ...
}
// ...
}
HashMap实现的区别
WeakHashMap使用链地址法,ThreadLocal使用线性探测法:
- 链地址法:如果计算出的索引位置已经有其他条目,那么新的条目不会替换掉原有条目,而是在该索引位置形成一个链表。新条目被添加到链表的头部或尾部(取决于具体的实现)
- 线性探测法:如果计算出的索引位置已经有其他条目,那从当前位置开始,按照固定顺序(通常是线性顺序)探测下一个位置,直到找到一个空闲的位置。将新条目插入探测到的第一个空闲位置
内存泄漏问题
为什么不一样呢?
- ThreadLocal:首先,要想在key(ThreadLocal对象)被回收后再找到其对应的value,就只能去遍历ThreadLocalMap(实际上ThreadLocalMap的expungeStaleEntries()就是靠遍历来找到所有失效Entry的),但是每次回收都去通过遍历来清除明显是不现实的,而且ThreadLocalMap的方法也是不暴露给外部的,所以我们只能选择在key被回收前通过它把对应的value先行置null,即,赶在key被回收前手动调用key.remove()。WeakHashMap是使用了引用队列,才能准确得知哪个Entry需要清理,既不需要依靠key来找Entry,也能实现在map.get()、put()、size()等各种操作后都顺带清理一次所有的失效Entry
- ThreadLocal不调key.remove()等方法就会内存泄漏的主要原因其实是:ThreadLocalMap本身很难被回收,因为它被所在线程thread对象持有。一旦没有手动清除Entry(key=null, value=A),对象A就会存活到线程结束
- 另外key.remove()、get()、set()等都并不能保证把map里的所有失效Entry清理掉,比如key.remove()只能保证把key对应的Entry清理掉