图解JVM:浅显易懂的深浅拷贝与Java引用类型解析

大家好,我是大圣,很高兴又和大家见面。

今天给大家带来图解 JVM 系列的第六篇文章,主要给大家补一些 JVM 方面相关的概念。本次大纲如下:

前面知识回顾

回顾

前面一篇文章主要说了 Java 对象在 JVM 里面的生命周期,如下图:

大家可以去这篇文章看一下,今天主要说 Java 的对象拷贝、引用、栈帧等知识点在 JVM 中的应用。

浅拷贝和深拷贝

我先用代码让大家直观了解,然后举一个简单的例子,让大家明白浅拷贝和深拷贝的概念。

案例代码

大家可以先看下面这段代码

java 复制代码
class Fruit {
    String name;

    public Fruit(String name) {
        this.name = name;
    }

    public Fruit(Fruit another) {
        this.name = another.name; // Deep copy for String (immutable)
    }
}

class ShoppingList implements Cloneable {
    int itemCount;
    Fruit fruitBasket;

    public ShoppingList(int itemCount, Fruit fruitBasket) {
        this.itemCount = itemCount;
        this.fruitBasket = fruitBasket;
    }

    // 浅拷贝实现
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    // 深拷贝实现
    public ShoppingList deepCopy() {
        return new ShoppingList(this.itemCount, new Fruit(this.fruitBasket));
    }
}

public class DeepCopyVsShallowCopy {
    public static void main(String[] args) throws CloneNotSupportedException {
        // 原始购物清单
        ShoppingList originalList = new ShoppingList(5, new Fruit("Apple"));
        System.out.println("Original List: " + originalList.itemCount + ", " + originalList.fruitBasket.name);

        // 浅拷贝
        ShoppingList shallowCopyList = (ShoppingList) originalList.clone();

        // 深拷贝
        ShoppingList deepCopyList = originalList.deepCopy();

        // 更改原始购物清单
        originalList.itemCount = 10;
        originalList.fruitBasket.name = "Banana";

        // 输出结果以查看差异
        System.out.println("Original List: " + originalList.itemCount + ", " + originalList.fruitBasket.name);
        System.out.println("Shallow Copy: " + shallowCopyList.itemCount + ", " + shallowCopyList.fruitBasket.name);
        System.out.println("Deep Copy: " + deepCopyList.itemCount + ", " + deepCopyList.fruitBasket.name);
    }
}

代码说明

这个代码定义了 Fruit 类,然后提供了两个构造函数,来对成员变量 name 进行赋值。

接着定又定义了 ShoppingList 类,其中有两个属性,一个属性是 itemCount,另外一个是 fruitBasket。然后定义了浅拷贝 clone 方法和深拷贝 deepCopy 方法。

最后再 DeepCopyVsShallowCopy 类中实现 main 方法对写的方法进行调用测试。

代码与 JVM 对应

补充一个小知识

当执行到 DeepCopyVsShallowCopy 这个类中的 main 方法中的下面这一行代码的时候

ini 复制代码
ShoppingList originalList = new ShoppingList(5, new Fruit("Apple"));

此时 ShoppingList 这个对象在 JVM 中是这样存储的,如下图:

简单解释一下这个图,在 JVM 当中如果是引用类型的对象的话,当我们执行

ini 复制代码
ShoppingList originalList = new ShoppingList(5, new Fruit("Apple"));

这条语句之后,originalList 变量其实存储的是一个地址,保存在栈上面,ShoppingList 对象和它的实例数据存储在堆上面,然后通过 originalList 变量保存的地址指向堆上面的这个对象,接着我们用 originalList 这个变量就可以拿到堆上面 ShoppingList 这个对象的实例数据。

细心的小伙伴会发现 ShoppingList 成员变量 fruitBasket,其实也是一个引用类型,这个时候我们该如何保存呢?大家继续看下面这张图:

