java垃圾回收--ZGC

一.什么是垃圾回收

       垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。

二.什么是垃圾

        在堆里面存放这java世界中几乎所有的实例对象,垃圾回收器在对堆进行回收前,第一件事情就是要确定这些对象中哪些是垃圾(即不可能再被任何途径使用的对象)。

1.引用计数算法

        引用计数算法(Reachability Counting)是通过在对象头中分配一个空间来保存该对象被引用的次数(Reference Count)。

如果该对象被其它对象引用,则它的引用计数加1,如果删除对该对象的引用,那么它的引用计数就减1,当该对象的引用计数为0时,那么该对象就会被回收。

        //先创建一个字符串,这时候"GC"有一个引用,就是 m。Reference Count = 1
        String m = new String("GC");

        //然后将 m 设置为 null,这时候"GC"的引用次数就等于0了,在引用计数算法中,意味着这块内容就需要被回收了。
        m = null;

       优点:引用计数算法是将垃圾回收分摊到整个应用程序的运行当中了,而不是在进行垃圾收集时,要挂起整个应用的运行,直到对堆中所有对象的处理都结束。

因此,采用引用计数的垃圾收集不属于严格意义上的"Stop-The-World"的垃圾收集机制。

       缺点:主流的java虚拟机都没有采用该算法,主要原因是因为它很难解决对象之间相互循环引用的问题。

 

public class ReferenceCountingGC {
        public Object instance;
        public ReferenceCountingGC(String name){}
    }

    public static void testGC(){
        //此时a和b的RC = 1
        ReferenceCountingGC a = new ReferenceCountingGC("objA");
        ReferenceCountingGC b = new ReferenceCountingGC("objB");

        //此时a和b的RC = 1 + 1
        a.instance = b;
        b.instance = a;

        //此时a和b的RC = 2 -1
        a = null;
        b = null;
    }

我们可以看到,最后这2个对象已经不可能再被访问了,但由于他们相互引用着对方,导致它们的引用计数永远都不会为0,通过引用计数算法,也就永远无法通知GC收集器回收它们。

 

2.可达性分析算法

     可达性分析算法(Reachability Analysis)的基本思路是,通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个

对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 节点到该节点不可达),则证明该对象是不可用的。

在 Java 语言中,可作为 GC Root 的对象包括以下4种:

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

三.垃圾回收算法

     简单介绍几种算法的思想。

1.标记-清除算法

    标记-清除(Mark-Sweep)算法是最基础的垃圾回收算法(后续的垃圾回收算法都是基于对其不足进行改进而得到的),如同它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要被回收的对象,

在标记完成后统一回收所有被标记的对象。

   主要的两个不足:

  • 一个是效率问题,标记和清除两个效率都不高
  • 空间问题,标记清除之后会产生大佬的不连续的内存碎片

2.复制算法

    复制(coping)算法的主要思想是:它将可有内存按照容量划分为大小相等的两块,每次只使用其中一块,当其中一块的内存用完了,就将还存活的对象复制到另外一块内存上,

然后把已经使用过的内存空间一次性清理掉。

   优点:每次都对整个半区进行内存回收,内存分配也就不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效

   缺点:代价太大,每次只能使用的内存缩小为原来的一半。

   实际应用:现在商业虚拟机都采用这种算法来回收新生代,IBM 公司的专业研究表明,新生代的对象有将近98%都是“朝生夕死",所以不需要按照1:1的比例来分配内存空间,

而是将又分 Eden 区和 Survivor 区,其中 Survivor 区又分 From 和 To 2个区。

通过 Minor GC 之后,Eden 会被清空,Eden 区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到 Survivor 的 From 区(若 From 区不够,则直接进入 Old 区)。

 

    ★关于Survivor 区:Survivor 分为2个区,一个是 From 区,一个是 To 区。每次执行 Minor GC,会将 Eden 区和 From 存活的对象放到 Survivor 的 To 区(如果 To 区不够,则直接进入 Old 区)。

 每次 Minor GC,会将之前 Eden 区和 From 区中的存活对象复制到 To 区域。第二次 Minor GC 时,From 与 To 职责兑换,这时候会将 Eden 区和 To 区中的存活对象再复制到 From 区域,以此反复。

