Java底层预备知识GC

Posted 赵广陆

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java底层预备知识GC相关的知识,希望对你有一定的参考价值。

一、标记算法

对象被判定为垃圾的标准:没有被任何对象引用的情况下,对于系统而言就是垃圾,占据的内存就要被释放,此对象也会被销毁。

判定对象不被引用的方法:1)引用计数算法;2)可达性分析算法

  • 引用计数算法

通过判断对象的引用数量来决定对象是否可以被回收。

堆中的每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1。当一个对象被创建的时候,若该对象实例分配一个引用变量,该对象实例的引用计数就设置为1,若该对象又被另外一个对象所引用,则该对象的引用计数器继续+1,而当该对象实例的某个引用超过了生命周期,或者被设置为一个新值的时候,该对象实例的引用计算便会-1。任何引用计数为0的对象实例可以被当作垃圾收集。

优点是执行效率高,程序执行受影响较小。只需过滤出引用计数器为0的对象,将其内存回收即可,可以交织在程序运行中。垃圾回收时可以做到几乎不打断程序的执行,对程序需要不被长时间打断的实时环境比较有利;缺点是实现过于简单,无法检测出循环引用的情况,导致内存泄漏。如父对象引用子对象,子对象引用父对象。

主流的Java垃圾收集器没有采用上述机制判断对象是否为垃圾,而用可达性分析算法对垃圾对象进行标记。

  • 可达性分析算法

通过判断对象的引用链是否可达来决定对象是否可以被回收。

可达性算法是从离散数学的图论引入的,程序把所有的引入关系看作一张图,通过一系列名为GC root作为起始点,从这些节点开始向下搜索,搜索所走过的路径就被称为引用链,当一个对象从GC root没有任何引用链相连,从图论上来说就是从GC root到这个对象是不可达的,这个时候就证明了这个对象是不可用的,它就被标记为垃圾了。

在这里插入图片描述

可以作为GC Root的对象

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

2)方法区中的常量引用的对象。比如,类里面定义的常量,该常量保存的某个对象的地址,那么被保存的对象也称为GC的根对象,当别的对象引用到它的时候就会形成关系链

3)方法区中的类静态属性引用的对象

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

5)活跃线程的引用对象

二、回收算法

  • 标记-清除算法(Mark and Sweep)

1)标记:从根集合进行扫描,对存活的对象进行标记,使用的是可达性算法来找到垃圾对象。

2)清除:标记完成后,对堆内存从头到尾进行线性遍历,如果发现对象没有被标识为可达对象,就将此对象占用的内存回收,并且将之前标记为可达的标识清除掉以便进行下一次垃圾回收,回收不可达对象内存。

在这里插入图片描述

标记-清除算法缺点:产生大量碎片化。由于标记清除不需要对象的移动,并且仅对不存活的对象进行处理,因此标记清除之后,会产生大量不连续的内存碎片,空间碎片太多,可能会导致以后在程序运行过程中,需要分配较大的对象时,无法找到足够的连续内存,而不得不提前触发另一次垃圾收集动作。

  • 复制算法(copying)

分为对象面和空闲面。复制算法将可用的内存按照容量和一定比例划分为两块或者多个块,并选择其中一块或者两块作为对象面,其他的作为空闲面。

对象则是在对象面上创建的。当被定义为对象面的块的内存使用完之后,就将还存活着的对象复制到其中一块空闲面上。

将对象面所有对象内存清除。将已使用过的内存空间一次清理掉。

复制算法优点:复制算法解决碎片化的问题。顺序分配内存,简单高效。适用于对象存活率低的场景,如年轻代。

在这里插入图片描述

  • 标记-整理算法(Compacting)

适用于老年代的对象回收。采用标记清除算法一样的对象标记,但在清除时有所不同。

1)标记:从根集合进行扫描,对存活的对象进行标记。

2)清除:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。

标记-整理算法是在标记-清除算法的基础上又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。

标记-整理算法优点:避免内存的不连续性。不用设置两块内存互换。适用于对象存活率极高的场景,如老年代的回收。

在这里插入图片描述

  • 分代收集算法(Generational Collector)

主流的垃圾回收算法。垃圾回收算法的组合拳。按照对象生命周期的不同划分区域以采用不同的垃圾回收算法(将堆内存进行进一步划分,不同的对象的生命周期以及存活情况是不一样的,将不同生命周期的对象分配到堆中不同的区域,并对堆内存不同区域采用不同的策略进行回收)。分代收集算法目的,提高JVM垃圾回收执行效率。

