面试篇之JVM

  • 面试篇之JVM
    • GC
      • 对象存活判断
        • 分代回收理论
        • 常见回收算法
        • HotSpot实现细节
        • 记忆集与卡表
        • 读写屏障
        • 回收器
          • 并发标记
          • G1

面试篇之JVM

GC

美团
1、JVM怎么判断一个类是不是垃圾?
2、说到GC ROOTS,你知道Java中哪些对象可作为GC ROOTS吗?
3、对象不可达是不是立即被回收死亡?
4、CMS垃圾回收器的回收过程
5、如何解决跨代引用?
字节
1、CMS收集器的流程,缺点;G1收集器的流程,相对于CMS收集器的优

腾讯
1、请阐述常见的GC策略
2、垃圾回收的时候服务怎么办
神策数据
1、G1了解不,说说G1
快手
1、并发标记的过程是怎么样的?

对象存活判断

引用计数
引用计数如何解决循环引用的问题:弱引用

可达性分析
来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为**“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链( Reference Chain ),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的**。
作为 GC Roots 的对象包括下面几种(重点是前面 4 种):

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;各个线程调用方法堆

  • 栈中使用到的参数、局部变量、临时变量等。

  • 方法区中类静态属性引用的对象;java 类的引用类型静态变量。

  • 方法区中常量引用的对象;比如:字符串常量池里的引用。

  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。

  • JVM 的内部引用( class 对象、异常对象 NullPointException 、 OutofMemoryError,系统类加载器)。(非重点)

  • 所有被同步锁( synchronized )持有的对象。(非重点)

  • JVM 内部的 JMXBean 、 JVMTI 中注册的回调、本地代码缓存等(非重 点)

  • JVM 实现中的“临时性”对象,跨代引用的对象(在使用分代模型回收时 只回收部分代的对象)(非重点)

  • 除了这些固定的 GC Roots 集合以外,跟进用户选用的垃圾回收器以及 当前回收的内存区域不同,还可能会有其他对象"临时"加入成为 GC Roots 。


以上的回收都是普通的对象,普通对象大都在堆区,而对于类( Class )的回收条件比较苛刻。
finalize
即使通过可达性分析判断不可达的对象,也不是“非死不可”,它还会处于“缓刑”阶段,真正要宣告一个对象死亡,需要经过两次标记过程,一次是没有找到与 GCRoots 的引用链,它将被第一次标记。随后进行一次筛选(如果对象覆盖了 finalize ),我们可以在 finalize 中去拯救,俗称对象的自我救赎
需要注意的是:
1、 finalize 只会执行一次,不会多次执行。
2、建议大家尽量不要使用 finalize ,因为这个方法太不可靠。
如果一个对象被判定为的确有必要执行 finalize 方法,会将该对象放入一个名为 F-QUEUE 的队列中,有虚拟机创建的名为 Finalizer 的线程去执行这些对象的 finalize 方法。

分代回收理论

当前商业虚拟机的垃圾回收器,大多遵循“分代收集”的理论来进行设计,这个理论大体上有三条法则:
1、 绝大部分的对象都是朝生夕死
2、 熬过多次垃圾回收的对象就越难回收
根据以上两个理论,朝生夕死的对象放一个区域,难回收的对象放另外一个区域,这个就构成了新生代和老年代,并且不同的分代采用的回收算法不一样。

但是分代收集也并不是简单划分一下内存区域这么简单,因为对象不是孤立的,对象之间存在跨代引用,譬如:现在要在新生代进行回收,但新生代的对象极有可能被老年代对象所引用,那为了找到这些可能存活的对象,不得不在既定的 GC Roots 之外,再遍历整个老年代对象确保可达性分析结果的正确性。反过来回收老年代也是一样。
但是这样无疑带来了性能负担,为了解决这个问题,分代收集理论添加了第三条法则:
3、跨代引用相对于同代引用来说仅仅占少数
正是因为只占少数,所以不应该为了为了这些少量的跨代引用而区扫描整个老年代,也不能浪费空间让每个对象都记录它是否存在跨代引用,所以为了解决这个问题只需要在新生代建立一个全局的数据结构叫做:记忆集( Remembered Set ),这个结构把老年代划分成若干小块,并标识哪块内存存在跨代引用,后续新生代发生 gc 时,只有包含了跨代引用的小内存区域才会
被加入到 GC Roots 进行扫描;当然这种方法需要在对象改变引用关系的时候维护记忆集中数据的正确性。这种做法相比垃圾收集时扫描整个老年代来说仍然时划算的。

