前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >GC垃圾收集器之美

GC垃圾收集器之美

原创
作者头像
Joseph_青椒
修改2023-08-06 11:43:02
4220
修改2023-08-06 11:43:02
举报
文章被收录于专栏:java_joseph

引言:

Java最早做了垃圾回收机制,也就是我们说的GC,jvm通过垃圾回收机器,也随着jdk版本的迭代,不断的再进步。

jvm中,最重要的就是垃圾回收器这里了,这篇文章将专门对垃圾回收做出讲解,包括基础认识,垃圾回收算法,垃圾回收器的更迭和将来趋势以及选择。

基础认识

JVM自动回收机制

为了我们开发不被内存泄漏和内存溢出困扰,jvm做了自动回收机制,指的是不用的垃圾对象被标记,然后被回收,释放占用的内存空间,但是万事皆有利弊,垃圾回收器不一定完全解决内存泄漏问题,编码时还要注意规范。

而且垃圾回收还会占用系统资源,影响程序的性能,回收的时候还会触发STW(stop the world),导致程序卡顿。

如何回收垃圾?

这个问题问的就是垃圾回收的方式,也就是垃圾回收算法,

有:标记清除,标记复制,标记整理。

垃圾回收器和垃圾回收算法的关系?

垃圾回收算法是垃圾回收期的方法论,而垃圾回收器是垃圾回收算法的具体落地实现。

如何判断哪些对象存活?回收哪些对象?

方法1:引用计数法

方法2:可达性分析算法

引用计数法

就是一个对象被其他对象持有,一个变量就+1,两个引用就+2,没有引用,为0的话,就是死对象了,可以被回收了。

引用对象有个问题,

就是循环引用。

循环引用

代码语言:javascript
复制
public class Main {
    public static void main(String[] args) {
        A a = new A();
        B b = new B();
        a.setB(b);
        b.setA(a);
        a = null;
        b = null;
        System.gc();
    }
}
​
class A {
    private B b;
​
    public void setB(B b) {
        this.b = b;
    }
}
​
class B {
    private A a;
​
    public void setA(A a) {
        this.a = a;
    }
}

看这个demo,虽然虚拟机栈中,a,b都指向了null,但是对于堆中的对象A和对象B,他们是互相持有,形成一个闭环,这就出现了循环引用问题。

如何解决,就是另一种判断垃圾的方法

可达性分析算法

这个方法,大家可以理解为一个树,但是又不是树,和树一样有根节点,但是长出来的是一个图,

image-20230804142007649
image-20230804142007649

从GC ROOT能推到的对象就是可用的,这样即使对象之间互相引用,也不会出现循环引用导致不能被回收的问题

GCROOT指的就是,比如,User user = new User();

user就是GCROOT

JVM中GC Roots包括一下几种,

无非就是对象的引用的地方,

虚拟机栈:指的是栈帧中,本地变量表中引用的对象

方法区:静态变量引用的对象,jdk1.7以后,静态变量引用的对象从方法区移动到了堆中

:常量引用的对象,字符串常量池也从jdk1.7以后,由方法区移动到了堆中。

本地方法栈:JNI,JNI就是Native方法,引用的对象,

总结:就是指向堆中的变量和指针。

垃圾回收算法

标记回收算法,有三种,

最基础的是标记清除算法。但是因为内存碎片问题,于是出现,标记复制算法,然而标记复制算法由于需要额外空间,就诞生了标记整理算法,也叫标记压缩算法。

算法并无好坏,要根据不同的算法来。

标记清除算法

这个算法,就是把死忘的对象标记出来,然后清除掉,那么清除完之后,就会出现碎片问题,因为存活的对象和死亡的对象在内存中是掺杂一起的,把死亡对象清走,留下的就是零零碎碎的位置。

image-20230804160007141
image-20230804160007141

标记复制算法

这个算法,就是为了避免内存碎片问题,标记存活的对象,移动到另一片空闲的位置,空闲区域,之后,以新移动的空间作为现在的活动区,之前活动区域,直接全清理掉。作为空闲区,方便下一次GC。

image-20230804160236558
image-20230804160236558

标记整理算法

这种算法,不会像标记清除算法那样,有内存碎片问题,且不会像标记复制算法那样,需要额外的空间。但是移动的操作,算法的效率就比标记辅助慢些,所以没有万金油,要根据合适的场景。