JDK6、JDK7的堆内存,分为年轻代(Young Generation)、老年代(Old Generation)、永久代(Permanent Generation)。

在这里插入图片描述

JDK8及其以后的版本,堆内存分为年轻代(Young Generation)、老年代(Old Generation)。年轻代的对象存活率低就会采用复制算法,老年代对象存活率高,就会采用标记-清除算法或者标记-整理算法。

在这里插入图片描述

  • 分代收集算法的GC分类

1)Minor GC:发生在年轻代中的垃圾收集动作,采用的复制算法。年轻代是几乎所有Java对象出生的地方,即Java对象申请的内存以及存放都是在年轻代的。Java中的大部分对象不需要长久的存活,具有朝生夕灭的性质。当一个对象被判断为死亡的时候,GC就有责任回收掉这部分对象的内存空间。新生代是GC收集垃圾的频繁区域。

2)Full GC:这种GC和老年代相关,由于对老年代的回收一般会伴随着年轻代的垃圾收集,因此,此种方式称为Full GC。

  • 分代收集算法年轻代

尽可能快速的收集掉那些生命周期短的对象。

1)Eden区:伊甸园,人类的起源。对象刚被创建出来时,其内存空间首先是被分配在Eden区的,如若Eden区放不下新创建的对象的时候,对象也有可能直接放到Survivor区,甚至是老年代中。

2)两个Survivor区:分别被定义为from区和to区,哪个是from区,哪个是to区也不是固定的,会随着垃圾回收的进行而相互转换,年轻代的目标就是尽可能快速收集掉那些生命周期较短的对象。一般情况下,所有新生成的对象首先都是放在年轻代的。

年轻代内存会按照8:1:1的默认比例划分为Eden区和两个Survivor区,绝大部分对象是在Eden区生成。新生代中98%的对象都是朝生夕死的,所以不需要按照1:1:1的比例来划分内存空间,而是将年轻代内存分为一块较大的Eden区和两块较小的Survivor区,每次使用Eden和其中的一块Survivor区。当进行垃圾回收的时候,将Eden和Survivor区存活的对象一次性复制到另一块Survivor区,最后清理掉Eden区和用过的Survivor区。当Survivor区空间不够用的时候,则需要依赖老年代,进行分配的担保。

eg:

暂且忽略Eden区和Survivor区的默认比例,并且假设每个对象的大小都是一样的。Eden区最多能保存4个对象,Survivor区最多能保存3个对象。

当程序开始运行时,若对象在Eden区出生并且被挤满会触发一次Minor GC,此时如果对象还存活,就会被复制到其中一块Survivor区中。假设复制到S0,S0就称为from区。

img

清理所使用过的Eden区,并且将存活对象的年龄+1,这是首次Minor GC的过程。

img

Eden区再次被填满,又会触发一次Minor GC,将Eden和S0中的存活对象拷贝到S1。同时对对象的年龄+1。S1从to区变成from区,S0从from区变成to区。

img

拷贝完后Eden和S0区域都被清空,便完成了第二次Minor GC。

img

Eden区又满了,触发第三次Minor GC。假设S1中有一个对象没有用了,需要被清除。存活的对象又会从Eden区和Survivor区拷贝到S0。同时各自存活对象的年龄又被+1。

img

拷贝完后S1和Eden再次被清空。

img

周而复始,对象在Survivor区每熬过一次Minor GC,年龄就+1。当对象的年龄达到某个值时,默认是15(可以通过-XX:MaxTenuringThreshold参数来调整岁数),这些对象会成为老年代。但这也不是一定的,对于一些较大的对象,即需要分配一块较大连续内存的对象,就会进入到老年代。

Minor GC采用复制算法,使得进行内存分配时,不需要考虑内存碎片等复杂情况,只需要移动堆顶指针按顺序分配即可。回收时一次性将某个区域清空。简单粗暴高效。

对象如何晋升到老年代

1)经历一定Minor次数依然存活的对象(长期存活的对象会进入老年代,对象在新生代经历一次Minor GC依然存活则年龄+1,年龄超过一定限制默认是15岁时就晋升到老年代)

2)Survivor区中存放不下的对象(如果是Eden区或者Survivor区放不下的对象会直接进入老年代,对象优先在Eden区分配,当Eden区没有足够的空间分配的时候,会触发一次Minor GC,每次Minor GC结束Eden区就会被清空,因为它会把Eden区还依然存活的对象放到Survivor区中,当Survivor区中放不下的时候,则有分派担保进入到老年代中,因为不能放不下了就将它干掉)