我们在堆上面会看到 ShoppingList 成员变量 fruitBasket 保存的是一个地址,然后这个地址指向对象 Fruit("Apple")。

这是因为如果一个引用类型的对象,比如 ShoppingList,这个引用类型对象的成员变量中如果有成员变量是也是一个对象的话,比如 fruitBasket ,此时这个引用类型的对象在堆上初始化保存这个成员变量的时候,就会保存其地址。

比如 ShoppingList 对象在堆上初始化的时候 fruitBasket 这个成员变量保存的就是 Ox...3E4E6CA 这个地址,然后再用这个 Ox...3E4E6CA 地址指向真正的 Fruit("Apple") 对象。

csharp 复制代码
System.out.println("Unmodified Original List: " + originalList.itemCount + ", " + originalList.fruitBasket.name);

所以这个代码打印出来的结果就 Unmodified Original List: 5, Apple

执行浅拷贝代码
ini 复制代码
        ShoppingList shallowCopyList = (ShoppingList) originalList.clone();

当执行过这一行代码之后,此时 JVM 上面的存储就会变成下面这样:

当执行浅拷贝之后,首先会在堆上首地址为Ox...5B6E4CA 的 ShoppingList 对象的实例数据和属性原封不动的拷贝一份,此时拷贝出来的 ShoppingList 对象就是一个新的对象了(首地址为 Ox...6A8C5D7),然后 shallowCopyList 变量会保存一个新的地址,指向这个 ShoppingList 新的对象(首地址为 Ox...6A8C5D7)。

此时我们用 shallowCopyList 这个变量去访问 首地址为 Ox...6A8C5D7 的 ShoppingList 对象的属性,然后修改这个对象属性的值的话,就会在首地址为 Ox...6A8C5D7的 ShoppingList 对象对应的堆上面修改,从而和上面那个首地址为 Ox...5B6E4CA 的 ShoppingList 对象的实例数据就没有关系了。

注意

此时浅拷贝生成对象首地址为 Ox...6A8C5D7 的ShoppingList 对象里面保存的实例变量 fruitBasket 对应的地址是不变的,和上面那个 对象首地址为 Ox...5B6E4CA 的 ShoppingList 对象里面保存的实例变量 fruitBasket 的值是一样的。因为这是规定。

浅拷贝时规定:当浅拷贝的时候,如果一个对象中的成员变量有引用类型的时候,比如 fruitBasket 字段(引用数据类型)在浅拷贝中只是复制了引用,这个引用还是会指向同一个对象。但是我们浅拷贝的那个 首地址为 Ox...5B6E4CA的ShoppingList 对象是会在堆上生成一个首地址为 Ox...6A8C5D7的对象的。

执行深拷贝代码
ini 复制代码
ShoppingList deepCopyList = originalList.deepCopy();

当执行过这一行代码之后,此时 JVM 上面的存储就会变成下面这样:

同样的当执行深拷贝之后,首先会在堆上首地址为Ox...5B6E4CA 的 ShoppingList 对象的实例数据和属性原封不动的拷贝一份,此时拷贝出来的 ShoppingList 对象就是一个新的对象了(首地址为Ox...7B3C9FK),然后 shallowCopyList 变量会保存一个新的地址,指向这个 ShoppingList 新的对象(首地址为Ox...7B3C9FK)。

注意

但是此时堆上首地址为Ox...7B3C9FK 的 ShoppingList 对象它的成员变量 fruitBasket 保存的地址是一个新的引用地址 Ox...4B5E1BD了,这是和浅拷贝不一样的地方。

小结

csharp 复制代码
// 更改原始购物清单
originalList.itemCount = 10;
originalList.fruitBasket.name = "Banana";

// 输出结果以查看差异
System.out.println("Original List: " + originalList.itemCount + ", " + originalList.fruitBasket.name);
System.out.println("Shallow Copy: " + shallowCopyList.itemCount + ", " + shallowCopyList.fruitBasket.name);
System.out.println("Deep Copy: " + deepCopyList.itemCount + ", " + deepCopyList.fruitBasket.name);