image-20230804160021684
image-20230804160021684

场景

这里介绍一下,堆空间,如何选择垃圾回收算法的

image-20230804153501325
image-20230804153501325

新生代,98%的对象都撑不过一次GC,所以新生代的死亡时很快的,存活的对象很少,这是我们想到标记复制算法!,复制存活的,就很符合这个算法,所以新生代采用的是标记复制算法,所以有两个分区,其中有一个分区,是空闲的,方便下次gc,复制存货对象到里面,就这一,存活的对象,在一次一次的GC后,到了15岁,就会进入Old区,

Old区,老年代,他没有进行分区,采用的是标记整理方法,Old区存活的对象很多,都是老油条,自然不适合用标记复制的方法,又不想用标记清除产生大量的内存碎片,就采用的标记整理算法。

垃圾回收器扫盲

分代收集算法思想

上面我们看堆空间采用垃圾回收期,采用新生代和老年代,采用的就是分代回收算法的思想,年轻代存活时间短,死的快,就采用高频回收,老年代都是老油条,老不死的,就进行低频回收。

分代算法根据对象特点,年轻代适合标记复制算法,刚才也讲到了,只复制少量存活的,老年代则适合标记清除,标记压

缩算法,因为存活的是多数的,清除少量死亡的。

通过新生代和老年代的划分,使得MinorGC的频率更高,早早的把死的快的对象回收完,减少Old区内存不足,发生FullGC的频率。

GC分类与术语

很多人对GC的概念,Minor GC ,YongGC,FullGC是什么不知道,为了下面垃圾回收器的讲解,这里科普一下

部分收集(Partial GC)

新生代收集

Young GC Minor GC

Eden、s1、s2的清理,都是发生在新生代

老年代收集

Major GC ,Old GC

整堆收集

Full GC,清理整个堆空间,包括年轻代和老年代,理解

触发FullGC 的场景

  • 手工调用System.gc( ), 建议执行Full GC,不一定会执行,可通过-XX:+ DisableExplicitGC 参数来禁止调用System.gc()
  • 老年代空间不足 , 通过Minor GC后进入老年代的平均大小大于老年代的可用内存

关于Old GC 与Full GC 的区分

这个问题在国内很混淆,我是这样理解的,当满足MinorGC 进入平均大小小于老年代的可用内存之后,会触发OldGC ,可以这么理解,OldGC大部分是由youngGC触发的,其实就是FullGC,我们只需要关注YoungGC,FullGC 就可以了,

其实也就是关注STW的长短,OldGC,也就是MajorGC速度要比MinerGC/YoungGC慢上10倍不止!

STW

STOP THE WORLD

垃圾回收过程中,用户线程运行到安全点(save Point),进入挂起状态,对外表现就是卡顿,

这个安全点,就是操作系统中,进行中断前保存的寄存器和PC程序计数器

所以应当经量减少FullGC发生的次数

JVM垃圾回收器速览

随着jdk对性能的追求,jdk版本更迭,垃圾回收器也在不断的变化,

jdk8盛行的时候,ParalNew与CMS一度成为面试必考的内容,随着时代的进步,出现了整堆垃圾回收器,G1与ZGC,对于旧版本,我们了解即可,JDK11属于长期支持(Long Term Support)LTS版本,其默认的G1,将是23年~25年的主流,不要说什么,jdk8不可能动摇,公司都是向钱看的,提高性能就是省钱,我们需要做的就是跟着时代进步,so,要做的就是,认识旧的,熟悉新的!

话不多说,上图,一图胜千言!

image-20230804223255225
image-20230804223255225

垃圾收集器分类

  • 新生代收集器
    • Serial 串行垃圾收集器
    • ParNew 年轻代的并行垃圾回收器
    • Parallel 并行垃圾收集器
  • 老年代收集器
    • Serial Old 串行老年代垃圾器
    • Parallel Old 老年代的并行垃圾回收器
    • CMS (ConcMarkSweep)并发标记清除
  • 整堆收集器:G1、ZGC

