浅谈Java引用类型

浅谈Java引用类型

本篇主要介绍了Java的几种引用类型,引用是垃圾回收的核心问题。


1. 可达性分析

可达性分析(Reachability Analysis)是垃圾回收的依据,用来判定对象是否存活,所谓存活即在栈上有没有引用指向堆上的对象。其主要算法为从GC ROOT开始作深度搜索,搜索过得路径为引用链,当一个对象从GC ROOT没有任何引用链与之关联,那么这个对象即符合了垃圾回收的标准,会在下一次GC中被回收。
Java中可以作为GC ROOT的对象包括:

栈帧中的本地变量表中引用的对象;

方法区中类的静态变量引用的对象;

方法区中常量引用的对象;

本地方法栈中Native方法引用的对象;

2 引用类型

狭义地理解引用就可以认为引用存储着另一块内存的起始地址,这种定义下对象只有两种状态:被引用和未被引用。然而Java从JDK1.2之后就提供了更灵活地对引用的定义,这些定义的引用类型,可以使得对象的生命周期不再那么绝对。下面按照引用强度从高到低依次介绍Java现存的4中引用类型。

2.1 强引用 (Strong Reference)

强引用就是最常见的直接引用,通过Object obj = new Object()引用,或者将引用指向另一个对象,都可以实现强引用。只要强引用存在,被引用的对象永远不会被垃圾回收。若要解除某个引用和对象的强引用,只需要显式地将null赋给该引用,之后在下一次GC时,原先被引用的在堆上的对象就会被回收。

public class Foo {

    public static void main(String[] args) {
        Foo foo = new Foo();
        foo = new Foo();
        System.gc();
    }

    public Foo() {
        System.out.println("Foo=" + this.hashCode() + " has created.");
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("Foo=" + this.hashCode() + " is about to be garbage collected.");
        super.finalize();
    }
}

显式执行System.gc()会触发垃圾回收,引用foo在被重新指向新的Foo对象后符合垃圾回收条件,会被回收掉。在回收前,由于我们重写了finalize()方法,将打印出该对象的信息。
这里写图片描述

另外,JDK本身也会利用显式地将引用置为null来处理不再被引用的对象,例如JDK提供的集合框架,在remove()方法时就会采用这种方法:

/*
     * Private remove method that skips bounds checking and does not
     * return the value removed.
     */
    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // Let gc do its work
    }

代码片段取自于ArrayList.remove()方法,最终执行的私有方法fastRemove()中就在底层数组中把需要移除的元素显式地标记为了null,并且利用GC去回收之前的元素对象。这种强引用关系,在任何情况下除非显式地解除,JVM不会有任何机制加以管理,即便在内存不足时,即将抛出OutOfMemoryError时也不会。

2.2 软引用 (Soft Reference)

软引用用来表示一些有用但是非必要的对象。Java1.2之后,提供了软引用的实现,SoftReference类。根据JavaDoc的定义,为了响应内存的需求,软引用对象会被垃圾回收器感知并清除,软引用通常用来实现内存敏感型的缓存。如果一个对象被确定为时软可达,垃圾回收器会保证在JVM抛出OutOfMemoryError之前清理所有这个对象的软引用。简单讲,软引用对象只会在内存不足时被回收,这样就实现了缓存,并且并不会因为大量的软引用缓存导致OOM,因为OOM之前会进行二次回收,如果内存还是不足才会真正抛出OOM。当引用类型Reference的对象被回收后,如果在构造引用类型的时候传入一个ReferenceQueue队列实例,则被回收的引用类型将被放入该队列。

/**
 * VM args: -Xmx20m -Xms20m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
 * 
 * @author Ken
 *
 */
public class SoftReferenceExample {

