JVM往期文章

  • 【JVM进阶之路】内存结构(一)
  • 【JVM进阶之路】玩转JVM中的对象(二)

上篇文章中讲到JVM中的对象以及判断对象的存活,那么对于“已死”的对象应该如何处理,怎么处理,这就需要靠JVM的垃圾回收机制。

本文脑图

GC的基本概念

垃圾回收(Garbage Collector,GC),JVM通过可达性分析判断那些对象可回收,而这些可回收的对象就是垃圾,为什么需要回收呢?

什么是GC

在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象;而在Java中,当没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾。 垃圾回收能自动释放内存空间,减轻编程的负担,JVM的一个系统级线程会自动释放该内存块。垃圾回收意味着程序不再需要的对象是"无用信息",这些信息将被丢弃。当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用。事实上,除了释放没用的对象,垃圾回收也可以清除内存碎片。

由于创建对象和垃圾回收器释放丢弃对象所占的内存空间,内存会出现碎片。碎片是分配给对象的内存块之间的空闲内存洞。碎片整理将所占用的堆内存移到堆的一端,JVM将整理出的内存分配给新的对象。

PS:什么是内存碎片

内存碎片一般是由于空闲的连续空间比要申请的空间小,导致这些小内存块不能被利用。产生内存碎片的方法很简单,举个栗子:

假设有一块一共有100个单位的连续空闲内存空间,范围是0~99

  • 如果你从中申请一块内存,如10个单位,那么申请出来的内存块就为0~9区间。
  • 这时候你继续申请一块内存,比如说5个单位大,第二块得到的内存块就应该为10~14区间。
  • 如果你把第一块内存块释放,然后再申请一块大于10个单位的内存块,比如说20个单位。因为刚被释放的内存块不能满足新的请求,所以只能从15开始分配出20个单位的内存块。
  • 现在整个内存空间的状态是0~9空闲,10~14被占用,15~24被占用,25~99空闲。其中0~9就是一个内存碎片了。
  • 如果10~14一直被占用,而以后申请的空间都大于10个单位,那么0~9就永远用不上了,造成内存浪费。
  • 如果你每次申请内存的大小,都比前一次释放的内村大小要小,那么就申请就总能成功。 通常来说,内存碎片是难以避免的,但却可以清除。

GC的意义

Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java中的对象不再有作用域的概念,只有对象的引用才有作用域。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。

分代回收机制

当前商业虚拟机的垃圾回收器,大多遵循分代收集的理论来进行设计,这个理论大体上是这么描述的:

  1. 绝大部分的对象都是朝生夕死
  2. 熬过多次垃圾回收的对象就越难回收。

根据以上两个理论,朝生夕死的对象放一个区域,难回收的对象放另外一个区域,这个就构成了新生代老年代

GC分类

  • 新生代回收(Minor GC/Young GC):指只是进行新生代的回收。

  • 老年代回收(Major GC/Old GC):指只是进行老年代的回收。目前只有 CMS 垃圾回收器会有这个单独的回收老年代的行为。(Major GC 定义是比较混乱,有说指是老年代,有的说是做整个堆的收集,这个需要你根据别人的场景来定,没有固定的说法) 。

  • 整堆回收(Full GC):收集整个 Java 堆和方法区(注意包含方法区)。

垃圾回收算法

复制算法

复制算法(Copying),将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可。其大致步骤为:

  1. 假设将内存分为A、B两块,开始A中且内存没有使用完,新对象o1~o4被分配到A中,如下:

  2. 当再有一个对象o5想添加到A中,发现内存已满,就把A中还存活的对象赋值到B中,把A内存清理(格式化),并把o5分配到B中,如下:

  3. 当后续对像添加发现内存满了之后继续上面的操作。

优点:实现简单,运行高效,没有内存碎片。

缺点:这种算法的代价是将内存缩小为了原来的一半。

复制算法的优化

