6、垃圾回收机制
6.1、对象成为垃圾的判断依据
在堆空间和元空间中,GC这条守护线程会对这些空间开展垃圾回收⼯作,那么GC如何判断这些空间的对象是否是垃圾,有两种算法:
- 引用计数法:
对象被引用,则计数器+1,如果计数器是0,那么对象将被判定为是垃圾,于是被回收。但是这种算法没有办法解决循环依赖的对象。因此JVM目前的主流⼚商Hotspot没有使⽤这种算法。
- 可达性分析算法:GC Roots根
- gc roots根节点: 在对象的引用中,会有这么⼏种对象的变量:来⾃于线程栈中的局部变量表中的变量、静态变量、本地⽅法栈中的变量,这些变量都被称为gc roots根节点
- 判断依据:gc在扫描堆空间中的某个节点时,向上遍历,看看能不能遍历到gc roots根节点,如果不能,那么意味着这个对象是垃圾。
6.2、对象中的finalize方法
Object类中有⼀个finalize方法,也就是说任何⼀个对象都有finalize方法。这个方法是对象被回收之前的最后⼀根救命稻草
- GC在垃圾对象回收之前,先标记垃圾对象,被标记的对象的finalize⽅法将被调用
- 调用finalize方法如果对象被引用,那么第⼆次标记该对象,被标记的对象将移除出即将被回收的集合,继续存活
- 调⽤finalize方法如果对象没有被引用,那么将会被回收
- 注意,finalize方法只会被调用⼀次。
重写Object
的finalize()
方法即可
@Override
protected void finalize() throws Throwable {
if(id%10==0){
list.add(this);
System.out.println("对象"+id+"没有被回收");
}else{
System.out.println("对象"+id+"被回收");
}
}
6.3、对象的逃逸分析
在jdk1.7之前,对象的创建都是在堆空间中创建,但是会有个问题,方法中的未被外部访问的对象
public void test1() {
User user = new User();
user.setId(1);
user.setName("xiaoming");
}
public User test2() {
User user = new User();
user.setId(1);
user.setName("xiaoming");
return user;
}
这种类似于test1()
中的user
的对象没有被外部访问,且在堆空间上频繁创建,当方法结束,需要被gc,浪费了性能。
所以在1.7之后,就会进行⼀次逃逸分析(默认开启),于是这样的对象就直接在栈上创建,随着⽅法的出栈而被销毁,不需要进行gc。
在栈上分配内存的时候:会把聚合量替换成标量,来减少栈空间的开销,也为了防止步栈上没有⾜够连续的空间直接存放对象。
-
标量:java中的基本数据类型(不可再分)
-
聚合量:引用数据类型。
7、垃圾回收算法
7.1、标记清除算法、复制算法、标记整理算法
7.2、分代回收算法
-
堆空间被分成了新生代(1/3)和老年代(2/3),新生代中被分成了eden(8/10)、survivor1(1/10)、survivor2(1/10)
-
对象的创建在eden,如果放不下则触发minor gc
-
对象经过⼀次minorgc 后存活的对象会被放⼊到survivor区,并且年龄+1
-
survivor区执行的复制算法,当对象年龄到达15.进⼊到老年代。
-
如果老年代放满。就会触发Full GC
7.3、对象进入到老年代的条件
- 大对象直接进⼊到⽼年代:大对象可以通过参数设置大小,多大的对象被认为是大对象。
-XX:PretenureSizeThreshold
- 当对象的年龄到达15岁时将进⼊到⽼年代,这个年龄可以通过这个参数设置:
-XX:MaxTenuringThreshold
-
根据对象动态年龄判断,如果s区中的对象总和超过了s区中的50%,那么下⼀次做复制的时候,把年龄大于等于这次最大年龄的对象都⼀次性全部放⼊到老年代。
-
老年代空间分配担保机制 :在minor gc时,检查老年代剩余可用空间是否大于年轻代里现有的所有对象(包含垃圾)。如果大于等于,则做minor gc。如果小于,看下是否配置了担保参数的配置:
-XX: -HandlePromotionFailure
,如果配置了,那么判断老年代剩余的空间是否小于历史每次minor gc 后进入老年代的对象的平均大小。如果是,则直接full gc,减少⼀次minor gc。如果不是,执行minor gc。如果没有担保机制,直接full gc。
8、垃圾回收器
垃圾回收机制,我们已经知道什么样的对象会成为垃圾。对象回收经历了什么——垃圾回收算法。那么谁来负责回收垃圾呢?
接下来就来讨论垃圾回收器。
8.1、Serial收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)
单线程执行垃圾收集,收集过程中会有较长的STW(stop the world),在GC时工作线程不能工作。虽然STW较长,但简单、直接。
新生代采用复制算法,老年代采用标记-整理算法。
8.2、Parallel收集器(-XX:+UseParallelGC -XX:+UseParallelOldGC)
使用多线程进行GC,会充分利用cpu,但是依然会有stw,这是jdk8默认使用的新⽣代和⽼年代的垃圾收集器。充分利用CPU资源,吞吐量高。
新生代采用复制算法,老年代采⽤标记-整理算法。
8.3、ParNew收集器(-XX:+UseParNewGC)
工作原理和Parallel收集器⼀样,都是使用多线程进行GC,但是区别在于ParNew收集器可以和CMS收集器配合工作。主流的方案:
ParNew收集器负责收集新生代。CMS负责收集老年代。
8.4、CMS收集器(-XX:+UseConcMarkSweepGC)
目标:尽量减少stw的时间,提升用户的体验。真正做到gc线程和用户线程几乎同时工作。CMS采用标记-清除算法。
-
初始标记: 暂停所有的其他线程(STW),并记录gc roots直接能引用的对象。
-
并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要STW,可以与垃圾收集线程⼀起并发运行。这个过程中,用户线程和GC线程并发,可能会导致已经标记过的对象状态发生改变。
-
重新标记:为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那⼀部分对象的标记记录,这个阶段的停顿时间⼀般会⽐初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的算法做重新标记。
-
并发清理:开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理。
-
并发重置:重置本次GC过程中的标记数据。
8.5、三色标记算法
在并发标记阶段,对象的状态可能发⽣改变,GC在进行可达性分析算法分析对象时,用三色来标识对象的状态
-
黑色:这个对象及其所有引用都已被GC Roots遍历,黑色的对象不会被回收
-
灰色:这个对象被GC Roots遍历过但其部分的引用没有被GC Roots遍历。在重新标记时重新遍历灰⾊对象。
-
白色:这个对象没有被GC Roots遍历过。在重新标记时该对象如果是⽩⾊的话,那么将会被回收。
8.6、垃圾收集器组合⽅案
不同的垃圾收集器可以组合使用,在使用时选择适合当前业务场景的组合。
年轻代 | 老年代 | 备注 |
---|---|---|
Serial | Serial Old | 简单、直接 |
Serial | CMS | |
ParNew | CMS | 推荐使用 |
ParNew | Serial Old | |
Parallel | Parallel Old | 吞吐量⾼、jdk8默认使用的组合 |
Parallel | Serial Old |
本文章参考B站 千锋教育JVM全套教程(含jvm调优、jvm虚拟机、jvm面试题、jvm源码详解)系统玩转java虚拟机全程干货无废话,仅供个人学习使用,部分内容为本人自己见解,与千锋教育无关。