注意

  • JDK8中默认使用: Parallel Scavenge GC + ParallelOld GC
  • JDK14 弃用了: Parallel Scavenge GC + Parallel OldGC
  • JDK9默认是用G1为垃圾收集器
  • JDK14 移除了 CMS GC
  • 查看默认垃圾收集器
    • JVM参数: -XX:+PrintCommandLineFlags 查看命令行相关参数(包含使用的垃圾收集器)
    • JDK8 -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=8589934592 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
    • JDK11 -XX:G1ConcRefinementThreads=9 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=8589934592 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC
    • JDK17 -XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=9 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=536870912 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=8589934592 -XX:MinHeapSize=6815736 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC

可以看到,以前常问的CMS,也在JDK14也给废弃了,曾几何时,jvm调优常用的ParNew和CMS组合,竟然被淘汰

说到这里,我们做的就是认识这些老的垃圾收集器,熟悉新一代收集器G1与ZGC,当然G1就可以了,ZGC虽然强大,但是指不定出现更强大的,近几年,还是G1将成为主流。

在讲这些垃圾回收器之前,先看下垃圾收集器应当关注哪些地方,

这么多垃圾回收器,我们要区分差别,就要看一些指标

垃圾回收器关注指标

吞吐量:

运行程序占总运行时间的比例

虚拟机100分钟,垃圾回收期花掉1分钟,吞吐量就是99%

暂停时间:

就是STW

gc时,程序被暂停的时间,比如100ms,这100ms程序是停止工作的

收集频率:

就是垃圾回收触发gc次数,当然是越少越好

需要重点关注的就是,吞吐量与暂停时间。

了解、认识,老垃圾回收器

Serial(新生代+老年代)

这类垃圾收集器就比较鸡肋了,在单核cpu环境比较高效,适用于小型的应用,一般javaweb,springboot不会采用这类收集器

算法,新生代采用标记复制,老年代采用标记整理

上面那张图,

image-20230804223255225
image-20230804223255225

Serial一般和CMS进行配合,或者和Serial Old,

Serial Old是Serial收集器的老年代版本,jdk5之前和Parallel配合使用,或者作为CMS的备选方案

新生代采用单线程进行标记复制算法的回收,老年代也是单线程,进行标记整理算法,都会发生STW

ParNew(新生代)

工作在年轻代上,和ParNew的区别就是将穿行改为了并行,其他基本和Serial一样,应用大型项目,单核比Serial低

算法:新生代采用复制算法,老年代采用标记整理算法

新生代多个线程进行标记复制算法,老年代取与对应的回收器

Parallel(新生代+老年代)

新生代垃圾收集器,

新生代采用标记复制,老年代采用标记整理

算法:新生代采用标记复制算法,老年代采用标记整理算法,

新生代多个线程进行标记复制,老年代标记整理也是多个线程,并行处理,都会发生STW

Parallel与ParNew区别

-XX:+UseParallelGC仅仅对年轻代有效,不可以和CMS收集器同时使用

-XX:+UseParNewGC设置年轻代为多线程收集,可以和CMS配合使用

CMS(老年代)

全程交错Concurrent Mark Sweep,是一款并发的,使用标记清除算法!的垃圾回收期

针对老年代使用的

适用于对响应要求较高的应用程序

整个过程分四步

初始标记、并发标记、重新标记、并发清除

老年代中,初始标记,单线程进行标记与GCROOT直接关联的对象,会发生STW,

并发标记,单个线程处理,此时不会影响用户线程的执行,不会STW

重新标记:处理并发标记出现错误标记的,多个线程重新标记,会发生STW

并发清理:线程与线程并发清理,此时不会STW

熟练掌握G1,认识ZGC(整堆垃圾回收器)

上面的很乱,主要区分就是垃圾回收器在新生代和老年代,独特的就是CMS是老年代的垃圾回收期,采用的标记清除,但是是并发的标记清除

上面分为新生代垃圾收集器,老年代垃圾收集器,而G1和ZGC属于整堆垃圾收集器,不划分制约,一个垃圾回收期就可以了

G1

Garbage First所以叫做G1

在JDK9的时候成为默认的垃圾收集器

核心思想:

将内存划分多个独立的区域Region,取消了物理上年轻代与老年代的物理划分,但是保留了逻辑上的年轻代与老年代,新增了一个H区存放大对象,

分代没有固定思想。年轻代与老年代的大小式动态分配的

局部采用标记复制算法,整体采用标记整理算法,没有内存碎片问题

