ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

四大引用类型揭秘:强引用到虚引用的深度解析

四大引用类型揭秘:强引用到虚引用的深度解析 在讲那四大引用类型之前, 我们需要把一个问题弄明白, 那就是啥叫引用?我们得弄清楚引用到底指的是什么意思。引用这个概念的实质, 指的是那种指针常量, 而在针对我们这里所阐述的这一篇特定文章展开的语境之下, 我们完全只需要建立起一种认知, 即明确地将理解为: 所谓的引用, 它其实就是归属于特殊类型的那一种指针。这里展示的是在Java编程语言里面, 那种比较标准的声明变量以及去创造一个新实例的具体代码写法。在这段代码里头, 字母o代表的就是这个东西的一个引用标识。你仔细看那个前面一部分, 它描述的是一个引用的类型属性。而后面紧跟着的那个带有括号的花括号new加上小括号的操作动作, 则是专门用来在内存里生成一个Java平台上的对象实体。然后这个名叫o的指针它就指向了那个通过new操作创造出来的实例对象。从意思上来讲这也就是说变量o成功引用了这个新生成的对象数据块。Object onew Object();我们可以用下面的这幅图来描述一下这个概念, 这里说的o, 引用了通过 new 创建出来的那个对象, 并且这种引用被大家称为强引用。咱们平常写代码的时候, 经常使用的那种 new 出新对象的写法, 基本上就属于强引用这种方式。那么究竟什么是强引用? 它为什么会被叫做强引用?什么是强引用强引用的“强”具体体现在哪里呢? 要是我们不把手动的操作去做成把引用变量设置为null, 垃圾回收器就肯定不会去回收那些处于强引用状态的对象。哪怕系统的内存已经变得严重不足了, 它也不会对正在受强引用影响的对象下手。在这种情况之下系统就会抛出那种叫做OOM的外部存储器耗尽异常。这种设定带来的好处就是让对象的存在变得更加的稳定。只要当前的软件应用还在正常的工作状态, 通过强引用来关联的对象就可以一直存在于内存中而不被删除。请你尝试去构想一下那样一种情形, 就是你刚刚创建出来的那个对象, 它还没有来得及把它自己应该执行完的那些任务给做完, 就因为电脑里的内存不够用了, 结果就被垃圾回收器给回收掉了, 这种状况发生起来确实是让人感到非常恐惧的一种可怕场景。同时, 强引用这个问题也同样存在弊端, 那就是很有可能会导致内存泄漏这样的情况出现, 关于内存泄漏到底是具体怎么个回事, 我们会在后头的部分继续去给你做一个更加详细的介绍和分享。如果我们手动的方式, 强去把它的引用设置成为了空null值之后, 那么gc它这个机制呢, 就会认为说现在这个对象是不存在所谓的引用关系的那个情况的了, 就会对它执行回收的动作这一件事情。在Java语言里的类对象层面, 它是存在一个特定的方法的, 当这种类型的对象被垃圾回收机制进行回收处理的时候就会触发调用这个方法的操作, 用户是可以借由对这种方法的使用状态来进行观测和分析的, 从而用来明确或证实该对象已经被垃圾回收器成功回收这一情况的发生。public class M {Overrideprotected void finalize() throws Throwable {-- --super.finalize();System.out.println(finalize);}}public class TestFinalize {public static void main(String[] args) throws IOException {-- --M mnew M();mnull;System.gc();//gc回收需要时间起见让程序一直等待不写也是能看到效果的。System.in.read();}}输出finalize可是, 当把变量m设定为空值的情况发生之时, 垃圾回收机制确实就能够把我们那个对象给清理掉。软引用在实际操作当中, 引用类型可以按照它们引用的强度来进行明确的分类工作, 而所谓的软引用就是那些相对比强引用本身所具备的强度要稍弱一些的引用类型。Java把这个软引用给封装到了某个类里面去, 然后这个软引用的特点就是, 等到内存变得不富裕、不够用的时候, 也就是gc在进行垃圾回收操作的时候, 它会把那个被软引用指向的对象给回收掉, 像这样做, 就不太容易就会让大家觉得发生了那个OOM, 下面的这一段代码就是一个关于软引用的例子, 我们这里new了一个size为10M大小的一个数组出来, 而且进行了包装, 之所以要用这么大个数组, 主要是为了来模拟一下那个OOM的情况, 在后面我们会用到这个。SoftReferencebyte[] srnew SoftReference(new byte[10*1024*1024]);现在, 我们先来看一下软引用的结构, 如下图所示, 请注意, 变量 sr 这个引用是强引用, 并且在内部会执行类似于下方的代码, 这里的变量同样也是强引用, 而软引用指的是图中虚线所覆盖的部分, 也就是内部存在一个指向我们用 new 创建的数组的指针, 在下图中名为 sr ref 的那个才是我们的真正的软引用内容至于 sr ref 为什么可以实现软引用的功能, 这属于 JVM 自身的事情, 我们完全不需要予以关心。byte[] byteArrnew byte[10*1024*1024]以下所举的例子模拟了在内存不足的状态之下, 垃圾回收器会回收软引用对象这个现象。用户能够借助以下的JVM参数将内存大小设定为20M这个数字。对于这种配置方法, 在IDEA这个软件里面是可以这样完成设置的。在前面先后两次调用 get 方法期间, 程序都可以顺利拿到对象, 这主要是由于当时系统里的剩余内存空间还是足够的。可是当代码接着执行 byte b new byte 这一行的时候, 就会再去创建一个容量为 10M 的数组对象。考虑到 Java 虚拟机自身运行也需要占据一部分内存资源, 这就导致总的内存需求瞬间超标, 从而引发了内存溢出的现象。不过在这个过程里, 系统并没有直接抛出 OOM 这种异常信息, 其关键原因在于虚拟机的垃圾回收机制在发现有软引用存在的情况下, 主动选择把那个大小为 10M 且被标记了软引用的旧对象给清理掉。随着这个旧对象被释放, 可用的内存自然就又变得宽裕起来了。所以到了第三次再次调用 get 方法的时候, 返回来的结果就只能是一个 null 值了, 最根本的原因就是之前那些带有软引用属性的对象, 已经在前面提到的垃圾回收流程中彻底消失了。在当前这个时间点, 我们再度新建出一个容量为10M的数组, 具体代码表现为byte b2 new byte。此时在内存中一共存在着两个强引用类型的数组对象, 它们所占据的空间总和达到了20M, 再叠加上新产生的虚拟机自身所占用的内存部分, 这种状况会直接致使程序触发OOM异常。public static void main(String[] args) throws IOException, InterruptedException {-- --SoftReferencebyte[] srnew SoftReference(new byte[10*1024*1024]);System.out.println(sr.get());System.gc();Thread.sleep(500);System.out.println(sr.get());byte[] bnew byte[10*1024*1024];System.out.println(sr.get());byte[] b2new byte[10*1024*1024];System.in.read();}输出[B135fbaa4[B135fbaa4nullException in thread main java.lang.OutOfMemoryError: Java heap spaceat ref.TestSoftRef.main(TestSoftRef.java:15)弱引用弱引用这种关系的强度, 是变得更加微弱了。只要一个对象是被弱引用给包裹住的, 那么在不管系统的内存空间是否充足的情况下, 要是垃圾收集器在进行工作的时候一旦发现了有弱引用的存在, 就会立即执行回收操作。在Java这个语言体系里面, 它是使用类来对弱引用进行表示的。对于下面展示的这段代码来讲, 它并没有处在内存不足的境地之中, 不过只要垃圾收集器去执行运行任务了, 那么被弱引用的对象就会被马上给进行回收处理。WeakReferenceM wrnew WeakReference(new M());System.out.println(wr.get());System.gc();Thread.sleep(10);System.out.println(wr.get());垃圾回收机制执行结束, 然后弱引用对象被进行回收操作, 接着对应的方法会被进行调用动作, 在后续再次调用获取数据的功能时是返回空值的。输出ref.M135fbaa4finalizenull为了能够对这一点有更加深入的了解, 接下来我们还将列举下面的例子。以下的这些程序代码有可能导致内存的泄露的状况出现, 用户很可能出于操作时的惯性思维而习惯性地按照这种方式来进行书写与编程行为, 但是必须明确的是这样去写代码的行为本身是存在问题的, 是不被提倡的正确做法。public class A {Overrideprotected void finalize() throws Throwable {-- --super.finalize();System.out.println(A finalize);}}WeakReferenceA wr new WeakReference(new A());//a强引用wr valueA a wr.get();//输出aSystem.out.println(a);System.gc();Thread.sleep(100);System.out.println(wr.get());输出ref.A135fbaa4ref.A135fbaa4我们这边发现, 在弱引用这个对象里头, 它里面的东西并没有被收走。我们调用get方法去获取那个弱引用所指向的对象, 也就是new A()这个东西, 同时用一个叫做a的变量把它给引用住了, 这样一来, 变量a就直接形成了对new A()的一个强引用状态, 由于它受到了这种强引用的保护, 即便wr.get这个操作本身是被包装起来的, 新的对象也不会马上被回收掉, 要是想让这个对象变成可以被回收的状态, 你就只能手动把a的值改成null了。如下图所示, new A()这个对象实际上受到了两个引用指向, 其中包含一个内部的弱引用, 还有一个是变量a所持有的强引用, 由于存在了强引用保护, 所以new A()这种情况是不会被gc回收的。在刚才那段代码的最后部分, 把以下的这部分代码添加进去, 这样就把由弱引用进行包裹的对象给回收掉了。a null;System.gc();//输出aSystem.out.println(wr.get());ref.A135fbaa4ref.A135fbaa4nullA finalize如果弱引用还被别的强引用所持有, 那么它未必是轻而易举就能回收的。这个实例向我们要展示的另外一个启示就是, 假如你碰上了那种相对复杂的局面, 那么就试着按照这样的方式去进行绘制图表, 以便把彼此之间的相互关系给梳理清楚。下面再次给出一个在难度上更高一些的案例, 为的是进一步加深读者的理解。这个例子非常的重要, 能够帮助我们深入理解弱引用和Java内存模型。现在有两个类, 分别是A类和B类。在A类的构造器里头创建B对象, 并且在A类内部有引用b指向new B()这个对象。里面做了些延时操作和输出, 这些操作的细节后面会解释。public class A {private B b;public A() {bnew B();}public B getB() {-- --return b;}Overrideprotected void finalize() throws Throwable {-- --super.finalize();//如果休眠太长后面的代码可能不会输出Thread.sleep(500);System.out.println(b in finalize b);System.out.println(A finalize);}}public class B {-- --Overrideprotected void finalize() throws Throwable {-- --super.finalize();System.out.println(B finalize);}}这个示例的主要目的, 是要去观察在游戏控制器回收发生之前, 以及在游戏控制器回收发生之后。WeakReferenceA wr new WeakReference(new A());A a wr.get();//输出aSystem.out.println(a);B ba.getB();//输出bSystem.out.println(b);System.gc();Thread.sleep(500);System.out.println(-------gc 执行后------);System.out.println(a);System.out.println(b);//anull;System.gc();System.out.println(-------anull 并执行gc------);Thread.sleep(500);System.out.println(a);System.out.println(b);输出ref.A135fbaa4ref.B45ee12a7-------gc 执行后------ref.A135fbaa4ref.B45ee12a7-------anull 并执行gc------nullref.B45ee12a7b in finalize ref.B45ee12a7A finalize现在需要重点关注的内容即将出现, 前述的那个示例或许仅仅借助于代码形式来观看会显得比较抽象, 大家不妨直接查看相关的图片内容, 此处的关键点具体包含的是变量a所代表的引用、存储在A类内部作为成员变量的那个b引用, 以及在main方法这个特定函数里面定义并使用的另一个独立的b引用, 当通过new关键字去创建一个A类的实例对象也就是常说的A()对象时, 这个新产生的对象本身实际上是存在有两个不同的引用链接的, 其中一个是由特定的机制所提供的软引用, 而另外一个则对应着前面提到的变量a所持有的引用, 倘若我们并未在程序中手动执行将该变量a赋值空值的操作, 那么由new A()语句创建出来的这个具体对象就会继续存在于内存中根本不会被垃圾回收器给进行回收处理, 关于这一情况的具体说明与之前在前面章节里所列举过的其他例子是完全一致的, 因此整体来看这样的逻辑和因果关系还是比较容易让人理解和接受的。目前的情况已经变得复杂了, 因为在A类的构造器中new了一个 new B()对象。咱们先看第一个问题, new A()和new B()之间到底存在着什么样的关系? 它们两者是不是一种嵌套的关系呢?下图之所以会采用这样的绘制方式, 根本原因在于new A()与new B()二者之间并不存在那种层层嵌套的关联状态。客观上讲, 它们实际上只是存放在堆内存区域里面的、地位完全平行的各类对象罢了。关于第二点疑问, 这里需要明确的是, 在对象A通过new关键字创建并被垃圾回收机制处理掉之后, 是否会导致对象B也会被连带着进行回收。下面这部分内容是非常关键的, 你应当多阅读几遍, 这样才能够非常深刻地理解弱引用和内存模型它们之间的相互关系。下面这部分内容是非常关键的, 你应当多阅读几遍, 这样才能够非常深刻地理解弱引用和内存模型它们之间的相互关系。下面这部分内容是非常关键的, 你应当多阅读几遍, 这样才能够非常深刻地理解弱引用和内存模型它们之间的相互关系。这种情况和之前的那个问题其实是一个道理, 使用new创建一个A的实例对象之后进行的回收操作, 这和我们另外使用new去创建一个B的实例对象之间, 又存在什么样的直接关联呢?哪怕是在A这个类的内部, 确实存在着一个被称为b的变量, 而且这个变量在某种机制下引用了通过new创建出来的B对象, 但是这一点仅仅只是属于A类自身的范畴内的事务, 这根本就和利用new创建出来的A类型的对象实体完全不是一码事。这里有一个问题, 需要被大家清楚地搞清楚。这个问题的核心在于: 使用关键字 new 去创建 A 类型的实例, 和 A 这个类本身, 它们完全是两码事。A 类内部所声明的那些引用变量, 是被放置在了内存里面的方法区之中。A 类和这些引用变量二者之间存在的关系, 只有到了程序彻底结束的时候, 才有可能发生销毁的操作。反过来看, 由 new 关键词所生成出来的对象, 它们有可能在代码运行过程中的任意一个时间点, 就被 JVM 给回收并销毁掉了。使用new关键字去创建B类型的对象, 和用new关键字去创建A类型的对象之间, 实际上是没有任何关联的, 可是使用了new关键字创建的B对象, 和那个类名叫做A的类却存在着某种联系, 这种联系的存在是因为类名叫做A的那个类里面是定义有一个名称为b的变量, 并且这个变量是指向由new关键字创建的B对象的。关于第三个问题, 也就是想要明确在什么样的情况下这个方法会被执行或者被触发。这个方法的调用时机, 是在垃圾回收程序启动之前。也就是说, 此时垃圾回收机制已经做好了回收对象的准备, 并且确认该对象具备了可被回收的条件。所以这个方法的主要作用, 就是用来完成在垃圾回收发生之前的各类善后处理工作。或许我们可以将它换一个别的名字, 比如填入特定的名称。在我上面提供的代码示例中, 我是特意在该方法内部设置了五十秒的休眠时间。这里需要注意的是, 如果让线程休眠的时间变得特别长, 那么这个方法就可能永远不会被调用。其根本原因就在于, 目标对象本身已经被彻底清理掉了, 而位于休眠动作后面的所有程序代码, 也就再也不可能得到执行的机会了。所以这种方法其实是不靠谱的, 咱们没必要去搞那些特别费时间的活儿, 不过只要把休眠的代码放在最后面, 那么前面的代码还是可以正常地输出内容的。把这三个问题给搞清楚了之后, 上面那个代码执行出来的结果, 就非常容易去理解了。把变量a赋值为null, 所以在垃圾回收进行的时候由new A()新建出来的对象会被回收掉。内部的变量b输出是有内容的, 这是因为由new A()新建出来的对象即使已经被回收了, 但是A类以及它包含的引用还是在方法区里面存在的。所以由new B()新建出来的对象会被A类里面的变量b和main方法里面的变量b同时引用到, 只有当这两个引用都变成null之后, 由new B()新建出来的那个对象才会被垃圾回收机制给回收掉。虚引用虚引用在Java语言里是通过类来表示的, 它算是所有类型引用中最弱的一种, 垃圾回收器任何时候都可能会对它进行回收操作, 乍一看跟弱引用挺像似的, 但咱们通常基本上不需要去在意这个引用到底是个啥情况, 因为在平常的开发工作当中, 虚引用基本是派不上用场的, 所以对于写代码的人来说, 最重要的还是要把弱引用搞清楚。
返回列表