Java-垃圾回收机制

1.JDK

2.类文件到虚拟机(类加载机制)

  • (1)装载(Load)查找和导入class文件
  • (2)链接(Link)
    • 【1】验证(Verify)保证被加载类的正确性
    • 【2】准备(Prepare)为类的静态变量分配内存,并将其初始化为默认值
    • 【3】解析(Resolve)把类中的符号引用转换为直接引用
  • (3)初始化(Initialize)
    • 对类的静态变量,静态代码块执行初始化操作
  • (4)类加载机制图解

  

  • (5)类装载器ClassLoader在装载(Load)阶段。
    • 其中第(1)步:通过类的全限定名获取其定义的二进制字节流,需要借助类装载 器完成,顾名思义,就是用来装载Class文件的。
    • 通过一个类的全限定名获取定义此类的二进制字节流
  • (6)运行时数据区(Run-Time Data Areas):类文件被类装载器装载进来之后,类中的内容(比如变量,常量,方法,对象等这些数 据得要有个去处,也就是要存储起来,存储的位置肯定是在JVM中有对应的空间)

  jvm内存划分 

    • 图解(jvm内存划分)

    

    • 【1】Method Area(方法区)

方法区是各个线程共享的内存区域,在虚拟机启动时创建。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

即时编译器编译后的代码:JVM有一个优化叫做JIT,也就是即使编译优化,Java是解释型语言,速度肯定是不如C这种编译型的,那么很明显的一个可行的优化就是把部分热点字节码也直接编译成可执行的机器码(也就是说的即时编译器编译后的代码),这样速度就和编译型的一样了。

方法区在JDK 8中就是Metaspace,在JDK6或7中就是Perm Space

    • 【2】Heap()
      • Java堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享。 Java对象实例以及数组都在堆上分配。
    • 【3】 Java Virtual Machine Stacks(虚拟机栈)

虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。

换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。

每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。

调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。

    • 【4】The pc Register(程序计数器)

我们都知道一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据
CPU调度来的。
假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获
得CPU执行权的时候,怎么能继续执行呢?这就是需要在线程中维护一个变量,记录线程执行到
的位置。

程序计数器占用的内存空间很小,由于Java虚拟机的多线程是通过线程轮流切换,并分配处理器执行时 间的方式来实现的,在任意时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换后能够 恢复到正确的执行位置,每条线程需要有一个独立的程序计数器(线程私有)。

    • 【5】Native Method Stacks(本地方法栈)

如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行,本地方法栈是线程私有的。

Native方法就是一个Java调用非Java代码的接口。一个native方法是指该方法的实现由非Java语言实现,比如用C或C++实现。

3.垃圾回收机制的概念

3.1、在JVM架构中,堆内存和垃圾回收器这两个部分和垃圾回收相关。堆内存是运行时用来存储实例对象的数据空间,垃圾回收器运行在堆内存上

3.2、运行时的Java实例对象存储在堆内存空间中。当一个对象不再被引用了,它变成可被从堆内存中回收空间

年轻代、老年代

3.3、堆内存的空间根据对象的存活时限主要分成了三部分:年轻代、老年代、永久代