常见回收算法

复制算法
原始的复制算法(Copying)是这样的:
1、将内存按容量划分为大小相等的两块,每次只使用其中的一块。
2、当其中一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

带来的好处是:
1、实现简单,运行高效,
2、每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,
存在的弊端是:
1、内存的使用率缩小为原来的一半。
2、内存移动是必须实打实的移动(复制),所以对应的引用(直接指针)需要调整。

适用场景
复制回收算法适合于新生代,因为大部分对象朝生夕死,那么复制过去的对象比较少,效率自然就高,另外一半的一次性清理是很快的
但是像 hotspot 这样的虚拟机大都对原生的复制算法进行了改进,因为它对内存空间的利用率不高,而且专门研究表明,新生代中的对象 98% 是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,所以改进后的复制回收策略叫做: Appel 式回收。
1、将新生代划分为一块较大的 Eden 区和两块较小的 Survivor 空间(你可以叫做 From 或者 To ) , HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1 。
2、每次使用 Eden 和其中一块 Survivor ,当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。

在这样的算法下,
1、每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10%的内存会被 “浪费”
2、当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10%的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(老年代)进行分配担保( Handle Promotion )。

标记-清除算法
标记-清除(Mark-Sweep)算法分为“标记”和“清除”两个阶段:
1、首先扫描所有对象标记出需要回收的对象,
2、在标记完成后扫描并回收所有被标记的对象,故需要两次扫描

注意:
1、回收效率略低,如果大部分对象是朝生夕死,那么回收效率降低,因为需要大量标记对象和回收对象,对比复制回收效率要低,所以该算法不适合新生代。
2、它的主要问题是在标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。
3、标记清除算法适用于老年代。

标记整理算法
算法逻辑如下:
1、首先标记出所有需要回收的对象,
2、在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动
3、然后直接清理掉端边界以外的内存。

注意:
1、标记整理需要扫描两遍
2、标记整理与标记清除算法的区别主要在于对象的移动对象移动不单单会加重系统负担,同时需要全程暂停用户线程才能进行,同时所有引用对象的地方都需要更新(直接指针需要调整)
3、标记整理算法不会产生内存碎片,但是效率偏低。
4、标记整理算法适用于老年代。
所以看到,老年代采用的标记整理算法与标记清除算法,各有优点,各有缺点。

HotSpot实现细节

STW
收集器在根节点枚举这步都是必须要暂停用户线程的( STW ),如果不这样的话在根节点枚举的过程中由于引用关系在不断变化,分析的结果就不准确。

安全点
收集器在工作的时候某些时间是需要暂停正在执行的用户线程的( STW ),这个暂停也并不是说用户线程在执行指令流的任意位置都能停顿下来开始垃圾收集, 而是需要等用户线程执行到最近的安全点后才能够暂停
安全点如何选取呢?,安全点的选取基本是以:”是否具有让程序长时间执行的特征“为标准选定的,而最明显的特征就是指令序列的复用,主要有以下几点:
1、方法调用
2、循环跳转,
3、异常跳转等等
对于安全点另一个问题是:垃圾收集器工作时如何让用户线程都跑到最近
的安全点停顿下来?有两种方案:
1、抢先式中断:不需要用户代码主动配合,垃圾收集发生时,系统把用户线程全部中断,如果发现用户线程中断的地方不在安全点上,就恢复这个线程执行让它执行一会再重新中断。不过现在的虚拟机几乎没有采用这种方式。
2、主动式中断:思想是当垃圾收集器需要中断线程的时候,不直接对线程操作,仅仅设置一个标志位,各个线程执行过程中会不停的去主动轮询这个标志,一旦发现中断标志为真时就自己再最近的安全点上主动挂起

