Java的强引用、软引用、弱引用、虚引用

简介: Java的强引用、软引用、弱引用、虚引用

背景


工程中用到guava的本地缓存。它底层实现和API接口上使用了强引用、软引用、弱引用。所以温故知新下,也夯实下基础。

 

预备知识


先来看下GC日志每个字段的含义


Young GC示例解释


[GC (Allocation Failure) [PSYoungGen: 273405K->20968K(278016K)] 480289K->473619K(737792K), 0.1090103 secs] [Times: user=0.19 sys=0.27, real=0.11 secs]


解释


[GC(产生GC的原因,例子中是由于分配内存失败)  [PSYoungGen: 年轻代回收前空间->年轻代回收后空间(年轻代总空间)] 堆区的回收前空间->堆区的回收后空间(堆区的总空间), GC耗时] [Times: 用户空间耗时 系统空间耗时, 实际耗时]


Full GC示例解释


[Full GC (Ergonomics) [PSYoungGen: 20968K->20805K(278016K)] [ParOldGen: 452651K->451654K(864256K)] 473619K->472460K(1142272K), [Metaspace: 5793K->5793K(1056768K)], 0.1565987 secs] [Times: user=0.70 sys=0.00, real=0.16 secs]


解释


[Full GC (产生GC原因,例子中是由于要放入老年代的对象超过了老年代的剩余空间) [PSYoungGen: ->年轻代回收前空间->年轻代回收后空间(年轻代总空间)] [ParOldGen: 老年代回收前空间->老年代回收后空间(老年代总空间)] 堆区的回收前空间->堆区的回收后空间(堆区的总空间), [Metaspace: 元空间的回收前空间->元空间的回收后空间(元空间的总空间)],  GC耗时] [Times: 用户空间耗时 系统空间耗时, 实际耗时]

 

创建一个10M的大对象,重写finalize方法。finalize()方法会在对象被回收前调用,一个对象只有一次被调用的机会。对象可以在这个方法里进行自救,逃过被垃圾回收。Java设计这个方法可以被覆写是为了让有些对象在回收前做一些检查,完成一些前置条件再被垃圾回收。正式代码不建议使用。因为是测试,所以为了验证效果,这里打印GC日志信息。


  byte[] bytes = new byte[10 * 1024 * 1024];
    int index;
    public Ref(int index) {
        this.index = index;
    }
    public byte[] getBytes() {
        return bytes;
    }
    @Override
    public void finalize() {
        System.out.println("index " + index + "'s " + bytes.length + "is going to be GG");
    }
}


为了测试,JVM参数统一为-Xms20M -XX:+PrintGCDetails。Xms20M表示堆内存设置最大为20M,-XX:+PrintGCDetails代表打印详细的GC信息。


1112728-20201118134159381-1840741217.png


强引用