3.4、Java堆内存中的对象分代存储(依据对象本身的存活时限)

  • 堆里面分为年轻代和老年代(java8 取消了永久代,采用了 Metaspace),年轻代包含 Eden+Survivor 区,survivor 区里面分为 from 和 to 区,内存回收时,如果用的是复制算法(对象能够存活多少),从 from 复制到 to,当经过一次或者多次 GC 之后,存活下来的对象会被移动到老年代,当 JVM 内存不够用的时候,会触发 Full GC,清理 JVM 老年区当新生区满了之后会触发 YGC,先把存活的对象放到其中一个 Survice区,然后进行垃圾清理。因为如果仅仅清理需要删除的对象,这样会导致内存碎片,因此一般会把 Eden 进行完全的清理,然后整理内存。那么下次 GC 的时候,就会使用下一个 Survive,这样循环使用。如果有特别大的对象,新生代放不下,就会使用老年代的担保,直接放到老年代里面。因为 JVM 认为,一般大对象的存活时间一般比较久远。
  • 年轻代

    • Eden区(所有实例在运行时最初都分配到eden区中)

    • S0 Survivor Space(老一些的对象被从eden区移动到S0区,其实是eden区中的对象经过一次对eden区的Young GC还存活的对象被移动到S0)

    • S1 Survivor Space(再老一些的对象被从S0区移动到S1区,其实是在Young GC过程中S0区已满,则会将eden区中还存活的对象和S0区中的存活对象移动到S1区中)

  • 老年代

    • 经过S0,S1中几轮迭代后还存活的对象被提升到老年代(标记整理 标记清理

  • 永久代

    • 包含一些元数据像类、方法等等

  • 永久代空间在JDK8特性中已经被移除

3.5、Java垃圾回收是一个自动运行的管理程序运行时使用的内存的进程。

  • 作为一个自动执行的进程,程序员不需要在代码中主动初始化GC。Java提供了System.gc()和Runtime.gc()这两个hook来请求JVM调用GC进程

3.6、尽管要求系统机制给程序员提供调用GC的机会,但是实际上这是由JVM负责决定的。JVM可以选择拒绝启动GC的请求,因此并不保证这些请求会真的调用垃圾回收

3.7、Eden Space:当一个实例被创建的时候,它最初被存放在堆内存空间的年轻代的Eden区中

  • Survivor Space(S0 和S1):作为minor回收周期的一部分,还活着的对象(还有引用指向它)被从eden区中移动到survivor空间S0。同样的,垃圾回收器扫描S0并将活着的实例移动到S1

  • 无用的对象被标记并回收。垃圾回收器决定这些被标记的实例是在扫描的过程中移出内存还是在另外独立的迁移进程中执行

  • Old Generation:老年代或者永久代是堆内存的第二个逻辑部分。当垃圾回收器在做minor GC周期中,S1 survivor区中还活着的实例会被提升到老年代中。S1区中不再被引用的对象被标记并清除

  • Major GC:在Java垃圾回收过程中实例生命周期的最后一个阶段。Major GC在垃圾回收过程中扫描属于Old Generation部分的堆内存。如果实例没有被任何引用关联,它们将被标记、清除;如果它们还被引用关联着,则将继续存留在old generation。

3.8、从上述过程可以看出:生存时限越长的对象,其被垃圾回收处理机制扫描的频率就越低

  • Fragmentation:

    • 一旦实例从堆内存中删除了,它们原来的位置将空出来给以后分配实例使用。显然这些空闲空间很容易在内存空间中产生碎片。为了能够更快地分配实例地址,需要对内存做去碎片化操作。根据不同垃圾回收器的策略,被回收的内存将在回收的过程同时或者在GC另外独立的过程中压缩整合

#cmd 管理员运行
jvisualvm

-Xms10M -Xmx10M

public class Person {
    private String name = "张三";
    private int age = 10;
}

public class TestHeap {
    public static void main(String[] args) throws InterruptedException {
        List<Person> persons = new ArrayList<>();
        while (true) {
            persons.add(new Person());
            Thread.sleep(1);
        }
    }
}

Major GC,Minor GC,Full GC

老年代区/Old区的GC我们称作为Major GC(速度慢,可以处理老年的对象垃圾回收)

Minor GC(速度快)指得是Young区的GCFull GC是Minor GC+Major GC +Mataspace GC

为什么需要Survivor区?只有Eden不行吗?

如果没有Survivor,Eden区每进行一次Minor GC ,并且没有年龄限制的话, 存活的对象就会被送到老年代。
这样一来,老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。
老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。
执行时间长有什么坏处?频发的Full GC消耗的时间很长,会影响大型程序的执行和响应速度。
假如增加老年代空间,更多存活对象才能填满老年代。虽然降低Full GC频率,但是随着老年代空间加大,一旦发生Full
GC,执行所需要的时间更长。
假如减少老年代空间,虽然Full GC所需时间减少,但是老年代很快被存活对象填满,Full GC频率增加。
所以Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16
次Minor GC还能在新生代中存活的对象,才会被送到老年代。

为什么需要两个Survivor区?

最大的好处就是解决了碎片化。也就是说为什么一个Survivor区不行?第一部分中,我们知道了必须设置Survivor区。假设现在只有一个Survivor区,我们来模拟一下流程:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。
永远有一个Survivor space是空的,另一个非空的Survivor space无碎片。

3.9、Java中存在四种类型的垃圾回收器:

  • 四种类型的垃圾回收器

    • Serial Garbage Collector

    • Parallel Garbage Collector :

      • 1、Parallel Scavenge Garbage Collector 适用 年轻代 ,算法:复制清除

      • 2、Parallel Old Garbage Collector 适用老年代,算法:标记整理,优点:没有内存空间碎片化 缺点:效率低

    • CMS Garbage Collector :

      • 适用老年代,算法 : 标记清理 优点:效率高(相比标记整理) 缺点:内存空间出现碎片化

    • G1 Garbage Collector

  • Serial Garbage Collector

    • 串行垃圾回收器控制所有的应用线程。它是为单线程场景设计的,只使用一个线程来执行垃圾回收工作。它暂停所有应用线程来执行垃圾回收工作的方式不适用于服务器的应用环境。它最适用的是简单的命令行程序

    • 使用-XX:+ UseSerialGC JVM参数来开启使用串行垃圾回收器

     

优点:简单高效,拥有很高的单线程收集效率
缺点:收集过程需要暂停所有线程
算法:复制算法
适用范围:新生代
应用:Client模式下的默认新生代收集器

  • Parallel Garbage Collector

    • 并行垃圾回收器也称作基于吞吐量的回收器。它是JVM的默认垃圾回收器。与Serial不同的是,它使用多个线程来执行垃圾回收工作。和Serial回收器一样,它在执行垃圾回收工作是也需要暂停所有应用线程

  • CMS Garbage Collector

    • 并发标记清除(Concurrent Mark Sweep,CMS)垃圾回收器,使用多个线程来扫描堆内存并标记可被清除的对象,然后清除标记的对象。CMS垃圾回收器只在下面这两种情形下暂停工作线程:

      • 在老年代中标记引用对象的时候

      • 在做垃圾回收的过程中堆内存中有变化发生

    • 对比与并行垃圾回收器,CMS回收器使用更多的CPU来保证更高的吞吐量。如果我们可以有更多的CPU用来提升性能,那么CMS垃圾回收器是比并行回收器更好的选择

    • 使用-XX:+ UseParNewGC JVM参数来开启使用CMS垃圾回收器。

    • CMS GC 收集器是一种以获取 最短回收停顿时间 为目标的收集器

       

  • G1 Garbage Collector

    • G1垃圾回收器应用于大的堆内存空间。它将堆内存空间划分为不同的区域,对各个区域并行地做回收工作。G1在回收内存空间后还立即对堆空闲空间做整合工作以减少碎片。CMS却是在全部停止(stop the world,STW)时执行内存整合工作。对于不同的区域G1根据垃圾的数量决定优先级

    • 使用-XX:UseG1GC JVM参数来开启使用G1垃圾回收器

     

使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个
大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再
是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

    • G1 GC是一个可以调整的停顿时间的GC

    • 在使用G1垃圾回收器时,开启使用-XX:+ UseStringDeduplacaton JVM参数。它会通过把重复的String值移动到同一个char[]数组来优化堆内存占用。这是Java 8 u 20引入的选项

3.10 JVM参数

  • 标准参数

    -version
    -help
  • -X参数

    获取方法: java -X

    非标准参数,也就是在JDK各个版本中可能会变动

    -Xms10m 初始化内存大小
  • -XX参数

    非标准化参数,相对不稳定,主要用于JVM调优和Debug

下面是些主要的与Java垃圾回收相关的JVM选项

  • 使用 jinfo 命令 查看或设置某个参数的值,

  • 使用 jps 命令 查看java进程,

     

下面是一些JVM运行内存的优化选项

JVM运行参数示例:

JRE、JDK、JVM 及 JIT 

  • JRE 代表 Java 运行时(Java run-time),是运行 Java 引用所必须的。
  • JDK 代表 Java 开发 工具(Java development kit),是 Java 程序的开发工具,如 Java编译器,它也包含 JRE。
  • JVM 代表 Java 虚拟机(Java virtual machine),它的责任是运行 Java 应用。
  • JIT 代表即时 编译(Just In Time compilation),当代码执行的次数超过一定的阈值时,会将 Java 字节 码转换为本地代码,如,主要的热点代码会被准换为本地代码,这样有利大幅度提高 Java 应用的性能。

4.Java中的垃圾判定与回收托管特征

4.1、GC主要处理的是对象的回收操作,那么什么时候会触发一个对象的回收的呢:

  • 对象没有引用

  • 作用域发生未捕获异常

  • 程序在作用域正常执行完毕

  • 程序执行了System.exit()

  • 程序发生意外终止(被杀进程等)

4.2、在JDK1.2之前,使用的是引用计数器算法,即当类被加载到内存以后,就会产生方法区,堆栈、程序计数器等一系列信息,当创建对象的时候,为这个对象在堆栈空间中分配对象,同时会产生一个引用计数器,同时引用计数器+1,当有新的引用的时候,引用计数器继续+1,而当其中一个引用销毁的时候,引用计数器-1,当引用计数器被减为零的时候,标志着这个对象已经没有引用了,可以回收了

4.3、引用计数算法在JDK1.2之前的版本被广泛使用,但是随着业务的发展,很快出现了一个问题:

  • 当我们的代码出现下面的情形时,该算法将无法适应

         

  • 这样的代码会产生如下引用情形 objA指向objB,而objB又指向objA,这样当其他所有的引用都消失了之后,objA和objB还有一个相互的引用,也就是说两个对象的引用计数器各为1,而实际上这两个对象都已经没有额外的引用,已经是垃圾了

GC ROOT

4.4、程序把所有的引用关系看作一张有向图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点

  • 目前java中可作为GC Root的对象

    • 虚拟机栈中引用的对象(本地变量表)

    • 方法区中静态属性引用的对象

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

    • 本地方法栈中引用的对象(Native对象)

4.5、根搜索有向图: 

     

5.Java的内存泄露

5.1、内存泄漏的定义:对象已经没有被应用程序使用,但是垃圾回收器没办法移除它们,因为还在被引用着

5.2、要想理解这个定义,我们需要先了解一下对象在内存中的状态。

  • 从刚才的图里面可以看出,里面有被引用对象和未被引用对象。未被引用对象会被垃圾回收器回收,而被引用的对象却不会。

  • 未被引用的对象当然是不再被使用的对象,因为没有对象再引用它。然而无用对象却不全是未被引用对象。其中还有被引用的。就是这种情况导致了内存泄漏

为什么发生内存泄露

5.3、

  • A对象引用B对象,A对象的生命周期(t1-t4)比B对象的生命周期(t2-t3)长的多。当B对象没有被应用程序使用之后,A对象仍然在引用着B对象。这样,垃圾回收器就没办法将B对象从内存中移除,从而导致内存问题,因为如果A引用更多这样的对象,那将有更多的未被引用对象存在,并消耗内存空间

  • B对象也可能会持有许多其他的对象,那这些对象同样也不会被垃圾回收器回收。所有这些没在使用的对象将持续的消耗之前分配的内存空间

5.4、下面是几条容易上手的建议,来帮助开发人员防止内存泄漏的发生

  • 不再使用的对象将指向其的引用置空指向null

  • 特别注意一些像HashMap、ArrayList的集合对象,它们经常会引发内存泄漏。当它们被声明为static时,它们的生命周期就会和应用程序一样长

  • 同样是集合,当原有对象的属性发生改变(hashCode变化),remove()方法可能会失效,导致内存泄露(后续详细讲解)

  • 特别注意系统中各种事件监听和回调。当一个监听器在使用的时候被注册,但不再使用之后却未被反注册

  • 如果一个类自己管理内存,那开发人员就得小心内存泄漏问题了。” 通常一些成员变量引用其他对象,初始化的时候需要置空

5.5、String类常用的截取字串的方法substring()在JDK1.6中如果滥用就会导致比较严重的内存泄露

  • 如果研究过Java的源代码,就会发现,Java中的字符串由char[]作为数据结构支持,String类中包含了3个成员:char[] value,int offset,int count,它们分别用于存放实际的字符序列,本字符串对象的第一个字符在字符数组中的位置以及字符串对象包含多少个字符

  • 在JDK6中,substring()方法创建了一个新的String对象,但是新的String对象中的value成员指向了和源字符串相同的字符数组,只不过offset和count的取值发生了改变

  • 这种情况下,如果你有一个非常长的字符串,但是只使用了substring()方法截取了很短的一部分字串来使用,根据JDK6的实现方式,虽然只是用了很短的字串,但是仍然保留了整个长字符串的所有字符,会引发大量的内存浪费并影响性能

  • 因此,在JDK6中,如果需要使用substring()方法,建议使用如下的方式,它将明确构建一个新的字符串(包括用于支持内容的字符数组):

  • 在JDK7中,substring()方法已经做出了改进,会在对内存中为新的字符串对象创建一个新的字符数组:

6.finalize方法

6.1、Java和C++不同,没有提供析构方法

  • Object中包含了一个叫做finalize()的方法,提供在对象被回收时调用以释放资源,默认情况下其不执行任何动作

  • 由于Object是Java继承体系的根,因此事实上所有的Java类都具备finalize方法

  • 当垃圾回收器确定了一个对象没有任何引用时,其会调用finalize()方法。但是,finalize 方法并不保证调用时机因此也 不建议重写finalize() 方法

  • 如果必须要重写finalize()方法,请记住使用super.finalize () 调用父类的清除方法,否则对象清理的过程可能不完整

6.2、每个对象只能被GC自动调用finalize( )方法一次。如果在finalize( )方法执行时产生异常(exception),则该对象仍可以被垃圾收集器收集

  • Java语言允许程序员为任何方法添加finalize( )方法,该方法会在垃圾收集器交换回收对象之前被调用。

  • 不要过分依赖该方法对系统资源进行回收和再利用,因为该方法调用后的执行结果是不可预知的

  • 当finalize( )方法尚未被调用时,System. runFinalization**( )**方法可以用来调用finalize( )方法,并实现相同的效果,对无用对象进行垃圾收集

6.3、还有一个理由让我们需要更加谨慎对待finalize方法,那就是它其实有可能会阻断垃圾回收器对本对象的回收,我们称为对象复活,造成逻辑混乱和内存泄露

  • 垃圾收集器跟踪每一个对象,收集那些不可到达的对象(即该对象没有被程序的任何“活的部分”所调用),回收其占有的内存空间。

  • 但在进行垃圾回收的时候,垃圾回收器会调用finalize( )方法,通过让其他对象知道它的存在,而使不可到达的对象再次“复活"为可到达的对象

  • 既然每个对象只能调用一次finalize( )方法,所以每个对象也只可能“复活"一次

public class ReviveByFinalize {
    //定义静态变量,初始值为null
    public static ReviveByFinalize caseForRevive;
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("我已经顺利满血复活");
        caseForRevive = this;
        System.out.println("关闭资源");
    }
}