安全区域
安全点的设计似乎完美的解决了如何停顿用户线程,它能保证用户线程在执行时,不太长时间内就会遇到可进入垃圾回收的安全点,但是如果用户线程本身就没在执行呢?比如用户线程处于 sleep 或者 blocked 状态,这个时候它就无法响应虚拟机的中断请求,没办法主动走到安全的地方中断挂起自己,对于这种情况就必须引入安全区域( Safe Regin )来解决
STW是为了在GC线程工作的时候,防止用户线程改变引用关系,安全区域是指能够确保在某一段代码片段之中, 引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。
当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,这段时间里 JVM 要发起 GC 就不必去管这些线程了。 当线程要离开安全区域时,它要检查 JVM 是否已经完成了根节点枚举(或者其他 GC 中需要暂停用户线程的阶段)

1、如果完成了,那线程就当作没事发生过,继续执行。
2、如果没完成,它就必须一直等待, 直到收到可以离开安全区域的信号为止。

记忆集与卡表

前面讲分代收集理论的时候提到过一个跨代引用的问题,为了解决跨代引用带来的问题,垃圾收集器在新生代建立了一个叫做:**记忆集( Remembered Set )**的数据结构存储老年代哪些区域存在跨代引用,以便在根节点扫描时将这些老年代区域加入 GC Roots 的扫描范围,这样避免将整个老年代都加入 GC Roots 的扫描范围。
当然跨代引用的问题并非只在回收新生代才有,回收老年代也是一样的,所以需要更进一步理解记忆集的原理和实现方式。
记忆集定义:是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
记忆集的实现:最常见的实现方式是通过卡表( Card Table )的方式去实现,卡表最简单的形式是一个字节数组( hotspot ),如下:

CARD_TABLE[this address >> 9 ] = 0

1、字节数组 CARD_TABLE 的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作:卡页( Card Page ),卡页大小一般是2的N次幂, hotspot 中是2的9次幂(地址右移9位),即512字节
2、如果卡表标识的起始地址是:0x0000,那数组的0,1,2号元素,分别对应的地址范围是:0x0000~ 0x01ff,0x0200 ~ 0x03ff,0x0400~0x05ff,如下:

3、一个卡页的内存中通常包含不止一个对象,只要卡页内存中有一个或多个对象的字段存在跨代引用指针,那就将卡表对应字节数组元素的值标识位1,称之为 Ditry ,没有则标识位0,垃圾收集器工作时只要筛查 CARD_TABLE 中为1的元素,就能轻易找到哪些卡页内存块中包含跨代引用,就把这些内存块加入到 GC Roots 的扫描范围内。

读写屏障

目前已经解决了用记忆集来缩减存在跨代引用时 GC Roots 的扫描范围,但是还没解决卡表如何维护的问题,比如:何时将卡表变脏?
答案似乎明显:非收集区域存在收集区域的引用时,对应卡表元素就变脏变脏的时间点原则上应发生在引用类型字段赋值的那一刻, 但问题时如何在引用类型字段赋值的那一刻去维护卡表呢?
如果是解释执行的字节码那相对好处理,虚拟机负责每条字节码的执行,有充分的介入空间,但如果是编译执行的场景呢?即时编译器编译后的代码已经是纯粹的机器指令了,所以必须找一个在机器码操作的层面,在赋值操作发生时来维护卡表。
hotspot 中是通过写屏障( write barrier )来维护的, 这里的读写屏障要和解决并发问题的 内存屏障 区分开来,这里的读写屏障类似于 spring 的 AOP ,比如以下代码是一个卡表更新的简化逻辑

void oop_field_store( oop* field,oop new_value) { //引用字段赋值 *field = new_value; //写后屏障,完成卡表更新 post_write_barrier(field,new_value);
}

当然这里还需要解决一个问题:卡表在高并发场景下面临着 伪共享 问题,一般处理器的缓存行( cache line )大小是64字节,由于卡表一个元素占一个字节,64个卡表元素共享同一个缓存行,这64个卡表元素对应的卡页总大小内存为:64*512bytes=32M,也就是说如果不同线程更新的对象引用正好处在这32M内存区域内,就会导致更新卡表时正好写入同一个缓存行而影响性能。
为了解决伪共享的问题,简单的解决方案就是不采用无条件的写屏障,而是先检查卡标记,只有当卡表元素未被标记过时才将其标记为变脏,即更新卡表的逻辑变更如下:

if (CARD_TABLE[this address >> 9] != 0 ) {
CARD_TABLE[this address >> 9] = 0;
}