    public static void main(String[] args) throws InterruptedException {

        // Reference queue
        ReferenceQueue<Object> queue = new ReferenceQueue<>();

        // SoftReference to strong reference
        SoftReference<Foo> reference = new SoftReference<>(new Foo(), queue);

        // ExplicitGC
        System.gc();

        // Before OOM
        System.out.println("Before OOM: softReference=" + reference);
        System.out.println("Before OOM: referent=" + reference.get());
        System.out.println("Before OOM: refereceQueue.poll=" + queue.poll());

        // OOM happened
        try {
            OOM();
        } catch (Throwable e) {
            System.err.println("OutOfMemoryError is caught so that GC is guranteed to be executed.");
        }

        // After OOM
        System.out.println("After OOM: softReference=" + reference);
        System.out.println("After OOM: referent=" + reference.get());
        System.out.println("After OOM: referenceQueue.poll=" + queue.poll());

    }

    private static void OOM() {
        List<Object> list = new ArrayList<Object>();
        while (true) {
            list.add(new Object());
        }
    }
}

示例代码中,首先实例化一个软引用对象,并将其底层对象指向一个新建的Foo实例,对于Foo对象,我们重写了其finalize()方法,意在让我们能感知其在何时被垃圾回。然后调用系统GC方法,显式地执行一次GC。最后模拟一次OOM,观察现象。

这里写图片描述

从结果可以看到,首先Foo对象被创建了,其hashcode=366712642, 随后在进行了一次显式的GC操作后,软引用对象及其引用的对象都没有被回收,因此引用队列中也无法获取元素。接着执行了OOM()方法,在真正抛出OOM之前,Foo对象的finalize()被调用了,意味着这个对象将被垃圾回收。随后,我们可以看到,软引用对象本身还维持着引用,不影响输出,但是其指向的Foo对象由于OOM的发生,被回收了,从引用队列中获取头元素,也可以得到刚才被回收的对象其关联的软引用对象,这也印证了,被软引用引用的对象,只有在OOM发生前才会被回收。

2.3 弱引用 (Weak Reference)

弱引用与软引用类似,但是它的强度比软引用更弱一些,也就是说它被回收的条件更为宽松,只要GC发生,弱引用必被回收。Java1.2之后,提供了弱引用的实现,WeakReference类。

/**
 * VM args: -Xmx20m -Xms20m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
 * 
 * @author Ken
 *
 */
public class WeakReferenceExample {

    public static void main(String[] args) throws InterruptedException {
        // Reference queue
        ReferenceQueue<Foo> queue = new ReferenceQueue<>();

        // WeakReference to strong reference
        WeakReference<Foo> reference = new WeakReference<>(new Foo(), queue);

        // Before GC
        System.out.println("Before GC: weakReference=" + reference);
        System.out.println("Before GC: referent=" + reference.get());
        System.out.println("Before GC: refereceQueue.poll=" + queue.poll());

        // ExplicitGC
        System.gc();

        // After GC
        System.out.println("After GC: weakReference=" + reference);
        System.out.println("After GC: referent=" + reference.get());
        System.out.println("After GC: referenceQueue.poll=" + queue.poll());

    }
}

示例代码基本与2.2中的相同,唯一不同的是,我们不再强制触发OOM,只执行GC。观察现象发现,即使没有产生OOM,任意的GC都会回收弱引用指向的对象。

这里写图片描述

同样地,当WeakReference所指向的对象被回收后,弱引用也会被加入引用队列。

2.4 虚引用 (Phantom Reference)

虚引用不改变对象的生命周期,垃圾回收对于引用指向的对象不会因为虚引用的存在而产生不一样的行为,只要对象本身符合垃圾回收条件,该对象即会被回收,并且虚引用和弱引用、软引用一样会被加入引用队列,因此虚引用只用于追踪对象是否被回收,可以通过检验队列中是否有虚引用来判断与之关联的对象是否已经回收。

/**
 * 
 * @author Ken
 *
 */
public class PhantomReferenceExample {

    public static void main(String[] args) throws InterruptedException {
        Object foo = new Object();
        System.out.println("Object=" + foo.hashCode() + " has been created.");
        ReferenceQueue<Object> queue = new ReferenceQueue<>();
        PhantomReference<Object> reference = new PhantomReference<>(foo, queue);

        foo = null;
        Thread.sleep(3000);
        System.gc();
        Thread.sleep(3000);

        // After GC
        System.out.println("After GC: softReference=" + reference);
        System.out.println("After GC: referent=" + reference.get());
        System.out.println("After GC: referenceQueue.poll=" + queue.poll());
    }
}