public class ReviveTest {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(ReviveByFinalize.caseForRevive);
        //给静态变量赋值
        ReviveByFinalize.caseForRevive = new ReviveByFinalize();
        System.out.println(ReviveByFinalize.caseForRevive);
        //取消对象引用,使其成为垃圾
        ReviveByFinalize.caseForRevive = null;
        //告诉垃圾回收器来回收垃圾对象 触发对象 finalize方法
        System.gc();
        Thread.sleep(1000);
        System.out.println(ReviveByFinalize.caseForRevive);
    }
}
/*ReviveByFinalize不写finalize方法时:
null
com.tjetc.ReviveByFinalize@1b6d3586
null
*/
/*触发对象 finalize方法:
null
com.tjetc.ReviveByFinalize@1b6d3586
我已经顺利满血复活
关闭资源
com.tjetc.ReviveByFinalize@1b6d3586
*/

7.强引用、软引用、弱引用及虚引用

7.1、在JDK1.2以前的版本中,当一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及状态,程序才能使用它

7.2、从JDK1.2版本开始,为了解决上述问题,把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期。

  • 这四种级别由高到低依次为:强引用软引用弱引用虚引用

       

7.3、下图描述了不同引用类型的垃圾回收特性

   

引用类型 垃圾回收时机 用途 生存时间
强引用 从来不会 对象的一般状态 JVM停止运行是终止
软引用 Java虚拟机内存不足时 对象缓存 内存不足时终止
弱引用 垃圾回收机制发现时 对象缓存 GC运行后终止
虚引用 unknow 监控对象回收 unknow

