• 【面试】JVM垃圾回收


    目录

    Java内存区域

    哪些数据区域需要回收

    哪些内存需要回收

    可达性分析算法

    基本思路

    RC Roots的对象有哪些?

    三色标记

    白色

    黑色

    灰色

    垃圾回收算法

    标记清除算法(Mark-Sweep)

    复制算法

    标记整理算法

    垃圾收集器

    Serial收集器

    ParNew 收集器

    Parallel Scavenge 收集器

    也是新生代收集器,也是使用复制算法的多线程收集器。

    Parallel Old 收集器

    CMS 收集器

    ​编辑

    CMS缺点

    G1收集器


    Java内存区域

            根据虚拟机规范,Java虚拟机管理的运行时数据区域可以分为两大类:第一类是线程私有,依赖用户线程的启动和结束而建立和销毁;第二部分是线程共享区域不依赖用户线程。

    哪些数据区域需要回收

            通过上面的介绍可以知道线程私有的数据区是不需要回收的,它是随着用户现场的启动而创建,随着用户线程的结束而销毁。垃圾回收主要回收的是线程共享数据区

    哪些内存需要回收

           在现实生活中什么是垃圾?垃圾就是我们已经不被使用的东西。在JVM中也同样是这个道理,那么哪些是垃圾呢?就是这个对象不被使用。

    可达性分析算法

    基本思路

    从GC Roots开始,根据引用关系向下搜索,如果某个对象可以被搜索到则称之为GC Roots到这个对象可达,如果某个对象从GC Roots到这个对象不可达时,说明这个对象不再被使用,可以被回收。

    RC Roots的对象有哪些?

    1. 在虚拟机栈中引用的对象,如当前只在运行方法所使用的参数,局部变量等
    2. 在方法区中类的静态数量引用的对象
    3. 方法区中常量引用的对象
    4. 本地方法栈中Native方法引用的对象
    5. JVM 内部的引用
    6. 持有同步锁(Synchronized)的对象

    三色标记

     三色标记主要用在并发的可达性分析场景,将遍历对象图按照以下规则标记为三种颜色

    白色

       白色表示对象尚未被垃圾收集器访问过。显然在可达性分析开始阶段所有对象都是白色,如果可达性分析结束对象还是白色,就表示这个对象从GC ROOT不可达,即改对象属于垃圾需要被回收

    黑色

    黑色表示对象已经被垃圾回收器访问过且这个对象所引用的对象也已经被访问过。它表示从GC Root对象可达,它是存活的对象。

    灰色

    表示这个对象已经被垃圾回收器访问过,但是这个对象至少有一个引用还没有被扫描过

    垃圾回收算法

    标记清除算法(Mark-Sweep)

    标记清除算法分为标记和清除两个阶段,第一阶段,标记出所有需要回收的对象,第二阶段,在标记完成后,回收掉所有被标记的对象。该算法存在的主要问题是空间碎片化,标记清除后产生大量不连续的内存碎片。

    复制算法

    复制算法是为了解决标记清除算法内存碎片化问题,它将内存分为两个相等的空间,每次只使用其中一个空间,在回收过程中将存活的对象复制到另外一个空间,复制完成中针对当前空间整体清除。主要存在的问题是浪费空间。

    标记整理算法

    标记整理算法是针对标记清除算法的优化,解决了标记清除算法内存空间碎片化问题;第一阶段是标记所有存活的对象,第二家阶段是将存活的对象想内存空间的一端移动,第三阶段完成存所有存活对象移动后,清除边界以外的空间。

    垃圾收集器

    新生代垃圾回收器:

    1. Serial, ParNew, Parallel Scavenge,G1

    老年代垃圾回收器:

    1. Serial Old, Parallel Old,CMS 与Serial Old 收集器,G1

    Serial收集器

    Serial,翻译成中文的意思是“串行”,顾名思义,这就是个单线程的收集器。仅仅使用一个线程去执行垃圾收集任务,而且收集任务期间,必须停掉其他的工作线程,直到垃圾收集完成。垃圾回收时停掉其他的线程的现象,就称为“Stop The World(STW)”。打个比方,我清扫房间的时候,任何人都不能在家里活动,以免给我捣乱,不然清扫工作怎么也没法做完。STW就是这么个意思,至于暂停应用多久,得看具体垃圾的情况了。

    Serial收集器是收集新生代的收集器,而Serial Old收集器是收集老年代的,上图也看到了它们之间有连线可搭配使用,看如下它们搭配使用的运行图:

    ①:新生代使用Serial收集器,采用复制算法,会暂停其他用户线程(STW)专心做垃圾回收。

    ②:老年代使用Serial Old收集器,采用标记整理算法,会发生STW。

    ParNew 收集器

    ParNew其实就是Serial的多线程版本,在新生代中使用多条线程进行垃圾回收。看如下逻辑图就一目了然了:

    ①:新生代使用ParNew收集器,可以看到有多条GC线程在进行垃圾回收,采用复制算法,会暂停其他用户线程(STW)专心做垃圾回收。

    ②:老年代使用Serial Old收集器,采用标记整理算法,会发生STW。 Parallel Scavenge 收集器

    Parallel Scavenge 收集器

    也是新生代收集器,也是使用复制算法的多线程收集器。

    看上去和ParNew收集器差不多,但是Parallel Scavenge最大的特点是更关注吞吐量。

    吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值: 吞吐量 = 运行用户代码时间 / (运行用户代码时间) + 垃圾收集时间 打个比方,虚拟机运行了100分钟,垃圾回收用了2分钟,那么吞吐量就是98%。

    按照公式来看,吞吐量越高的虚拟机,自然是垃圾收集时间也越短,理所当然的用户体验也要更好。Parallel Scavenge收集器会根据当前系统的运行情况,动态调整某些参数来提供最合适的停顿时间或最大的吞吐量,这就是GC的自适应调节策略,这也是其与ParNew收集器最明显的区别。 

    Parallel Old 收集器

    Parallel Old 是 Parallel Scavenge收集器的老年代版本,运用多线程和标记整理算法收集。从最上面的搭配图也可以看到,Parallel Old 只能与Parallel Scavenge配对使用。这样的组合,在注重吞吐量和CPU资源的场合使用比较合适。如下是逻辑运行图:

    ①:新生代使用Parallel Scavenge收集器,可以看到有多条GC线程在进行垃圾回收,采用复制算法,会暂停其他用户线程(STW)专心做垃圾回收。

    ②:老年代使用Parallel Old收集器,使用多线程采用标记整理算法,会发生STW。

    CMS 收集器

    CMS收集器是一种以获取最短回收停顿时间为目标的收集器。在B/S架构模型的网站上,运用CMS收集器十分广泛,因为网站上更希望停顿越短越好,用户体验才能更好。

    CMS收集器是基于标记清除算法实现的,但是其运行过程相对来说更复杂了,整个过程分成下图4个步骤:

    ①:初始标记(initial mark)

    在图中可以看出这个步骤是单线程处理的,并且用户线程并未运行,是因为出现了STW。这个过程只是标记一下GC Roots能直接关联到的对象,速度很快。

    ②:并发标记(concurrent mark) 标记从初始化标记对象可达的存活对象。

    ③:重新标记(remark)

    重新标记阶段是为了修正并发标记期间,因用户线程继续运行导致标记产生变动的那一部分对象的标记。看起来有点绕,其实意思就是在并发标记时,用户线程也会产生需要标记的对象,这部分对象不能漏了标记,所以就需要重新标记过程。在图中可以看到,没有用户线程在运行,说明需要STW。

    ④:并发清除(concurrent sweep)

    并发清除这个阶段看图也能类比了,有GC线程与用户线程并发运行,GC线程清理掉那些标记的对象,用户线程正常运行。 第一,第三步的初始标记(Initial Mark)和重新标记(Remark)依然会引发STW 整体来看,CMS收集器的垃圾回收过程是与用户线程一起并发执行的。

    CMS缺点

    但是CMS收集器还是有一下三个缺点:

    1. 因为是使用并发收集,虽然不会导致用户线程停顿,但是会占用一部分线程而导致应用程序变慢,总的吞吐量会降低。
    2. CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的发生。因为在并发清理阶段,用户线程还在运行,自然就还有新的垃圾不断产生,这部分垃圾出现在标记过程之后,CMS也束手无策,只能等待下次GC时再清理,这一部分垃圾就叫“浮动垃圾”。
    3. CMS是基于标记清除算法实现的,前面的文章也提到过标记清除算法的缺点,就是会产生大量的空间碎片。空间碎片过多时,就会给大对象的空间分配带来麻烦。比如老年代有足够的空间,但是找不到连续的足够大的空间,而不得不触发一次Full GC。为了解决这个问题,CMS收集器提供了-XX:+UseCMSFullGCsBeforeCompaction参数,用于设置执行了多少次不压缩的FGC后来一次碎片整理(默认是0,每次进入FGC时都进行碎片整理)。

    G1收集器

            G1是一个分代的,增量的,并行与并发的标记-复制垃圾回收器。它的设计目标是为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量

            G1采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。因此,在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区  。一个大小达到甚至超过分区大小一半的对象称为巨型对象(Humongous Object)。当线程为巨型分配空间时,不能简单在TLAB进行分配,因为巨型对象的移动成本很高,而且有可能一个分区不能容纳巨型对象。因此,巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区(Humongous Region)

     

    G1回收器大致可以分为以下四个步骤

    1. 初始标记(Initial Marking):仅仅是标记一下GC Roots能够直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能够正确在可用的Region中分配对象。这个阶段需要STW,但是耗时很短。
    2. 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,扫描整个对象图,找出需要回收的对象
    3. 最终标记(Final Marking):对用户线程做一个短暂的STW,用于处理并发阶段结束后仍遗留下来的SATB记录
    4. 选择回收:在需要回收的Region中选择回收成本低,回收价值高的Region 进行回收

  • 相关阅读:
    js操作数组的方法
    51单片机学习:LCD1602液晶显示实验
    [深度学习]基于C++和onnxruntime部署yolov10的onnx模型
    自定义IDOC配置
    style=“width: ___“ VS width=“___“
    向NS-3添加新模块_ns3.37添加新模块_ns3.37不同版本模块移植
    Linux学习笔记
    Golang语言中的逃逸分析详解
    Anaconda 克隆环境
    14个SpringBoot优化小妙招,看完后同事说写代码像写诗!
  • 原文地址:https://blog.csdn.net/zhangwei_david/article/details/125174785