3)新生成的大对象直接进入到老年代当中(-XX:+PretenuerSizeThreshold可以通过这个参数控制大对象的大小,如果超过这个参数的对象,一经生成直接放入到老年代中。)

常用的调优参数

1)-XX:SurvivorRatio : Eden和一个Survivor的比值,默认是8比1。

2)-XX:NewRatio : 老年代和年轻代内存大小的比例。新生代和老年代的总内存大小由-Xms、-Xmx参数决定的。

3)-XX:MaxTenuringThreshold : 对象从年轻代晋升到老生代经过GC次数的最大阈值。

  • 分代收集算法老年代

老年代是存放生命周期较长的对象。在年轻代中经过了n次垃圾回收依然存活的对象就会被放到老年代中。

老年代的内存比新生代的内存大,大概比例是2比1。新生代使用的是复制算法,复制成本较低。老年代对象存活率较高,没有额外空间分配担保,使用的算法是标记-清除算法、标记-整理算法进行回收。

当触发老年代的垃圾回收的时候,通常也伴随着对新生代堆内存的回收,即对整个堆进行垃圾回收,这便是所谓的Full GC。Major GC通常是和Full GC等价的,即收集整个GC堆。Full GC比Major GC慢,一般慢十倍以上,但执行频率低。

当Eden区空间不足的时候,会触发Minor GC回收年轻代的内存空间。

触发Full GC的条件

1)老年代空间不足。创建大对象直接放入到老年代,如果老年代空间不足,就会触发Full GC。

2)永久代空间不足。针对JDK7以及之前的版本,当系统中需要加载的类,调用的方法很多,同时持久代中没有足够的空间去存放类信息,方法的信息,就会触发一次Full GC。

3)CMS GC的时候出现promotion failed,concurrent mode failure。对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure。当这两种情况发生的时候,可能会触发Full GC。promotion failed是在进行Minor GC的时候,Survivor空间是放不下了,对象只能放入到老年代,而此时老年代也放不下了,此时就会造成promotion failed;concurrent mode failure是在执行CMS GC的时候,同时有对象要放入老年代中,而此时老年代空间不足就会造成concurrent mode failure。

4)Minor GC晋升到老年代的平均大小大于老年代的剩余空间。

5)调用System.gc()。在程序中调用该方法,显示触发Full GC,对老年代和新生代进行回收,但是此方法提醒虚拟机需要在这里进行回收,但是回收不回收还是看虚拟机。

6)使用RMI来进行RPC或者管理的JDK应用,每小时执行1次Full GC。

  • 分代收集算法关键词

1)Stop-the-World:JVM由于要执行GC而停止了应用程序的执行。在任何一种GC算法中都会发生。当Stop-the-World发生时,除了GC所需的线程,所有线程都处于等待状态,直到GC任务完成。多数GC优化通过减少Stop-the-world发生的时间来提高程序性能,从而使系统具有高吞吐,低停顿的效果。

2)Safepoint:垃圾收集器里面的安全点。

分析过程中对象引用关系不会发生变化的点。在可达性分析中,要分析那个对象没有引用的时候,必须在一个快照的状态点进行,在这个点所有的线程都被冻结了,不可以出现分析过程中对象引用关系还在不停变化的情况,因此分析结果需要在某个节点具备确定性,该节点便叫做安全点。程序不是那个点就停顿下来的,而是到达安全点才会停顿下来。

产生Safepoint的地方是方法调用、循环跳转、异常跳转等等。一旦GC发生,所有的线程都跑到最新的安全点才会停顿下来,如果发现线程不在安全点,就恢复线程,等其跑到安全点再说。

安全点数量得适中,安全点的数量不能太少,太少就会让GC等待太长时间;也不能太多,因为太多会增加程序运行的负荷。

三、常见的垃圾收集器

  • JVM的运行模式

Server和Client

Client启动速度较快,采用的是轻量级的虚拟机。

Server启动速度较慢,启动进入稳定期,长期运行之后,Server模式程序运行比Client快,这是因为Server模式采用的是重量级的虚拟机,对程序采用了更多的优化。

java -version可以查看当前虚拟机使用的哪种运行模式。

  • 垃圾收集器之间的联系

垃圾收集器和JVM实现紧密相关的,虚拟机所处的区域,说明它是属于新生代的收集器还是老年代的收集器,如果两个收集器之间有连线,就说明它们可以搭配使用。