7.4、强引用

  • 以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用

  • 如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。

  • 当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题

    String str = new String("abc");
    //强引用
    String strong = str;

7.5、软引用

  • 如果一个对象只具有软引用,那就类似于可有可物的生活用品。

  • 如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。

  • 只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存

    String str = new String("abc"); 
    //softReference对str的软引用
    SoftReference<String> softReference = new SoftReference<>(str);

7.6、弱引用

  • 如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象

    String str = new String("abc"); 
    //weakReference对str的弱引用
    WeakReference<String> weakReference = new WeakReference<>(str);

7.7、虚引用

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

  • 虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是 否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动

  • 由于虚引用的特点,绑定应用队列后是finalize()方法的理想替代品,一旦虚引用被加入引用队列,就没有任何办法获取虚引用指向的对象,因此不存在对象复活的隐患

    String str = new String("abc");
    //phantomReference对str的虚引用和与之关联引用队列
    ReferenceQueue<String> queue = new ReferenceQueue<>();
    PhantomReference<String> phantomReference = new PhantomReference<>(str, queue);

8.引用队列

8.1、正如之前说的,软引用、弱引用、虚引用均可以和一个引用队列绑定使用

  • ReferenceQueue是作为 JVM GC**与上层Reference对象管理之间的一个消息传递方式**,它使得我们可以对所监听的对象引用可达发生变化时做一些处理

  • 我们希望当一个对象被gc掉的时候通知用户线程,进行额外的处理时,就需要使用引用队列了。ReferenceQueue即这样的一个对象,当一个obj被gc掉之后,其相应的引用对象(软引用、弱引用、虚引用),即ref对象会被放入queue中。我们可以从queue中获取到相应的对象信息,同时进行额外的处理。比如反向操作,数据清理