在jdk1.7之后 , hotspot 虚拟机增加了一个参数 - XX:+UseCondCardMark ,用来解决是否开启卡表更新前的条件判断,开启会增加一次额外的条件判断开销,但能够避免伪共享问题,两者各有性能损耗,是否开启需要根据实际情况来测试权衡,默认是关闭的。

回收器

常见的垃圾回收器如下:

SerialGC

ParallerGC

CMS
CMS的整体执行过程分成5个步骤,其中标记阶段包含了三步,具体细节如
下:
1、初始标记:标记 GC Roots 直接关联的对象,会导致 STW ,但是这个没多少对象,时间短 。
2、并发标记:从 GC Roots 开始关联的所有对象开始遍历整个可达路径的对象,这步耗时比较长,所以它允许用户线程和GC线程并发执行,并不会导致STW ,但面临的问题是可能会漏标,多标,等问题。
3、重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段会导致 STW ,但是停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
4、并发清除;将被标记的对象清除掉,因为是标记-清除算法,不需要移动存活对象,所以这一步与用户线程并发运行。
5、重置线程:重置GC线程状态,等待下次CMS的触发,与用户线程同时运行。

当然,在CMS中也会出现一些问题,主要有以下几点:
1、CPU敏感:对处理器资源敏感,毕竟采用了并发的收集、当处理核心数不足 4 个时,CMS 对用户的影响较大,因为CMS默认启动的回收线程数量是:
(CPU核数+3)/ 4。
2、浮动垃圾:由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉,这一部分垃圾就称为“浮动垃圾”(比如用户线程运行产生了新的 GC Roots )。
由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收,在 1.6 的版本中老年代空间使用率阈值(92%) ;如果预留的内存不够存放浮动垃圾,就会出现 Concurrent ModeFailure ,这时虚拟机将临时启用 Serial Old 来替代 CMS,冻结用户线程的执行了回收老年代,这样会导致很长的停顿时间。
3、空间碎片:这是由于CMS采用的是标记-清除算法导致的,当碎片较多时,给大对象的分配带来很大的麻烦,为了解决这个问题,CMS 提供一个参数: -XX:+UseCMSCompactAtFullCollection ( HotSpot™ 64-Bit Server VM is deprecated ),一般是开启的,如果分配不了大对象,就进行内存碎片的整理过程;这个地方一般会使用 Serial Old ,因为 Serial Old是一个单线程,回收时会暂停用户线程,然后进行空间整理。所以如果分配的对象较大,且较时,CMS 发生这样的情况会很卡。

ParNew
ParNew 收集器实质上是 Serial 收集器的多线程并行版本,除了使用多个线程并行收集外,其他行为和能使用的参数跟 Serial 收集器完全一致,可以和Serial Old 搭配使用。

并发标记

到目前为止,所有收集器在根节点枚举遍历其直接关联的对象时是要 STW的,并发收集器在继续往下进行可达性标记时是允许用户线程并发执行的,这样有效的减少了整体 STW 时间, 那这个并发标记到底是如何工作的呢?这就是我们要说的三色标记。

算法概述
首先约定好jvm在GC时会对对象进行颜色标记,按照对象是否被访问过这个条件将对象标记成以下三种颜色:
白色:表示该对象尚未被收集器访问过,在可达性分析结束后,仍为白色
的对象表示不可达,即为垃圾。要被回收
灰色:表示该对象已被收集器访问过,但是这个对象至少存在一个引用还未被扫描
黑色:表示该对象已被收集器访问过,并且它的所有引用都已被扫描,黑色对象是安全存活的。

另外:对于黑色对象
1、如果有其他对象的引用指向了黑色对象,无需重新扫描一遍
2、黑色对象不可能绕过灰色对象直接指向白色对象。

下面我们根据可达性分析算法来看一下三色标记的过程:

三色标记过程
初始状态
首先所有对象都是白色的,进行 GC Roots 枚举, STW ,枚举后只有 GC Roots 是黑色的

初始标记
初始标记仅仅只是标记一下 GC Roots 能直接关联的对象,速度很快,也会STW 。

并发标记
这个阶段是并发执行, GC 线程扫描整个引用链,分两种情况:
1、没有子节点,将本节点标记为黑色。
2、有子节点,将当前节点标记为黑色,子节点标记为灰色。
就这样继续沿着对象图遍历下去:

重新标记
这一阶段是修正在并发标记阶段因用户线程并发执行而产生的一系列问题,继续标记,直至灰色对象没有其它子节点引用时结束,这一阶段需要STW 。

扫描完成后,黑色对象就是存活的对象,白色对象就是已消亡可回收的对象。

三色标记的问题
在并发标记阶段的时候,因为用户线程与 GC 线程同时运行,有可能会产生多标或者漏标。
多标
如下图,假设已经遍历到 E(变为灰色了),此时应用程序将 D > E 的引用断开。

D > E 的引用断开之后, E、F、G 三个对象不可达,应该要被回收的。然而因为 E 已经变为灰色了,其仍会被当作存活对象继续遍历下去。最终的结果是:这部分对象仍会被标记为存活,即本轮 GC 不会回收这部分内存
这部分本应该回收但是没有回收到的内存,被称之为 浮动垃圾 。浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除。
另外,针对并发标记开始后的新创建的对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能会变为垃圾,这也算是浮动垃圾的一部分。

漏标
假设 GC 线程已经遍历到 E(变为灰色了),此时应用线程断开 E > G 的引
用,同时添加 D > G 的引用。

切回到 GC 线程,因为 E 已经没有对 G 的引用了,所以不会将 G 置为灰色;尽管因为 D 重新引用了 G,但因为 D 已经是黑色了,不会再重新做遍历处理。最终导致的结果是:G 会一直是白色,最后被当作垃圾进行清除。这直接影响到了应用程序的正确性,是不可接受的
当然漏标的发生有两个条件:
1、一个或者多个黑色对象重新引用了白色对象;即黑色对象成员变量增加了新的引用
2、灰色对象断开了白色对象的直接或间接引用;即灰色对象原来成员变量的引用发生了变化。
对于这种情况,我们需要将 G 这类对象记录下来,作为灰色对象在重新标
记阶段继续向下遍历,当然这个阶段需要 STW 。

读写屏障
针对于漏标问题,JVM 团队采用了读屏障与写屏障的方案。其目的很简
单,就是在读写前后将 G 这类对象给记录下来。
读屏障

oop oop_field_load(oop* field) { // 读屏障-读取前操作 pre_load_barrier(field); return *field;
}

当读取成员变量之前,先记录下来,这种做法是保守的,但也是安全的。因为条件1中【一个或者多个黑色对象重新引用了白色对象】,重新引用的前提是:得获取到该白色对象,此时已经读屏障就发挥作用了。
写屏障
所谓的写屏障,其实就是指给某个对象的成员变量赋值操作前后,加入一些处理(类似 Spring AOP 的概念)。

void oop_field_store(oop* field, oop new_value) { // 写屏障-写前操作 pre_write_barrier(field); *field = new_value; // 写屏障-写后操作 post_write_barrier(field, value);
}

不管是条件1还是条件2中,都有对一个灰色对象或者黑色对象的属性进行写操作。

增量更新与原始快照
解决漏标问题,只要破坏漏标的两个条件之一即可,不同收集器采用的方案也不一样,
增量更新(Incremental Update)
1、主要针对对象新增的引用,利用写屏障将其记录下来,这样破坏了条件1
2、后续重新扫描时还会继续从记录下来的新增引用深度扫描下去
CMS收集器采用的是这种方案。

原始快照(Snapshot At The Beginning,SATB)
1、当某个对象断开其属性的引用时,利用写屏障,将断开之前的引用记录下来,
2、尝试保留开始时的对象引用图,即原始快照,当某个时刻的 GC Roots确定后,当时的对象引用图就已经确定了。
3、后续标记是按照开始时的快照走,比如 E > G ,即使期间发生变化,通过写屏障记录后,保证标记还是按照原本的视图来,
4、SATB破坏的是漏标条件2,主要针对是引用的减少。G1收集器采用的是这种方案。