代码中看到,当foo被显式设为null即符合了垃圾回收条件,foo最终会被回收,并且引用将被加入队列。

这里写图片描述

3. TheadLocal中的内存泄漏问题

谈到WeakReference就不得不谈谈其在ThreadLocal中的应用。限于篇幅,这一节不会详细介绍ThreadLocal的原理。概括地讲,ThreadLocal是线程独立的容器,ThreadLocal中存放的数据,可以贯穿整个线程的生命周期,其实现依靠了Thread类中的成员变量ThreadLocal.ThreadLocalMap threadLocals,因此每一个Thread实例维护一个ThreadLocalMap实例,实现了线程的隔离。最后最关键地,我们可以看到ThreadLocalMap是一个类似于HashMap的数据结构,底层也是数组,数组桶中的元素为ThreadLocalMap.Entry,这个Entry继承WeakReference<ThreadLocal>, 这些看清楚了,ThreadLocal本身不存储数据,它只是作为key去从映射表中获取value,而这个key是弱引用。下图很好地展示了,线程及其ThreadLocal对象之间的引用关系。
这里写图片描述

3.1 内存泄漏原因

上文讲到了,ThreadLocal对象作为弱引用key存放在ThreadLocalMap中,那么当外部没有强引用指向这个ThreadLocal对象,加上ThreadLocalMap持有的是弱引用,那么这个对象会在下一次GC中被回收。产生的现象,Map出现key=null的Entry,而此时的value是访问不到的,因为ThreadLocal已经没有强引用了,所以value就成了残留对象,并且是Entry持有的强引用,无法被回收造成内存泄漏。强引用链如下:

Thread引用–>Thread对象–>ThreadLocalMap对象–>Entry对象–>value对象

此时就产生了所谓的StaleEntry,事实上,JDK在实现的时候已经考虑了这种场景,ThreadLocalMap中有的私有方法replaceStaleEntry()以及expungeStaleEntry()就是用来处理StaleEntry,而这些私有方法,在ThreadLocal中的set(),get(),remove()中都会最终执行来清除线程ThreadLocalMap中的残留数据。

然而事情不会那么简单,当我们碰到以下两种场景,内存泄漏仍然会存在:

ThreadLocal对象为静态使之与线程同生命周期, ThreadLocal无法被回收。
无法保证会清除线程ThreadLocalMap的方法一定会被执行。

接下来的问题,如果我们不使用WeakReference会怎么样?
由于ThreadLocal对象被ThreadLocalMap持有,如果ThreadLocal是强引用实现,那么由于ThreadLocalMap的生命周期与线程相同,会造成ThreadLocal本身也会产生内存泄漏;反之,如果用弱引用实现,那么起码可以保证ThreadLocal不会产生内存泄漏,最坏的情况是value产生泄漏。而value有机会在下一次的清除操作中回收。

因此总结下内存泄漏的根因,根本上与弱引用有关,但不是弱引用直接造成的,而是由于ThreadLocal在用完的时候没有显式地移除该ThreadLocal对应的Entry。

3.2 ThreadLocal使用的正确姿势

每次使用完ThreadLocal后,手动调用remove()方法,清除数据,不但能够避免内存泄漏,更重要的是,当线程本身是线程池看护的,如果不是每次调用remove(),ThreadLocal无法被回收,因为线程并没有被销毁,它只是被池化了,如果再加上糟糕的编程习惯,极有可能造成业务逻辑的混乱。

4. 总结

本文从可达性分析入手介绍了GC的基本准侧,进而展开了对Java引用类型的简单介绍,最后给出了弱引用在JDK中的应用场景,并总结了其使用中可能遇到的内存泄漏问题,给出了最佳实践。


以上

© 著作权归作者所有

引用

[1] 深入理解Java虚拟机 JVM高级特性与最佳实践, 周志明著,chapter 3.2

[2] ThreadLocal内部机制 https://www.jianshu.com/p/0ce314da0248

[3] 深入分析 ThreadLocal 内存泄漏问题 http://www.importnew.com/22039.html

<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、付费专栏及课程。

余额充值