JVM-GC

JVM组成

JVM-GC

;

指的是java虚拟机栈,是一块线程私有的内存空间,每个线程包含一个栈区,栈中只保存基本数据类型的数据和自定义对象的引用,

java堆是java虚拟机所管理的内存中最大的一块。java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例

方法区

方法区是一个规范,是个概念,逻辑上的称呼。
1.7之前使用永久代来实现方法区,java8中使用元空间

永久代元空间存储位置物理上是堆的一部分,和新生代,老年代地址是连续的,大小不好分配,不合理容易发生GC本地内存(操作系统的内存空间),最大可利用空间是整个系统内存的可用空间存储内容类信息(也就是类里面的成员变量,成员方法,构造器,类加载器)、常量(存放在运行时常量池)、静态变量、即时编译后的代码类信息(也就是类里面的成员变量,成员方法,构造器,类加载器)、常量(存放在运行时常量池)、即时编译后的代码,(静态变量,字符串常量储存在堆中)触发GC永久代的垃圾收集和老年代捆绑在一起,因此无论哪个满了,都会触发fullGC。类元数据的空间占用达到参数”MaxMetaspaceSize”设置的值,将会触发对死亡对象和类加载器的垃圾回收

运行时常量池

方法区一部分,Class文件中除了有类型的版本,字段,方法,接口等描述信息外还有一项信息是常量池表(Constant Pool Table)用于存放编译器生成的各种字面量和符号引用。这部分内容将在类加载后存放到方法区的运行时常量池中

程序计数器(ProgramCounter)寄存器

也叫PC寄存器,是一块较小的内存空间,内容总是指向下一条将被执行指令的地址,保存有当前正在执行的JVM指令的地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量,可以看做当前线程所执行的字节码的行号指示器。

JVM中的程序计数器并不像汇编语言中的程序计数器一样是物理概念上的CPU寄存器,但是逻辑作用上是等同的

每个线程启动的时候,都会创建一个PC寄存器
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

每一个线程都有它自己的PC寄存器,也是该线程启动时创建的
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,

每个线程之间计数器互不影响,独立存储

本地方法栈

保存native方法进入区域的地址

垃圾回收机制

回收算法

引用计数法

每个对象都有个计数counter,任何对象引用到了该对象,counter+1,引用失效时,counter-1,counter=0时,GC时清除
无法处理循环引用问题,可能会造成死锁、counter变化时影响系统性能

根搜索算法

当一个对象到GC Roots没有任何引用链相连时,证明此对象是不可用的。GC时清除

GC Roots对象

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象

回收算法

算法描述优缺点标记清除法先按根搜索算法标记对象是否可清除,后清除回收效率低,回收后内存过于碎片化复制算法将内存一份为二,每次使用一块,GC时将活着的对象复制到另一块空间,原空间全部清除效率高,无碎片化,内存使用率低标记整理法先按根搜索算法标记对象是否可清除,后将活着的对象移动到一起,剩下的垃圾空间进行全部清除效率低,无碎片化分代模型按对象生存周期分老年代和新生代,使用不同的算法分区模型将整个空间分为连续的不同小空间,每个小空间都独立使用,独立回收可以预测STW时间,控制一次回收多少个空间

GC分类及触发条件

  • MinorGC 也叫Young GC,清理年轻代,eden、survivor区域
  • Eden区满时
  • 新创建的对象大于eden区所剩空间
  • OldGC 清理老年代
  • MajorGC 清理永久代,概念没有统一定义,可视为同OldGC
  • MixedGC G1收集器独有,用于收集young gen及部分old gen的GC,堆内存占用率超45%默认启动
  • FullGC 清理整个堆空间(老年代、年轻代、永久代)
  • 调用System.gc,可能执行
  • 老年代达到回收阀值,永久代不够
  • 触发了GC空间分配担保机制
  • 老年代空间不够
    • 新产生的大对象直接进入老年代发现老年代空间不足
    • 分代年龄达到阀值晋升到老年代,发现空间不足
    • 由eden、survivor向另一个survivor进行复制时,对象大于survivor可以内存空间,则把对象直接存入老年代
    • 动态对象年龄判断
  • CMS GC 并发收集
  • ZGC

GC担保机制
在分配失败后,发生在使用复制算法的MinoGC之前,JVM会检查
老年代最大可用的连续空间是否大于新生代所有对象的总空间

  • 若大于,则此次MinorGC视为安全的
  • 若小于,且HandlePromotionFailure=true则检查最大可用的连续空间是否大于历次晋升老年代的平均大小
  • 若大于 ,则此次MinorGC视为有风险的
  • 若小于,则FullGC
  • 若小于,且HandlePromotionFailure=false,则FullGC

