-
@Date: 2021/10/18 22:35
-
@Description:
*/
public class CommonPojo {
public CommonPojo instance;
public static void main(String[] args) {
//创建对象,此时如果对于引用计数法来说
//每个对象的引用计数都为1
CommonPojo one = new CommonPojo();
CommonPojo two = new CommonPojo();
//对象之间互相引用,此时每个对象的引用计数都会变为2
one.instance = two;
two.instance = one;
//结束引用,引用如果简单进行减一,下面GC可以进行吗?
one = null;
two = null;
//进行GC
System.gc();
}
}
上一串代码如果使用简单的引用计数法,两个对象都是不可能被GC回收的
可达性分析算法
Java一般都是通过可达性分析算法来判定对象是否存活的
可达性分析算法的基本思路是:
-
可达性分析算法拥有一系列的GC Roots,这些GC Roots根对象会作为起始节点集,有引用的对象都会与GC Roots进行直接连接或者间接连接,像一棵树一样
-
算法会从这些GC Roots开始,根据引用关系进行向下搜索对象,搜索过程中所走过的路径就会称为引用链
-
如果某个对象到GC Roots之间没有任何引用链相连,用图论的说法就是对象不可到达GC Roots时,证明此对象就是无引用的,可以进行回收
举个栗子
从上面这副图中,可以看到从obj1~obj6对于GC Roots都是可到达的,所以这些对象都会被认为是有引用对象,但对于obj7~obj9,可以看到,这三个对象虽然互相引用,但对于GC Roots是不可达的,所以这三个对象是无引用的,可以进行回收
在Java中,固定作为GC Roots的对象包括以下几种
-
在虚拟机栈中,栈帧中的本地变量表引用的对象,比如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等(比如上边代码的栗子中,one和two都可以作为GC Root,此时他们为null了,那么其里面的instance都会变成不可达对象)
-
在方法区中类静态属性引用的对象(方法区存放一些类信息),比如类的静态变量
-
在方法区中常量引用的对象,比如字符串常量池的引用
-
在本地方法栈中引用的对象,即Native方法需要用到的对象
-
Java虚拟机内部的引用,比如基本数据类型的class对象,一些常驻的异常对象,比如NullPointException、OOM等,还有类加载器
-
被同步锁持有的对象,即Synchroniced,即加上了monitor关键字的对象
-
反映Java虚拟机内部情况的JMXBean、JVMTI中注册的问题、本地代码缓存等
除了这些固定的GC Roots集合之外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象来临时当GC Roots
因为要考虑到Java堆里面也是会分为多个内存区域的,如果只针对某个内存区域进行垃圾收集时,那就得要考虑堆中的其他区域有没有引用你这个内存区域的对象,因为每个区域都不是孤立封闭的,某个区域李的对象完全有可能被堆中其他区域的对象所引用。所以考虑GC Roots时,不仅要针对当前区域,还要考虑其他区域才是完整正确的,如果只考虑当前区域的,当前区域没有GC Roots,导致对象被GC回收了,但其他区域是对该对象是有引用的,那就发生GC错误了
再谈引用
前面已经提到了引用链这个概念,可达性分析算法中根据对象是否有引用链可达GC Roots来判断该对象是否被回收
在JDK1.2之前,Java的引用都是很传统的定义,如果reference类型的数据存储的数值代表某块内存、某个对象的地址,就称该referenct为某个对象的引用,这样的定义仅仅只能给对象两种状态,被引用和不被引用,对于一些特殊对象就无法进行形容了,比如想要一个内存足够就保留(即保留其引用),内存不够就不进行保留的对象(引用删除),所以在JDK1.2之后,Java对于引用的概念进行了补充
在Java中有四种引用,强度从大到小依次如下
-
强引用:强引用起始就是传统的引用,也就是引用赋值,reference类型的数据存储的是某块内存、某个对象的地址,强引用是永远不会被GC的
-
软引用:软引用是用来描述一些还有用,但非必须的对象,一般也是不会对软引用进行GC回收的,只有当发生OOM时(发生内存溢出异常时),GC收集器才会考虑回收软引用,并且如果软引用回收完了,还是内存仍然溢出,才会抛出OOM异常,Java使用SoftReference来实现软引用
-
弱引用:弱引用也是用来形容一些非必须的对象,但是弱引用的强度比软引用还要更低一点,弱引用只能苟活到下一次GC回收,即下一次GC回收肯定会回收弱引用,对于软引用只要不是内存溢出或引用关系消失就不会回收,Java使用WeakReference类来实现弱引用
-
虚引用:虚引用被称为幽灵引用或者幻影引用,是最弱的一种引用,甚至不会影响对象的生存时间,也就是说虚引用的存在不会影响对象的被GC,也不能通过虚引用去获取对象,虚引用的唯一作用就是当GC回收被虚引用的对象时,可以收到一个系统通知,GC收集器可以针对这个系统通知来进行处理,Java使用PhantomReference来实现虚引用
生存还是死亡
前面提到过,回收对象首先要判断该对象是生存的还是死亡的,可达性分析算法仅仅只是判断该对象是否可达而已,但即使被判定为不可达对象,也并不代表了该对象已经死亡
在Java中,宣告一个对象是否死亡,是要经历两次标记过程的,如果对象在进行可达性分析时,发现没有与GC Roots相连接的引用链,此时将会第一次进行第一次标记,随后对第一次标记的对象进行筛选,筛选的条件是此对象是否有必要去执行finalize方法,假如对象没有重写finalize方法,或者finalize方法已经被虚拟机调用过了,那么虚拟机将这两种情况都会视为没有必要去执行
判断对象已经死亡的两次标记
-
第一次标记:Java虚拟机使用可达性分析算法判断出对象对于GC Roots不可达,此时对该对象进行第一次标记
-
第二次标记:从第一次标记中进行筛选,判断对象的finalize方法(该方法来自Object,gc回收对象都会调用对象的这个方法)是否被覆盖重写、或者是否已经被Java虚拟机执行过,如果进行覆盖重写了,并且还没被Java虚拟机执行过,此时就会进行第二次标记,否则的话,都会Java虚拟机都会视该对象为没有必要去执行
-
注意这里,第二次标记仅仅只是判断finalize方法是否有必要执行,假如没有覆盖重写,或者已经执行过finalize方法了,那就没必要进行finalize方法了,直接进入“即将进行清除”的集合,所以对象的finalize方法往往只会执行一次
当对象完成了两次标记之后,Java虚拟机就会判定这个对象有必要去执行finalize方法(其实前面的两个标记都是用来判断该对象是否有必要去执行finalize方法),那么这个对象将会被放置在一个为F-Queue的队列之中,并且会稍后由一条虚拟机自动建立的、低调度优先级的Finalizer线程去执行他们的finalize方法,但这里的执行,并不意味着一定会执行完成,也就是不会承诺会等待这个finalize方法执行完毕,这样做的原因是,如果某个对象的finalize方法执行缓慢,甚至发生了死循环,会严重地降低了F-Queue队列中其他对象进行GC,甚至还会导致整个负责内存回收的子系统的发生崩溃
虽然对象的finalize方法已经被判断为有必要去执行,但是并不一定会进行最后的GC,因为可以在finalize方法中进行停止,也就是说finalize方法是对象逃脱死亡命运的最后一次机会
GC收集器会对F-Queue中的所有对象进行第二次小规模的标记,只要对象在finalize方法重新与引用链上的任何一个GC Roots或者对象建立关系即可,比如将自己的this指针给了某个静态常量和变量,那么在第二次标记时,GC就会将其移除“即将回收”的集合;如果对象在finalize方法没有重新与GC Roots建立关联,那就真的要被回收了
但这里要注意一个点,如果在finalize进行自救,仅仅只能自救一次,因为前面提到过,finalize方法往往只会执行一次,假如第一次自救成功,在finalize方法将自己自救,那么第二次在finalize方法自救会失败的
举个栗子,运行下面的代码
public class GCFinalizeDo {
public static GCFinalizeDo gcFinalizeDo;
/**
-
重写finalize方法进行自救
-
@throws Throwable
*/
@Override
protected void finalize() throws Throwable {
super.finalize();
//与静态变量进行关联,自救
GCFinalizeDo.gcFinalizeDo = this;
System.out.println(“==尝试自救”);
}
public static void main(String[] args) throws InterruptedException {
GCFinalizeDo.gcFinalizeDo = new GCFinalizeDo();
GCFinalizeDo.gcFinalizeDo = null;
System.gc();
if(gcFinalizeDo != null){
System.out.println(“=自救成功”);
}else{
System.out.println(“=自救失败”);
}
}
}
结果如下
有点诡异,先输出自救失败,又进行尝试自救,这是因为Finalizer线程执行finalize方法的优先级比较低,前面提到过Finalizer是一个自动建立的、低调度优先级的线程
改动一下
public class GCFinalizeDo {
public static GCFinalizeDo gcFinalizeDo;
/**
-
重写finalize方法进行自救
-
@throws Throwable
*/
@Override
protected void finalize() throws Throwable {
super.finalize();
//与静态变量进行关联,自救
GCFinalizeDo.gcFinalizeDo = this;
System.out.println(“==尝试自救”);
}
public static void main(String[] args) throws InterruptedException {
GCFinalizeDo.gcFinalizeDo = new GCFinalizeDo();
GCFinalizeDo.gcFinalizeDo = null;
System.gc();
//先睡5S
Thread.sleep(500);
if(gcFinalizeDo != null){
System.out.println(“=自救成功”);
}else{
System.out.println(“=自救失败”);
}
}
}
整个过程如下
-
修改静态变量的赋值,让其指向一个对象地址
-
将静态变量指向的地址为null,此时原先指向的对象就需要发生GC
-
为了避免该对象发生GC,在finalize方法里面对该对象重新进行引用
-
最后自救成功
下面进行两次GC,看结果会怎样
public static void main(String[] args) throws InterruptedException {
GCFinalizeDo.gcFinalizeDo = new GCFinalizeDo();
GCFinalizeDo.gcFinalizeDo = null;
System.gc();
//先睡5S
Thread.sleep(500);
if(gcFinalizeDo != null){
System.out.println(“=自救成功”);
}else{
System.out.println(“=自救失败”);
}
//第二次GC测试
GCFinalizeDo.gcFinalizeDo = null;
System.gc();
//先睡5S
Thread.sleep(500);
if(gcFinalizeDo != null){
System.out.println(“=自救成功”);
}else{
System.out.println(“=自救失败”);
}
}
}
可以看到,尝试自救只输出了一次,这也证明了每个对象的finalize方法仅仅只会执行一次,也就是自救的机会只有一次
对于finalize方法,并不鼓励使用,因为finalize运行代价高昂,而且具有不确定性,无法保证各个对象的调用顺序,如果说再GC后要进行处理而调用这个方法,那还不如使用finally去完成,所以说这个方法真的除了自救之外没啥用途了,而且自救还会发生不确定性
回收方法区
方法区被称为HotSpot虚拟机中的元空间或者永久代(元空间就是元数据的空间,而元数据其实就是类对象)
方法区一般是没有垃圾收集行为的,但还是存在着一些收集器支持对方法区进行回收,这是因为方法区进行垃圾收集的性价比相对于Java堆来说通常也是比较低的,在Java堆的新生代中,对常规应用进行一次垃圾收集通常可以回收70%至99%的内存空间,而方法区由于存放的是元数据和常量,判定条件比较苛刻,所以其区域垃圾收集的回收成果往往会远低于此
方法区的垃圾回收主要关于两部分内容
-
废弃的常量
-
不再使用的c类、符号和字段
对于常量来说还比较容易判断,只要判断虚拟机中没有地方引用这个常量即可,但对于类的判断就比较复杂了
对于类的判断,需要判断三个方面
-
该类所有的实例是不是都已经被回收,也就是Java堆中不存在该类以及任何派生子类的实例
-
加载该类的类加载器是不是已经被回收
-
该类的class是不是已经没有地方进行引用,即没有地方通过反射来访问该类
只有满足上面三个条件,Java虚拟机才允许对该无用类进行回收,这里还只是允许而已,还要涉及到垃圾收集器是否支持回收无用类
经过前面的判断,我们已经可以决定出哪些对象可以进行回收了,下面就来看看如何进行垃圾收集
从判断对象消亡的角度出发、垃圾收集算法还可以划分为引用计数式垃圾收集和追踪式垃圾收集,这两类又通常被称为直接垃圾收集和间接垃圾收集,在Java中主要采用追踪式垃圾收集
分代收集理论
分代收集,顾名思义就是按照年龄、年代来进行收集
分代收集又建立在两个分代假说之上
-
弱分代假说:绝大部分对象都是朝生夕灭的,发生垃圾回收就被回收掉
-
强分代假说:熬过越多次垃圾收集过程的对象就越难消灭
根据这两个假说,JVM收集器对于Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄用经过的垃圾回收次数来表示)分配到不同的区域之中去存储。
分代分区域的优点就在于,可以将朝生夕灭的对象集中起来,因为这些对象都很难熬过垃圾回收,那么每次对这块区域进行垃圾回收时,只要考虑如何保留少量的存活即可,不需要去标记那些大量将要被回收的对象,这样就能以较低代价回收到大量的空间;相反,如果剩下那些难消灭的对象,那么也可以把这些难以消灭的对象集中起来,只要考虑标记少量被回收的对象,根据年龄来进行这样的区域划分,虚拟机便可以用较低的频率来回收这个区域,同时兼顾乐垃圾收集的时间开销和内存的空间有效利用
因为进行区域划分,让垃圾收集器有了工作范围这一性质,所以根据工作范围,就有了各种的收集器,如下几种
-
部分收集:PartialGc,目标不是整个Java堆,而是部分
-
MinorGc/YoungGc:新生代收集
-
MajorGc/OldGc:老年代收集,目前只有CMS收集器会单独收集老年代
-
MixedGc:混合收集,目标是整个新生代和部分老年代,目前只有GI收集器支持
-
整堆收集:FullGc,目标是整个Java堆和方法区
同时,针对不同的区域安排与里面存储对象的存亡特征,需要采用相匹配的垃圾收集算法(如何标记、如何清除,垃圾收集器采用的算法)
-
标记——复制算法
-
标记——清除算法
-
标记——整理算法
Java虚拟机一般将Java堆划分成新生代和老年代两个区域
-
新生代:对应的就是弱分代,刚来的,朝生夕灭,没熬过垃圾回收,每次垃圾回收都会出现大量的新生代对象死亡
-
老年代:对应的就是强分代,熬过的垃圾回收多,每次垃圾回收都只有少量的对象死亡
-
新生代每次存活后的对象都会晋升到老年代中存放
分代收集不仅仅只是划分区域来收集这么简单,因为对象不是孤立的,是存在引用关系的,甚至会出现跨代引用的,比如新生代引用了老年代
举个栗子
比如现在要进行一次仅限于新生代区域内的收集,也就是MinocGC,但新生代中的对象完全有可能会被老年代所引用,那么这里就要再加多一层判断,判断老年代是否引用了新生代,那么此时新生代是没有意识到老年代引用了它,新生代不仅要固定的GC Roots看是否有标记,还有额外去遍历老年代中所有对象从而确保可达性分析结果的正确性,也就是说还要去考虑老年代的情况
举个栗子
老年代引入了新生代,新生代进行回收时,无法通过老年代最终到达GC Roots,所以也会被回收,所以回收新生代的时候,要遍历老年代,看有没有老年代用到该新生代对象
此时,就需要为分代收集理论添加第三条原则
- 跨代引用假说:跨代引用相对于同代引用来说仅仅占极少数
这条假说之所以成立,是因为存在互相引用关系的两个对象是应该倾向于同时生存或者同时消亡的,比如一个新生代引用了老年代,老年代会称为GC Roots,那么此时新生代不能被GC清除,那么新生代在熬过了一轮GC之后就会变成老年代,此时就不存在跨代引用了
现在分代理论就变成了三条原则
- 弱分代:对象都是朝生夕灭的