想要提高程序员自身的内功心法无非就是: 数据结构跟算法 + 操作系统 + 网络 ,而所有的Java代码都是在JVM上运行的,了解了JVM好处就是:
写出更好更健壮的代码。
提高Java的性能,排除问题。
面试必问 ,要对知识有一定的深度 。
1、简述JVM 内存模型
从宏观上来说JVM 内存区域 分为三部分 线程共享区域 、 线程私有区域 、 直接内存区域 。
1.1、线程共享区域
1.1.1、堆区
堆区Heap是JVM中最大的一块内存区域,基本上所有的对象实例都是在堆上分配空间。堆区细分为 年轻代 和 老年代 ,其中年轻代又分为Eden、S0、S1 三个部分,他们默认的比例是 8:1:1的大小。
1.1.1元空间
方法区:
在 《Java虚拟机规范》中只是规定了有 方法区 这么个 概念 跟它的 作用 。 HotSpot 在JDK8之前 搞了个 永久代 把这个概念实现了。用来主要存储类信息、常量池、静态变量、JIT编译后的代码等数据。
PermGen(永久代)中类的元数据信息在每次 FullGC 的时候可能会被收集,但成绩很难令人满意。而且为PermGen分配多大的空间因为存储上述多种数据很难确定大小。因此官方在JDK8提出移除永久代。
官方解释移除永久代:
This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.
即:移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。
元空间:
在Java中用 永久代 来存储类信息,常量,静态变量等数据不是好办法,因为这样很容易造成内存溢出。同时对永久代的性能调优也很困难,因此在JDK8中 把 永久代 去除了,引入了元空间 metaspace ,原先的class、field等变量放入到metaspace。
总结:
元空间的本质和永久代类似,都是对JVM规范中方法区的实现 。不过元空间与永久代之间最大的区别在于: 元空间并不在虚拟机中,而是使用本地内存 。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过参数来指定元空间的大小。
1.2、直接内存区域
直接内存:
一般使用 Native 函数操作C++代码来实现直接分配堆外内存,不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。这块内存不受Java堆空间大小的限制,但是受本机总内存大小限制所以也会出现OOM异常。分配空间后 避免了在Java堆区跟Native堆中来回复制数据 ,可以有效提高读写效率, 但它的创建、销毁却比普通Buffer慢 。
PS: 如果使用了 NIO ,本地内存区域会被频繁地使用,此时 jvm内存 ≈ 方法区 + 堆 + 栈+ 直接内存
1.3、线程私有区域
程序计数器、虚拟机栈、本地方法栈跟线程的声明周期是一样的。
1.3.1、程序计数器
课堂上比如你正在看小说《诛仙》,看到1412章节时,老师喊你回答问题,这个时候你肯定要先应付老师的问题,回答完毕后继续接着看,这个时候你可以用书签也可以凭借记忆记住自己在看的位置,通过这样实现继续阅读。
落实到代码运行时候同样道理, 程序计数器 用于记录当前线程下虚拟机正在执行的字节码的指令地址。它具有如下特性:
线程私有
多线程情况下,在同一时刻所以为了让线程切换后依然能恢复到原位,每条线程都需要有各自独立的程序计数器。
没有规定OutOfMemoryError
程序计数器存储的是字节码文件的行号,而这个范围是可知晓的,在一开始分配内存时就可以分配一个绝对不会溢出的内存。
执行Native方法时值为空
Native方法大多是通过C实现并未编译成需要执行的字节码指令,也就不需要去存储字节码文件的行号了。
1.3.2、虚拟机栈
方法的出入栈:调用的方法会被打包成 栈桢 ,一个栈桢至少需要包含一个局部变量表、操作数栈、桢数据区、动态链接。
动态链接:
当栈帧内部包含一个指向运行时常量池引用前提下,类加载时候会进行符号引用到直接引用的解析跟链接替换。
局部变量表:
局部变量表是栈帧重要组中部分之一。他主要保存函数的参数以及局部的变量信息。局部变量表中的变量作用域是当前调用的函数。函数调用结束后,随着函数栈帧的销毁。局部变量表也会随之销毁,释放空间。
操作数栈:
保存着Java虚拟机执行过程中数据
方法返回地址:
方法被调用的位置,当方法退出时候实际上等同于当前栈帧出栈。
比如执行简单加减法:
执行 javap -c *.class :
1.3.3、本地方法栈
跟虚拟机栈类似,只是为使用到的Native方法服务而已。
2、判断对象是否存活
JVM空间不够就需要 Garbage Collection 了,一般共享区的都要被回收比如堆区以及方法区。在进行内存回收之前要做的事情就是 判断那些对象是死的,哪些是活的 。常用方法有两种 引用计数法 跟 可达性分析 。
2.1、引用计数法
思路是给 Java 对象添加一个引用计数器,每当有一个地方引用它时,计数器 +1;引用失效则 -1,当计数器不为 0 时,判断该对象存活;否则判断为死亡(计数器 = 0)。
优点:
实现简单,判断高效。
缺点:
无法解决 对象间 相互循环引用 的问题
step1: GcObject实例1的引用计数+1,实例1引用数 = 1
step2: GcObject实例2的引用计数+1,实例2引用数 = 1
step3: GcObject实例2的引用计数+1,实例2引用数 = 2
step4: GcObject实例1的引用计数+1,实例1引用数 = 2
step5: GcObject实例1的引用计数-1,结果为 1
step6: GcObject实例2的引用计数-1,结果为 1
如上分析发现实例1跟实例2的引用数都不为0而又相互引用,这两个实例所占有的内存则无法释放。
2.2、可达性分析
很多主流商用语言(如Java、C#)都采用 引用链法 判断对象是否存活,大致的思路就是将一系列的 GC Roots 对象作为起点,从这些起点开始向下搜索。在Java语言中,可作为 GC Roots的对象包含以下几种:
第一种是 虚拟机栈中的引用的对象 ,在程序中正常创建一个对象,对象会在堆上开辟一块空间,同时会将这块空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,这种情况是最常见的。
第二种是我们 在类中定义了全局的静态的对象 ,也就是使用了 static 关键字,由于虚拟机栈是线程私有的,所以这种对象的引用会保存在共有的方法区中,显然将方法区中的静态引用作为GC Roots是必须的。
第三种便是 常量引用 ,就是使用了 static final 关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也应该作为GC Roots。
第四种是在使用 JNI 技术时,有时候单纯的Java代码并不能满足我们的需求,我们可能需要在Java中调用C或C++的代码,因此会使用 Native方法 ,JVM内存中专门有一块本地方法栈,用来保存这些对象的引用,所以本地方法栈中引用的对象也会被作为GC Roots。
GC Root步骤主要包含如下三步:
2.1.1 可达性分析
当一个对象到 GC Roots 没有任何引用链相连时 ,则判断该对象不可达。
注意: 可达性分析仅仅只是判断对象是否可达,但还不足以判断对象是否存活 / 死亡。
2.1.2 第一次标记 & 筛选
筛选的条件对象 如果没有重写finalize或者调用过finalize 则将该对象加入到F-Queue中
2.1.3 第二次标记 & 筛选
当对象经过了第一次的标记 & 筛选,会被进行第二次标记 & 准备被进行筛选。 经过F-Queue筛选后如果对象还没有跟GC Root建立引用关系则被回收 ,属于给个二次机会。
2.3、四大引用类型
2.3.1 强引用
强引用(StrongReference)是使用最普遍的引用。垃圾回收器绝对不会回收它,内存不足时宁愿抛出OOM导致程序异常,平常的new 对象就是。
2.3.2 软引用
垃圾回收器在内存充足时不会回收软引用(SoftReference)对象,不足时会回收它,特别适合用于创建缓存。
2.3.3 弱引用
弱引用(WeakReference)是在扫描到该对象时无论内存是否充足都会回收该对象。 ThreadLocal 的Key就是弱引用。
2.3.4 虚引用
如果一个对象只具有虚引用(PhantomReference)那么跟没有任何引用一样,任何适合都可以被回收。主要用跟踪对象跟垃圾回收器回收的活动。
3、垃圾回收算法
为了挥手回收垃圾操作系统一般会使用 标记清除 、 复制算法 、 标记整理 三种算法,这三种各有优劣,简单介绍下:
3.1、标记清除
原理:
算法分为 标记 和 清除 两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
缺点:
标记清除之后会产生大量不连续的内存碎片,导致触发GC。
3.2、标记复制
原理:
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
缺点:
这种算法的代价是将内存缩小为了原来的一半,还要来回移动数据。
3.3、标记整理
原理:
首先标记出所有需要回收的对象,在标记完成后,后续步骤是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
缺点:
涉及到移动大量对象,效率不高。
总结:
3.4 、三色标记跟读写屏障
前面说的三种回收算法都说到了先 标记 ,问题是如何标记的呢? **说话说一半,小心没老伴 **!
接下来的知识点个人感觉面试应该问不到那么深了,但是为了 必须Mark下 ! CMS 、 G1标记时候一般用的是 三色标记法 ,根据可达性分析从GC Roots开始进行遍历访问,可达的则为存活对象,而最终不可达说明就是需要被GC对象。大致流程是把遍历对象图过程中遇到的对象,按 是否访问过 这个条件标记成以下三种颜色:
白色:尚未访问过。
黑色:本对象已访问过,而且本对象 引用到 的其他对象 也全部访问过了。
灰色:本对象已访问过,但是本对象 引用到 的其他对象 尚未全部访问完 。全部访问后会转换为黑色。
假设现在有白、灰、黑三个集合(表示当前对象的颜色),遍历访问过程:
1、初始时所有对象都在白色集合中。
2、将GC Roots 直接引用到的对象挪到灰色集合中。
3、从灰色集合中获取对象:第一步将本对象 引用到的 其他对象 全部挪到灰色集合中,第二步将本对象 挪到黑色集合里面。
4、重复步骤3,直至灰色集合为空时结束。
5、结束后仍在白色集合的对象即为GC Roots 不可达 ,可以尝试进行回收。
当STW时对象间的引用是不会发生变化的,可以轻松完成标记。当支持并发标记时,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。
3.4 .1、浮动垃圾
状况:GC线程遍历到E(E是灰色),一个业务线程执行了D.E = null,此时E应该被回收的。但是GC线程已经认为E是灰色了会继续遍历,导致E没有被回收。
3.4 .2、漏标
GC线程遍历到E(灰色了)。业务线程执行了E–>G断开,D–>G链接的操作。GC线程发现E无法到达G,因为是黑色不会再遍历标记了。最终导致漏标G。
漏标的必备两个条件: 灰到白断开 , 黑到白建立 。
漏标解决方法:
将对象G存储到特定集合中,等并发标记遍历完毕后再对集合中对象进行 重新标记 。
3.4.2.1、CMS方案
这里比如开始B指向C,但是后来B不指向C,A指向D,最简单的方法是 将A变成灰色 ,等待下次进行再次遍历。
CMS中可能引发 ABA 问题:
1、回收线程 m1 正在标记A,属性A.1标记完毕,正在标记属性A.2。
2、业务线程 m2 把属性1指向了C,由于CMS方案此时回收线程 m3 把A标记位灰色。
3、回收线程 m1 认为所有属性标记完毕,将A设置为黑色,结果C漏标。所以CMS阶段需要重新标记。
3.4.2.2、读写屏障
漏标的实现是有三步的,JVM加入了读写屏障,其中读屏障则是拦截第一步,写屏障用于拦截第二和第三步。
写屏障 + SATB(原始快照) 来破坏 灰到白断开。
写屏障 + 增量更新 来破坏 黑到白建立。
读屏障 一种保守方式来破坏灰到白断开后白的存储,此时用读屏障OK的。
现代使用可达性分析的垃圾回收器几乎都借鉴了三色标记的算法思想,尽管实现的方式不尽相同。对于读写屏障,以Java HotSpot VM为例,其 并发标记时对漏标 的处理方案如下:
CMS : 写屏障 + 增量更新
G1 : 写屏障 + SATB
ZGC : 读屏障
CMS中使用的增量更新,在重新标记阶段除了需要遍历 写屏障的记录,还 需要重新扫描遍历GC Roots(标记过的不用再标记),这是由于CMS对于astore_x等指令不添加写屏障的原因。
4、GC流程
核心思想就是 根据各个年代的特点不同选用不同到垃圾收集算法 。
年轻代 :使用 复制算法
老年代 : 使用 标记整理 或者 标记清除 算法。
为什么要有年轻代:
分代的好处就是 优化GC性能 ,如果没有分代每次扫描所有区域能累死GC。因为很多对象几乎就是 朝生夕死 的,如果分代的话,我们把新创建的对象放到某一地方,当GC的时候先把这块存 朝生夕死 (80%以上)对象的区域进行回收,这样就会腾出很大的空间出来。
4.1、 年轻代
HotSpot JVM把年轻代分为了三部分:1个 Eden 区和2个 Survivor 区(分别叫 from 和 to )。默认比例为 8:1:1 。一般情况下,新创建的对象都会被分配到 Eden 区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到 Survivor 区。对象在Survivor区中每熬过一次 Minor GC 年龄就会增加1岁,当它的年龄增加到一定次数(默认 15 次)时,就会被移动到年老代中。年轻代的垃圾回收算法使用的是 复制算法 。
年轻代GC过程:
GC开始前,年轻代对象只会存在于 Eden 区和名为 From 的 Survivor 区,名为 To 的 Survivor 区永远是空的。如果新分配对象在 Eden 申请空间发现不足就会导致GC。
yang GC : Eden 区中所有存活的对象都会被复制到 To ,而在 From 区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值可以通过 -XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到 To 区域。经过这次GC后, Eden 区和 From 区已经被清空。这个时候, From 和 To 会交换他们的角色,也就是新的 To 就是上次GC前的 From ,新的 From 就是上次GC前的 To 。不管怎样 都会保证名为To的Survivor区域是空的 。 Minor GC 会一直重复这样的过程,直到 To 区被填满, To 区被填满之后,会将所有对象移动到年老代中。这里注意如果yang GC 后空间还是不够用则会 **空间担保 **机制将数据送到Old区
卡表 Card Table:
为了支持高频率的新生代回收 ,虚拟机使用一种叫做 卡表 (Card Table)的数据结构,卡表作为一个比特位的集合, 每一个比特位可以用来表示年老代的某一区域中的所有对象是否持有新生代对象的引用 。
新生代GC时不用花大量的时间扫描所有年老代对象,来确定每一个对象的引用关系, 先扫描卡表 ,只有卡表的标记位为1时,才需要扫描给定区域的年老代对象。而卡表位为0的所在区域的年老代对象,一定不包含有对新生代的引用。
4.2、 老年代
老年代GC过程:
老年代中存放的对象是存活了很久的,年龄大于15的对象 或者 触发了老年代的 分配担保 机制存储的大对象。在老年代触发的gc叫 major gc 也叫 full gc 。 full gc会包含年轻代的gc 。 full gc 采用的是 标记-清除 或 标记整理 。在执行 full gc 的情况下,会阻塞程序的正常运行。老年代的gc比年轻代的gc效率上 慢10倍以上 。对效率有很大的影响。所以 一定要尽量避免老年代GC !
4.3、 元空间
永久代的回收会随着 full gc 进行移动,消耗性能。每种类型的垃圾回收都需要特殊处理 元数据。将元数据剥离出来,简化了垃圾收集,提高了效率。
-XX:MetaspaceSize 初始空间的大小。达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:
如果释放了大量的空间,就适当降低该值;
如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize:
最大空间,默认是没有限制的。
4.4 、垃圾回收流程总结
大致的 GC回收流程 如 上图 ,还有一种设置就是 大对象直接进入老年代 :
如果在新生代分配失败且对象是一个不含任何对象引用的大数组,可被直接分配到老年代。通过在老年代的分配避免新生代的一次垃圾回收。
设置了-XX:PretenureSizeThreshold 值,任何比这个值大的对象都不会尝试在新生代分配,将在老年代分配内存。
内存回收跟分配策略
优先在Eden上分配对象,此区域垃圾回收频繁速度还快。
大对象直接进入老生代。
年长者(长期存活对象默认15次)跟 进入老生代。
在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象会群体进入老生代。
空间分配担保(担保minorGC),如果Minor GC后 Survivor区放不下新生代仍存活的对象,把Suvivor 无法容纳的对象直接进入老年代。
5、垃圾收集器
5.1、 垃圾收集器
堆heap是垃圾回收机制的重点区域。我们知道垃圾回收机制有三种 minor gc 、 major gc 和 full g c。针对于堆的就是前两种。年轻代的叫 minor gc ,老年代的叫 major gc 。
JDK7、JDK8 默认垃圾收集器 Parallel Scavenge(新生代)+ Parallel Old(老年代)
JDK9 默认垃圾收集器 G1 ,服务端开发常见组合就是 ParNew + CMS
工程化使用的时候使用指定的垃圾收集器组合使用,讲解垃圾收集器前先普及几个重要知识点:
STW
java中 Stop-The-World 机制简称STW,是指执行垃圾收集算法时Java应用程序的 其他所有线程都被挂起 (除了垃圾收集帮助器之外)。是Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码虽然可以执行但不能与JVM交互,如果发生了STW 现象多半是由于gc引起 。
吞吐量
吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%
垃圾收集时间
垃圾回收频率 * 单次垃圾回收时间
并行收集
指多条垃圾收集线程并行工作,但此时用户线程仍 处于等待状态 。
并发收集
指 用户线程与垃圾收集线程同时工作 (不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。
5.2、 新生代
新生代有 Serial 、 ParNew 、 Parallel Scavenge 三种垃圾收集器。
5.3、 老年代
老年代有 Serial Old 、 Parallel Old 、 CMS 三种垃圾收集器。
5.3.1、CMS
CMS (Concurrent Mark Sweep)比较重要这里 重点说一下 。
CMS的初衷和目的:
为了消除Throught收集器和Serial收集器在Full GC周期中的长时间停顿。是一种 以获取最短回收停顿时间为目标 的收集器,具有自适应调整策略,适合互联网站 跟B/S 服务应用。
CMS的适用场景:
如果你的应用需要 更快的响应 ,不希望有长时间的停顿,同时你的 CPU资源也比较丰富 ,就适合使用CMS收集器。比如常见的Server端任务。
优点:
并发收集、低停顿。
缺点:
CMS收集器对CPU资源非常敏感 :在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。
无法处理浮动垃圾 :由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自然会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为 浮动垃圾 。 如果内存放不下浮动垃圾这时 JVM 启动 Serial Old 替代 CMS 。
空间碎片 :CMS是基于 标记-清除 算法实现的收集器,使用 标记-清除 算法收集后,会产生 大量碎片 。
CMS回收流程:
初始标记 : 引发STW , 仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快。
并发标记 : 不引发STW ,正常运行 所有Old 对象是否可链到GC Roots
重新标记 : 引发STW ,为了修正并发标记期间,因用户程序继续运作而导致标记产生改变的标记。这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。
并发清除 : 不引发STW ,正常运行,标记清除算法来清理删除掉标记阶段判断的已经死亡的对象。
总结:
并发标记 和 并发清除 的耗时最长但是不需要停止用户线程。 初始标记 和 重新标记 的耗时较短,但是需要停止用户线程,所以整个GC过程造成的停顿时间较短,大部分时候是可以和用户线程一起工作的。
之前的GC收集器对Heap的划分:
以前垃圾回收器是 新生代 + 老年代 ,用了CMS效果也不是很好,为了减少STW对系统的影响引入了G1(Garbage-First Garbage Collector), G1 是一款面向服务端应用的垃圾收集器,具有如下特点:
1、 并行与并发 :G1能充分利用多CPU、多核环境下的硬件优势,可以通过并发的方式让Java程序继续执行。
2、 分代收集 :分代概念在G1中依然得以保留,它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象来获得更好的收集效果。
3、 空间整合 :G1从整体上看是基于 标记-整理 算法实现的,从局部(两个Region之间)上看是基于 复制算法 实现的,G1运行期间不会产生内存空间碎片。
4、 可预测停顿 :G1比CMS牛在能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
G1作为JDK9之后的服务端默认收集器,不再区分年轻代和老年代进行垃圾回收,G1默认把堆内存分为N个分区,每个1~32M(总是2的幂次方)。并且提供了四种不同Region标签 Eden 、 Survivor 、 Old 、 Humongous 。H区可以认为是Old区中一种特别专门用来存储大数据的,关于H区数据存储类型一般符合下面条件:
当 0.5 Region <= 当对象大小 <= 1 Region 时候将数据存储到 H区
当对象大小 > 1 Region 存储到连续的H区。
同时G1中引入了 RememberSets 、 CollectionSets 帮助更好的执行GC 。
1、 RememberSets : RSet 记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)
2、 CollectionSets : Csets 是一次GC中需要被清理的regions集合,注意G1每次GC不是全部region都参与的,可能只清理少数几个,这几个就被叫做Csets。在GC的时候,对于old -> young 和old -> old的跨代对象引用,只要扫描对应的 CSet 中的 RSet 即可。
G1进行GC的时候一般分为 Yang GC 跟 Mixed GC 。
Young GC : CSet 就是所有年轻代里面的Region
Mixed GC : CSet 是所有年轻代里的Region加上在全局并发标记阶段标记出来的收益高的Region
5.4.1、Yang GC
标准的年轻代GC算法,整体思路跟CMS中类似。
5.4.2、Mixed GC
G1中是 没 有Old GC的,有一个把老年代跟新生代同时GC的 Mixed GC,它的 回收流程 :
1、 初始标记 : 是STW事件 ,其完成工作是标记GC ROOTS 直接可达的对象。标记位RootRegion。
2、 根区域扫描 : 不是STW事件 ,拿来RootRegion,扫描整个Old区所有Region,看每个Region的 Rset 中是否有RootRegion。有则标识出来。
3、 并发标记 : 同CMS并发标记 不需要STW ,遍历范围减少,在此只需要遍历 第二步 被标记到引用老年代的对象 RSet。
4、 最终标记 : 同 CMS 重新标记 会STW ,用的 SATB 操作,速度更快。
5、 清除 : STW操作 ,用 复制清理算法 ,清点出有存活对象的Region和没有存活对象的Region(Empty Region),更新Rset。把Empty Region收集起来到可分配Region队列。
回收总结:
1、经过global concurrent marking,collector就知道哪些Region有存活的对象。并将那些完全可回收的Region(没有存活对象)收集起来加入到可分配Region队列,实现对该部分内存的回收。对于有存活对象的Region,G1会根据统计模型找出收益最高、开销不超过用户指定的上限的若干Region进行对象回收。这些选中被回收的Region组成的集合就叫做collection set 简称Cset!
2、在MIX GC中的Cset = 所有年轻代里的region + 根据global concurrent marking统计得出收集收益高的若干old region 。
3、在YGC中的Cset = 所有年轻代里的region + 通过控制年轻代的region个数来控制young GC的开销 。
4、YGC 与 MIXGC 都是采用多线程复制清理,整个过程会STW。 G1的 低延迟原理 在于其回收的区域变得精确并且范围变小了。
G1提速点:
1 重新标记 使X区域直接删除。
2 Rset 降低了扫描的范围,上题中两点。
3 重新标记阶段使用 SATB 速度比CMS快。
4 清理过程为选取部分存活率低的Region进行清理,不是全部,提高了清理的效率。
总结:
就像你把房子打扫干净,你可能只把显眼而比较大的垃圾打扫了,犄角旮旯的你没打扫。
一句话总结G1思维: 每次选择性的清理大部分垃圾来保证时效性跟系统的正常运行 。