通过动态的判断进行垃圾回收,引入MixedGC,动态回收,尽量避免FullGC的发生,从而提高响应速度

这些很关键,大家多看多回顾

注意点!:

由于G1的特性,动态分配年轻代与老年代空间的 ,所以不手工设置年轻代大小,比如使用 -Xmn 选项或 -XX:NewRatio 等设置年轻代大小

暂停时间的目标不要设置太小,通常100~200ms比较合理,太短的化,每次只能回收很小一部分,可能导致垃圾堆积

我把这里放到最上面,因为这是学完之后总结的,大家可以从这里来带着疑问往下阅读。

Region分区

image-20230805125712620
image-20230805125712620

Region进行了分区处理,但是Region还是保留了类型的

Eden,Survivor、Old还是和之前一样,有这样分代的思想,有一个H区,专门存放大对象,H区也是属于老年代的

关于H区的特点,看上面的脑图即可

注意:

1:是动态变化的,可能垃圾回收之前是年轻代,护手之后变成老年代,这样回收更精细化

2:整体标记整理,局部标记复制,不会产生内存碎片

垃圾回收模式

除了YongGC,FullGC,还有混合GC:MixedGC

MixedGC是多数对象转移到old 区的时候,避免内存不足,提前进行回收一部分Old区,来避免未来FullGC的发生

使用

-XX:InitiatingHeapOccupancyPercent=n

决定默认:45%,即 当老年代大小占整个堆大小百分比达到该阀值时触发

此外,不仅新增了这一个MixedGC,

YoungGC与之前也有不同,不再等Eden区满,而是预估回收需要的时间,接近这个时间,通过动态的计算,回收ROI(投入产出比更高的对象),

如果接近参数-XX:MaxGCPauseMills设定的值,会触发Young GC

通过上述动态的垃圾回收策略,避免FullGC的发生,提高应用程序的响应速度。

MixedGC精讲

MixedGC与YoungGC同样,动态的计算ROI,因为MixedGC也是包括YoungGC的,总之动态计算式G1的一个很厉害的特性,计算每个Region回收的ROI。

比如

  • Region1预计可以回收1.5MB内存,预计耗时2MS,投产比ROI=1.5/2
  • Region2预计可以回收1MB内存,预计耗时1MS,投产比ROI=1/1
  • Region3预计可以回收0.5MB内存,预计耗时1MS,投产比ROI=0.5/1

那么只选一个的化,就会选择Region2

G1的MixGC垃圾收集分为下面几个步骤

  • 初始标记(STW)
    • 记录下GC Roots能直接引用的对象,并标记所有存活的对象,会执行一次年轻代GC,需要暂停所有线程,速度很快
    • 注意式直接引用对象,说白了就是第一个链接GCROOT的对象
    image-20230805131233682
    image-20230805131233682
  • 并发标记
    • 与应用线程一起工作,进行可达性分析
    • g1收集器会对堆内存进行并发标记,找出所有存活的对象,并记录它们所在的Region
    • 这里说人话就是,在初始标记之后,就可以随着用户线程进行可达性分析,找到哪些存活 ,哪些该死
  • 最终标记(STW)
    • 修正并发标记期间, 部分因程序运行导致发生变化的那一部分对象,根据算法修复一些引用的状态
  • 筛选回收(STW)
    • 根据用户指定的期望,即-XX:MaxGCPauseMillis 制定计划,回收ROI最符合预期的进行回收
    • 筛选回收,就要根据ROI进行排序,根据YoungGC设置的期望停顿时间进行回收,这里很重要,最大停顿时间过小,那么能帅选处来回收的就笑,太小的话就会出现问题!

