<导航

理解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

内存溢出可能发生的两种情况:

  1. 必须的资源确实很大,堆内存设置太小 (通过-Xmx来调整)
  1. 发生内存泄漏,创建大量对象,且生命周期长,不能被回收

内存泄漏Memory Leak: 对象不会被程序用到了,但是不能回收它们

对象不再使用并且不能回收就会一直占用空间,大量对象发生内存泄漏可能发生内存溢出OOM

广义内存泄漏:不正确的操作导致对象生命周期变长

  1. 单例中引用外部对象,当这个外部对象不用了,但是因为单例还引用着它导致内存泄漏
  2. 一些需要关闭的资源未关闭导致内存泄漏

三、强引用

强引用是程序代码中普遍存在的引用赋值,比如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的原因。

应用场景:

浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。

  1. 如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建;
  2. 如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出。

这时候就可以使用软引用,很好的解决了实际的问题:

// 获取浏览器对象进行浏览
    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();
         }
 ​
     }
 }
View Code

结果:

/*
 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 依葫芦画瓢的,没有树化的逻辑。考虑到我们已经对 HashMap 做过详细分析,所以我们没有必要重复分析 WeakHashMap 的每个细节,而是把重心放在 WeakHashMap 与 HashMap 不同的地方。

先用一个表格整理 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 如何清理无效数据?

在通过 put / get /size 等方法访问 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
posted @ 2023-01-22 23:14  字节悦动  阅读(1017)  评论(0编辑  收藏  举报