前言
在看阿里开源的 TransmittableThreadLocal Agent
时发现了在对类进行增强的流程中使用了 WeakHashMap
com.alibaba.ttl3.agent.TtlExtensionTransformletManager
1 2 3 4 5 6 7 8 9 10 private final WeakHashMap<ClassLoader, ?> collectedClassLoaderHistory = new WeakHashMap <>(512 );private final WeakHashMap<ClassLoader, Map<String, TtlTransformlet>> classLoader2ExtensionTransformlets = new WeakHashMap <>(512 ); 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 ()); System.gc(); TimeUnit.SECONDS.sleep(1 ); System.out.println(map.size()); 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 private static class Entry <K,V> extends WeakReference <Object> implements Map .Entry<K,V> { V value; final int hash; Entry<K,V> next; 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; } ... }
可以看到 Entry
继承了
WeakReference
,并且在构造器中调用了父类构造器,将 key
包装为了弱引用,并且提供了一个队列用于存放被驱逐的 Entry
对象
1 2 3 4 private final ReferenceQueue<Object> queue = new ReferenceQueue <>();
驱逐元素
在调用 size
、get
等方法时,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) { @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; while (p != null ) { Entry<K,V> next = p.next; if (p == e) { if (prev == e) table[i] = next; else prev.next = next; e.value = null ; size--; break ; } prev = p; p = next; } } } }
概括流程如下:
遍历 ReferenceQueue
内的对象
找到哈希表中的位置遍历链表上的元素
从链表中移除过期(被回收)节点
将 value 设置为 null,使该条目能被垃圾回收器回收
减少哈希表 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; private Entry<K,V> lastReturned; private int expectedModCount = modCount; private Object nextKey; private Object currentKey; 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; while (e == null && i > 0 ) e = t[--i]; entry = e; index = i; if (e == null ) { currentKey = null ; return false ; } nextKey = e.get(); 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); System.out.println(s2 == s3); System.out.println(s2 == s4);
验证
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()); System.out.println(map2.entrySet());
结果可以看出 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_214 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 { 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 ); h = Math.min(i, Integer.MAX_VALUE - (-low) -1 ); } catch ( NumberFormatException nfe) { } } high = h; cache = new Integer [(high - low) + 1 ]; int j = low; for (int k = 0 ; k < cache.length; k++) cache[k] = new Integer (j++); 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); System.out.println(i2 == i3); Integer i4 = 128 ;Integer i5 = 128 ;Integer i6 = new Integer (128 );System.out.println(i4 == i5); System.out.println(i5 == i6);
验证
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());
因为 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);