这种机制最大的好处就是,整个过程中,永远有一个 Survivor space 是空的,另一个非空的 Survivor space 是无碎片的。那么,Survivor 为什么不分更多块呢?比方说分成三个、四个、五个?显然,

如果 Survivor 区再细分下去,每一块的空间就会比较小,容易导致 Survivor 区满,两块 Survivor 区可能是经过权衡之后的最佳方案。

3.标记整理算法

     标记整理算法(Mark-Compact)标记过程仍然与标记 --- 清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。

   优点:解决了标记-清除算法的两个不足:内存只能使用一半和内存碎片问题。

   缺点:从上图可以看到,它对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多。

 

4.分代收集算法

   这种算法并没有什么新的思想,只是根据对象存活周期不同将内存划分为几块。一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最合适的算法。

   新生代:每次Minor GC都发现大量对象死去,只有少量存活,那就采用复制算法。

   老年代:对象存活率高,采用“标记-清理” 或者“标记-整理”算法。

 

四.垃圾收集器

     如果说回收算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现,下面介绍几种主要的垃圾回收器

1.serial收集器

    serial收集器是最基本、发展历史最基础的收集器,这是一个单线程的收集器,运行示意图如下:

2.ParNew收集器

serial收集器的多线程版本,示意图如下:

3.CMS收集器

CMS(Concurrent Mark Sweep)收集器,以获取最短回收停顿时间【也就是指Stop The World的停顿时间】为目标,多数应用于互联网站或者B/S系统的服务器端上。其中“Concurrent”并发是指垃圾收集的线程和用户执行的线程是可以同时执行的。

CMS是基于“标记-清除”算法实现的,整个过程分为4个步骤:
1、初始标记(CMS initial mark)。
2、并发标记(CMS concurrent mark)。
3、重新标记(CMS remark)。
4、并发清除(CMS concurrent sweep)。
注意:“标记”是指将存活的对象和要回收的对象都给标记出来,而“清除”是指清除掉将要回收的对象。

 

  • 其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。
  • 初始标记只是标记一下GC Roots能直接关联到的对象,速度很快。
  • 并发标记阶段【也就说明不会阻碍业务线程继续执行,因为它所以还会有下面要说的“重新标记”阶段了】就是进行GC Roots Tracing【其实就是从GC Roots开始找到它能引用的所有其它对象】的过程。
  • 重新标记阶段则是为了修正并发标记期间因用户程序继续动作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
  • CMS收集器的动作步骤如下图所示,在整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,因此,从总体上看,CMS收集器的内存回收过程是与用户线程一起并发执行的:

 

缺点:

   1.CMS收集器对CPU资源十分敏感:

并发意味着多线程抢占CPU资源,即GC线程与用户线程抢占CPU。这可能会造成用户线程执行效率下降。

CMS默认的回收线程数是(CPU个数+3)/4。这个公式的意思是当CPU大于4个时,保证回收线程占用至少25%的CPU资源,这样用户线程占用75%的CPU,这是可以接受的。

但是,如果CPU资源很少,比如只有两个的时候怎么办?按照上面的公式,CMS会启动1个GC线程。相当于GC线程占用了50%的CPU资源,这就可能导致用户程序的执行速度忽然降低了50%,50%已经是很明显的降低了。

2.CMS收集器无法处理浮动垃圾