img

  • 年轻代常见的垃圾收集器

  • Serial收集器(-XX:+UseSerialGC,复制算法)

在程序启动的时候,通过设置UseSerialGC参数使得年轻代使用该垃圾收集器回收。Serial收集器是java最基本、历史最悠久的收集器,jdk1.3版本之前,年轻代收集器的唯一选择。

单线程收集,进行垃圾收集时,必须暂停所有工作线程。单线程的意义不仅仅是说明只会使用一个CPU或者一条收集线程去完成垃圾收集工作。更重要的是,在它进行垃圾收集的时候,必须暂停其它所有工作线程,直到它收集结束。

简单高效,Client模式下默认的年轻代收集器。用户桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆,年轻代停顿时间会在几十毫秒到最多一百毫秒之间。

  • ParNew收集器(-XX:+UseParNewGC,复制算法)

在程序启动的时候,通过设置UseParNewGC参数使得年轻代使用该垃圾收集器回收。

多线程收集,其余的行为,特点和Serial收集器一样。是Server模式下虚拟机年轻代首选的收集器。

单核执行效率不如Serial,因为存在线程交互开销,在多核下执行才有优势。默认开启的收集线程数和CPU数量相同,在CPU数量非常多的情况下,可以使用参数限制垃圾收集的线程数。

在Server模式下ParNew收集器是一个非常重要的收集器,因为除Serial收集器外,目前只有它能与CMS收集器配合工作。

  • Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法)

在程序启动的时候,通过设置UseParallelGC参数使得年轻代使用该垃圾收集器回收。

系统的吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。系统的吞吐量等于运行用户代码时间除以CPU总消耗时间的比值。

比起关注用户线程停顿时间,更关注系统的吞吐量。Parallel Scavenge收集器类似ParNew收集器,使用多线程进行垃圾回收。停顿时间短适合用于用户交互的程序,良好的相应速度,可以提升用户的体验;高吞吐量则可以高效率利用CPU时间,尽可能快的完成运算任务,主要适合在后台运算,而不需要太多交互任务的情况。

在多核下执行才有优势,Server模式下默认的年轻代收集器。如果对垃圾收集器运作原理不熟悉,在优化过程中遇到困难了,可以使用Parallel Scavenge收集器,配合自适应调节策略,即在启动参数中加入-XX:+UseAdaptiveSizePolicy这个参数会把内存管理调优任务交给虚拟机去完成。

  • 老年代常见的垃圾收集器

  • Serial Old(MSC)收集器(-XX:+UseSerialOldGC,标记-整理算法)

在程序启动的时候,通过设置UseSerialOldGC参数使得老年代使用该垃圾收集器回收,是Serial GC的老年代版本。

单线程收集,进行垃圾收集的适合,必须暂停所有工作线程。

简单高效,Client模式下默认的老年代收集器。

  • Parallel Old收集器(-XX:+UseParallelOldGC,标记-整理算法)

在程序启动的时候,通过设置UseParallelOldGC参数使得老年代使用该垃圾收集器回收。

多线程,吞吐量优先。在jdk1.6之后才开始提供的。

在注重吞吐量和CPU资源敏感的场合,都可以优先考虑Parallel Old收集器加Parallel Scavenge收集器。

  • CMS收集器(-XX:+UseConcMarkSweepGC,标记-清除算法)

在程序启动的时候,通过设置UseConcMarkSweepGC参数使得老年代使用该垃圾收集器回收。

CMS收集器占据了老年代垃圾收集器的半壁江山,划时代的意义就是几乎可以做到垃圾回收线程集合可以和用户线程做到同时工作,“几乎”是因为还不能做到完全不需要停止用户线程的,只是尽可能的缩短了停顿时间。如果应用程序对停顿比较敏感,并且在应用程序运行的时候,可以提供更大的内存和更多的CPU,也就是更厉害的硬件,使用CMS来收集会带来好处。如果在JVM中有相对较多存活时间较长的对象,会更适合使用CMS。

CMS垃圾收集器的整个过程可以分为六步

1)初始化标记:stop-the-world。在这个阶段需要虚拟机停顿正在执行的任务。这个过程从垃圾收集器的根对象开始,只扫描到能和根对象关联的对象并做标记,所以这个过程虽然暂停了整个JVM,但是很快就完成了

2)并发标记:并发追溯标记,程序不会停顿。紧随初始标记阶段,在初始标记的基础上继续向下追溯标记,并发标记阶段,并发标记的线程和用户执行的线程并发执行,所以程序不会停顿。