如何使用G1

  • 开启G1垃圾收集器
  • 设置堆的最大内存
  • 设置最大的停顿时间
  • 相关参数
    • -XX:+UseG1GC
      • 开启G1,jdk9以后都是默认的,当然JDK8也可以使用
    • -XX:G1HeapRegionSize=n
      • Java 堆大小划 分出约 2048 个区域,默认是堆内存的1/2000;配置需要为2的N次幂,1MB~32MB
      • 使用G1垃圾回收器最小堆内存应为 1MB*2048=2GB ,低于2GC就不要使用了!
    • -XX:MaxGCPauseMillis=n
      • 设置最大停顿时间,单位为毫秒,默认为200毫秒(JVM会尽力实现,但不能保证达到)
    • -XX:ParallelGCThreads=n
      • 设置 STW 工作线程数的值。一般设置为逻辑处理器的数量,最多为 8
      • 是在STW阶段,并行执行【垃圾收集动作】的线程数
    • -XX:ConcGCThreads=n
      • 在【并发标记】阶段,并发执行标记的线程数,一般将 n 设置为并行垃圾回收线程数 (ParallelGCThreads) 的 1/4
    • -XX:InitiatingHeapOccupancyPercent=n
      • 就是触发Mixed的参数,上面讲过,就是当Old区到达45%的时候,会发生MixedGC,提前GC,避免FullGC的发生
  • 使用参数 //输入 -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xms524m -Xmx524m -XX:+PrintCommandLineFlags ​ //输出 -XX:G1ConcRefinementThreads=9 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=549453824 -XX:MaxGCPauseMillis=100 -XX:MaxHeapSize=549453824 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC
  • 应用场景
    • 适用于大型项目,需要高并发,低延迟的场景,大内存的应用,精细化动态的收集垃圾,提高了内存使用率。

ZGC

说明:

ZGC号称最强的,但是还没有接受检验,他的停顿时间能低于10ms,G1上面我们推荐设置100~200MS的最大停顿时间,所以ZGC是非常强大的,但是近几年还是G1,另外也不一定过几年会出现更厉害的,转型也是选择G1,就像HTTP3.0一样,转型过去的话,至少得10年以后了,所以,ZGC懂得他的奥秘之处,理解、认识即可

使用 –XX:+UseZGC 启用

通过染色指针、读屏障技术,将STW控制在10ms以内!!!

ZGC改进了标记复制算法,

与ZG1类似,进行Region,但是抛弃了分代思想,Region可以动态的创建于销毁

Region

他的Region分为了三种

小型页面、SmallRegion

中型页面 MediumRegion

大型页面 Large Region,容量大小不固定,2MB的倍数即可

特点:

不需要分代(不需要区分代,不需要复杂的回收算法)、

并发处理(几乎所有操作都并发)、

低停顿时间(10ms以内)

可伸缩性(处理不同规模的程序,不论大小)

工作流程

  • 初始标记(STW) 初始标记和G1一样,寻GCROOT直接引用的对象,处理时间,和GCROOT速度正比,所以G1不区分内存大小,适用于不同规模程序,就有可伸缩的特性
  • 并发标记(没有STW): 于用户线程一起,找到可达的对象,有标记错误的问题
  • 再标记(STW): 通过算法找到漏标的对象
  • 并发转移准备(没有STW) : ROI计算,找到要最值得回收的GC分页
  • 初始转移(STW): 转移初始标记的存活对象和做对象重定位,时间和GC Roots的数量成正比,时间不随堆的大小而增加。
  • 并发转移(没有STW): 对转移并发标记的存活对象做转移

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基础认识
    • JVM自动回收机制
      • 如何回收垃圾?
        • 垃圾回收器和垃圾回收算法的关系?
          • 如何判断哪些对象存活?回收哪些对象?
            • 引用计数法
            • 循环引用
            • 可达性分析算法
        • 垃圾回收算法
          • 标记清除算法
            • 标记复制算法
              • 标记整理算法
                • 场景
                • 垃圾回收器扫盲
                  • 分代收集算法思想
                    • GC分类与术语
                      • 部分收集(Partial GC)
                      • 整堆收集
                      • 关于Old GC 与Full GC 的区分
                      • STW
                  • JVM垃圾回收器速览
                    • 垃圾回收器关注指标
                      • 吞吐量:
                      • 暂停时间:
                      • 收集频率:
                  • 了解、认识,老垃圾回收器
                    • Serial(新生代+老年代)
                      • ParNew(新生代)
                        • Parallel(新生代+老年代)
                          • CMS(老年代)
                          • 熟练掌握G1,认识ZGC(整堆垃圾回收器)
                            • G1
                              • 核心思想:
                              • 注意点!:
                              • Region分区
                              • 垃圾回收模式
                              • MixedGC精讲
                              • 如何使用G1
                            • ZGC
                              • Region
                              领券
                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档