浮动垃圾(Floating Garbage,就是指在之前判断该对象不是垃圾,由于用户线程同时也是在运行过程中的,所以会导致判断不准确的, 可能在判断完成之后在清除之前这个对像已经变成了垃圾对象,所以有可能本该此垃圾被回收但是没有被回收,

只能等待下一次GC再将该对象回收,所以这种对像就是浮动垃圾。主要产生在并发清理阶段。

 3.CMS收集器是基于“标记清除-算法”,收集完成后回产生大量空间碎片

4.G1收集器

      G1(Garbage First)垃圾收集器是当今垃圾回收技术最前沿的成果之一。早在JDK7就已加入JVM的收集器大家庭中,成为HotSpot重点发展的垃圾回收技术。同优秀的CMS垃圾回收器一样,

G1也是关注最小时延的垃圾回收器,也同样适合大尺寸堆内存的垃圾收集,官方也推荐使用G1来代替选择CMS。

特点:

1、并行与并发:

      G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。

2、分代收集:

      分代概念在G1中依然得以保留。虽然G1可以不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。也就是说G1可以自己管理新生代和老年代了。

3、空间整合

      由于G1使用了独立区域(Region)概念,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。

4、可预测的停顿:

      这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用这明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

   在G1收集器之前收集器进行的收集范围都是整个新生代或老年代,而G1不再是这样:

 

G1打破了以往将收集范围固定在新生代或老年代的模式,GI 将 Java 堆空间分割成了若干相同大小的 区域,即 region,包括 Eden、Survivor、 Old、 Humongous 四种类型。

其中, Humongous 是特殊的 Old 类型,专门放置大型对象(大于G1中region大小50%的对象)。这样的划分方式意昧着不需要一个连续的内存空间管理对象。 GI 将 空间分为多个区域,优先回收垃圾最多的 区域。 

Region的数值是在1M到32M字节之间的一个2的幂值数,JVM会尽量划分2048个左右、同等大小的Region。

    在JVM运行的时候,从运行管理角度不需要预先设置分区是新生代分区还是老年代分区,而是在内存分配时决定:当新生代需要空间时,则分区被加入到新生代中;当老年代需要内存空间时,则分区被加入老年代分区。

事实上,G1通常的运行状态是:映射G1的虚拟内存随着时间的推移在不同的代之间切换。例如:一个G1分区被指定为新生代,经过一次新生代的回收之后,整个新生代分区都会被划入待使用的分区,那它既可以作为新生代分区使用,也可以作为老年代使用。

G1工作示意图:

筛选回收:

G1会跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要的时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region.4

 

跨代引用问题:

Region不可能是孤立的。一个对象分配在某个Region中,它并非只能被本Region 中的其他对象引用,而是可以与整个Java堆任意的对象发生引用关系。那在做可达性判定确定对象是否存活的时候,必须扫描整个Java堆才能保证准确性!这个问题其
实并非在G1中才有,只是在G1中更加突出而已。在以前的分代收集中,新生代的规模一般都比老年代要小许多,新生代的收集也比老年代要频繁许多,那回收新生代中的对象时也面临相同的问题,如果回收新生代时也不得不同时扫描老年代的话,

那么Minor GC的效率可能下降不少。在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对引用,虚拟机都是使用 Remembered Set 来避免全堆扫描的。

 

Remembered Set:

RSet是一个抽象概念,记录对象在不同代际之间的引用关系,目的是加速垃圾回收的速度(减少回收过程中STW时间)。G1中每个Region都有一个与对应的Remembered Set。通常有两种方法记录引用关系,分别为point out和point in。比如a=b(a引用b),

若采用point out结构,则在a的RSet中记录b的地址;若采用point in结构,则在b的RSet中记录a的地址。G1的RSet采用的是point in结构,即谁引用了我。

当进行内存回收时,在GC根节点的枚举范围中加入RSet即可保证不对全堆扫描也不会有遗漏。

 

G1的gc停顿分析:

标记阶段停顿分析

  • 初始标记阶段:初始标记阶段是指从GC Roots出发标记全部直接子节点的过程,该阶段是STW的。由于GC Roots数量不多,通常该阶段耗时非常短。
  • 并发标记阶段:并发标记阶段是指从GC Roots开始对堆中对象进行可达性分析,找出存活对象。该阶段是并发的,即应用线程和GC线程可以同时活动。并发标记耗时相对长很多,但因为不是STW,所以我们不太关心该阶段耗时的长短。
  • 再标记阶段:重新标记那些在并发标记阶段发生变化的对象。该阶段是STW的。

清理阶段停顿分析

  • 清理阶段清点出有存活对象的分区和没有存活对象的分区,该阶段不会清理垃圾对象,也不会执行存活对象的复制。该阶段是STW的。

复制阶段停顿分析

  • 复制算法中的转移阶段需要分配新内存和复制对象的成员变量。转移阶段是STW的,其中内存分配通常耗时非常短,但对象成员变量的复制耗时有可能较长,这是因为复制耗时与存活对象数量与对象复杂度成正比。对象越复杂,复制耗时越长。

初始标记因为只标记GC Roots,耗时较短。再标记因为对象数少,耗时也较短。清理阶段因为内存分区数量少,耗时也较短。转移阶段要处理所有存活的对象,耗时会较长。因此,G1停顿时间的瓶颈主要是标记-复制中的转移阶段STW

5.新一代垃圾收集器-ZGC

   我们知道每一款新的垃圾收集器都是针对上一款的收集器的不足进行优化而产生的,ZGC也不例外,ZGC也是为了解决G1的不足,首先我们看下G1有哪些不足

  1. 停顿时间过长,通常G1的停顿时间要达到几十到几百毫秒;这个数字其实已经非常小了,但是我们知道垃圾回收发生导致应用程序在这几十或者几百毫秒中不能提
    供服务,在某些场景中,特别是对用户体验有较高要求的情况下不能满足实际需求。
  2. 内存利用率不高,通常引用关系(Remembered Set)的处理需要额外消耗内存,一般占整个内存的1%~20%左右。
  3. 支持的内存空间有限,不适用于超大内存的系统,特别是在内存容量高于100GB的系统中,会因内存过大而导致停顿时间增长。

 ZGC的三大目标:

  • 停顿时间不超过10ms
  • 对程序吞吐量影响小于15%;
  • 支持8MB~4TB级别的堆(未来支持16TB)

 

     ZGC如何达成这一目标的呢?简单的说就是ZGC把一切能并发处理的工作都并发执行。

     1.ZGC是在G1的基础上发展起来的,我们知道G1中实现了并发标记,所以标记已经不影响停顿时间了。从上传的G1的GC停顿分析也可以看出,G1中的停顿时间主页来自于垃圾回收(YGC和混合回收)阶段中的复制算法。在G1中这一阶段都是STW的,

而ZGC就是把对象的复制转移也并发的执行,从而满足停顿时间10ms以下。

     2.在G1中可能发生FGC,每次FGC的停顿时间不可控,而在目前的ZGC中,每次垃圾回收都是FGC,而每次停顿时间都在10ms以下,从而FGC停顿时间不可控这一存在G1中的问题也被解决了。

     3.ZGC除了并发转移,还对整个垃圾回收进入STW的过程进行了改进,把原来的串行改成了并发执行。下图是不同垃圾回收器在并发粒度上的区别:

对于非串行回收器,不支持并发执行分为串行执行和并行执行两种情况。

 

理解ZGC触发时机

相比于CMS和G1的GC触发机制,ZGC的GC触发机制有很大不同。ZGC的核心特点是并发,GC过程中一直有新的对象产生。如何保证在GC完成之前,新产生的对象不会将堆占满,是ZGC参数调优的第一大目标。因为在ZGC中,当垃圾来不及回收将堆占满时,会导致正在运行的线程停顿,持续时间可能长达秒级之久。

ZGC有多种GC触发机制,总结如下:

  • 阻塞内存分配请求触发:当垃圾来不及回收,垃圾将堆占满时,会导致部分线程阻塞。我们应当避免出现这种触发方式。日志中关键字是“Allocation Stall”。
  • 基于分配速率的自适应算法:最主要的GC触发方式,其算法原理可简单描述为”ZGC根据近期的对象分配速率以及GC时间,计算出当内存占用达到什么阈值时触发下一次GC”。自适应算法的详细理论可参考彭成寒《新一代垃圾回收器ZGC设计与实现》一书中的内容。通过ZAllocationSpikeTolerance参数控制阈值大小,该参数默认2,数值越大,越早的触发GC。我们通过调整此参数解决了一些问题。日志中关键字是“Allocation Rate”。
  • 基于固定时间间隔:通过ZCollectionInterval控制,适合应对突增流量场景。流量平稳变化时,自适应算法可能在堆使用率达到95%以上才触发GC。流量突增时,自适应算法触发的时机可能会过晚,导致部分线程阻塞。我们通过调整此参数解决流量突增场景的问题,比如定时活动、秒杀等场景。日志中关键字是“Timer”。
  • 主动触发规则:类似于固定间隔规则,但时间间隔不固定,是ZGC自行算出来的时机,我们的服务因为已经加了基于固定时间间隔的触发机制,所以通过-ZProactive参数将该功能关闭,以免GC频繁,影响服务可用性。 日志中关键字是“Proactive”。
  • 预热规则:服务刚启动时出现,一般不需要关注。日志中关键字是“Warmup”。
  • 外部触发:代码中显式调用System.gc()触发。 日志中关键字是“System.gc()”。
  • 元数据分配触发:元数据区不足时导致,一般不需要关注。 日志中关键字是“Metadata GC Threshold”。

着色指针:

ZGC仅支持64位系统,它把64位虚拟地址空间划分为多个子空间,如下图所示:

其中,[0~4TB) 对应Java堆,[4TB ~ 8TB) 称为M0地址空间,[8TB ~ 12TB) 称为M1地址空间,[12TB ~ 16TB) 预留未使用,[16TB ~ 20TB) 称为Remapped空间。

当应用程序创建对象时,首先在堆空间申请一个虚拟地址,但该虚拟地址并不会映射到真正的物理地址。ZGC同时会为该对象在M0、M1和Remapped地址空间分别申请一个虚拟地址,且这三个虚拟地址对应同一个物理地址,但这三个空间在同一时间有且只有一个空间有效。

ZGC之所以设置三个虚拟地址空间,是因为它使用“空间换时间”思想,去降低GC停顿时间。“空间换时间”中的空间是虚拟空间,而不是真正的物理空间。后续章节将详细介绍这三个空间的切换过程。

与上述地址空间划分相对应,ZGC实际仅使用64位地址空间的第0~41位,而第42~45位存储元数据,第47~63位固定为0。

ZGC堆内存:

     GI 将 Java 堆空间分割成了若干相同大小的区域;ZGC则是分为一个个的页面:

  • 小页面,固定大小2MB,存放小于256KB的小对象
  • 中页面,固定大小32MB,存放大于256KB小于4MB的对象
  • 大页面,大小不固定,可以动态变化,但必须是2MB的整数倍,用于放大于4MB的大对象,每个大型Region只会放一个大对象,所以实际容量可能会小于中型Region,最小到4MB。大型RegionZGC实现中不会被重分配,因为复制一个大对象代价太高。

在进行垃圾回收时,ZGC对于不同页面的回收策略也不同,简单的说,小页面优先回收;中页面和大页面尽量不回收。

 

 

ZGC并发处理算法

ZGC强大之一是他数据的处理转移是并发执行的,下面简单说下ZGC并发处理算法

ZCC初始化之后,整个内存空间的地址视图被设置为Remaped,当进人标记阶段时的视图转变为Markedo(也称为M0)或者Makedl(也称为M1),从标记阶段结束进人转移阶段时的视图再次设置为Remapped.ZGC通过视图的切换加上SATB算法实现并发
处理。具体算法如下:
1.初始化阶段
在ZGC初始化之后,此时地址视图为Remapped,程序正常运行,在内存中分配对象,满足一定条件后(ZGC垃圾回收的触发时机)垃圾回收启动,此时进入标记阶段。
2.标记阶段
第一次进入标记阶段时视图为M0,在标记阶段,应用程序和标记线程并发执行,那么对象的访问可能来自标记线程和用程序线程。

  1. 标记线程:标记线程访问对象就是对对象进行标记。它从根集合开始标记对象,在标记前先判断对象的地址视图是 Remapped 还是MO:
  • 如果发现对象的地址视图是M0,说明对象是在进入标记阶段之后新分配的对象或者对象已经完成了标记,也就是说对象是活跃的,无须处理。
  • 如果发现对象的地址视图是Remapped,说明对象是前一阶段分配的,而且通过根集合可达,所以把对象的地址视图从Remapped调整为MO.应用程序线程,在正常运行用户代码时访问对象,所做的工作有:应用程序线程如果创建新的对象,则对象的地址视图为MO.

       2.应用程序线程,在正常运行用户代码时访问对象,所做的工作有:

  • 应用程序线程如果创建新的对象,则对象的地址视图为MO.
  • 如果应用程序线程访问对象并且对象的地址视图是Remapped,说明对象是前一阶段分配的,按照SATB的算法,只要把该对象的视图从Remapped调整为MO就能防止对象漏标。注意,只标记应用线程访问到的对象还不够,实际上还需
    要把对象的成员变量所引用的对象都进行标记,可以通过递归的方式完成标记。
  • 如果应用程序线程访问对象并且对象的地址视图是M0,说明对象是在进入标记阶段之后新分配的对象或者对象已经完成了标记,无须额外处理,直接访问。

所以,在标记阶段结束之后,对象的地址视图要么是MO,要么是Remapped.如果对象的地址视图是M0,说明对象在标记阶段被标记或者是新创建的,是活跃的;如果对象的地址视图是Remapped,说明对象在标记阶段既不能通过根集合访问到,
也没有应用程序线程访问它,所以是不活跃的,即对象所使用的内存可以被回收。

3.并发转移阶段

 标记结束后就进入转移阶段,此时地址视图子啊此被设置为Remaped,转移阶段会把活跃对象转移到新的内存中,并回收对象转移前的内存空间。转移阶段会把
应用程序和标记线程并发执行,那么对象的访问可能来自转移线程和应用程序线程。

       1. 转移线程:移线程仅仅根据标记阶段标记的活跃对象进行转移,所以只需要针对对象活跃信息表中记录的对象进行转移。当转移线程访问对象时:

  • 如果对象在对象活跃信息表中并且对象的地址视图为M0,则转移对象,转移以后对象的地址视图从M0调整为Remapped。
  • 如果对象在对象活跃信息表中并且对象的地址视图为Remapped,说明对象已经被转移,无须处理。

       2.应用程序线程,在正常运行用户代码时访问对象,所做的工作有:

  • 应用程序线程如果创建新的对象,则对象的地址视图为Remapped.
  • 如果应用程序线程访问对象并且对象不在对象活跃信息表中,则说明对象是新创建的或者对象无须转移,无须额外处理。
  • 如果应用程序线程访问对象并且对象在对象活跃信息表中,且对象的地址视图为Remapped,说明对象已经被转移,无须额外处理。
  • 为Remapped,说明对象已经被转移,无须额外处理。如果应用程序线程访问对象并且对象在对象活跃信息表中,且对象的地址视图为M0,说明对象是标记阶段标记的活跃对象,所以需要转移对象,在对象转
    移以后,对象的地址视图从M0调整为Remapped;注意,只把应用线程读到的对象进行转移还不够,实际上还需要把对象的成员变量所引用的对象都进行转
    移,但是ZGC对这一实现做了优化,由转移线程完成对象成员变量的转移。

至此,ZGC的一个垃圾回收周期中,并发标记和并发转移就结束了。我们提到在标阶段存在两个地址视图M0和M1,上面的算法过程显示只用到了一个地址视图,为什设计成两个?

简单地说是为了区别前一次标记和当前标记。

这3个地址视图代表的含义是:

  • M1:本次垃圾回收中识别的活跃对象。
  • MO:前一次垃圾回收的标记阶段被标记过的活跃对象,对象在转移阶段未被转移,
    但是在本次垃圾回收中被识别为不活跃对象。
  • Remapped:前一次垃圾回收的转移阶段发生转移的对象或者是被应用程序线程访问的对象,但是在本次垃圾回收中被识别为不活跃对象。

图示:

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值