贫瘠之地

华北无浪漫,死海扬起帆
多少个夜晚,独自望着天

0%

Java 从弱引用看常量池

前言

在看阿里开源的 TransmittableThreadLocal Agent 时发现了在对类进行增强的流程中使用了 WeakHashMap

com.alibaba.ttl3.agent.TtlExtensionTransformletManager

1
2
3
4
5
6
7
8
9
10
// NOTE: use WeakHashMap as a Set collection, value is always null.
private final WeakHashMap<ClassLoader, ?> collectedClassLoaderHistory = new WeakHashMap<>(512);

// Map: ExtensionTransformlet ClassLoader -> ExtensionTransformlet ClassName -> ExtensionTransformlet instance(not include from parent classloader)
private final WeakHashMap<ClassLoader, Map<String, TtlTransformlet>> classLoader2ExtensionTransformlets =
new WeakHashMap<>(512);

// Map: ExtensionTransformlet ClassLoader -> ExtensionTransformlet ClassName -> ExtensionTransformlet instance(include from parent classloader)
private final WeakHashMap<ClassLoader, Map<String, TtlTransformlet>> classLoader2ExtensionTransformletsIncludeParentCL =
new WeakHashMap<>(512);

这里使用了弱引用元素的 HashMap,应该是只用于 JVM 启动的类加载阶段,所以使用了特殊的引用类型

之前没怎么关注过引用类型,所以看了一下 API 使用了一下,感觉引用类型可以用来验证常量池等现象,加深对引用的理解

虽然更像是八股文的内容,不过还是想记录下来

强软弱虚

在这里重复一下 Java 中四种引用类型的概念

在 Java 中,有四种类型的引用:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference);它们在垃圾回收方面具有不同的特性,适用于不同的场景

  • 强引用(Strong Reference)
    • 强引用是最常见的引用类型,它会阻止被引用对象被垃圾回收
    • 当一个对象有强引用与之关联时,垃圾回收器不会回收该对象,即使内存空间不足
  • 软引用(Soft Reference)
    • 软引用是一种相对强引用较弱的引用类型
    • 当内存空间足够时,软引用不会被垃圾回收,但当内存空间不足时,垃圾回收器会尝试回收软引用对象
  • 弱引用(Weak Reference)
    • 弱引用比软引用更弱,它的生命周期更短
    • 当垃圾回收器执行垃圾回收时,无论内存是否足够,弱引用都会被回收
  • 虚引用(Phantom Reference)
    • 虚引用是最弱的引用类型,几乎没有实际的引用作用
    • 虚引用的主要作用是跟踪垃圾回收器的回收过程,通过与引用队列(Reference Queue)联合使用


常见的应用场景

类型 场景
强引用 适用于需要确保对象一直存在的场景,例如全局变量、静态变量等
软引用 适用于对内存敏感的缓存场景,可以在内存不足时释放一些缓存对象
弱引用 适用于临时对象的缓存、对象关联性的辅助引用等场景,可以快速释放不再被引用的对象
虚引用 适用于需要在对象被回收时执行特定操作的场景,例如对象销毁的清理操作

WeakHashMap

当 WeakHashMap 中的某个 entry 的 key 不再被日常使用时,该 entry 将自动被删除

更准确地说,给定 key 的映射的存在不会阻止该键被垃圾收集器丢弃,即使其可最终化、最终确定,然后回收

当一个 key 被丢弃时,它的 entry 实际上会从映射中删除,因此此类的行为与其他 Map 实现有些不同

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) throws InterruptedException {
Map<Object, Object> map = new WeakHashMap<>();
map.put(new Object(), new Object());

// 等待 GC
System.gc();
TimeUnit.SECONDS.sleep(1);

System.out.println(map.size()); // 0
System.out.println(map.keySet()); // []
}

需要注意 System.gc() 不一定会确保执行了 GC 操作,并且 GC 是异步的,所以后续又进行了 1s 的 sleep,不过从结果上来看是进行了 GC 操作

