理解java对象的引用
一、引用
在栈上的reference类型存储的数据代表某块内存地址,称reference为某内存、某对象的引用。
实际上引用分为很多种,从强到弱分为:强引用 > 软引用 > 弱引用 > 虚引用。
平常我们使用的引用实际上是强引用,各种引用有自己的特点,下文将一一介绍。
强引用就是Java中普通的对象,而软引用、弱引用、虚引用在JDK中定义的类分别是SoftReference、WeakReference、PhantomReference。
下图是软引用、弱引用、虚引用、引用队列(搭配虚引用使用)之间的继承关系。
引用、指针和句柄有什么区别
引用、指针和句柄都具有指向对象地址的含义,可以将它们都简单地理解为一个内存地址。只有在具体的问题中,才需要区分它们的含义:
- 1、引用(Reference): 引用是 Java 虚拟机为了实现灵活的对象生命周期管理而实现的对象包装类,引用本身并不持有对象数据,而是通过直接指针或句柄 2 种方式来访问真正的对象数据;
- 2、指针(Point): 指针也叫直接指针,它表示对象数据在内存中的地址,通过指针就可以直接访问对象数据;
- 3、句柄(Handler): 句柄是一种特殊的指针,句柄持有指向对象实例数据和类型数据的指针。使用句柄的优点是让对象在垃圾收集的过程中移动存储区域的话,虚拟机只需要改变句柄中的指针,而引用持有的句柄是稳定的。缺点是需要两次指针访问才能访问到对象数据。
句柄访问:
二、内存溢出与内存泄漏
为了更清除的描述引用之间的作用,首先需要介绍一下内存溢出和内存泄漏
当发生内存溢出时,表示JVM没有空闲内存为新对象分配空间,抛出OutOfMemoryError(OOM)
当应用程序占用内存速度大于垃圾回收内存速度时就可能发生OOM
抛出OOM之前通常会进行Full GC,如果进行Full GC后依旧内存不足才抛出OOM
JVM参数-Xms10m -Xmx10m -XX:+PrintGCDetails
内存溢出可能发生的两种情况:
- 必须的资源确实很大,堆内存设置太小 (通过-Xmx来调整)
- 发生内存泄漏,创建大量对象,且生命周期长,不能被回收
内存泄漏Memory Leak: 对象不会被程序用到了,但是不能回收它们
对象不再使用并且不能回收就会一直占用空间,大量对象发生内存泄漏可能发生内存溢出OOM
广义内存泄漏:不正确的操作导致对象生命周期变长
- 单例中引用外部对象,当这个外部对象不用了,但是因为单例还引用着它导致内存泄漏
- 一些需要关闭的资源未关闭导致内存泄漏
三、强引用
强引用是程序代码中普遍存在的引用赋值,比如List list = new ArrayList();
只要强引用在可达性分析算法中可达时,垃圾收集器就不会回收该对象,因此不当的使用强引用是造成Java内存泄漏的主要原因
四、软引用
当内存充足时不会回收软引用
只有当内存不足时,发生Full GC时才将软引用进行回收,如果回收后还没充足内存则抛出OOM异常
JVM中针对不同的区域(年轻代、老年代、元空间)有不同的GC方式,Full GC的回收区域为整个堆和元空间
软引用使用SoftReference
1、内存充足情况下的软引用
public static void main(String[] args) { int[] list = new int[10]; SoftReference listSoftReference = new SoftReference(list); list = null; //[I@61bbe9ba System.out.println(listSoftReference.get()); }
如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
软引用可用来实现内存敏感的高速缓存。
// 强引用 String strongReference = new String("abc"); // 软引用 String str = new String("abc"); SoftReference<String> softReference = new SoftReference<String>(str);
软引用可以和一个引用队列(ReferenceQueue
)联合使用。如果软引用所引用对象被垃圾回收,JAVA
虚拟机就会把这个软引用加入到与之关联的引用队列中。
ReferenceQueue<String> referenceQueue = new ReferenceQueue<>(); String str = new String("abc"); SoftReference<String> softReference = new SoftReference<>(str, referenceQueue); str = null; // Notify GC System.gc(); System.out.println(softReference.get()); // abc Reference<? extends String> reference = referenceQueue.poll(); System.out.println(reference); //null
注意:软引用对象是在jvm内存不够的时候才会被回收,我们调用System.gc()方法只是起通知作用,JVM什么时候扫描回收对象是JVM自己的状态决定的。就算扫描到软引用对象也不一定会回收它,只有内存不够的时候才会回收。
OutOfMemoryError
之前回收软引用对象,而且虚拟机会尽可能优先回收长时间闲置不用的软引用对象。对那些刚构建的或刚使用过的**"较新的"软对象会被虚拟机尽可能保留**,这就是引入引用队列ReferenceQueue
的原因。应用场景:
浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
- 如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建;
- 如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出。
这时候就可以使用软引用,很好的解决了实际的问题:
// 获取浏览器对象进行浏览 Browser browser = new Browser(); // 从后台程序加载浏览页面 BrowserPage page = browser.getPage(); // 将浏览完毕的页面置为软引用 SoftReference softReference = new SoftReference(page); page = null; // 回退或者再次浏览此页面时 if(softReference.get() != null) { // 内存充足,还没有被回收器回收,直接获取缓存 page = softReference.get(); } else { // 内存不足,软引用的对象已经回收 page = browser.getPage(); // 重新构建软引用 softReference = new SoftReference(page); page = null; }
2、内存不充足情况下的软引用
(JVM参数:-Xms5m -Xmx5m -XX:+PrintGCDetails)
//-Xms5m -Xmx5m -XX:+PrintGCDetails public class SoftReferenceTest { public static void main(String[] args) { int[] list = new int[10]; SoftReference listSoftReference = new SoftReference(list); list = null; //[I@61bbe9ba System.out.println(listSoftReference.get()); //模拟空间资源不足 try{ byte[] bytes = new byte[1024 * 1024 * 4]; System.gc(); }catch (Exception e){ e.printStackTrace(); }finally { //null System.out.println(listSoftReference.get()); } } }
注意:如果上面示例中没有显示的list = null;这行代码,那么即便内存不足时通知执行了fullGc,该软引用对象也不会被回收,因为还有list这个强引用存在。
也可以直接初始化时就避免将其强引用
public static void main(String[] args) { SoftReference listSoftReference = new SoftReference(new int[10]); //[I@61bbe9ba System.out.println(listSoftReference.get()); //模拟空间资源不足 try{ byte[] bytes = new byte[1024 * 1024 * 400]; System.gc(); }catch (Exception e){ e.printStackTrace(); }finally { //null System.out.println(listSoftReference.get()); } }
五、弱引用
无论内存是否足够,当发生GC时都会对弱引用进行回收
弱引用使用WeakReference
1、内存充足情况下的弱引用
public static void test1() { WeakReference<int[]> weakReference = new WeakReference<>(new int[1]); //[I@511d50c0 System.out.println(weakReference.get()); System.gc(); //null System.out.println(weakReference.get()); }
下面的代码会让一个弱引用再次变为一个强引用:
String str = new String("abc"); WeakReference<String> weakReference = new WeakReference<>(str); // 弱引用转强引用 String strongReference = weakReference.get();
2、WeakHashMap
JDK中有一个WeakHashMap,使用与Map相同,只不过节点为弱引用。
当key的引用不存在引用的情况下,发生GC时,WeakHashMap中该键值对就会被删除
public static void test2() { WeakHashMap<String, String> weakHashMap = new WeakHashMap<>(); HashMap<String, String> hashMap = new HashMap<>(); String s1 = new String("3.jpg"); String s2 = new String("4.jpg"); hashMap.put(s1, "图片1"); hashMap.put(s2, "图片2"); weakHashMap.put(s1, "图片1"); weakHashMap.put(s2, "图片2"); //只将s1赋值为空时,堆中的3.jpg字符串还会存在强引用,所以要remove hashMap.remove(s1); s1=null; s2=null; System.gc(); //4.jpg=图片2 test2Iteration(hashMap); //4.jpg=图片2 test2Iteration(weakHashMap); } private static void test2Iteration(Map<String, String> map){ Iterator iterator = map.entrySet().iterator(); while (iterator.hasNext()){ Map.Entry entry = (Map.Entry) iterator.next(); System.out.println(entry); } }
未显示删除weakHashMap中的该key,当这个key没有其他地方引用时就删除该键值对。
再看一组测试案例:
字面量赋值方式当s1=null后仍旧没有回收是由于在字符串常量池中还有强引用这个对象,所以没有被回收。
jdk7之后的字符串常量池都放在堆中。是一个固定大小的hashtable,既存储了引用也存储了对象,所以导致字面量赋值的弱引用没有被回收。
但是这里有个疑问,字符串常量池中的对象什么时候才会被GC回收呢,网上没有找到确切的解释,有说字符串常量池不会被GC回收,有的说会被回收但是说不清什么时机回收。 手动调用system.gc()触发的是fullGc,示例中我调用了几次并线程休眠了几秒,这个强引用还一直存在,所以猜测是字符串常量池空间不够时可能会触发Gc回收字符串常量池空间不够时可能会触发Gc回收。
有时间限制下jvm参数限制再打印下gc日志研究,后续再补上。
public static void main(String[] args) { test2(); } public static void test2() { WeakHashMap<String, String> weakHashMap = new WeakHashMap<>(); String s1 = "3.jpg"; String s2 = new String("4.jpg"); weakHashMap.put(s1, "图片1"); weakHashMap.put(s2, "图片2"); s1=null; s2=null; System.gc(); //3.jpg=图片1 test2Iteration(weakHashMap); } private static void test2Iteration(Map<String, String> map){ Iterator iterator = map.entrySet().iterator(); while (iterator.hasNext()){ Map.Entry entry = (Map.Entry) iterator.next(); System.out.println(entry); } } }
下面这个示例也是一个道理
如果str改为new String的 弱引用对象就会被回收。
六、软引用,弱引用适用的场景
数据量很大占用内存过多可能造成内存溢出的场景
比如需要加载大量数据,全部加载到内存中可能造成内存溢出,就可以使用软引用、弱引用来充当缓存,当内存不足时,JVM对这些数据进行回收
使用软引用时,可以自定义Map进行存储Map<String,SoftReference<XXX>> cache
使用弱引用时,则可以直接使用WeakHashMap
软引用与弱引用的区别则是GC回收的时机不同,软引用存活可能更久,Full GC下才回收;而弱引用存活可能更短,发生GC就会回收。
七、虚引用
使用PhantomReference创建虚引用,需要搭配引用队列ReferenceQueue使用
无法通过虚引用得到该对象实例(其他引用都可以得到实例)
虚引用只是为了能在这个对象被收集器回收时收到一个通知
1、引用队列搭配虚引用使用
public class PhantomReferenceTest { private static PhantomReferenceTest reference; private static ReferenceQueue queue; @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("调用finalize方法"); //搭上引用链 reference = this; } public static void main(String[] args) { reference = new PhantomReferenceTest(); //引用队列 queue = new ReferenceQueue<>(); //虚引用 PhantomReference<PhantomReferenceTest> phantomReference = new PhantomReference<>(reference, queue); Thread thread = new Thread(() -> { PhantomReference<PhantomReferenceTest> r = null; while (true) { if (queue != null) { r = (PhantomReference<PhantomReferenceTest>) queue.poll(); //说明被回收了,得到通知 if (r != null) { System.out.println("实例被回收"); } } } }); thread.setDaemon(true); thread.start(); //null (获取不到虚引用) System.out.println(phantomReference.get()); try { System.out.println("第一次gc 对象可以复活"); reference = null; //第一次GC 引用不可达 守护线程执行finalize方法 重新变为可达对象 System.gc(); TimeUnit.SECONDS.sleep(1); if (reference == null) { System.out.println("object is dead"); } else { System.out.println("object is alive"); } reference = null; System.out.println("第二次gc 对象死了"); //第二次GC 不会执行finalize方法 不能再变为可达对象 System.gc(); TimeUnit.SECONDS.sleep(1); if (reference == null) { System.out.println("object is dead"); } else { System.out.println("object is alive"); } } catch (InterruptedException e) { e.printStackTrace(); } } }
结果:
/* null 第一次gc 对象可以复活 调用finalize方法 object is alive 第二次gc 对象死了 实例被回收 object is dead */
第一次GC时,守护线程执行finalize方法让虚引用重新可达,所以没死
第二次GC时,不再执行finalize方法,虚引用已死
虚引用回收后,引用队列有数据,来通知告诉我们reference这个对象被回收了
2、使用场景
GC只能回收堆内内存,而直接内存GC是无法回收的,直接内存代表的对象创建一个虚引用,加入引用队列,当这个直接内存不使用,这个代表直接内存的对象为空时,这个虚内存就死了,然后引用队列会产生通知,就可以通知JVM去回收堆外内存(直接内存)
八、WeakHashMap
1、WeakHashMap 的特点
WeakHashMap 是使用弱键的动态散列表,用于实现 “自动清理” 的内存缓存。
-
1、WeakHashMap 使用与 Java 7 HashMap 相同的 “数组 + 链表” 解决散列冲突,发生散列冲突的键值对会用头插法添加到单链表中;
-
2、WeakHashMap 依赖于 Java 垃圾收集器自动清理不可达对象的特性。当 Key 对象不再被持有强引用时,垃圾收集器会按照弱引用策略自动回收 Key 对象,并在下次访问 WeakHashMap 时清理全部无效的键值对。因此,WeakHashMap 特别适合实现 “自动清理” 的内存活动缓存,当键值对有效时保留,在键值对无效时自动被垃圾收集器清理;
-
3、需要注意,因为 WeakHashMap 会持有 Value 对象的强引用,所以在 Value 对象中一定不能持有 key 的强引用。否则,会阻止垃圾收集器回收 “本该不可达” 的 Key 对象,使得 WeakHashMap 失去作用。
-
4、与 HashMap 相同,WeakHashMap 也不考虑线程同步,也会存在线程安全问题。可以使用 Collections.synchronizedMap 包装类,其原理也是在所有方法上增加 synchronized 关键字。
2、WeakHashMap 与 HashMap 和 LinkedHashMap 的区别
WeakHashMap 与 HashMap 都是基于分离链表法解决散列冲突的动态散列表,两者的主要区别在键 Key 的引用类型上:
HashMap 会持有键 Key 的强引用,除非手动移除,否则键值对会长期存在于散列表中。而 WeakHashMap 只持有键 Key 的弱引用,当 Key 对象不再被外部持有强引用时,键值对会被自动被清理。
WeakHashMap 与 LinkedHashMap 都有自动清理的能力,两者的主要区别在于淘汰数据的策略上:
LinkedHashMap 会按照 FIFO 或 LRU 的策略 “尝试” 淘汰数据,需要开发者重写 removeEldestEntry()
方法实现是否删除最早节点的判断逻辑。而 WeakHashMap 会按照 Key 对象的可达性淘汰数据,当 Key 对象不再被持有强引用时,会自动清理无效数据。
3、重建 Key 对象不等价的问题
WeakHashMap 的 Key 使用弱引用,也就是以 Key 作为清理数据的判断锚点,当 Key 变得不可达时会自动清理数据。此时,如果使用多个 equals
相等的 Key 对象访问键值对,就会出现第 1 个 Key 对象不可达导致键值对被回收,而第 2 个 Key 查询键值对为 null 的问题。这说明 equals
相等的 Key 对象在 HashMap 等散列表中是等价的,但是在 WeakHashMap 散列表中是不等价的。
因此,如果 Key 类型没有重写 equals 方法,那么 WeakHashMap 就表现良好,否则会存在歧义。例如下面这个 Demo 中,首先创建了指向 image_url1
的图片 Key1,再重建了同样指向 image_url1
的图片 Key3。在 HashMap 中,Key1 和 Key3 等价,但在 WeakHashMap 中,Key1 和 Key3 不等价。
class ImageKey { private String url; ImageKey(String url) { this.url = url; } public boolean equals(Object obj) { return (obj instanceOf ImageKey) && Objects.equals(((ImageKey)obj).url, this.url); } } WeakHashMap<ImageKey, Bitmap> map = new WeakHashMap<>(); ImageKey key1 = new ImageKey("image_url1"); ImageKey key2 = new ImageKey("image_url2"); // key1 equalsTo key3 ImageKey key3 = new ImageKey("image_url1"); map.put(key1, bitmap1); map.put(key2, bitmap2); System.out.println(map.get(key1)); // 输出 bitmap1 System.out.println(map.get(key2)); // 输出 bitmap2 System.out.println(map.get(key3)); // 输出 bitmap1 // 使 key1 不可达,key3 保持 key1 = null; // 说明重建 Key 与原始 Key 不等价 System.out.println(map.get(key1)); // 输出 null System.out.println(map.get(key2)); // 输出 bitmap2 System.out.println(map.get(key3)); // 输出 null
4、Key 弱引用和 Value 弱引用的区别
不管是 Key 还是 Value 使用弱引用都可以实现自动清理,至于使用哪一种方法各有优缺点,适用场景也不同。
- Key 弱引用: 以 Key 作为清理数据的判断锚点,当 Key 不可达时清理数据。优点是容器外不需要持有 Value 的强引用,缺点是重建的 Key 与原始 Key 不等价,重建 Key 无法阻止数据被清理;
- Value 弱引用: 以 Value 作为清理数据的判断锚点,当 Value 不可达时清理数据。优点是重建 Key 与与原始 Key 等价,缺点是容器外需要持有 Value 的强引用。
类型 | 优点 | 缺点 | 场景 |
---|---|---|---|
Key 弱引用 | 外部不需要持有 Value 的强引用,使用更简单 | 重建 Key 不等价 | 未重写 equals |
Value 弱引用 | 重建 Key 等价 | 外部需要持有 Value 的强引用 | 重写 equals |
5、WeakHashMap 的属性
先用一个表格整理 WeakHashMap 的属性:
版本 | 数据结构 | 节点实现类 | 属性 |
---|---|---|---|
Java 7 HashMap | 数组 + 链表 | Entry(单链表) | 1、table(数组) 2、size(尺寸) 3、threshold(扩容阈值) 4、loadFactor(装载因子上限) 5、modCount(修改计数) 6、默认数组容量 16 7、最大数组容量 2^30 8、默认负载因子 0.75 |
WeakHashMap | 数组 + 链表 | Entry(单链表,弱引用的子类型) | 9、queue(引用队列) |
WeakHashMap 与 HashMap 的属性几乎相同,主要区别有 2 个:
- 1、ReferenceQueue: WeakHashMap 的属性里多了一个 queue 引用队列;
- 2、Entry:
WeakHashMap#Entry
节点继承于WeakReference
,表面看是 WeakHashMap 持有了 Entry 的强引用,其实不是。注意看 Entry 的构造方法,WeakReference 关联的实际对象是 Key。 所以,WeakHashMap 依然持有 Entry 和 Value 的强引用,仅持有 Key 的弱引用。
说一下 ReferenceQueue queue 的作用?
ReferenceQueue 与 Reference 配合能够实现感知对象被垃圾回收的能力。在创建引用对象时可以关联一个实际对象和一个引用队列,当实现对象被垃圾回收后,引用对象会被添加到这个引用队列中。在 WeakHashMap 中,就是根据这个引用队列来自动清理无效键值对。
为什么 Key 是弱引用,而不是 Entry 或 Value 是弱引用?
首先,Entry 一定要持有强引用,而不能持有弱引用。这是因为 Entry 是 WeakHashMap 内部维护数据结构的实现细节,并不会暴露到 WeakHashMap 外部,即除了 WeakHashMap 本身之外没有其它地方持有 Entry 的强引用。所以,如果持有 Entry 的弱引用,即使 WeakHashMap 外部依然在使用 Key 对象,WeakHashMap 内部依然会回收键值对,这与预期不符。
其次,不管是 Key 还是 Value 使用弱引用都可以实现自动清理。至于使用哪一种方法各有优缺点,适用场景也不同,这个在前文分析过了。
WeakHashMap 如何清理无效数据?
expungeStaleEntries()
方法清理 Key 对象已经被回收的无效键值对。其中会遍历 ReferenceQueue 中持有的弱引用对象(即 Entry 节点),并将该结点从散列表中移除。6、总结
-
1、WeakHashMap 使用与 Java 7 HashMap 相同的 “数组 + 链表” 解决散列冲突,发生散列冲突的键值对会用头插法添加到单链表中;
-
2、WeakHashMap 能够实现 “自动清理” 的内存缓存,其中的 “Weak” 指键 Key 是弱引用。当 Key 对象不再被持有强引用时,垃圾收集器会按照弱引用策略自动回收 Key 对象,并在下次访问 WeakHashMap 时清理全部无效的键值对;
-
3、WeakHashMap 和 LinkedHashMap 都具备 “自动清理” 的 能力,WeakHashMap 根据 Key 对象的可达性淘汰数据,而 LinkedHashMap 根据 FIFO 或 LRU 策略尝试淘汰数据;
-
4、WeakHashMap 使用 Key 弱引用,会存在重建 Key 对象不等价问题。
https://juejin.cn/post/7165044834590261256