复制回收算法适合于新生代,因为大部分对象朝生夕死,那么复制过去的对象比较少,效率自然就高,另外一半的一次性清理是很快的。所以,复制算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么复制算法的效率将会大大降低,因为能够使用的内存缩减到原来的一半。

那么有没有 什么方式去解决内存利用率的问题?当然有,

一种更加优化的复制回收分代策略:具体做法是分配一块较大的 Eden 区和两块较小的 Survivor 空间(你可以叫做 From 或者 To,也可以叫做 Survivor1 和Survivor2)

专门研究表明,新生代中的对象 98%是朝生夕死的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor[1]。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10%的内存会被浪费。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10%的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

对象可以无限制的在新生代复制吗,答案是否定的,新生代采用复制算法的原因是因为其中的对象基本都是朝生夕死的,那对于存活的对象每次发生Young GC时,对象头中的分代年龄便会增加1岁,当它的年龄增加到一定程度时(一般是15岁),就会被移动到老年代中。

看到一个有趣的解释,一个对象的这一辈子

我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的From区,自从去了Survivor区,我就开始漂了,有时候在Survivor的From区,有时候在Survivor的To区,居无定所。直到我15岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了老年代那边,老年代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在老年代里,我也不知道能活多久,可能很久,也可能…

标记清除算法

标记清除算法(Mark-Sweep),分为标记和清除两个阶段:

  1. 首先扫描所有对象标记出需要回收的对象。
  2. 在标记完成后扫描回收所有被标记的对象。

优点:算法简单、容易实现,且不会移动对象

缺点:回收效率略低,因为需要扫描两遍。如果大部分对象是朝生夕死,那么回收效率降低,因为需要大量标记对象和回收对象,对比复制回收效率要低。 最主要的问题是标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。回收的时候如果需要回收的对象越多,需要做的标记和清除的工作越多,所以标记清除算法适用于老年代(简单来说就是新生代的对象太多了,影响效率)。

标记整理算法

标记整理算法(Mark-Compact),算法分为标记、整理和清除三个阶段:

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

优点:不会产生内存碎片。

缺点:两遍扫描、指针需要调整,因此效率偏低。

我们知到标记整理与标记清除算法的区别主要在于对象的移动。对象移动不单单会加重系统负担,同时需要全程暂停用户线程才能进行,同时所有引用对象的地方都需要更新(直接指针需要调整)。

通常,老年代采用的标记整理算法与标记清除算法。

PS:为什么新生代不用标记清除算法或标记整理算法?

经典的垃圾回收器

在新生代中,每次垃圾回收时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成回收。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清理或者标记—整理算法来进行回收。

对于算法的实现主要是垃圾回收器在不同条件下有不同的分类,这些条件包括内存大小,是否多线程,STW(Stop The World)的时间等等。

PS:Stop The World(STW),单线程进行垃圾回收时,必须暂停所有的工作线程,直到它回收结束。这个暂停称之为Stop The World,但是这种 STW 带来了恶劣的用户体验,例如:应用每运行一个小时就需要暂停响应 5 分。这个也是早期 JVM 和 java 被 C/C++语言诟病性能差的一个重要原因。所以 JVM 开发团队一直努力消除或降低 STW的时间。

并行和并发

并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态。

并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定影响。

垃圾回收器的分布

Serial/Serial Old

Serial(中文翻译为串行,按顺序的),所以根据名字可以知道它是一个单线程工作的收集器,但JVM刚诞生就只有这种,最古老的,单线程,独占式,成熟,适合单 CPU,一般用在客户端模式下。

这种垃圾回收器只适合几十兆到一两百兆的堆空间进行垃圾回收(可以控制停顿时间再 100ms 左右),但是对于超过这个大小的内存回收速度很慢(STW的时间变长),所以对于现在来说这个垃圾回收器已经是一个鸡肋。

运行过程

PS:Safepoint是什么

参数设置

在JVM中可以通过参数-XX:+UseSerialGC 去设置