Entry

WeakHashMap 实现弱引用元素的关键在于内部类 Entry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 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) {
// key 和 queue
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
...
}

可以看到 Entry 继承了 WeakReference,并且在构造器中调用了父类构造器,将 key 包装为了弱引用,并且提供了一个队列用于存放被驱逐的 Entry 对象

1
2
3
4
/**
* Reference queue for cleared WeakEntries
*/
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

驱逐元素

在调用 sizeget 等方法时,WeakHashMap 通过 expungeStaleEntries 方法来驱逐元素

1
2
3
4
5
6
public int size() {
if (size == 0)
return 0;
expungeStaleEntries();
return size;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 清理表中的过期(无用)条目。
*/
private void expungeStaleEntries() {
// 遍历队列中的每个对象,直到队列为空
for (Object x; (x = queue.poll()) != null; ) {

synchronized (queue) { // 使用 synchronized 来确保线程安全

// 将从队列中获取的对象强转为 Entry<K,V> 类型
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;

// 获取该条目在表数组中的索引
int i = indexFor(e.hash, table.length);

Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
// 遍历在 table[i] 中的链表节点
while (p != null) {
Entry<K,V> next = p.next;
// 判断当前节点是否等于过期(被回收)的节点
if (p == e) {
// 从链表中移除过期(被回收)节点
if (prev == e)
table[i] = next;
else
prev.next = next;
// 将 value 设置为 null,使该条目能被垃圾回收器回收
e.value = null;
// 减少哈希表的大小
size--;
break;
}
prev = p;
p = next;
}
}
}
}

概括流程如下:

  1. 遍历 ReferenceQueue 内的对象
  2. 找到哈希表中的位置遍历链表上的元素
  3. 从链表中移除过期(被回收)节点
  4. 将 value 设置为 null,使该条目能被垃圾回收器回收
  5. 减少哈希表 size

有两点需要注意

  • 清理逻辑中不能清空 e.next,因为过时的 entry 可能正在被 HashIterator 迭代器使用
  • 在清理对象时使用 synchronized 上锁,lock 对象为 ReferenceQueue 实例

迭代器

上面在注意中提到,迭代器可能使用过时的 entry

那说明迭代器逻辑中并没有使用 expungeStaleEntries 相关逻辑,那么迭代器是如何驱逐过期对象呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private abstract class HashIterator<T> implements Iterator<T> {
private int index; // 当前遍历的索引位置
private Entry<K,V> entry; // 当前遍历到的 Entry
private Entry<K,V> lastReturned; // 最后返回的 Entry
private int expectedModCount = modCount; // 预期修改计数,用于检查并发修改
private Object nextKey; // 下一个键,需要强引用以防止在 hasNext 和 next 之间的键消失
private Object currentKey; // 当前键,需要强引用以防止在 nextEntry 和任何使用 entry 之间的键消失

HashIterator() {
index = isEmpty() ? 0 : table.length;
}

// 判断是否存在下一个元素
public boolean hasNext() {
Entry<K,V>[] t = table;

// 当下一个键为空时进行循环
while (nextKey == null) {
Entry<K,V> e = entry;
int i = index;
// 当entry为null并且index大于0时,从table的index-1位置开始依次向前查找,直到找到非null的entry
while (e == null && i > 0)
e = t[--i];
entry = e;
index = i;
// 如果entry为null,说明没有找到下一个元素
if (e == null) {
currentKey = null;
return false;
}
// 采用强引用保持键,防止键在此期间被回收
nextKey = e.get();
// 如果键为null,说明该Entry已经被回收,此时跳到下一个Entry
if (nextKey == null)
entry = entry.next;
}
return true;
}

String

字符串常量池

字符串常量池是 Java 中的一个特殊区域,用于存储字符串常量的唯一实例;它是在堆内存中的一部分,与堆中的其他对象分开存储

特点与使用:

  • 字符串不可变
  • 常量池中相同字面量字符串唯一,编译器会自动优化
  • 使用双引号括起来的字符串字面量(如 String s = "Hello World")在编译时会自动放入字符串常量池
  • String 类的 intern() 方法手动将字符串添加到常量池中
  • 如果使用 String 构造器则会创建一个新对象,该对象和字符串常量池中是不同对象
1
2
3
4
5
6
7
8
String s1 = new String("Hello World");
String s2 = "Hello World";
String s3 = "Hello World";
String s4 = s1.intern();

System.out.println(s1 == s2); // false
System.out.println(s2 == s3); // true
System.out.println(s2 == s4); // true

验证

WeakHashMap 的弱引用是指 key,将 key 分别设置为上述 String 的三种创建方式

1
2
3
4
5
6
7
8
9
10
11
12
Map<Object, Object> map1 = new WeakHashMap<>();
map1.put("Hello World", 1);
map1.put(new String("Hello World"), 2);

Map<Object, Object> map2 = new WeakHashMap<>();
map2.put("Hello World", 1);
map2.put(new String("Hello World").intern(), 3);

System.gc();

System.out.println(map1.entrySet()); // [Hello World=2]
System.out.println(map2.entrySet()); // [Hello World=3]

结果可以看出 new String("") 创建出的对象已经被移除了

Integer

Integer 的 valueOf 和缓存

在我们声明一个 Integer 时,其实字节码调用的是其 valueOf 方法

1
2
3
4
5
6
int i1 = 1;
int i2 = 2;
int i3 = 6;
Integer i4 = 1;
Integer i5 = 2;
Integer i6 = 6;

对应的字节码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 0 iconst_1
1 istore_1
2 iconst_2
3 istore_2
4 bipush 6
6 istore_3
7 iconst_1
8 invokestatic #2 <java/lang/Integer.valueOf>
11 astore 4
13 iconst_2
14 invokestatic #2 <java/lang/Integer.valueOf>
17 astore 5
19 bipush 6
21 invokestatic #2 <java/lang/Integer.valueOf>
24 astore 6
26 return

可以分为两类来看:

所以对于 Integer 而言,实际上的直接赋值被字节码转换为了方法的调用

valueOf 就带有了为人熟知的缓存值

1
2
3
4
5
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];

static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;

cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);

// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}

private IntegerCache() {}
}

也就是八股文中常说的 -128 ~ 127

1
2
3
4
5
6
7
8
9
10
11
Integer i1 = 127;
Integer i2 = 127;
Integer i3 = new Integer(127);
System.out.println(i1 == i2); // true
System.out.println(i2 == i3); // false

Integer i4 = 128;
Integer i5 = 128;
Integer i6 = new Integer(128);
System.out.println(i4 == i5); // false
System.out.println(i5 == i6); // false

验证

1
2
3
4
5
6
7
Map<Object, Object> map = new WeakHashMap<>();
map.put(127, 1);
map.put(128, 2);

System.gc();

System.out.println(map.entrySet()); // [127=1]

因为 128 在调用 valueOf 时并不在缓存中,最后返回 new Integer(i) 导致该对象没有强引用存在,被回收了

Map 作为 Set 使用

在 TransmittableThreadLocal 中其实是将 WeekHashMap 当作 Set 来使用,value 恒为 null,在其注释也有说明

NOTE: use WeakHashMap as a Set collection, value is always null.

这种场景比较常见,Spring 中就有将 ConcurrentHashMap 作为 Set 使用的例子,这样就可以简单地实现一个具有和某个 Map 同样功能的 Set 了

此外从 Java 6 开始 java.util.Collections 类提供了一个 newSetFromMap 方法,该方法能够基于指定的 Map 对象创建一个新的 Set 对象

1
2
3
4
5
6
7
Set<Integer> set = Collections.newSetFromMap(new WeakHashMap<>());
set.add(127);
set.add(128);

System.gc();

System.out.println(set); // [127]