所以当执行完这几行代码,大家知道输出的是什么吗?

Original List: 10, Banana

Shallow Copy: 5, Banana

Deep Copy: 5, Apple

此时 JVM 堆上的存储如下图所示:

说到这里不知道大家有没有理解 Java 里面的浅拷贝和深拷贝呢?如果不理解,没关系,继续往下看,下面我给大家补了一些的理论知识。

举例说明

假设你有一张购物清单,上面列出了你需要购买的物品。这张清单就是一个对象。

浅拷贝

你将这张购物清单复印了一份。现在你有两张清单,原始的和复印的。但这两张清单上都有一项是"购买水果篮"。

水果篮里面的水果(比如苹果和香蕉)可以更换。如果你更改原始清单上的水果篮内容(比如从苹果换成橙子),复印的清单上的水果篮内容也会跟着变化。这是因为两张清单上的"购买水果篮"项实际上指向的是同一个水果篮。

浅拷贝专业解释

创建一个新的对象,然后将原始对象的字段值复制到新对象中。对于字段是基本数据类型的,复制的是值;对于字段是对象的,则复制的是引用,而不是引用的对象本身。

这意味着如果原始对象中的某个对象字段发生变化,浅拷贝的对象中的相应字段也会跟着变化。

深拷贝

深拷贝:你不仅复制了购物清单,还为清单上的每一项(比如水果篮)创建了一个完全新的副本。现在,即使你更改原始清单上的水果篮内容,复印的清单上的水果篮内容仍然保持不变,因为它是独立的。

深拷贝专业解释

创建一个新的对象,然后递归地复制原始对象中的所有字段,包括那些引用其他对象的字段。这样,即使原始对象的字段发生变化,也不会影响到深拷贝出来的对象,因为所有的字段都是独立的。

小结

其实浅拷贝的对象共享部分数据,而深拷贝的对象是完全独立的。在实际编程中,选择使用哪种拷贝方式取决于你的具体需求,特别是当涉及到对象的独立性和内存效率时。

Java 引用

Java中的引用可以根据它们与垃圾回收器(GC)的交互方式分类。主要有四种类型的引用。

强引用(Strong Reference)

定义

这是最常见的引用类型。如果一个对象具有强引用,那么它永远不会被垃圾回收器回收,只要这个强引用还存在。

特点

不会被垃圾回收:只要强引用指向一个对象,该对象就不会被GC回收,这就保证了在使用该对象的过程中,它不会突然被回收造成程序错误。

可能导致内存泄漏:如果强引用一直保持而没有适当的解除引用,它可能导致内存泄漏,如 JDK1.6 中的 String类的 substring方法有这个内存泄漏的bug,还有就是特别是在一些长生命周期的对象或集合中。

使用场景

在Java的日常编程中,大部分引用都是强引用。例如,当你这样创建一个对象时:

ini 复制代码
Object obj = new Object();

这里的 obj 就是一个强引用,指向 Object 的一个实例。只要 obj 这个引用存在,垃圾回收器就不会回收其指向的 Object 实例。

软引用(Soft Reference)

软引用是Java中的一种特殊引用类型,它允许对象在内存足够时保留在内存中,但在内存不足时被垃圾回收器回收。软引用通过 java.lang.ref.SoftReference 类实现。

定义

软引用是一种比强引用更弱的引用类型。它允许对象在内存足够的情况下保留,但在JVM内存不足时,这些对象可能被垃圾回收器回收。软引用通常用于实现内存敏感的缓存。

特点

内存充足时保留:如果JVM内存充足,软引用指向的对象不会被回收,从而允许长时间保留。

内存不足时回收:当JVM内存不足时,这些对象可能会被垃圾回收器回收,以释放内存。

用于实现缓存:软引用适合于实现缓存,因为它允许在内存紧张时自动清除缓存项,防止 OutOfMemoryError