Parallel Scavenge/Parallel Old

Parallel(中文翻译为并行),为了提高回收效率,从 JDK1.3 开始,JVM 使用了多线程的垃圾回收机制,关注吞吐量的垃圾收集器,高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即:

吞吐量 = 运行用户代码时间/(运行用户代码时间+垃圾收集时间)

虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%。该垃圾回收器适合回收堆空间上百兆~几个 G。

运行过程

参数设置

在JVM中可以通过参数-XX:+UseParallelGC 去设置

JDK1.8默认就是以下组合,-XX:+UseParallelGC 新生代使用 Parallel Scavenge,老年代使用 Parallel Old

同时,收集器提供了两个参数用于精确控制吞吐量,分别控制的停顿时间的-XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的-XX:GCTimeRatio 参数。

  • 对于-XX:MaxGCPauseMillis ,不要认为如果把这个参数的值设置得更小一点就能使得系统的垃圾收集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的。系统把新生代调得小一些,收集 300MB 新生代肯定比收集 500MB 快,但这也直接导致垃圾收集发生得更频繁,原来 10 秒收集一次、每次停顿 100 毫秒,现在变成 5 秒收集一次、 每次停顿 70 毫秒。停顿时间的确在下降,但吞吐量也降下来了。
  • 对于-XX:GCTimeRatio ,参数的值则应当是一个大于 0 小于 100 的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。 例如:把此参数设置为 19,那允许的最大垃圾收集时占用总时间的 5% (即 1/(1+19)), 默认值为 99,即允许最大 1% (即 1/(1+99))的垃圾收集时间。

因此,如果手动去控制参数大小实际是不可控的,因为每个项目是不同的,所以JDK提供了默认了一个默认参数-XX:+UseAdaptiveSizePolicy,默认是开启的。

这是一个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRatio)、 晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

ParNew

多线程垃圾回收器,与 CMS 进行配合,对于 CMS(CMS 只回收老年代),新生代垃圾回收器只有 Serial 与 ParNew 可以选。和 Serial 基本没区别,唯一的区别:多线程,多 CPU 的,停顿时间比 Serial 少。(在 JDK9 以后,把 ParNew 合并到了CMS了)

CMS

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java 应用集中在互联网站或者 B/S 系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS 收集器就非常符合这类应用的需求。

运行过程

从名字(Mark Sweep)上就可以看出,CMS 收集器是基于标记—清除算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为 4 个步骤,如下:

  1. 初始标记,时间较短,仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快。
  2. 并发标记,和用户的应用程序同时进行,进行 GC Roots 追踪的过程,标记从 GC Roots 开始关联的所有对象开始遍历整个可达分析路径的对象。这个时间比较长,所以采用并发处理(垃圾回收器线程和用户线程同时工作)。
  3. 重新标记,时间较短,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
  4. 并发清除,用户和应用程序同时进行,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

概念性的东西往往难以记住,我们换个方式来理解,假如你去饭店吃饭:

  1. 当服务员刚上菜的时候,是两只大龙虾,服务员告诉你为了不影响其他顾客用餐不要把龙虾壳乱扔(这个大龙虾我们把他当做比较明显的GC Roots对象),这个时候你的状态是在听取意见(暂停)而并没有开始进餐,也就是对应的初始标记
  2. 服务员说完之后你开始进餐,你在吃龙虾的过程中,把龙虾壳放在了你正前面的盘子里面,这个时候服务员对你说如果盘子里的龙虾壳(垃圾)满了他就来清理,此时你吃你的,他说他的,互不干扰,是一个同时进行的过程,这个过程可以理解为并发标记
  3. 当你面前的盘子满了之后,你把盘子从你的正前面放到了另外的一个位置,并且你叫服务员来清理一下,然后准备等清理之后在继续吃第二只龙虾(也就是说你现在处于暂停的状态),但此时服务员手上都是垃圾,于是他又重新记住了盘子的位置说等下就来,这个过程可以理解为重新标记
  4. 此时,服务员重新给了你一个新的盘子,然后你就继续吃下一只龙虾,服务员就把装满龙虾壳的盘子给收走了,你吃你的,他收他的,这个过程可以理解为并发清除