3)并发预清理:查找执行并并发标记阶段从年轻代晋升到老年代的对象。通过重新扫描,减少下个阶段重新标记的工作,因为下个阶段会stop-the-world。

4)重新标记:暂停虚拟机,扫描CMS堆中的剩余对象。这个过程从垃圾收集器的根对象开始向下追溯,并处理对象关联。需要stop-the-world。

5)并发清理:清除垃圾对象,程序不会停顿。

6)并发重置:重置CMS收集器的数据结构。等待下一次垃圾回收。

并发标记,也就是和用户线程同时工作,就是一边丢垃圾,一边打扫,这样就会带来如果垃圾的产生是在标记后发生的,那么这次垃圾就只能等待下次再回收了,当然等待垃圾标记了过后,垃圾自然不会和用户线程产生冲突,而清理过程就能和用户线程同时处理了。对于此类垃圾回收器,有一个比较显著不可避免的一个问题,就是它所采用的是标记-清除算法,也就是说它不会压缩存活的对象,这样就会带来内存空间碎片化的问题,如果出现需要分配一个连续的较大的内存空间,则只能触发一次GC。

  • G1收集器(-XX:+UseG1GC,复制 + 标记-整理算法)

在程序启动的时候,通过设置UseG1GC参数使得年轻代使用该垃圾收集器回收。

即用于年轻代,又用于老年代的收集器。G1收集器的使命是未来替换掉JDK1.5发布的CMS收集器。

Garbage First收集器的特点。
1)并行和并发:使用多个CPU来缩短stop-the-world的停顿时间,与用户线程并发执行。

2)分代收集,独立管理整个堆,能够采用不同的方式,去处理新创建的对象,和以及存在一段时间熬过多次GC旧对象,以获得更好的收集效果。

3)空间整合,基于标记-整理算法,这样就解决了内存碎片的问题。

4)可预测的停顿,可以建立可以预测的停顿时间模型,能让使用者明确指定在一个长度为m毫秒时间片段内,消耗在垃圾收集器上的时间不得超过m毫秒。

Garbage First收集器之前收集器收集的范围都是整个年轻代的,或者老年代的,Garbage First收集器Java堆的内存布局与其他收集器有很大的差别,将整个Java堆内存划分成多个大小相等的独立区域Region。

虽然保留了年轻代和老年代的概念,年轻代和老年代不再是物理隔离了。它们是一部分不再连续的Region的集合,这就意味着在分配空间的时候不需要连续的内存空间,即不需要在JVM启动的时候决定哪些Region是属于老年代,哪些属于年轻代。随着时间推移,年轻代Region被回收以后,就会变为可用状态,这个时候可以把它分配成老年代。Garbage First年轻代收集器是并行stop-the-world收集器,和其它的hotspot GC一样,当一个年轻代GC发生的时候,整个年轻代会被回收。G1的老年代收集器有所不同,它在老年代不需要整个老年代进行回收,只有一部分Region被调用。Garbage First GC的年轻代由Eden Region、Survivor Region组成。JVM分配Eden Region失败之后就会触发一个年轻代回收,这意味着Eden区间满了,GC开始释放空间,第一个年轻代收集器会移动所有的存储对象,从Eden Region到Survivor Region,这就是Copy-to-Survivor的过程。

CMS和G1的区别?

参考:JVM: G1和CMS的区别

参考:Java垃圾回收机制

四、常见面试题

  • Object的finalize()方法的作用是否与C++的析构函数作用相同?

与C++的析构函数不同,析构函数调用确定,而它是不确定的。

当垃圾回收器宣告一个对象死亡时,至少经过两次标记过程,如果对象经过可达性分析后发现没有和GC Roots相连接的引用链,就会被第一次标记,并且判断是否执行finalize()方法。如果对象覆盖finalize()方法且未被引用过,这个对象就会被放置在F-Queue队列中,并在稍后由一个虚拟机自动建立的低优先级的Finalizer线程区执行触发finalizer( )方法,但不承诺等待其运行结束,即方法执行随时可能会被终止。

finalize()方法的作用是为对象创建逃脱死亡的最后一次机会。但是不建议使用,运行代价高昂,不确定性大,且无法保证各个对象的调用顺序。

Java中的强引用、软引用、弱引用、虚引用有什么用?

1)强引用(Strong Reference),最普遍的引用,例如Object obj = new Object();这里new一个对象实例来,这里面的obj就是一个强引用。

如果一个对象具有强引用,当内存空间不足的时候,Java虚拟机宁可抛出OutOfMemoryError终止应用程序,也不会回收具有强引用的对象。