动态对象年龄判断
survivor空间中相同年龄所有对象大小总和大于servivor空间一半,年龄大于等于该年龄的对象晋升老年代,无需分代年龄达标

垃圾回收器分类

JVM-GC

垃圾回收器工作区域回收算法工作线程用户线程是否可并行标记垃圾位置描述Epsilon用于测试的无操作收集器serial新生代复制算法单线程否对象头的Markword简单parNew新生代复制算法多线程否对象头的Markwordserial的多线程版本的实现Parallel Scavenge新生代复制算法多线程否对象头的Markword追求高吞吐量,高效利用CPU,尽快完成程序运算任务,适合后台应用等交互要求不高的场景ConcurrentMarkSweep老年代标记清除算法多线程是对象头的Markword追求最短STW的收集器,高并发,低停顿,产生内存碎片和浮动垃圾SerialOld老年代标记整理算法单线程否对象头的Markwordseria收集器老年代版本ParallelOld老年代标记整理算法多线程否对象头的Markword配合Parallel Scavenge使用,吞吐量优先G1新生代+老年代标记整理,region之间复制算法多线程是记录在与对象相互独立的数据结构行,相当于堆内存的1/64大小的BitMap的结构JDK12发布的垃圾收集器,可预测STW时间进行回收ZGC复制算法多线程是标记信息记在引用对象的指针上JDK13推出的低延迟垃圾回收期,支持4TB级别的堆,停顿时间不超过10ms

吞吐量
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,
即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)
比如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

使用Parallel Scavenge收集器,配合自适应调节策略(-XX:+UseAdaptiveSizePolicy开关打开),不需要手动指定新生代大小、Eden区和Survivor参数等细节参数了,虚拟机会根据当前系统的运行情况,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量

; 回收器的搭配

-XX:+UseConcMarkSweepGC 使用parNew+CMS+serial old的组合(GC线程与用户线程并发执行,需预留一部分用户线程执行的内存,如果预留的线程无法满足用户线程执行,会出现Concurrent mode failure并发模式失败,退化使用serialOld进行回收老年代)

-XX:+UseSerialGC 使用serial+serial old的组合
-XX:+UseParNewGC 使用parNew+serial old的组合
-XX:+UseParallelGC 使用parallel scavenge+serial old的组合 JDK9 server模式下默认
-XX:+UseParallelOldGC 使用parallel scavenge+parallel Old的组合
-XX:+UseG1GC 使用G1

serial + serialOld

JVM-GC

; parNew + serialOld

JVM-GC

ParallelScavenge + ParallelOld

JVM-GC

; CMS

JVM-GC

cms中年轻代满是指Eden代满,Survivor满不会引发GC。
当Eden区满的时候,就会copy到Survivor区,
再次满时,存活对象都copy到另一个survivor区,
循环直至达到晋升老年代条件

运行示意图

JVM-GC

初始标记(STW):仅标记GCRoots能直接关联到的对象
并发标记:从初始标记的对象往下找,遍历这些对象所引用到的对象,并将这些被引用的对象打上标记
重新标记(STW):修正并发标记阶段用户线程对对象的产生引用的改变,重新标记下
并发清理:回收不用的垃圾,回收过程中新产生的垃圾叫浮动垃圾,下一轮回收
并发重置:重新调整堆大小,卡标志灯,为下一次GC做好数据结构支撑

; 模式

CMS GC分为Background和Foreground两种模式,前者就是我们常规理解中的并发收集,后者类似SerialOld

  • Background CMS GC:并发的收集模式,只收集老年代GC
  • Foreground CMS GC:退化为单线程串行GC模式,有2种算法实现
  • 不带压缩动作的算法,收集Old取,和普通的cms算法比较相似,暂停时间相对MSC算法短一些
  • 带压缩动作的算法,MSC(Mark Sweep Compact)算法,作用整个堆,对整个堆进行垃圾回收的FullGC

MSC开启条件
-XX:+UseCMSCompactAtFullCollection 且 -XX:CMSFullGCsBeforeCompaction=0
其中0数字表示多少次fullGC会压缩一次,默认为0,表示每次FullGC都会压缩

CMS GC退化
CMS CG并发收集退化成FullGC

  • 晋升失败(Promotion Failed)
    年轻代放不下,对象只能放到老年代,此时老年代也放不下,老年代容量不够或者是过于碎片化连续空间不够
  • 显示调用System.gc或 jmap -histo:live pid 命令强制触发
    增加参数-XX:+DisableExplicitGC,则显示调用失效