也就是说,你吃东西的过程当做用户线程,而服务员收垃圾的过程当做GC,感觉是挺好理解的,反正我是记住了,哈哈哈。

存在的问题

CMS相比前面讲到的回收器是比较优秀的,主要就是体现在它的并发和低停顿,但同时它也存在一些缺点,主要表现在这3个方面:

  1. CPU 敏感:CMS 对处理器资源敏感,因为采用了并发的收集、当处理核心数不足 4 个时,CMS 对用户的影响较大。

  2. 浮动垃圾:由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为浮动垃圾。

  3. 并发模式失败:由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。在 1.6 的版本中老年代空间使用率阈值(92%) 如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。

    简单来说就是在老年代内存要满的时候会进行Full GC,但在Full GC的过程中可能会有新的对象进入老年代,那此时必定会进入STW的状态,并且CMS会自动切换到用Serial old垃圾收集器来回收。Serial是一个单线程的垃圾回收器。那这种情况出现是不是会严重降低我们的执行效率?

  4. 内存碎片:CMS采用的是标记 - 清除算法,因此会导致产生不连续的内存碎片。

总体来说,CMS 是 JVM 推出了第一款并发垃圾收集器,所以还是非常有代表性。但是最大的问题是 CMS 采用了标记清除算法,所以会有内存碎片,当碎片较多时,给大对象的分配带来很大的麻烦,为了解决这个问题,CMS 提供一个参数:-XX:+UseCMSCompactAtFullCollection,一般是开启的,如果分配不了大对象,就进行内存碎片的整理过程。

那为什么 CMS 采用标记-清除?

在实现并发的垃圾回收时,如果采用标记整理算法,那么还涉及到对象的移动(对象的移动必定涉及到引用的变化,这个需要暂停业务线程来处理栈信息,这样使得并发收集的暂停时间更长,而CMS的主要目的就是为了降低STW的时间),所以使用简单的标记-清除算法才可以降低 CMS 的 STW 的时间。该垃圾回收器适合回收堆空间几个G~ 20G 左右。

G1

G1(Garbage First),被Oracle官方称为全功能的垃圾收集器

设计思想

随着 JVM 中内存的增大,STW 的时间成为 JVM 急迫解决的问题,但是如果按照传统的分代模型,总跳不出 STW 时间不可预测这点。为了实现 STW 的时间可预测,首先要有一个思想上的改变。G1 将堆内存化整为零,将堆内存划分成多个大小相等独立区域(Region),每一个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间。回收器能够对扮演不同角色的 Region 采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

Region到底是什么?

Region 可能是 Eden,也有可能是 Survivor,也有可能是 Old,另外 Region 中还有一类特殊的 Humongous 区域,专门用来存储大对象。 G1 认为只要大小超过了一个 Region 容量一半的对象即可判定为大对象。每个 Region 的大小可以通过参数-XX:G1HeapRegionSize 设定,取值范围为 1MB~32MB,且应为 2 的 N 次幂。而对于那些超过了整个 Region 容量的超级大对象,将会被存放在 N 个连续的 Humongous Region 之中,G1 的进行回收大多数情况下都把 Humongous Region 作为老年代的一部分来进行看待。

因此,对于G1最主要的特点是G1的内存区域是不固定的。如下E变为S,O变为了E:

运行过程