通过将对象设置为null来弱化引用,使其被回收。如果我们不使用这个对象了,需要通过将对象的引用设置为null方法来弱化引用,使其被回收,即将刚才的obj设置为null,或者等待它超过对象的生命周期范围,这个时候GC就认为该对象不存在引用了,就可以回收这个对象了。具体什么时候收集,取决于系统。

2)软引用(Soft Reference),表示一个对象处在有用但非必须的状态。

当内存空间充足的时候,GC就不会回收该对象;当内存空间不足的时候,GC会回收该引用的对象的内存。

软引用可以实现内存敏感的高速缓存。不用太担心OutOfMemoryError的问题,因为软引用的对象内存会在内存不足的时候进行回收,同时由于一般情况下内存空间是充足的,相关对象就一直存在便于复用。软引用也可以和引用队列配合使用。

String str = new String("abc");// 强引用,创建的对象实例赋值给强引用str



SoftReference<String> softReference = new SoftReference<String>(str);// 软引用,使用SoftReference类型,泛型类型是String的,然后将强引用str包装起来,此时softReference就是软引用了。

3)弱引用(Weak Reference),用来描述非必须的对象,类似软引用,强度比软引用更弱一些。

弱引用具有更短的生命。GC在扫描的过程中,一旦发现有被弱引用关联的对象,就会将它回收了。换言之,无论此时内存是否紧缺,GC都将回收被弱引用关联的对象。

被回收的概率也不大,因为GC线程优先级比较低。由于垃圾回收是一个优先级很低的线程,因此不一定会很快发现哪些子句有弱引用的对象。

适用于引用偶尔被使用且不影响垃圾收集的对象。用法和软引用一样,弱引用也可以和引用队列配合使用。

String str = new String("abc");// 强引用,创建的对象实例赋值给强引用str



WeakReference<String> weakReference = new WeakReference<String>(str);// 弱引用,使用WeakReference类型,泛型类型是String的,然后将强引用str包装起来,此时weakReference就是弱引用了。

4)虚引用(Phantom Reference),顾名思义就是形同虚设,与其他几种引用不同,虚引用不会决定对象的生命周期。

如果一个对象仅持有虚引用,那么它就和没有任何引用一样。任何时候都可能被垃圾收集器回收。

跟踪对象被垃圾收集器回收的活动,起哨兵作用。

虚引用和软引用和弱引用的一个区别,就是必须和引用队列ReferenceQueue联合使用。GC在回收一个对象的时候,如果发现该对象具有虚引用,那么在回收之前会首先将该对象的虚引用加入到与之关联的引用队列当中,程序可以通过判断引用队列是否已经加入虚引用来了解被引用的对象是否被GC回收,因此起到一个哨兵的作用。

String str = new String("abc");// 强引用,创建的对象实例赋值给强引用str



ReferenceQueue queue = new ReferenceQueue();// ReferenceQueue对象



PhantomReference ref = new PhantomReference(str,queue);// 虚引用

Java中的强引用,软引用,弱引用,虚引用的等级:强引用 > 软引用 > 弱引用 > 虚引用

引用类型被垃圾回收时间用途生存时间
强引用从来不会对象的一般状态JVM停止运行时终止
软引用在内存不足时对象缓存内存不足时终止
弱引用在垃圾回收时对象缓存GC运行后终止
虚引用Unknown标记、哨兵Unknown

类层次结构

img

ReferenceQueue引用队列

ReferenceQueue名义上是一个队列,但是无实际存储结构,存储逻辑依赖于内部节点之间的关系来表达。Queue类似于一个链表的结构,这里的节点其实就是Reference本身。链表的容器,其自己只存储当前的head节点,而后面的节点由每个Reference节点自己通过next来保存即可。

存储关联的且被GC的软引用,弱引用以及虚引用,这三个引用都可以保存到引用队列里面,如果在创建一个引用对象的时候,指定了ReferenceQueue,那么当引用对象指向的对象达到合适的状态的时候,GC会把引用对象本身添加到这个队列里面,方便我们处理它。

以上是关于Java底层预备知识GC的主要内容,如果未能解决你的问题,请参考以下文章

Java NIO预备知识:I/O底层原理与网络I/O模型

[NTUSTISC pwn LAB 7]Return to libc实验(puts泄露libc中gadget片段定位)

面向面试编程代码片段之GC

synchronized底层实现

Java GC(垃圾回收)机制知识总结

JVM结构GC工作机制详解