8.2、实现了一个队列的入队(enqueue)和出队(poll还有remove)操作,内部元素就是泛型的Reference,并且Queue的实现,是由Reference自身的链表结构所实现的

  • 引用队列的入队操作是由垃圾回收器完成的,当其发现回收的对象具备软引用、弱引用或虚引用时,会自动将对象的引用对象入队

  • 我们只需要在必要时执行出队操作即可监控到有哪些对象被回收并执行相关的资源操作

public class DataClass {
}

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
//自定义虚引用
public class MyPhantomReference extends PhantomReference<DataClass> {
    String name;
    /**
     * @param referent 新的虚引用将引用的对象
     * @param q        要在其中注册引用的队列
     */
    public MyPhantomReference(DataClass referent, ReferenceQueue<? super DataClass> q, String name) {
        super(referent, q);
        this.name = name;
    }
}

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
public class TestPhantomReference {
    public static void main(String[] args) throws InterruptedException {
        //创建DataClass对象
        DataClass dataClass = new DataClass();
        //创建引用队列
        ReferenceQueue<DataClass> queue = new ReferenceQueue<>();
        //创建虚引用对象
        MyPhantomReference myPhantomReference = new MyPhantomReference(dataClass,queue,"test");
        //变量指向空
        dataClass = null;
        //通知垃圾回收器
        System.gc();
        //休眠
        Thread.sleep(200);
        //循环从引用队列获取数据,打印
        Reference<? extends DataClass> ref = null;
        while((ref = queue.poll()) != null){
            MyPhantomReference myRef = (MyPhantomReference)ref;
            System.out.println("自定义虚引用的name:"+myRef.name);
        }
    }
}
/*自定义虚引用的name:test*/
posted @ 2022-04-26 09:43  carat9588  阅读(124)  评论(0编辑  收藏  举报