JVM_Entry_NO_Env(void, JVM_GC(void))
    JVMWrapper("JVM_GC");
    if (!DisalbeExplicitGC){
        Universe::heap()->collect(GCCause::_java_lang_system_gc);
    }
JVM_END

Backgroud CMS采用的标记清理算法会导致内存碎片问题,从而埋下发生FullGC导致长时间STW的隐患。
解决Background CMS碎片化问题,没有特效药,只有偏方,降低cms gc触发阀值(旧生代或者持久代已经使用的空间达到设定的百分比时)、夜深人静时悄悄的强制触发FullGC或者重启!!!

G1(垃圾优先收集器)

运行示意图

JVM-GC

初始标记(STW):仅标记GCRoots能直接关联到的对象
并发标记:从GCRoots开始对对象进行可达性分析,找出存活的对象
最终标记(STW):修正在并发标记阶段,用户线程对对象产生引用的改变的那一部分记录,且记录的变化在线程Remembered Set Logs里面,把 Remembered Set Logs 里面的数据合并到Remembered Set中
筛选回收(STW):对各个region的回收价值和成本排序,根据用户期望的STW时间按计划回收

; region的角色

JVM-GC

Old区:存放老对象,达到晋升年龄标准的对象
Survivor区:幸存区空间,存放存活的对象
Eden区:存放新生的对象
Humongous区:存放大对象(对象大小是否超过单个普通Region区的50%),对象很大时可跨区存放,

将内存划分为多个大小相等1M~32M的独立区域region。每一份region逻辑上在某一时刻只属于某一个分代角色,清除回收后可重新分配

G1跟踪各个region垃圾堆积的大小,后台维护一个优先列表CollectionSet(CSet),它记录了GC要收集的Region集合,集合里的Region可以是任意年代的

每次GC时会根据 指定的期望停顿时间或默认的期望停顿时间,优先从列表中选择收集回收价值最大的region,这样建立了可预测的停顿时间模型。

RSet

  • CardTable
    CMS以及之前的大部分的分代收集器为了解决跨代引用问题,实现记忆集时,都采用的为卡表CardTable的结构,记录着”我引用了谁的对象”
  • RSet
    而在G1中则实现了一种新的数据结构:Remembered Set,简称为RSet,在有些地方也被称为”双向卡表”。在每个Region区中都存在一个RSet,记录了其他Region中的对象引用当前Region中对象的关系,也就是记录着”谁引用了我的对象”

RSet本质上就是一个哈希表结构(HashTable),Key为其他引用当前区内对象的Region起始地址,Value则是一个集合,里面的元素为其他Region中每个引用当前区内对象的地址

  • 当发生YGC时,扫描标记对象时,只需要选定目标新生代Region的RSet作为根集,这些RSet中记录了Old → Young的跨代引用,GC扫描时只需要扫描这些RSet即可,从而避免了扫描整个Old区。
  • 当发生MixedGC时,Old类型的Region也有RSet记录着Old → Old的引用关系,而Old → Young的跨代引用可以从Young类型的Region中得到,这样在发生MixedGC时也不会出现整堆扫描的情况,

所以引入RSet在很大程度上减少了大量的GC工作

ZGC

运行示意图

JVM-GC
  • 初始标记(STW):仅标记GCRoots能直接关联到的对象
  • 并发标记:从GCRoots开始对堆中的对象进行可达性分析,找出存活的对象;(对象重定位指针着色),更新染色指针中的Marked 0、Marked 1标志位。
  • 再标记(STW)::修正在并发标记阶段,用户线程对对象产生引用的改变的那一部分记录
  • 并发转移准备:非强引用并发标记、重置转移集、回收无效页面(区)、根据特定的查询条件统计得出本次收集过程要选择目标回收页面、初始化转移集(表),为初始转移准备
  • 初始转移(STW):只转移GCRoots直连对象,如果对象在转移的分区集合中,则在新的分区分配对象空间,并为Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。如果不在转移分区集合中,则将对象标记为 Remapped
  • 并发转移:从上一步转移的根对象出发,遍历目标区域中的所有对象,做并发转移处理。

; 结构

JVM-GC
ZGC中没有新生代和老年代的概念,只有一块一块的内存区域Region/Page页,页动态的创建销毁。
  • 小页: 容量固定为2MB,用于放置小于256KB的小对象
  • 中页: 容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。·
  • 大页:容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象

指针染色