先来做个实验(代码已经上传github:https://github.com/xiexiaojing/yuna)


@Test
public void  testRawStrong() {
    List<Ref> list = Lists.newArrayList();
    for(int i=0; i<100; i++) {
        list.add(new Ref(i));
        System.out.println(list.get(i));
    }
}


这段代码由上线的设置可知,由于最大设置20M堆空间,所以很快触发了GC。


1112728-20201118134235224-854073512.png


不过Xmx这个值是建议内存最大使用值。如果内存使用超过这个值,jvm认为还有内存可以使用,也会将对象一直往堆里面放。所以2次GC之后JVM自动扩容了,之后就不再频繁GC。最终用到了满足程序需要的内存。


1112728-20201118134303922-1606108072.png


强引用是直接new出来调用的对象,大家都知道。由上面实验可知,在系统内存很富裕的情况下,因为强引用内存不能被释放,所以会多申请了很多内存。

 

软引用


软引用会在系统将要发生内存溢出异常之前,将会把这些软引用对象列进回收范围进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。


用实验说明一下,为了防止JVM自动调整堆大小,我们把堆设置-Xmx200M。


@Test
public void  testRawSoft() {
    List<SoftReference<Ref>> list = Lists.newArrayList();
    for(int i=0; i<100; i++) {
        list.add(new SoftReference<>(new Ref(i)));
        System.out.println(list.get(i).get());
    }
}


从下面实验结果可以看到数次的 GC之后,内存要撑不住的时候,Ref的软引用对象触发了finalize方法。这意味着它将要被内存回收了。说明GC会引发软引用里对象的内存回收,即使这个软引用本身还被强引用(list调用)着。


1112728-20201118134423477-1565474927.png


最终回收了这些内存也不能避免OOM的结局:


1112728-20201118134528094-659866362.png


因为软引用通常情况下就是这样,只有内存马上要溢出了才触发它的GC。就好像扁鹊见蔡桓公的时候,蔡桓公的病已经很深了,马上就没救了。所以有了下面弱引用的方法:有病早治。

 

弱引用


弱引用是发生了一次垃圾回收后,既存的弱引用对象就开始回收。通常,一个弱引用对象仅能生存到下一次垃圾回收前。


用实验说明一下,为了防止JVM自动调整堆大小,我们把堆设置-Xmx200M。


@Test
public void  testRawWeak() {
    List<WeakReference<Ref>> list = Lists.newArrayList();
    for(int i=0; i<100; i++) {
        list.add(new WeakReference<>(new Ref(i)));
        System.out.println(list.get(i).get());
    }
}


从下面的实验结果可知在发生了一次GC之后,已经生成的软引用对象都都回收了。下一次GC,这中间产生的软引用对象也都被回收了。


1112728-20201118134613100-1412856008.png


最终,由于GC及时,整个过程没有爆发OOM,平安的结束了。


1112728-20201118134638742-1403271732.png


虚引用


虚引用也叫幻影引用。任何时候可能被GC回收,就像没有引用一样。


并且他必须和引用队列一起使用,用于跟踪垃圾回收过程,当垃圾回收器回收一个持有虚引用的对象时,在回收对象后,将这个虚引用对象加入到引用队列中,用来通知应用程序垃圾的回收情况。


先来实验一下,从下面结果可看到从一开始取出来就是空对象,基本上刚创建出来就被回收了。


1112728-20201118134705679-693604898.png


一个像是从来没有存在过的幻影有什么用呢?Java的Unsafe类和NIO都可以直接访问堆外内存。堆外内存GC管不了,这时候虚引用就排上用场了。我们可以通过引用队列跟踪垃圾回收,做好善后。

 
在Guava中使用强软弱引用


@Test
public void testStrong() {
    Cache<Integer, Ref> cache = CacheBuilder.newBuilder().expireAfterAccess(1, TimeUnit.DAYS).build();
    for(int i=0; i<100; i++) {
        cache.put(i, new Ref(i));
        System.out.println(cache.getIfPresent(i));
    }
    System.out.println(cache.stats().loadSuccessCount());
}
@Test
public void testSoft() {
    Cache<Integer, Ref> cache = CacheBuilder.newBuilder().softValues().build();
    for(int i=0; i<100; i++) {
        cache.put(i, new Ref(i));
        System.out.println(cache.getIfPresent(i));
    }
}
@Test
public void testWeak() {
    Cache<Integer, Ref> cache = CacheBuilder.newBuilder().weakKeys().weakValues().build();
    for(int i=0; i<100; i++) {
        cache.put(i, new Ref(i));
        System.out.println(cache.getIfPresent(i));
    }
}


Guava在没有显示设置强、软、弱引用的情况下默认是强引用。这个结论我没有看任何书,而是通过跟踪源码,debug得到的结论。当显示设置为软引用或者弱引用时,运行时GC触发和对象回收之间的关系和自己手动直接测试的结果是一样的,大家可以动手实践下。


 总结


Java的强软弱虚引用被回收的时机不同:强引用是引用被释放才会回收;软引用是没释放,但是快OOM了就会被回收;弱引用是引用没释放,但是发生了GC后就会被回收;虚引用随时会回收,好像没有存在过,但是会有一个队列来跟踪它的垃圾回收情况。

 

相关文章
|
5月前
|
缓存 Java 程序员
Java面试题:解释强引用、软引用、弱引用和虚引用在Java中是如何工作的?
Java面试题:解释强引用、软引用、弱引用和虚引用在Java中是如何工作的?
40 1
|
6月前
|
缓存 Java 数据库连接
java面试题目 强引用、软引用、弱引用、幻象引用有什么区别?具体使用场景是什么?
【6月更文挑战第28天】在 Java 中,理解和正确使用各种引用类型(强引用、软引用、弱引用、幻象引用)对有效的内存管理和垃圾回收至关重要。下面我们详细解读这些引用类型的区别及其具体使用场景。
96 3
|
5月前
|
Java 运维
开发与运维引用问题之软引用又在Java特点如何解决
开发与运维引用问题之软引用又在Java特点如何解决
49 0
|
6月前
|
缓存 监控 算法
【Java】谈一谈虚引用
【Java】谈一谈虚引用
106 0
|
7月前
|
缓存 Java
【JAVA】强引用、软引用、弱引用、幻象引用有什么区别?
幻象引用:幻象引用是最弱的引用类型,几乎不影响对象的生命周期。它们主要用于在对象被回收前进行某些预处理操作,例如在对象被销毁时执行特定的清理任务。
64 0
|
7月前
|
Java
【JVM】深入理解Java引用类型:强引用、软引用、弱引用和虚引用
【JVM】深入理解Java引用类型:强引用、软引用、弱引用和虚引用
528 0
|
7月前
|
缓存 Java 程序员
Java垃圾回收: 什么是强引用、软引用、弱引用和虚引用?
Java垃圾回收: 什么是强引用、软引用、弱引用和虚引用?
79 2
|
3天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
5天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
5天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。