G1 的运作过程大致可划分为以下四个步骤:

  1. 初始标记,仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region 中分配新对象。 这个阶段需要停顿线程,但耗时很短,而且是借用进行 Minor GC 的时候同步完成的,所以 G1 收集器在这个阶段实际并没有额外的停顿。

    TAMS 是什么?

    要达到 GC 与用户线程并发运行,必须要解决回收过程中新对象的分配(虽然我在进行垃圾回收,但是还是会有新对象产生),所以 G1 为每一个 Region 区域设计了两个名为 TAMS(Top at Mark Start)的指针,从 Region 区域划出一部分空间用于记录并发回收过程中的新对象。这样的对象认为它们是存活的,不纳入垃圾回收范围。

  2. 并发标记,从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后 ,并发时有引用 变动的对象,这些对象会漏标 , 漏标的对象会被一 个叫做SATB(snapshot-at-the-beginning)算法来解决(不理解先不急,往下看)。

  3. 最终标记,对用户线程做另一个短暂的暂停,用于处理并发阶段结后仍遗留下来的最后那少量的 SATB 记录(漏标对象)。

  4. 筛选回收,负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划(也就是说用户可以指定停顿时间),可以自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

存在的问题

没有一款垃圾收集器是完美无缺的,只能分场景选择最适合的垃圾收集器,对于于 G1来说,主要存在以下问题:

  1. 跨代引用,堆空间通常被划分为新生代和老年代。由于新生代的垃圾收集通常很频繁,如果老年代对象引用了新生代的对象,那么回收新生代的话,需要跟踪从老年代到新生代的所有引用,所以要避免每次 Young GC 时扫描整个老年代,减少开销。
  2. 并发情况下的漏标问题,通过三色标记来分析。

三色标记(重点)

针对CMS和G1存在的漏标问题,JVM通过三色标记算法来解决。

在三色标记法之前的算法叫 Mark-And-Sweep(标记清除)。这个算法会设置一个标志位来记录对象是否被使用。最开始所有的标记位都是 0,如果发现对象是可达的就会置为 1,一步步下去就会呈现一个类似树状的结果。等标记的步骤完成后,会将未被标记的对象统一清理,再次把所有的标记位设置成 0 方便下次清理。

这个算法最大的问题是 GC 执行期间需要把整个程序完全暂停,不能异步进行 GC 操作。因为在不同阶段标记清扫法的标志位 0 和 1 有不同的含义,那么新增的对象无论标记为什么都有可能意外删除这个对象。对实时性要求高的系统来说,这种需要长时间挂起的标记清扫法是不可接受的。所以就需要一个算法来解决 GC 运行时程序长时间挂起的问题,那就三色标记法。

三色标记最大的好处是可以异步执行,从而可以以中断时间极少的代价或者完全没有中断来进行整个 GC。

根据GC Roots可达性分析算法遍历对象的过程中,按照对象是否访问过这个条件标记成以下三种颜色:

  • **黑色:**根对象,或者该对象与它的子对象都被扫描过。
  • **灰色:**对本身被扫描,但是还没扫描完该对象的子对象。
  • **白色:**未被扫描对象,如果扫描完所有对象之后,最终为白色的为不可达对象,既垃圾对象。

存在的问题

需要注意的是,对象是否需要被回收主要是通过可达性分析来判断的,但是在GC的并发标记过程中,程序还是在跑的状态,因此对象之间的引用可能会发生改变,这样可能出现两种后果:

  • 多标,把原本死亡的对象错误标记为存活,导致的后果是在本次垃圾回收不会收集。

  • 漏标,把原本存活的对象错误标记为已死亡,这就是非常致命的后果了,程序肯定会因此发生错误。如下:

针对CMS的解决方案-增量更新

对于存在的漏标问题,CMS主要是采用增量更新算法(Incremental Update)来解决。

什么是增量更新?

当一个白色对象被一个黑色对象引用,将黑色对象重新标记为灰色,让垃圾回收器重新扫描。

有何缺点?

上面重新扫描的情况是比较简单的,一旦A对象的引用很多,那必定会扫描这个黑色对象的所有引用,耗时增加。

针对G1的解决方案-SATB

