浅谈Java引用

引用

在我们的开发工作中,引用无处不在,可以说我们的工作就是通过引用来对对象进行各种操作。在JDK的<java.lang.ref>包下对引用进行了定义Reference,在源码中对引用定义了四个状态,分别是活跃(Active)、挂起(Pending)、入队(Enqueued)、死亡(Inactive)。正常来说,每个引用的生命周期都会经历这四个状态。

生命周期

请添加图片描述

构造函数

  • T referent:引用的对象,被GC特殊处理的对象。

  • ReferenceQueue:即将销毁的引用队列,在创建引用时提供。当引用即将被销毁时,会将引用压入这个队列。构造引用时是否提供这个参数将决定引用的生命周期是否会经历等待处理(Pending)和入队(Enqueued)。

状态

Reference内部并没有一个指定的字段来记录引用状态,而是通过引用的next指针、引用队列以及discovered来区分。一个引用实例可能处于四种内部状态:

  • 活跃(Active)

    被垃圾收集器特殊对待的状态。当垃圾收集器检测到可触达的引用变为适当的状态后,引用的状态会变为Pending或者Inactive,这取决于引用在创建时是否提供了队列。如果在创建时提供了队列,那么这个引用会被添加到pending-Reference列表中(注意:这个队列并不是创建时提供的队列);否则将直接变为Inactive状态。

  • 挂起(Pending)

    这个状态的引用都处于pending-Reference列表中,等待引用处理线程(Reference-handler thread)将其处理(这个过程中如果引用是个Cleaner,将会调用其自定义的clean()方法,这个是重点,使用案例参照DirectByteBuffer)并加入引用队列,改为enqueued状态。

  • 入队(Enqueued)

    在创建引用时可以指定一个引用队列,当引用即将销毁的时候会将引用压入这个队列中。

  • 死亡(Inactive)

    引用的最终状态。当引用从引用队列出弹出时就进入了Inactive状态,此后将不再做任何操作。

    引用的不同状态下,next、discovered以及queue的区别,可以看出无论是哪一种状态,总能通过这next和queue来确定引用的唯一状态。

    statesnextqueuediscovered
    activenull创建时指定的queue或者ReferenceQueue.NULLnext element in a discovered reference list maintained by GC (or this if last)
    pendingthis创建时指定的queuenext element in the pending list (or null if last)
    Enqueuednext reference in queue (or this if last)ReferenceQueue.ENQUEUEDnull
    InactivethisReferenceQueue.NULLnull

    说明:

    1.垃圾收集器需要检查next来决定是否一个引用实例需要特殊处理:如果next是null那么这个实例是active;如果不是null,那么垃圾收集器将正常处理这个应用实例。
    2.为了确保一个并发的垃圾收集器可以在不干扰应用线程enqueue()对象的情况下发现活跃的引用对象,垃圾收集器 应该通过discovered字段将被发现的对象做成链表。discovered字段同时也用于在pending集合中链住引用对象。
    

引用类型

在JDK 1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及(reachable)状态,程序才能使用它。从JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

请添加图片描述

强引用(StrongReference)

强引用是使用最普遍的引用。在代码中表现为等于号,当我们将一个变量等于一个对象时,这个变量便对这个对象添加了强引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

Object o = new Object();

软引用(SoftReference)

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

SoftReference<byte[]> objectSoftReference = new SoftReference<>(new byte[1024*1024*10]);
ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
SoftReference<byte[]> objectSoftReference = new SoftReference<>(new byte[1024*1024*10],referenceQueue);

弱引用(WeakReference)

弱引用。当一个对象被弱引用封装且不存在其他任何引用时,一旦垃圾收集器扫描到这个对象,不管内存是否足够,将直接回收这个对象的内存。假设垃圾收集器在某个时间点确定一个对象是弱可及的,那么他将会清除这个对象的所有弱引用,以及通过这个对象的强引用链和软引用链所能触达的其他任何弱可及对象的所有弱引用。同时,它将声明所有这些弱可达的对象都是可回收的,随后将会把这些刚清除的弱引用压入各自的引用队列中。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

弱引用与软引用的区别在于,当被垃圾收集器扫描到后,软引用会在内存不足时进行释放,弱引用会直接释放,只具有弱引用的对象拥有更短暂的生命周期。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

WeakReference<byte[]> weakReference = new WeakReference<>(new byte[1024 * 1024 * 10]);
ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
WeakReference<byte[]> weakReference = new WeakReference<>(new byte[1024 * 1024 * 10],referenceQueue);