使用场景

csharp 复制代码
import java.lang.ref.SoftReference;

public class SoftReferenceCache {

    public static void main(String[] args) {
        // 创建一个对象
        String str = new String("Hello, Soft Reference");

        // 创建软引用
        SoftReference<String> softRef = new SoftReference<>(str);

        // 清除强引用
        str = null;

        // 从软引用中获取数据
        String cachedData = softRef.get();
        if (cachedData != null) {
            System.out.println("Cached data: " + cachedData);
        } else {
            System.out.println("Cached data has been garbage collected");
        }

        // 执行内存紧张的操作...

        // 再次尝试从软引用获取数据
        cachedData = softRef.get();
        if (cachedData != null) {
            System.out.println("Cached data after memory intensive operations: " + cachedData);
        } else {
            System.out.println("Cached data has been garbage collected after memory intensive operations");
        }
    }
}

在这个例子中,我们创建了一个字符串对象,并将其包装在一个软引用中。随后,我们尝试从软引用中获取数据。在进行内存密集型操作后(例如分配大量内存),如果系统内存变得紧张,软引用指向的对象可能会被垃圾回收器回收。这种行为使得软引用非常适合用于实现那些应对内存压力能自动调整的缓存系统。

弱引用(Weak Reference)

弱引用在Java中是通过 java.lang.ref.WeakReference 类实现的,它提供了一种比软引用更弱的引用类型。

定义

弱引用是一种引用类型,它不阻止其所指向的对象被垃圾回收器回收。一个对象只要被弱引用指向,并且没有其他强引用指向它,那么这个对象就可能在下一次垃圾回收时被销毁。

特点

快速被回收:只要垃圾回收器运行,而对象只被弱引用指向,它就会被回收。

适用于临时引用:弱引用通常用于实现那些对生命周期要求不严格的临时引用场景。

常用于缓存和映射:弱引用在Java集合中非常有用,例如,在 java.util.WeakHashMap 中使用。

使用场景

弱引用通常用于实现缓存机制或者作为映射的一部分,以允许对象在不再需要时自动被回收。以下是一个简单的例子,展示了如何使用弱引用:

typescript 复制代码
import java.lang.ref.WeakReference;

public class WeakReferenceExample {

    public static void main(String[] args) {
        // 创建一个对象
        String strongString = new String("I am a strong reference");

        // 创建弱引用
        WeakReference<String> weakRef = new WeakReference<>(strongString);

        // 清除强引用
        strongString = null;

        // 在清除强引用后,尝试从弱引用获取对象
        String retrievedString = weakRef.get();
        System.out.println("Retrieved from weak reference: " + retrievedString);

        // 执行垃圾回收
        System.gc();

        // 再次尝试从弱引用获取对象
        retrievedString = weakRef.get();
        if (retrievedString == null) {
            System.out.println("Object has been garbage collected");
        } else {
            System.out.println("Retrieved from weak reference after GC: " + retrievedString);
        }
    }
}

在这个例子中,我们首先创建了一个强引用的字符串对象 strongString,然后创建了一个指向这个字符串的弱引用 weakRef。接着,我们删除了原始字符串对象的强引用。在垃圾回收之前,我们仍然可以通过弱引用获取该字符串。但是一旦进行垃圾回收,由于没有强引用指向该字符串,它就可能被回收,这时通过弱引用就无法再获得该字符串对象。

弱引用的这种特性使得它非常适合用于实现那些对象生命周期与应用程序逻辑无关的缓存系统。

虚引用(Phantom Reference)

虚引用在Java中是通过 java.lang.ref.PhantomReference 类实现的,它是Java提供的四种引用类型中最弱的一种。

定义

虚引用是一种完全不影响其所引用对象生命周期的引用类型。即使持有虚引用,垃圾回收器仍然会回收该对象。虚引用主要用于跟踪对象被垃圾回收的状态,而不是用来获取对象的引用。