总结
基于可达性分析的 GC 算法,标记过程几乎都借鉴了三色标记的算法思想,尽管实现的方式不尽相同,比如标记的方式有栈、队列、多色指针等。
对于读写屏障,以 Java HotSpot VM 为例,其并发标记时对漏标的处理方
案如下:
1、 CMS :写屏障 + 增量更新
2、 G1、Shenandoah :写屏障 + 原始快照
另外,他们各自的总结如下:
1、原始快照相对增量更新来说效率更高,因为不需要在重新标记阶段再次深度扫描被删除引用对象,当然原始快照可能造成更多的浮动垃圾。
2、而 CMS 对增量引用的根对象会做深度扫描,G1 因为很多对象都位于不同的 region,CMS 就一块老年代区域;重新深度扫描对象的话 G1 的代价会比CMS 高,所以 G1 选择原始快照不深度扫描对象,只是简单标记,等到下一轮GC 再深度扫描。

G1

G1全称:Garbage First,是一种服务器式垃圾收集器,针对具有大内存的多处理器机器。它试图以高概率满足垃圾收集 (GC) 暂停时间目标,同时实现高吞吐量。是垃圾收集器发展史上里程碑式的成果,它开创了收集器面向局部收集的设计思路以及基于 Region 的内存布局形式。

G1是全堆操作且与应用程序线程并发执行,并通过多种技术实现高性能和暂停时间目标。G1的产生是为解决CMS算法产生空间碎片和其它一系列的问题缺陷,oracle官方计划在jdk9中将G1变成默认的垃圾收集器,以替代CMS。
JDK9默认G1为垃圾收集器的提案:https://openjdk.java.net/jeps/24
8 将CMS标记为丢弃的提案:https://openjdk.java.net/jeps/291

设计思想
作为CMS的替代者和继承人,设计者希望能够建立起具有:“停顿时间模型” 的收集器,通过该模型的意思是:要达到在指定时间为M毫秒内,垃圾收集耗时大概率不超过N毫秒的目标。
1、思想转变:要实现这个目标,首先要有一个思想上的转变,G1收集器出
现之前的其他所有收集器,他们的收集范围要么是新生代( Minor GC ),要么
是老年代( Major GC ),要么是整堆( Full GC ),而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集( Collection Set , CSet )进行回收,衡量标准不再是它属于哪个分代,而是哪个回收集中存放的垃圾最多,回收收益最大,就回收哪个

2、新的内存布局:当然G1能达到这个目标的关键在于G1开创了基于Region 的堆内存布局,当然也依然遵循了分代收集理论,但是堆内存布局与其他收集器有明显差异,G1不在坚持固定大小以及固定数量的分代区域划分,而是把连续的java堆内存划分成多个大小相等的独立区域( Region ),每个Region 可以根据需要扮演新生代的 Eden , Survivor ,或者老年代。收集器能够对扮演不同角色的 Region 采用不同的策略去处理,这样无论是对于新创建的对象还是对于熬过很多次垃圾收集的旧对象都有很好的收集效果。
Region 的大小可以通过参数 -XX:G1HeapRegionSize=value 设定,取值范围为 1MB~32MB,且应为 2 的 N 次幂。
Region 中还有一类特殊的 Humongous 区域,专门用来存储大对象。 G1认为只要大小超过了一个 Region 容量一半的对象即可判定为大对象,对于那些超过了整个 Region 容量的超级大对象,将会被存放在 N 个连续的Humongous Region 之中。
G1仍然保留了新生代,老年代的概念,只不过它们不再连续和固定的了,

3、回收策略:G1之所以能建立可预测的“停顿时间模型”的原因在于它将Region 作为单次回收的最小单元,即每次回收的空间都是 Region 的整数倍,同时G1会去追踪各个 Region 里面垃圾的“价值”(回收所获得的空间大小以及回收所需要的时间的经验值),然后在后台维护一个优先级列表,每次根据用户设定停顿时间( -XX:MaxGCPauseMillis=time ,默认200毫秒),优先回收价值收益最大的那些 Region 。
回收时G1 将存活的对象从堆的一个或多个 Region 复制到堆上的单个其他Region ,并在此过程中压缩和释放内存。这个工作是在多处理器上并行执行,以减少暂停时间并提高吞吐量。因此,每次垃圾回收时,G1 都会不断努力减少碎片