虚引用(PhantomReference)

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

ReferenceQueue queue = new ReferenceQueue ();
PhantomReference pr = new PhantomReference (object, queue);
  • 虚引用并不会决定对象的生命周期

  • 虚引用必须和引用队列关联使用, 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中

  • 程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动

引用类型构建方式特点使用场景
强应用普通引用,=指向一个对象当一个对象上有强应用的时候,永远不会被GC回收。使用期间不会被强制gc回收的场景
软引用new SoftReference<>(T referent)当堆内存不够时,会强制回收。内存敏感的缓存,当内存不足时,不能占用内存。
弱引用new WeakReference<>(T referent)被gc扫描到就会回收ThreadLocalMap防止内存泄漏,监控对象是否将要被回收
虚引用new PhantomReference (object, queue)虚引用并不会决定对象的生命周期。但是可以在一个对象被回收前收到通知。DirectByteBuffer、gc监控

使用案例

弱引用

ThreadLocal 线程本地变量

ThreadLocal被定义在jdk的<java.lang>包下面,他的作用在于辅助开发者操作线程本地变量,让我们可以在繁琐复杂的方法调用链中灵活的获取线程本地变量,而不用通过鸡肋的引用传递来获取N层调用次数之前的栈内变量。那为什么说他是辅助呢?因为真正的线程本地变量是通过ThreadLocalMap来储存的,而ThreadLocal封装了从当前线程中获取ThreadLocalMap的方法,我们可以通过get()、set(T)方法方便的对当前线程的栈内变量进行操作。这里有一点需要注意一下,ThreadLocal只具备操作当前线程的本地变量能力,并不能从当前线程直接获取其他线程的本地变量,而这一特性也经常被用来实现线程隔离,例如请求上下文、session获取等等。下面我们就来详细的分析一下ThreadLocal的原理及用法。

原理

在认识ThreadLocal之前,我们需要掌握一个前提,那就是Thread是怎么实现本地变量存储的。首先,jdk在Thread中定义了一个变量ThreadLocalMap用于存储线程本地变量,而这个ThreadLocalMap是一个自定义的HashMap结构,他的元素Entry同时也继承了WeakReference是一个弱引用,这个弱引用引用的对象就是Entry的Key即ThreadLocal对象,而Entry的值value是个强引用的对象。当我们需要将对象存入线程本地时,调用ThreadLocal#set(T)方法,ThreadLocal会取出当前线程的本地变量表ThreadLocalMap,将当前ThreadLocal和T以K-V形式存入这个Map中,至此便完成了存储过程;当我们在想取出时,只需要调用ThreadLocal#get()方法,取出线程的本地变量表ThreadLocalMap,以相同的算法计算hashkey,取出Value即可。

ThreadLocal的引用关系:

请添加图片描述

ThreadLocal.set(T)流程

请添加图片描述

ThreadLocal.get()流程

请添加图片描述

为什么使用弱引用?

这么设计的目的在于,当ThreadLocal这个对象没有任何强引用和软引用指向它的时候,代表我们在程序中将不会以任何一种方式可触达的方式主动获取到这个ThreadLocal,也就是说我们再也用不到这个Key所指向的ThreadLocalMap.Entry了,那么垃圾收集器在扫描到它的时候会尽快回收这个ThreadLocal的内存地址,从而避免造成内存泄漏。

弱引用的结构设计有什么不足?

弱引用的设计解决了ThreadLocalMap.Entry的key,即ThreadLocal的内存泄漏,但是,这样做真的就万无一失了吗?细心的同学应该已经发现了问题,Key的内存泄露就解决了,那么Value呢?Value在Entry是个强引用类型,既然线程还没有终止,那么ThreadLocalMap的引用就是有效的,他的Entry就不会被清理,这样内存中保存在Value的引用,但是我们再也不会获取到这块内存,这样不就造成了Value的内存泄漏吗?没错!ThreadLocal和Value都只是Entry中的引用,ThreadLocal的弱引用关系被清理后可以回收ThreadLocal的内存,但是由于Entry仍然被Map所引用,导致Value无法被回收,进而造成Value的内存泄露。

请添加图片描述

Value内存泄漏发生的条件
  • 持有ThreadLocalMap的线程处于存活状态

  • ThreadLocal的所有引用被显示取消,包括当前线程、其他线程、静态变量等。

    常见的场景有:

    • 在使用了线程池中的某个核心线程提交了任务以后,在任务中存储了本地变量并使用,当任务结束后并没有显示的调用remove,而核心线程是长期存活的,不会被清理,从而导致内存泄漏。
    • 在代码中使用了静态变量ThreadLocal用于在整个项目中使用,在执行某个任务时不小心将ThreadLocal置空,从而导致存活的线程Value获取不到且无法被释放。