ZGC核心技术之一,在此之前的GC信息都是保存在对象头中,ZGC对指针进行了改造,GC信息保存在指针内,指针染色对于指针中的64个比特位全部都使用了,估ZGC指针不能被压缩,也不支持32位机器。

JVM-GC
  • 0-41 位表示地址记录,ZGC最大能够支持2^42=4TB的堆内存,jdk13拓展到44位能最大支持16TB
  • 42-45位 标志位
  • Finalizable=1000 表示这个对象只能通过finalizer才能访问
  • Remapped=0100 表示这个对象未指向RelocationSet(表示GC的页面集合)中
  • Marked1=0010 标记对象,GC使用(用于ZGC分次,标记区分存活对象。两个标志位交替使用,)
  • Marked1=0000 标记对象,GC使用
  • 46-63位 预留位

防止不同GC周期之间的标记混淆,所以搞了两个Marked标识,每当新的一次GC开始时,都会交换使用的标记位。例如:第一次GC使用M0,第二次GC就会使用M1,第三次又会使用M0…,因为ZGC标记完所有分区的存活对象后,会选择分区进行回收,因此有一部分区域内的存活对象不会被转移,那么这些对象的标识就不会复位,会停留在之前的Marked标识(比如M0),如果下次GC还是使用相同M0来标记对象,那混淆了这两种对象。为了确保标记不会混淆,所以搞了两个Marked标识交替使用

; ZGC基于染色指针的并发处理过程

  • 在第一次GC发生前,堆中所有对象的标识为:Remapped。
  • 第一次GC被触发后,GC线程开始标记,开始扫描,如果对象是Remapped标志,并且该对象根节点可达的,则将其改为M0标识,表示存活对象。
  • 如果标记过程中,扫描到的对象标识已经为M0,代表该对象已经被标记过,或者是GC开始后新分配的对象,这种情况下无需处理。
  • 在GC开始后,用户线程新创建的对象,会直接标识为M0。
  • 在标记阶段,GC线程仅标记用户线程可直接访问的对象还是不够的,实际上还需要把对象的成员变量所引用的对象都进行递归标记。

在「标记阶段」结束后,对象要么是M0存活状态,要么是Remapped待回收状态。最终,所有被标记为M0状态的活跃对象都会被放入「活跃信息表」中。等到了「转移阶段」再对这些对象进行处理,流程如下

  • ZGC选择目标回收区域,开始并发转移。
  • GC线程遍历访问目标区域中的对象,如果对象标识为M0并且存在于活跃表中,则把该对象转移到新的分区/页面空间中,同时将其标识修正为Remapped标志。
  • GC线程如果扫描到的对象存在于活跃表中,但标识为Remapped,说明该对象已经转移过了,无需处理。
  • 用户线程在「转移阶段」新创建的对象,会被标识为Remapped。
  • 如果GC线程遍历到的对象不是M0状态或不在活跃表中,也无需处理。

染色指针作用

  • 一旦某个分区中的存活对象被移走,该分区就可以立即回收并重用,不必等到整个堆中所有指向该Region区的引用都被修正后才能清理。
  • 颜色指针可以大幅减少在GC过程中内存屏障的使用数量,ZGC只使用了读屏障。
  • 颜色指针具备强大的扩展性,它可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。

读屏障

  • 读屏障的手段解决了对象漏标问题,记录谁读取了当前白色对象,然后在「再次标记」重新标记一下这些黑色对象
  • ZGC指针的”自愈”:GC发生后,堆中一部分存活对象被转移,当应用线程读取对象时,利用读屏障通过指针上的标志来判断对象是否被转移,如果读取的对象已经被转移,那么则修正当前对象引用为最新地址(去转移表中查),作用是下次其他线程再读取该转移对象时,可以正常访问读取到最新值

ShenandoahGC

ShenandoahGC的内存布局与G1很相似,也会将堆内存划分为一个个 大小相同的Region区域,也同样有存放大对象的Humongous区,你可以把ShenandoahGC看做G1收集器的修改版,它比G1收集器实现上来说更为激进,一味追求极致低延迟。但ShenandoahGC和ZGC一样,也没有实现分代的架构,所以在触发GC时也不会有新生代、年老代之说,只会存在一种覆盖全局的GC类型。

运行示意图

