14-理解Java中的不同引用类型:强引用、软引用、弱引用和虚引用

简介: 这篇文章将深入探讨Java中的四种引用类型:强引用、软引用、弱引用和虚引用。我们将逐一解释每种引用类型的特性和用途。

Java有不同的引用类型,分别是:强引用、软引用、弱引用、虚引用,不同的引用类型跟我们的垃圾回收也有着不同的规则。

强引用

我们直接通过new关键字创建出来的对象都叫强引用对象,比如:

Object obj = new Object();

强引用的特点:

  1. 强引用可以直接访问目标对象。
  2. 强引用所指向的对象在任何时候都不会被系统回收。JVM宁愿抛出OOM异常,也不会回收强引用所指向的对象。
  3. 强引用可能导致内存泄漏。
软引用

软引用是除了强引用外,最强的引用类型。可以通过java.lang.ref.SoftReference使用软引用。一个持有软引用的对象,不会被JVM很快回收,JVM会根据当前堆的使用情况来判断何时回收(只有当JVM认为内存不足时,才会试图去回收软引用的对象,JVM 会确保在抛出OutOfMemoryError 之前,清理软引用指向的对象 )。因此,软引用可以用于实现对内存敏感的高速缓存

代码示例:

User user = new User();
SoftReference<User> softReference = new SoftReference<>(user);
user = null;//销毁强引用
System.gc();//手动垃圾回收
System.out.println(softReference.get());//打印软引用中user对象地址值:demo2.User@b4c966a

注意:触发软引用回收的点在于当内存空间已经装不下的时候以及内存空间很紧张的时候执行回收。

代码示例:

/**
 * @Description: 需要先配置参数 -Xms2M -Xmx3M,将 JVM 的初始内存设为2M,最大可用内存为 3M
 */
public class Test {
   
   
    private static List<Object> list = new ArrayList<>();
    public static void main(String[] args) {
   
   
        testSoftReference();
    }
    private static void testSoftReference() {
   
   
        for (int i = 0; i < 10; i++) {
   
   
            byte[] buff = new byte[1024 * 1024];
            SoftReference<byte[]> sr = new SoftReference<>(buff);
            list.add(sr);
        }

        System.gc(); //主动通知垃圾回收

        for(int i=0; i < list.size(); i++){
   
   
            Object obj = ((SoftReference) list.get(i)).get();
            System.out.println(obj);
        }

    }
}

打印结果:

null
null
null
null
null
null
null
null
null
[B@10f87f48
弱引用

弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。

我们以同样的方式来测试弱引用:

private static void testWeakReference() {
   
   
        for (int i = 0; i < 10; i++) {
   
   
            byte[] buff = new byte[1024 * 1024];
            WeakReference<byte[]> sr = new WeakReference<>(buff);
            list.add(sr);
        }

        System.gc(); //主动通知垃圾回收

        for(int i=0; i < list.size(); i++){
   
   
            Object obj = ((WeakReference) list.get(i)).get();
            System.out.println(obj);
        }

    }

打印结果如下:

null
null
null
null
null
null
null
null
null
null

可以发现所有被弱引用关联的对象都被垃圾回收了。

虚引用

虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。虚引用在创建时必须传入一个引用队列作为参数,当垃圾收集器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。

public class PhantomReference<T> extends Reference<T> {
   
   

    public T get() {
   
   
        return null;
    }

    public PhantomReference(T referent, ReferenceQueue<? super T> q) {
   
   
        super(referent, q);
    }
}

为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。

由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。

【生存还是死亡?】对象的finalization机制

现在理解了GC Roots和引用类型的概念后,也就知道了哪些对象可以被回收,哪些对象不能回收。

有GC Roots引用的对象不能回收,没有GC Roots引用的对象可以回收,如果有GC Roots引用,但是如果是软引用或者弱引用的,也有可能被回收掉。

问:假设没有GC Roots引用的对象,是一定立马被回收吗?

答:其实不是的,这里有一个 finalize()方法可以拯救他自己

示例代码:

/**
* 此代码演示了两点:
* 1.对象可以在被GC时自我拯救。
* 2.这种自救的机会只有一次, 因为一个对象的finalize()方法最多只会被系统自动调用一次
*/
public class TestFinalize {
   
   
    public static TestFinalize testFinalize;

    @Override
    protected void finalize() throws Throwable {
   
   
        super.finalize();
        System.out.println("当前类的finalize方法执行");
        testFinalize = this;
    }

    public static void main(String[] args) {
   
   
        testFinalize = new TestFinalize();
        //对象第一次成功拯救自己
        testFinalize = null;
        System.gc();
        System.out.println("第一次 gc......");
        try {
   
   
            //因为Finalizer方法优先级很低, 暂停0.5秒, 以等待它
            Thread.sleep(500);
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        }
        if(testFinalize == null){
   
   
            System.out.println("对象已死");
        }else{
   
   
            System.out.println("对象依然存活");
        }

 // 下面这段代码与上面的完全相同, 但是这次自救却失败了
        testFinalize = null;
        System.gc();
        System.out.println("第二次 gc......");
        try {
   
   
            Thread.sleep(500);
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        }
        if(testFinalize == null){
   
   
            System.out.println("对象已死");
        }else{
   
   
            System.out.println("对象依然存活");
        }
    }
}

打印结果:

第一次 gc......
当前类的finalize方法执行
对象依然存活
第二次 gc......
对象已死

注意:finalize方法只会被调用一次!

永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。理由包括下面三点:

  • 在 finalize()时可能会导致对象复活
  • finalize()方法的执行时间是没有保障的,它完全由Gc线程决定,极端情况下,若不发生GC,则 finalize()方法将没有执行机会
  • 一个糟糕的fnalize()会严重影响Gc的性能

从功能上来说, finalize()方法与C++中的析构函数比较相似,但是Java采用的是基于垃圾回收器的自动内存管理机制,所以 finalize()方法在本质上不同于c++中的析构函数。

其次它的运行代价高昂, 不确定性大, 无法保证各个对象的调用顺序, 如今已被官方明确声明为不推荐使用的语法。 有些教材中描述它适合做“关闭外部资源”之类的清理性工作, 这完全是对finalize()方法用途的一种自我安慰。 finalize()能做的所有工作, 使用try-finally或者其他方式都可以做得更好、更及时, 所以笔者建议大家完全可以忘掉Java语言里面的这个方法。

小结

到这儿已经把为什么要回收垃圾,以及哪些对象会被回收,哪些对象不会被回收的情况给大家介绍清楚了。接下来我们就要正式进入垃圾回收的算法以及内存分配策略的学习了。

目录
相关文章
|
5月前
|
缓存 Java 程序员
Java面试题:解释强引用、软引用、弱引用和虚引用在Java中是如何工作的?
Java面试题:解释强引用、软引用、弱引用和虚引用在Java中是如何工作的?
40 1
|
5月前
|
缓存 算法 Java
Java面试题:深入探究Java内存模型与垃圾回收机制,Java中的引用类型在内存管理和垃圾回收中的作用,Java中的finalize方法及其在垃圾回收中的作用,哪种策略能够提高垃圾回收的效率
Java面试题:深入探究Java内存模型与垃圾回收机制,Java中的引用类型在内存管理和垃圾回收中的作用,Java中的finalize方法及其在垃圾回收中的作用,哪种策略能够提高垃圾回收的效率
44 1
|
2月前
|
存储 Java 程序员
【一步一步了解Java系列】:何为数组,何为引用类型
【一步一步了解Java系列】:何为数组,何为引用类型
34 1
|
2月前
|
存储 Java 编译器
[Java]基本数据类型与引用类型赋值的底层分析
本文详细分析了Java中不同类型引用的存储方式,包括int、Integer、int[]、Integer[]等,并探讨了byte与其他类型间的转换及String的相关特性。文章通过多个示例解释了引用和对象的存储位置,以及字符串常量池的使用。此外,还对比了String和StringBuilder的性能差异,帮助读者深入理解Java内存管理机制。
28 0
|
6月前
|
缓存 Java 数据库连接
java面试题目 强引用、软引用、弱引用、幻象引用有什么区别?具体使用场景是什么?
【6月更文挑战第28天】在 Java 中,理解和正确使用各种引用类型(强引用、软引用、弱引用、幻象引用)对有效的内存管理和垃圾回收至关重要。下面我们详细解读这些引用类型的区别及其具体使用场景。
95 3
|
6月前
|
缓存 Java 开发者
深入理解Java的五种引用类型
深入理解Java的五种引用类型
|
5月前
|
Java 运维
开发与运维引用问题之软引用又在Java特点如何解决
开发与运维引用问题之软引用又在Java特点如何解决
49 0
|
9天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
38 6
|
23天前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
21天前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####