实现细节
G1将堆内存“化整为零”的思路看起来不难理解,但是有很多细节问题需要解决:
1、跨 Region 引用如何解决:前面我们知道通过记忆集( RSet )解决跨代
引用,但是在G1中,每个 Region 都需要维护自己的记忆集,记录别的 Region指向自己,但是G1中的 Region 数量要比传统收集器的分代数量明显多的多,所以G1中使用记忆集要比其他收集器有着更高的内存占用负担,根据经验,G1至少要耗费大约相当于java堆容量的10%~20%。
2、并发标记问题:如何保证并发标记阶段GC收集线程与用户线程互不干
扰,当然G1是通过原始快照( SATB )解决的(CMS是通过增量更新实现的)。
另外一个需要解决的就是并发回收阶段如何处理用户线程新创建对象的内存分配,G1的做法是为每个 Region 设计了两个名为 TAMS ( Top at Mark Start )的指针,把 Region 中的一部分空间划分出来用于存放并发回收过程中的新对象分配。G1收集器在本次回收时默认这些对象是存活的,不回收的。
3、如何建立可靠的可预测模型:用户通过 -XX:MaxGCPauseMillis=time
参数指定的停顿时间只是一个期望值,但是G1怎么做才能满足用户的期望呢?

G1收集器在收集过程中会记录每个 Region 的回收耗时,每个 Region 记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析出平均值,标准偏差,置信度等统计信息。根据这些信息决定 Region 的回收价值。

参数设置
1、启用G1收集器: -XX:+UseG1GC

2、设置分区大小: -XX:G1HeapRegionSize=value ,

3、设置最大GC暂停时间: -XX:MaxGCPauseMillis=time

4、设置堆的最大内存,对于需要大堆( >6GB )且GC延迟需求有限(稳定且可
预测的暂停时间低于0.5秒)的应用程序,推荐使用G1收集器。

运行过程

1、初始标记:标记出 GC Roots 直接关联的对象,并且修改TAMS指针的值,这个阶段速度较快,STW,单线程执行,
2、并发标记:从 GC Root 开始对堆中的对象进行可达性分析,找出存活对象,这个阶段耗时较长,但可以和用户线程并发执行。
3、重新标记:修正在并发标记阶段因用户程序执行而产生变动的标记记录,即处理 SATB 记录。STW,并发执行。
4、筛选回收:筛选回收阶段会对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划,筛出后移动合并存活对象到空Region,清除旧的,完工。因为这个阶段需要移动对象内存地址,所以必须STW。

思考一下,这属于什么算法呢???
答:从Region的动作来看G1使用的是标记-复制算法。而在全局视角上,类
似标记 - 整理

总结
G1前面的几步和CMS差不多,只有在最后一步,CMS是标记清除,G1需要
合并Region属于标记整理
优缺点
1、并发性:继承了CMS的优点,可以与用户线程并发执行。当然只是在并发标记阶段。其他还是需要STW
2、分代GC:G1依然是一个分代回收器,但是和之前的各类回收器不同,它同时兼顾年轻代和老年代。而其他回收器,或者工作在年轻代,或者工作在老年代;
3、空间整理:G1在回收过程中,会进行适当的对象移动,不像CMS只是简单地标记清理对象。在若干次GC后,CMS必须进行一次碎片整理。而G1不同,它每次回收都会有效地复制对象,减少空间碎片,进而提升内部循环速度。
4、可预测性:为了缩短停顿时间,G1建立可预存停顿的模型,这样在用户设置的停顿时间范围内,G1会选择适当的区域进行收集,确保停顿时间不超过用户指定时间。

几点建议:
1、如果应用程序追求低停顿,可以尝试选择G1;
2、经验值上,小内存6G以内,CMS优于G1,超过8G,尽量选择G1
3、是否代替CMS只有需要实际场景测试才知道。(如果使用G1后发现性
能还不如CMS,那么还是选择CMS)