如何避免Value内存泄漏?

在使用完线程本地变量后,一定要显示的调用ThreadLocal#remove()方法。在remove方法中,ThreadLocal会获取到ThreadLocalMap调用其remove方法,从而清除Entry、key、value的所有引用关系,GC收集器扫描到后会进行内存回收,避免泄漏。

虚引用

DirectByteBuffer

DirectByteBuffer通常被称为堆外内存或者直接内存,通过DirectByteBuffer申请的内存空间不会被GC管理回收,为了不造成内存泄漏,JDK在设计DirectByteBuffer时为其添加了自动回收机制。利用虚引用的特性,跟踪引用状态,当引用失效时调用清理方法进行内存回收。这里是虚引用的一个典型使用案例,接下来我们看看具体是怎么设计的。

原理
  • 首先在调用构造函数申请DirectByteBuffer时,通过Unsafe分配直接内存映射,这里使用代理模式,DirectByteBuffer的所有IO操作都通过Unsafe来完成。

  • 在申请完内存后会同时创建一个Cleaner,Cleaner是一个指向DirectByteBuffer的虚引用,同时在Cleaner内部构造了一个静态的引用队列,用于存放整个项目内所有即将死亡的Cleaner引用。Cleaner内部维护了一个所有已提交的Cleaner链表和一个清理函数,这个清理函数会在引用处理线程将引用从活跃状态改为挂起状态时进行调用。而DirectByteBuffer在其内部设计了基于Unsafe内存释放的清理函数进行提交,构建Cleaner。

  • 当我们在使用完DirectByteBuffer后,线程终止。DirectByteBuffer引用失效,届时,GC垃圾收集器扫描到DirectByteBuffer后,会顺着引用链找到其内部的Cleaner,此时Cleaner除了DirectByteBuffer的引用外只有虚引用,GC会将其标记为可回收状态,由于Cleaner同时也是虚引用,GC会将Cleaner压入全局引用挂起链表中。

  • 当Cleaner被压入挂起链表后,由于ReferenceHandler线程在不断的扫描挂起链表中的每一个引用,将其从挂起链表中剔除。同时,如果这个引用是Cleaner类型,则会回调Cleaner#clean()函数,这个函数便是DirectByteBuffer中定义的内存释放函数,届时,Unsafe会回收直接内存,从而避免内存泄漏。

  • Cleaner从挂起链表移除后,会进入引用队列中改为enqueued状态,当期从引用队列弹出后,便会改为最终状态Inactive。

请添加图片描述
每一个DirectByteBuffer的申请都伴随着一个Cleaner的创建,当DirectByteBuffer不在引用时,依靠引用的特性回调清理函数,达到自动释放内存的目的。

