转载声明:文章来源https://blog.csdn.net/binbinxyz/article/details/141787028
引用计数法
引用计数法(Reference Counting)的算法是:给每个对象添加一个引用计数器,有一个引用,计数器值加1;当引用失效,计数器值减1;任何时刻计数器值为0的对象就是不可能再被使用的。
这种方法实现简单,判定效率也很高,但有个问题:它很难解决对象之间相互循环引用的问题。
package org.hbin.gc;
/**
* VM args: -XX:+PrintGC -XX:+PrintGCDetails
* @author Haley
* @version 1.0
* 2024/9/1
*/
public class ReferenceCountingGC {
private Object instance;
private byte[] array = new byte[1024 * 1024 * 5];
public static void main(String[] args) {
ReferenceCountingGC a = new ReferenceCountingGC();
ReferenceCountingGC b = new ReferenceCountingGC();
a.instance = b;
b.instance = a;
a = null;
b = null;
System.gc();
}
}
运行上述代码,并打印GC日志:
[GC (System.gc()) 12918K->616K(125952K), 0.0010921 secs]
[Full GC (System.gc()) 616K->459K(125952K), 0.0097270 secs]
从运行结果可以看到,GC日志中包含12918K->616K,说明并没有因为这两个对象互相引用而不回收,也从侧面说明虚拟机并不是通过引用计数法来判断垃圾的。
根可达性分析算法
根可达性分析算法的基本思路:通过一系列称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索路径称为引用链(Reference Chain)。当一个对象到GC Roots没有任何引用链相连(图论:从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
在Java中,可作为GC Roots的对象包括:
虚拟机栈(栈帧中的局部变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即native方法)引用的对象
JVM中GC Roots的构成非常复杂,根据程序执行的语义、语言特性的支持及JVM内部优化实现,可以划分为Java根、JVM根和其他根。
Java根用于找到Java程序执行时产生的对象,包括:
类元数据对象:利用类加载器来跟踪Java程序运行时加载的类元数据对象
Java对象:通过线程栈帧跟踪Java程序的活跃对象
JVM根主要指JVM为了运行Java程序所产生的一些对象,这些对象可以简单地被认为是全局对象。主要有:
Universe:Java程序运行时需要一些全局对象,比如Java支持8种基本数据类型,这些基本类型的信息需要对象来描述(作为全局对象也是为了性能考虑),这些对象就存放在Universe中。
Monitor:全局监视器对象,对于Monitor对象主要是用于锁相关,可能存在只有Monitor对象引用到内存空间的对象,所以Monitor是JVM的根之一。
JNI:JVM执行本地代码时使用API产生的对象,例如通过JNI API在堆中创建对象,这些对象只在JNI API中使用,所以需要单独管理这些对象。
JVMTI:使用JVM提供的接口用于调试、分析Java程序。
System Dictionary:JVM在设计类加载时,对于基本的类,比如Java中经常使用的基础类,会通过系统加载器加载这些类,而这些类在运行Java程序一直都需要,所以这些类被单独加载,单独标记。
Management:JVM提供的内存管理API,用于JVM内存的统计信息,在使用这些API时需要创建Java对象,所以需要标记。
AOT:在JDK9引入了提前编译。在AOT的编译过程中会把全局对象和编译优化的代码对象放在可执行文件中,当执行时会用到这些对象,所以在回收时需要标记。
其他根主要有:
语言特性的弱引用
JVM弱根,例如String.intern()产生的对象等。
这些根共同构成了GC Roots集合,实际上根的确定和虚拟机运行时密切相关。对于弱根的处理在不同的GC实现中也会有所不同。
当前主流编程语言的垃圾收集器基本上都是依靠可达性分析算法来判定对象是否存活的,理论上该算法要求全过程都基于一个能保障一致性的快照中才能够进行分析,这意味着必须全程冻结用户线程的运行。要根节点枚举这个步骤中,由于GC Roots相比起整个Java堆中全部的对象毕竟还是极少数,且在各种优化技巧(如OopMap)的加持下,它带来的停顿已经是非常短暂且相对固定(不随堆容量而增长)。可从GC Roots再继续往下遍历,这一步骤的停顿时间就必定会与Java堆容量直接成正比例关系了:堆越大,存储的对象越多,对象图结构越复杂,要标记更多对象而产生的停顿时间自然就更长。
标记阶段是所有追踪式垃圾收集算法的共同特征,如果这个阶段会随着堆变大而等比例增加停顿时间,其影响就会涉及几乎所有的垃圾收集器。同理,如果能削减这部分停顿时间的话,那收益也将会是系统性的。
想解决或者降低用户线程的停顿,要先搞清楚为什么必须在一个能保障一致性的快照上才能进行对象图的遍历?这里引入三色标记(Tri-color Marking),把垃圾收集器遍历对象过程中遇到的对象,按照是否访问过标记成三种颜色:
白色:表示对象尚未被访问过。显然在刚开始,所有的对象都是白色的;分析结束时,仍然是白色的对象,即代表不可达。
黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接指向某个白色对象,而不经过灰色对象。
灰色:表示对象已经访问过,但这个对象上至少存在一个引用还没有被扫描过。
如果整个扫描过程中,用户线程是冻结的,只有收集器线程在工作,那不会有任何问题。但如果用户线程与收集器线程是并发工作呢?收集器线程标记过程中,用户线程随时可能修改任何引用关系,这可能出现两种后果:
原本消亡的对象错误标记为存活:这不是好事儿,但可以容忍,只是产生一些浮动垃圾,它们会逃过本次收集,下次收集再清理掉就好。
原本存活的对象错误标记为消亡:这是非常致命的,程序会因此发生错误。
整个扫描标记过程和异常情况如下:
Wilson于1994年在理论上证明:当且仅当以下两个条件同时满足时,会产生“对象消失”的问题,即原本应该是黑色的对象被误标为白色:
赋值时插入了一条或多条从黑色对象到白色对象的新引用
赋值时删除了全部从灰色对象到该白色对象的直接或间接引用
因此,要解决这个问题,只需要破坏这两个条件中的任意一个即可。由此,分别产生了两种解决方案:
增量更新:Incremental Update。增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。也简化理解:黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。
原始快照:Snapshot At The Beginning, STAB。原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。也可以简化理解:无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。
以上无论是对引用关系记录的插入还是删除,虚拟机的记录操作都是通过写屏障来实现的。实际上在并发标记的时候对应有两种不同的实现,分别是读屏障和写屏障。屏障技术是在读或写操作时执行一段代码,其目的是调整对象的颜色从而保证正确性。但是读操作远多于写操作,所以读屏障的效率一般低于写屏障的效率。
在HotSpot虚拟机中,两种解决方案都有实际应用。例如,CMS是基于增量更新来做并发标记的,G1、Shenandoah则是用原始快照来实现。
引用分类
在JDK1.2之前,引用的定义是:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。
JDK1.2之后,Java对引用的概念进行了扩充,将引用分为四种:强引用、软引用、弱引用、虚引用,这四种引用强度依次逐渐减弱。最后还有一个特殊的引用FinalReference。
强引用:Strong Reference,代码中普遍存在的,类似Object obj = new Object()这类的引用。只要引用还存在,垃圾收集器永远不会回收掉这些对象。
软引用:Soft Reference,用来描述一些还有用但非必需的对象。在系统将要发生内存溢出之前,将会把这些对象列进回收范围之中进行二次回收。如果这次回收还不骨足够的内存,才会抛出内存溢出异常。相关Java类:SoftReference
弱引用:Weak Reference,也用来描述非必需对象,但它的强度比软引用更弱一些。当垃圾回收器工作时,无论当前内存是否足够,都会回收掉弱引用关联的对象。相关Java类:WeakReference。
虚引用:也称幽灵引用或幻影引用,它是最弱的一种引用关系。虚引用不影响对象的生成时间,也无法通过虚引用来取得一个对象实例。设置虚引用的唯一目的是能在这个对象被垃圾回收器回收时收到一个系统通知。相关Java类:PhantomReference。
两个常用子类:
sum.misc.Cleaner:用于DirectByteBuffer对象回收的时候对于堆外内存的回收。
java.lang.ref.Cleaner:用于在被引用的对象回收的时候触发一个动作,在OpenJDK9中将完全替代Object.finalize()方法。
最终引用:FinalReference,包权限,开发者无法直接继承扩展。它只有一个子类Finalizer,并且由final修饰,无法继承扩展。由于构造方法是私有的,所以只能由HotSpot VM通过调用register()方法将被引用的对象封装为Finalizer对象。
在类加载过程中,如果当前类重写了finalize(),则其对象会被封装为FinalReference对象,这样FinalReference对象的referent字段就指向了当前类的对象。
Finalizer对象链会保存全部的只存在FinalizerReference引用且没有执行过finalize()方法的Finalizer对象,防止Finalizer对象在其引用的对象之前被GC回收。在GC过程中,如果发现referent对象不可达,则Finalizer对象会添加到queue列表中,所有在queue队列中的对象都会调用finalize()方法。
帖子还没人回复快来抢沙发