面试篇之JVM(GC 可达性分析 回收算法 卡表 G1)相关推荐

  1. java 卡表,关于jvm:聊一聊Java垃圾回收与卡表技术

    文章收录地址:Java-Bang 专一于零碎架构.高可用.高性能.高并发类技术分享 在读博士的时候,我已经写过一个统计 Java 对象生命周期的动态分析,并且用它来跑了一些基准测试. [腾讯云]云产品 ...

  2. 【11-JVM面试专题-说说你知道的垃圾回收算法?垃圾回收器你知道吗?CMS、G1和ZGC垃圾回收器你有过了解吗?】

    JVM面试专题-说说你知道的垃圾回收算法?垃圾回收器你知道吗?CMS.G1和ZGC垃圾回收器你有过了解吗? JVM面试专题-说说你知道的垃圾回收算法?垃圾回收器你知道吗?CMS.G1和ZGC垃圾回收器 ...

  3. JVM:可达性分析算法

    在堆里存放着几乎多有的java对象实例,垃圾搜集器在对堆进行回收之前,第一件事情就是确定这些对象之中哪些还"存活"着(即通过任何途径都无法使用的对象). 一.可达性分析算法 在Ja ...

  4. JVM 的可达性分析法和四种引用

    JVM的垃圾回收机制的三个问题 回收哪些数据? 什么回收? 在哪里回收? 本就回答垃圾回收机制是回收哪些数据? 所谓"要回收的垃圾"无非就是那些不可能再被任何途径所使用的对象.无需 ...

  5. JVM GC日志分析

    常用JVM参数 http://blog.csdn.net/gzh0222/article/details/8223277  分析gc日志后,经常需要调整jvm内存相关参数,常用参数如下 -Xms:初始 ...

  6. 小师妹学JVM之:GC的垃圾回收算法

    文章目录 简介 对象的生命周期 垃圾回收算法 Mark and sweep Concurrent mark sweep (CMS) Serial garbage collection Parallel ...

  7. JVM GC(垃圾回收机制)

    在学习Java GC 之前,我们需要记住一个单词:stop-the-world .它会在任何一种GC算法中发生.stop-the-world 意味着JVM因为需要执行GC而停止了应用程序的执行.当st ...

  8. JVM GC(垃圾回收机制)Minro GC,Major GC/Full GC

    在学习Java GC 之前,我们需要记住一个单词:stop-the-world .它会在任何一种GC算法中发生.stop-the-world 意味着JVM因为需要执行GC而停止了应用程序的执行.当st ...

  9. 深入浅出JVM内存模型+垃圾回收算法

    文章目录 前言 JVM内存模型 1. 程序计数器(记录当前线程) 2. Java栈(虚拟机栈) 3. 本地方法栈 4. 堆 5.方法区 6.直接内存 JVM垃圾回收 垃圾判断标准 1. 引用计数法 2 ...

最新文章

  1. 保持联系,随时回来看看~
  2. vsftp认证之pam_mysql
  3. github报错“remote: Support for password authentication was removed on August 13, 2021. Please use a p”
  4. php怎么读取上传文件的md5,PHP获取上传文件的md5码_PHP教程
  5. 推荐一波腾讯专家的技术书单,请收下!
  6. nginx rewrite规则和参考
  7. 【项目管理】敏捷原则
  8. mysql 性能 比较好_MySQL性能优化的最佳20+条经验
  9. 免费当天澳洲运营商全天下载量达1841TB
  10. 图片处理拓展篇 : 图片转字符画(ascii)
  11. http://www.cnoug.org/viewthread.php?tid=9292
  12. raid卡 4k 设置 linux,硬盘“大户”看过来,手把手教你组建 RAID 磁盘阵列
  13. BZOJ3711 : [PA2014]Druzyny
  14. [讲课视频]谈谈C/C++中的整数
  15. vs 发生错误,需要终止调试... HRESULT=0x8000ffff。ErrorCode=0x0 解决办法
  16. 个人账号密码管理体系(密码篇)
  17. Linux align函数,linux内核中ALIGN解析(示例代码)
  18. 普通u盘linux不识别,Linux识别不了u盘怎么办
  19. 大数据专业学校课程安排 (仅供参考)
  20. Wi-Fi Display协议介绍

热门文章

  1. 用上帝视角俯瞰单点登录的前世与今生(内含两种实现方式的源码)
  2. 倍福模块维修倍福控制器维修CX1030-0123
  3. EasyRecovery for Mac(数据恢复软件)中文免费版
  4. 省域数字政府社会治理建设方案(附下载)
  5. 【问题集合】VDP2019虚拟现实设计平台常见问题集合
  6. 免费版pdf转换器下载
  7. 景联文科技:争创中国自动驾驶点云数据标注第一服务商
  8. MATLAB指令求解二维PDE三维PDE
  9. Linux 3.3.5系统移植(refer)
  10. 剑指offer(C++)-JZ26:树的子结构(数据结构-树)