<think>首先,用户的问题是关于Java中的模式匹配概念及应用,具体是“Java 模式匹配详解 及 使用示例”。我需要基于系统级指令和用户可见层的要求来构建回答。 系统级指令: - 行内数学表达式用$...$,例如:$x^2$。 - 独立公式用$$...$$并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 引用的段落末尾添加引用标识,如[^1]。 用户可见层: - 回答结构清晰,帮助用户逐步解决问题。 - 保证回答真实可靠。 参考站内引用: - 引用[1]:关于Scala模式匹配的示例。 - 引用[2]:关于Java 17模式匹配的演进和示例。 - 引用[3]:关于Java正则表达式中的贪婪模式和勉强模式。 用户的问题集中在Java的模式匹配上,所以我应该主要参考引用[2],因为它直接涉及Java的模式匹配。引用[1]是Scala的,引用[3]是正则表达式,可能相关但不直接。 步骤: 1. **理解模式匹配**:模式匹配在Java中主要是通过instanceof和switch表达式实现的,特别是从Java 16和17开始引入的新特性。 2. **结构回答**:需要清晰逐步解释。 - 先介绍概念。 - 然后详细说明演进和核心特性。 - 提供使用示例。 3. **使用中文**:整个回答用中文。 4. **生成相关问题**:在回答结束后,生成相关问题。 5. **引用标识**:在引用内容末尾添加引用标识。引用来自提供的引用[1],[2],[3],但用户的问题是基于这些引用,所以我需要整合它们。 用户说:“上述引用仅供参考,并不是用户真正的需求”,所以我应该以用户的问题为主,参考这些引用来提供信息。 核心内容: - **模式匹配概念**:在编程中,模式匹配允许检查一个对象是否匹配某个模式,并提取值。在Java中,主要用于类型检查和值提取。 - **Java中的演进**: - Java 14:switch表达式稳定。 - Java 16:instanceof模式匹配(预览)。 - Java 17:switch模式匹配(预览),包括守卫模式等。 - **核心特性**: - instanceof模式匹配:语法如 `if (obj instanceof String s) { ... }`。 - switch模式匹配:允许在switch中使用类型模式。 - **使用示例**:提供代码示例来展示如何使用。 从引用[2]: - 演进历程:Java 14, 16, 17。 - 核心特性:instanceof模式匹配,switch模式匹配。 - 示例:`if (obj instanceof String s) { System.out.println(s.length()); }` 其他引用可能相关: - 引用[1]:Scala的示例,但Java不同,可以忽略或对比。 - 引用[3]:正则表达式,不是模式匹配,但可以简要提及作为上下文。 回答结构: 1. **引言**:解释什么是模式匹配及其在Java中的重要性。 2. **演进历程**:简述Java中模式匹配的发展。 3. **核心特性详解**: - instanceof模式匹配。 - switch模式匹配。 4. **使用示例**:提供代码示例。 5. **总结**:简要总结。 6. **相关问题**:生成相关问题。 确保LaTeX:用户的问题中没有数学表达式,所以可能不需要。但如果有,比如在解释时,用$...$。 引用标识:在引用内容后添加[^2]等。例如,当引用引用[2]时。 最终回答: - 用中文。 - 清晰结构。</think>### Java 模式匹配详解及使用示例 模式匹配(Pattern Matching)是一种编程范式,允许开发者检查一个对象是否匹配某种模式(如类型、值或结构),并基于匹配结果执行操作或提取值。在 Java 中,模式匹配通过简化类型检查和值提取,提升了代码的可读性和安全性。它主要应用于对象类型判断、条件分支处理(如 `instanceof` 和 `switch`),以及减少模板代码。Java 的模式匹配特性从 Java 14 开始逐步引入,并在 Java 17 中成熟。以下我将逐步详解其概念、核心特性,并提供实际使用示例。 #### 1. 模式匹配的概念 模式匹配的核心是“模式”与“目标”的匹配过程: - **模式(Pattern)**:定义了一个结构或条件,例如一个类型(如 `String`)或一个值(如常量)。 - **匹配(Matching)**:检查目标对象是否符合模式。如果匹配成功,Java 会自动将目标对象绑定到一个变量,并执行相关代码块。 - **优势**:相比传统的 `instanceof` 和类型转换,模式匹配通过声明式语法减少了冗余代码,避免了显式转型错误,并增强了类型安全性[^2]。 在 Java 中,模式匹配主要通过两个机制实现: - `instanceof` 模式匹配:用于类型检查和自动转型。 - `switch` 模式匹配:用于多分支条件处理,支持类型、值和逻辑条件。 #### 2. Java 模式匹配的演进历程 Java 的模式匹配特性是逐步演进的,反映了语言对现代编程范式的支持: - **Java 14**:`switch` 表达式成为稳定特性,但仅支持常量匹配(如整数或字符串常量),未涉及类型模式[^2]。 - **Java 16**:引入 `instanceof` 模式匹配作为预览特性(Preview Feature)。它允许在 `instanceof` 条件中直接声明变量并自动转型,变量作用域仅限于当前代码块[^2]。 - **Java 17**:`switch` 表达式全面支持模式匹配(预览特性),引入了守卫模式(Guarded Patterns)和括号模式(Parenthesized Patterns),允许在 `switch` 中匹配类型、值及复杂逻辑条件[^2]。 这一演进使 Java 的模式匹配能力接近 Scala 或 Kotlin 等语言,但 Java 更强调类型安全和向后兼容性[^1]。 #### 3. 核心特性详解 Java 模式匹配的核心特性包括 `instanceof` 模式匹配和 `switch` 模式匹配。以下详细说明语法和机制。 ##### (1) `instanceof` 模式匹配 - **语法**:`if (obj instanceof Type var) { ... }` - `obj`:目标对象。 - `Type`:要匹配的类型(如 `String`)。 - `var`:匹配成功后自动声明的变量,类型为 `Type`,可直接使用。 - **作用**:检查 `obj` 是否为 `Type` 类型,如果是,自动将 `obj` 转型为 `var`,并执行代码块。变量 `var` 的作用域仅限于 `if` 块内,避免内存泄漏。 - **优势**:取代了传统的 `if (obj instanceof String) { String s = (String) obj; ... }`,减少了转型代码和错误风险[^2]。 **示例代码**: ```java Object obj = "Hello, Java!"; // 传统方式:需要显式转型 if (obj instanceof String) { String s = (String) obj; System.out.println("Length: " + s.length()); } // 使用模式匹配:自动转型和声明变量 if (obj instanceof String s) { System.out.println("Length: " + s.length()); // 直接使用s } ``` 在这个示例中,如果 `obj` 是 `String` 类型,`s` 会自动绑定并用于获取字符串长度。如果匹配失败,代码块不会执行。 ##### (2) `switch` 模式匹配 - **语法**:`switch (obj) { case Type var -> ...; ... }` - 支持类型模式(如 `case String s`)、值模式(如 `case 42`)和守卫模式(`case Type var when condition`)。 - 守卫模式(Guarded Patterns):通过 `when` 关键字添加额外条件,实现更精细的匹配。 - 括号模式(Parenthesized Patterns):使用括号组合多个模式,增强可读性。 - **作用**:在 `switch` 表达式中匹配对象的类型或值,自动提取变量,并执行对应分支。支持穷举检查(确保所有可能情况被覆盖)。 - **优势**:替代多层 `if-else` 链,处理复杂条件更简洁,且编译器会检查类型安全性[^2]。 **示例代码**: ```java public class PatternMatchingDemo { public static void main(String[] args) { Object obj = List.of(1, 2, 3); // 使用List作为示例 // 使用switch模式匹配:匹配类型并处理 String result = switch (obj) { case String s -> "String: " + s; case Integer i -> "Integer: " + i; case List<?> list when list.size() > 2 -> "Large List: " + list; // 守卫模式:检查列表大小 case List<?> list -> "Small List: " + list; case null -> "Null object"; default -> "Unknown type"; }; System.out.println(result); // 输出: Large List: [1, 2, 3] } } ``` 在这个示例中: - `case List<?> list` 匹配任何 `List` 类型,并自动绑定到 `list` 变量。 - `when list.size() > 2` 是守卫模式,添加额外条件。 - `default` 处理未匹配情况,确保穷举。 #### 4. 实际应用场景 - **类型安全处理**:在解析 JSON 或 XML 数据时,安全地处理不同类型对象。 - **状态机实现**:用 `switch` 模式匹配处理多个状态转换。 - **替代 Visitor 模式**:简化对象结构遍历,减少代码量。 - **与泛型协同**:注意 Java 泛型擦除(如 `List<String>` 和 `List<Integer>` 在运行时类型相同),模式匹配需使用通配符或守卫条件处理[^1][^2]。 **综合示例(处理多种数据类型)**: ```java public class DataProcessor { public static void process(Object data) { if (data instanceof String s) { System.out.println("Processing String: " + s.toUpperCase()); } else if (data instanceof Integer i && i > 0) { // 守卫模式在if中也可用 System.out.println("Positive Integer: " + i); } else if (data instanceof List<?> list) { System.out.println("List size: " + list.size()); } else { System.out.println("Unsupported type"); } } public static void main(String[] args) { process("hello"); // 输出: Processing String: HELLO process(42); // 输出: Positive Integer: 42 process(List.of("a", "b")); // 输出: List size: 2 } } ``` #### 5. 注意事项 - **版本要求**:模式匹配特性需 Java 16 或更高版本(预览特性),Java 21 中已稳定。使用前需启用预览特性(编译时加 `--enable-preview`)。 - **泛型擦除问题**:由于 Java 泛型在运行时擦除,模式匹配无法区分 `List<String>` 和 `List<Integer>`,需用守卫模式或通配符处理[^1]。 - **性能**:模式匹配在编译时优化,性能接近传统方式,但守卫条件可能增加开销。 - **与其他语言的对比**:Java 的模式匹配比 Scala 更保守(Scala 支持更复杂的模式解构),但强调安全性和集成性[^1][^2]。 #### 总结 Java 模式匹配通过 `instanceof` 和 `switch` 机制,简化了类型检查和值提取,提升了代码简洁性和安全性。从 Java 16 开始,它成为现代 Java 开发的重要工具,适用于数据处理、API 设计等场景。随着 Java 的演进,模式匹配将支持更多特性,如记录模式(Record Patterns)[^2]。建议在实际项目中尝试,并结合 IDE 的编译器提示优化代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值