特点

无法通过虚引用获取对象:通过虚引用是无法获取到所引用的对象的,get() 方法总是返回 null。

垃圾回收跟踪:虚引用主要用于跟踪对象何时被垃圾回收器回收,通常与 ReferenceQueue 配合使用。

资源清理:虚引用可以用于在对象被回收时执行一些重要的清理工作。

使用场景

虚引用通常用于在对象被回收时获得一个通知,从而进行一些特定的资源清理操作。以下是一个使用虚引用的例子:

typescript 复制代码
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public class PhantomReferenceExample {

    public static void main(String[] args) {
        // 创建一个对象和一个引用队列
        String str = new String("Phantom Reference");
        ReferenceQueue<String> queue = new ReferenceQueue<>();

        // 创建虚引用
        PhantomReference<String> phantomRef = new PhantomReference<>(str, queue);

        // 清除强引用
        str = null;

        // 强制垃圾回收
        System.gc();

        // 检查引用队列中是否存在引用对象
        System.out.println("Phantom Reference in queue: " + (queue.poll() != null));
    }
}

在这个例子中,我们创建了一个字符串对象 str 和一个引用队列 queue。然后创建了一个指向字符串的虚引用 phantomRef。在清除了字符串的强引用并进行垃圾回收后,我们可以通过检查引用队列来确定对象是否已被垃圾回收。

虚引用主要用于在对象被回收时进行一些特殊的资源清理工作,比如清理堆外内存(Direct Memory)等。虚引用为了确保这些资源被妥善处理,提供了一种确保对象被回收时收到通知的机制。

小结

四种引用的区别

强引用阻止其指向的对象被回收。

软引用和弱引用允许其指向的对象在内存不足时被回收。

虚引用不影响其指向的对象的生命周期。

用途

强引用用于普通对象引用。

软引用用于实现内存敏感的缓存。

弱引用用于创建临时引用,常用于缓存和映射。

虚引用用于在对象被回收时接收系统通知。

可访问性

通过强引用、软引用和弱引用可以访问对象。

虚引用不可用于访问对象。

为什么要了解这四种引用

个人觉得主要是防止我们写的代码出现内存泄漏,来及时的清理不必要的资源。

总结

本文回顾

本文讲了 Java 中的浅拷贝和深拷贝与 JVM 的联系,我个人觉得这个还是非常有必要掌握的,掌握了这个,我们这个对我们写的 Java 程序更加游刃有余。

后面又讲了 Java 中的四种引用,主要提高我们写代码的性能。

预告

如果小伙伴们看到这里了,你们真的很棒。大纲里没有说完的,接下来我会持续的更新的,把 JVM 系列写完。

本文由博客一文多发平台 OpenWrite 发布!

相关推荐
小白学大数据1 小时前
如何使用Selenium处理JavaScript动态加载的内容?
大数据·javascript·爬虫·selenium·测试工具
15年网络推广青哥1 小时前
国际抖音TikTok矩阵运营的关键要素有哪些?
大数据·人工智能·矩阵
节点。csn2 小时前
Hadoop yarn安装
大数据·hadoop·分布式
arnold662 小时前
探索 ElasticSearch:性能优化之道
大数据·elasticsearch·性能优化
NiNg_1_2343 小时前
基于Hadoop的数据清洗
大数据·hadoop·分布式
成长的小牛2334 小时前
es使用knn向量检索中numCandidates和k应该如何配比更合适
大数据·elasticsearch·搜索引擎
goTsHgo4 小时前
在 Spark 上实现 Graph Embedding
大数据·spark·embedding
程序猿小柒5 小时前
【Spark】Spark SQL执行计划-精简版
大数据·sql·spark
隔着天花板看星星5 小时前
Spark-Streaming集成Kafka
大数据·分布式·中间件·spark·kafka
奥顺5 小时前
PHPUnit使用指南:编写高效的单元测试
大数据·mysql·开源·php