对于G1来讲,主要是通过SATB(snapshot-at-the-beginning)的方式来处理,简单来说就是在标记之前记录下一个快照(可理解为照片),然后再回收之前和前面的快照进行对比即可,其步骤可以简单的描述为:

  1. 如下图,在灰色对象B取消对白色对象的引用之前(即B.c=null),先把它记录下来。

  2. 在线程1和线程2完成标记之前在对比,发现原来B和C的连线断了,而A和C之间却相连,也就是引用。

  3. 再以这个引用指向的白色对象为根,直接对它的引用进行扫描。

所以,SATB可以简单理解为,当一个灰色对象取消了对白色对象的引用,那么这个白色对象变为灰色下次继续被扫描。这也就回到了上面讲的G1运行过程中的第3步,最终标记

有何缺点?

如果说在快照对比的时候发现这个白色对象并没有黑色对象去引用它,但是对比之后仍然把它置为灰色,此时本应该是要被回收的,但实际还是没有被回收,在这次的GC存活下来,这就是所谓的浮动垃圾,但相比增量更新来说,只是浪费了一点空间,但是却节约了时间。

为什么会存在漏标对象?

分析了漏标问题的解决方案,我们可以得出如果产生漏标对象,必然:

  1. 至少有一个黑色对象(也就是被标记的对象)指向了一个白色对象。
  2. 删除了灰色对象到白色对象的直接或间接引用。

为什么G1用SATB?CMS用增量更新?

跨代引用

堆空间通常被划分为新生代和老年代,所谓跨代引用,一般是指老年代对象引用了新生代的对象。如下图的X和Y引用:

我们知道新生代的垃圾收集通常很频繁(朝生夕死),如果老年代对象引用了新生代的对象,那么在回收新生代(Young GC)的时候,需要跟踪从老年代到新生代的所有引用。

记忆集

跨代引用主要存在于Young GC的过程中,除了常见的GC Roots之外,如果老年代有对象引用了的新生代对象,那么老年代的对象也属于GC Roots(如上图中的老年代对象B和C)对象,但是如果每次进行Young GC我们都需要扫描一次老年代的话,那我们进行垃圾回收的代价实在是太大了,因此收集器在新生代上建立一个全局的称为记忆集的数据结构来记录这种引用关系。

Rset(Remember Set),简单来说就是一种抽象数据结构用来存老年代对象新生代的引用(即引用X和Y)。

卡表