JVM-GC
  • 初始标记阶段:标记GCRoots直接可达的对象,会发生STW,但非常短暂。
  • 并发标记阶段:和用户线程一同工作,从根对象出发,标记堆中所有对象。
  • 最终标记阶段:同比G1、ZGC中的重新标记阶段,会触发STW,会在该阶段中修正并发标记过程中由于用户线程修改引用关系的导致的漏标错标对象,使用STAB机制实现。同时在该阶段中也会选择出回收价值最大的区域作为目标区域等待回收。
  • 并发回收阶段:与用户线程并发执行,会待回收区域中的存活对象复制到其他未使用的Region区中去,然后会将原本的Region区全部清理并回收。

上述过程中,前面的阶段与G1差异不大,重点在于最后的回收阶段,它是与用户线程并发执行的,所以也会造成新的问题出现:

  • 问题①:回收过程中,如果一个对象被复制到新的区域,用户线程通过原本指针访问时如何定位对象呢?
    通过BrooksPointers转发指针解决
  • 问题②:在并发回收过程中,如果复制的时候出现了安全性问题怎么办?
    在转发指针的基础上,采用了读+写屏障解决

额外增加了转发指针,所以也存在两个问题:
①访问对象时,速度会更慢,因为需要至少经过一次地址转发。
②需要更多的空间存储多出来的这根指针

; 三色标记

  • 白色:尚未访问过的对象
  • 黑色:本对象访问过了,本对象直接引用到的其他对象访问过了
  • 灰色:本对象访问过了,本对象直接引用到的其他对象没有全部访问过

三色标记过程

JVM-GC
  • 初始时,所有的对象都在白色集合
  • 将GCRoots能直接关联到的对象挪到灰色集合
  • 从灰色集合中获取对象,该对象本身挪到黑色集合,本对象直接引用到的对象标记为灰色
  • 重复上一步,直至灰色集合为空结束
  • 结束后,白色集合即为GCRoots不可达的对象,可进行回收

; 多标-浮动垃圾

  • 假设遍历到E,E被标记为灰色,此时 D.fieldE = null, D断开引用,E已经是灰色被视为仍能存活对象,继续遍历。最终E这次不会被回收,下一轮清除
  • 并发标记开始后产生的新对象,都视为黑色,本轮不进行清除,期间发生引用变化变成垃圾,也是下一轮清除

JVM-GC

漏标

标记过程中,产生漏标的问题

  • 灰色对象断开了白色对象的引用,黑色对象重新引用了该被断开的白色对象
  • 灰色对象断开了白色对象的引用,当GC标记完这个灰色对象,标记成了黑色后,之前断开的白色对象又重新与之建立了应用关系

出现这两种情况时,因为重新建立引用的白色对象”父节点”已经被标记黑色了,所以GC线程不会再次标记该对象以及其成员对象,所以这些白色对象会被一直停留在白色集合中。

最终导致的结果就是这些依旧存在引用的存活对象会被”误判”为垃圾对象清除掉,直接影响到应用程序的正确性,是不可接受的。

JVM-GC

; 漏标问题的解决方案

1、增量更新

关注引用的增加
D引用G时,产生了引用后将D重新标志为灰色,下一次扫描时再遍历D。

CMS中为解决该问题的手段为:写后屏障+增量更新

采用了写后屏障记录了更改引用的对象,然后通过溯源对发生改动的节点进行了重新扫描
// HotSpot中对象字段写屏障
void oop_field_store(oop field, oop new_value) {
pre_write_barrier(field); // 写前屏障
field = new_value; // 赋值操作:新值替换老值
post_write_barrier(field, value); // 写后屏障
}

2、SATB快照

关注引用的删除
snapshot at the beginning,在遍历时做个快照,当E和G的引用小时时候把这个引用推到GC栈,保证G还能扫描到,配合Rset,查找谁指向了白色的引用,这样效率会更高,但同时需要,每次给对象赋值引用时要额外的记录

G1中为解决该问题的手段为:写前屏障 + SATB快照

在引用被更改前先记录一下原本的引用信息
void pre_write_barrier(oop field) {
// 处于GC并发标记阶段且该对象没有被标记(访问)过
if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {
oop old_value =
field; // 获取旧值
remark_set.add(old_value); // 记录 原来的引用对象
}
}

ZGC中为解决该问题的手段为:读屏障

导致漏标问题2种情况: 重新引用或重新连接 白色对象,都得先读取到白色对象
记录谁读取了当前白色对象,然后在「再次标记」重新标记一下这些黑色对象即可
void pre_load_barrier(oop field, oop old_value) {
if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {
oop old_value =
field;
remark_set.add(old_value); // 记录读取到的对象
}
}

Original: https://blog.csdn.net/WangMapleWang/article/details/127785199
Author: 枫火木烈王
Title: JVM-GC

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/656928/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球