卡表(CardTable)在很多资料上被认为是对记忆集的实现(我其实不大能理解,但先这样吧

【JVM进阶之路】垃圾回收机制和GC算法之三色标记(三)相关推荐

  1. 详解JVM内存管理与垃圾回收机制2 - 何为垃圾

    随着编程语言的发展,GC的功能不断增强,性能也不断提高,作为语言背后的无名英雄,GC离我们的工作似乎越来越远.作为Java程序员,对这一点也许会有更深的体会,我们不需要了解太多与GC相关的知识,就能很 ...

  2. 详解JVM内存管理与垃圾回收机制5 - Java中的4种引用类型

    在Java语言中,除了基础数据类型的变量以外,其他的都是引用类型,指向各种不同的对象.在前文我们也已经知道,Java中的引用可以是认为对指针的封装,这个指针中存储的值代表的是另外一块内存的起始地址(对 ...

  3. JVM内存区域(Java内存区域)、JVM垃圾回收机制(GC)初探

    一.JVM内存区域(Java内存区域) 首先区分一下JVM内存区域(Java内存区域)和Java内存模型(JMM)的概念.Java线程之间的通信采用的是共享内存模型,这里提到的共享内存模型指的就是Ja ...

  4. JVM原理(Java代码编译和执行的整个过程+JVM内存管理及垃圾回收机制)

    转载注明出处: http://blog.csdn.net/cutesource/article/details/5904501 JVM工作原理和特点主要是指操作系统装入JVM是通过jdk中Java.e ...

  5. 【JVM】Java垃圾回收机制(GC)详解

    Java垃圾回收机制(GC)详解 一.为什么需要垃圾回收? 如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收.除非内存无限大,我们可以任性的分配不回收,但是事实并非如 ...

  6. 【Java 虚拟机原理】垃圾回收算法 ( Java 虚拟机内存分区 | 垃圾回收机制 | 引用计数器算法 | 引用计数循环引用弊端 )

    文章目录 一.Java 虚拟机内存分区 二.垃圾回收机制 三.引用计数器算法 ( 无法解决循环引用问题 ) 一.Java 虚拟机内存分区 Java 虚拟机内存分区 : 所有线程共有的内存区域 : 堆 ...

  7. java gc 可达性_JAVA--GC 垃圾回收机制----可达性分析算法

    在JVM 中,java 为我们提供可有效的垃圾回收机制,GC ,GC的创建无疑是为了缓解内存压力.保存有效数据.回收垃圾无效数据: 在此之前GC在我的理解中,一直只是个概念,内存中出现垃圾,GC来回收 ...

  8. 什么是垃圾回收机制(GC)

    JS的垃圾回收机机制 什么是垃圾回收机制(GC)? 内存管理 Javascript垃圾回收方法 哪些操作会造成内存泄漏? 什么是垃圾回收机制(GC)? 早期的计算机语言,比如C和C++,需要开发者手动 ...

  9. Erlang 垃圾回收机制(GC)

    为了创建高高响应的软实时系统,需要快速的垃圾回收机制,启动GC时不会造成系统响应时间的延迟;erlang作为Immutable(不可变)语言,有较高的垃圾生成率,也需要高效的垃圾回收机制. Memor ...

最新文章

  1. 第三章 改进神经网络的学习方式(上中)
  2. Hyperledger Fabric 核心模块(3)cryptogen 工具
  3. v-model实现数据的双向绑定
  4. Ubuntu文件压缩、解压缩、打包
  5. 字符串连接“+”int、char、string
  6. VirtualBox虚拟机与主机互相访问开启
  7. 【Pytorch神经网络理论篇】 06 神经元+神经网络模型+全连接网络模型
  8. python做炫酷的界面_用python打造可视化爬虫监控系统,酷炫的图形化界面
  9. 多个类共同继承一个父类
  10. 平面设计模板素材|越发流行的霓虹渐变趋势
  11. Spring MVC学习总结(17)——SpringMVC之接口规范与Controller规范
  12. Android开发笔记(十九)底部标签栏TabBar
  13. 在N多气象服务构成的疯狂数据城 AS8000挑起大梁
  14. Illustrator 教程,如何在 Illustrator 文档中缩放和平移?
  15. 【MFC开发(1)】MFC的介绍、与Qt的区别
  16. 如何让WebStorm正常显示出中文提示,使得中文能够正常输入,请看这里
  17. 【Tensorflow教程笔记】使用 TPU 训练 TensorFlow 模型
  18. ax200 兼容性问题 老路由器_我的华硕AX89X 160频宽和MU-MIMO问题,小米10测速-路由器交流...
  19. 身份证号码组件:lt;idcardgt; —— 快应用组件库H-UI
  20. 如何在Dev-c++中打c语音的代码

热门文章

  1. java面条对折问题
  2. 【知识积累】Edge vs Fog Computing 边缘计算和雾计算的基本介绍
  3. QGtkStyle was unable to detect the current GTK+ theme
  4. 九秀直播助手下载|九秀直播助手下载
  5. 敏捷转型行动笔记:内部敏捷教练实践
  6. Could not connect to archive.ubuntukylin.com:10006 (120.240.95.35), connection timed out
  7. 编写c语言的开篇——Hello World
  8. 独身主义也不赖:选择不结婚究竟是选择了什么?
  9. 不想做却不得不做某份工作,怎么破?
  10. 解决office for mac 的word显示字体乱码问题