JVM(四)分代垃圾回收机制和垃圾回收算法

一、什么是GC

​ GC (Garbage Collection)垃圾回收,顾名思义就是专门回收垃圾的。,在C/C++中,我们需要用到内存的时候,需要先手动申明一下,使用完后又需要在手动回收一下,这两部非常麻烦而且还经常会出这个方面的问题。而这一切在Java中就已经被自动执行掉了,所以我们写代码的时候都不用再管这些无效的数据。

二、GC分类

​ 在目前主流的虚拟机中,大多都是根据分代收集的理论来进行设计的。因为在虚拟机中绝大部分的对象都是朝生夕死的,而熬过了多次的垃圾回收后的对象就越难被回收。所以前面的理论堆就被划分成了两个区域,新生代老年代,前者主要存储那些朝生夕死的对象,后者存放难死的对象。

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

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

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

三、垃圾回收算法

1、复制算法(Copying)

​ 将一块内存区域进行对半分,当有一半的内存使用完时将还存活的对象放到另一半内存区域中,原来的内存区域进行回收,不用考虑内存碎片区域,只要按顺序分配内存就行。实现简单,运行高效。

​ 但是这样也有个缺点就是对内存的利用率只有50%,于是在JVM中就有了以下的解决办法:

Appel式回收

​ Eden区的添加,一般来说的内存区域的分配为:Eden:80%,Survivor:20%(From 10%,To 10%),当Survivor区不够用的时候,就需要老年代进行分配担保。

2、标记-清除法(Mark-Sweep)

​ 算法分为“标记”和“清理”两个阶段:第一步扫描需要标记所有可以被回收的对象,第二遍扫描需要清理被第一步标记的对象,效率略低。因为需要大量的标记对象和清除所以回收效率是不复制算法的,如果大部分的对象是朝生夕死的那么标记的对象就会更多,效率会更低。

​ 它还有个主要问题就是会产生大量的内存碎片导致大对象无法进行存储,从而不得不提前触发其他的垃圾回收。

3、标记-整理法(Mark-Compact )

​ 步骤与清除法步骤一致但是,它的第二步是整理标记之外的所有对象,将所有对象向前移动之后直接清除掉这些对象所在之外的内存区域。标记法不会存在内存碎片,但是效率是遍低的。

​ 整理法和清除法的主要区别就是一个是回收对象,一个整理对象,而移动对象还会需要暂停所有的业务线程后更新所有对象的引用(直接指针需要调整)。

四、JVM垃圾回收器

1、Serial/Serial Old

​ JVM诞生初期所采用的垃圾回收器,单线程,独占式,适合单CPU。

​ 它只适合堆内存几十兆到几百兆,如果超过的这个内存的大小则会大大的降低回收效率,所以在目前很鸡肋。

Stop The World(STW

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

2、Parallel/Parallel Old

​ 为了提高JVM的回收效率,从JDK 1.3开始,JVM使用了多线程的垃圾回收器,关注吞吐量的垃圾回收器,可以更高效的利用CPU时间,从而尽快完成程序的运算任务。

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

​ 该垃圾回收器适合回收堆空间上百兆~几个G。

JVM参数设置

JDK1.8 默认就是以下组合

-XX:+UseParallelGC 新生代使用 Parallel Scavenge,老年代使用 Parallel Old

-XX:MaxGCPauseMillis

不过不要异想天开地认为如果把这个参数的值设置得更小一点就能使得系统的垃圾收集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐 量和新生代空间为代价换取的:系统把新生代调得小一些,收集 300MB 新生代肯定比收集 500MB 快,但这也直接导致垃圾收集发生得更频繁,原来 10 秒收集一次、每次停顿 100 毫秒,现在变成 5 秒收集一次、 每次停顿 70 毫秒。停顿时间的确在下降,但吞吐量也降下来了。

-XX:GCTimeRatio

-XX:GCTimeRatio 参数的值则应当是一个大于 0 小于 100 的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。

例如:把此参数设置为 19, 那允许的最大垃圾收集时占用总时间的 5% (即 1/(1+19)), 默认值为 99,即允许最大 1% (即 1/(1+99))的垃圾收集时间由于与吞吐量关系密切,ParallelScavenge 是“吞吐量优先垃圾回收器”。

-XX:+UseAdaptiveSizePolicy

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

3、ParNew/CMS

ParNew

​ 多线的垃圾回收器与Parallel差不多,唯一的区别:多线程,多 CPU 的,停顿时间比 Serial 少。(在 JDK9 以后,把 ParNew 合并到了 CMS 了) 。

Concurrent Mark Sweep(CMS)

​ 此类垃圾回收器是追求最短的回收停顿时间(STW)为目标的。目前还有是很大一部分的 Java 应用集中在互联网或者 B/S 系统的服务端上,这类应用比较重视服务的响应速度,希望停顿时间更短以提升用户的体验。

​ Mark Sweep 从名字上可以看出来,这个回收器采用的是标记 - 清除法。而它的步骤比起前面的几个回收器都更麻烦些。

​ 整体过程分为 4 个步骤:

初始标记:只标记与 GC Root 有直接关联的对象,这类的对象比较少,标记快。

并发标记:标记与初始化标记的对象有关联的所有对象,这类的对象比较多所以采用的并发,与用户线程一起跑。

重新标记:修正那些并发标记时候标记产生异动的对象标记,这块的时间比初始标记稍长一些,但是比起并发标记要快很多。

并发清除:与用户线程一起运行,进行对象回收。

-XX:+UseConcMarkSweepGC ,表示新生代使用 ParNew,老年代的用 CMS。

缺点

CPU敏感:因为采用的并发的技术所以对处理器的核心要求较大。

浮动垃圾:在CMS进行并发清楚的时候因为采用的是并发的轻快,所以在清除的时候用户线程会产出新的垃圾。

​ 因此在进行回收的时候需要预留一部分的空间来存放这些产生垃圾(JDK 1.6 设置的阈值为92%)。

​ 但是如果用户线程产出的垃圾比较快,预留内存放不下的时候就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。

内存碎片:因为采用的是 标记 - 清除 法所以会产生内存碎片。

特点:

​ 总体来说因为 CMS 是 JVM 产生的第一个并发垃圾收集器,所以还是具有代表性的。为什么采用 标记 - 清除 法,因为在实现 CMS 的时候如果还整理对象的话,那么需要再暂停业务线程,进行一个对象的整理那么 STW 的时间会更长,为了追求 STW 的时间所以没有采用 标记 - 整理。

但是最大的问题是 CMS 采用了标记清除算法,所以会有内存碎片,当碎片较多时,给大对象的分配带来很大的麻烦,为了解决这个问题,CMS 提供一个 参数:-XX:+UseCMSCompactAtFullCollection,一般是开启的,如果分配不了大对象,就进行内存碎片的整理过程。 这个地方一般会使用 Serial Old ,因为 Serial Old 是一个单线程,所以如果内存空间很大、且对象较多时,CMS 发生这样情况会很卡。

​ 该垃圾回收器适合回收堆空间几个 G~ 20G 左右。

4、 Garbage First (G1)

​ G1 垃圾回收器的设计思想与前面所有的垃圾回收器的都不一样,前面垃圾回收器采用的都是 分代划分 的方式进行设计的,而 G1 则是将堆看作是一个整体的区域,这个区域被划分成了一个个大小一致的独立区域(Region),而每个区域都可以根据需要扮演Eden、Survivor以及老年代区域。当进行对象回收的时候就可以根据每个区域的情况进行一个回收,从而效率。

Region

​ 上面讲到除了每个Region可以扮演不同的区域,还有一个类似老年代的区域 Humongous 区域,用来专门存放大对象的。当一个对象超过了Region区空间的一半大小则判定为大对象。(每个 Region 的大小可以通过参数-XX:G1HeapRegionSize 设定,取值范围为 1MB~32MB,且应为 2 的 N 次 幂。)

​ 而对于那些超过了整个 Region 容量的超级大对象,将会被存放在 N 个连续的 Humongous Region 之中,G1 的进行回收大多数情况下都把 Humongous Region 作为老年代的一部分来进行看待。

开启参数 :-XX:+UseG1GC `

分区大小:-XX:+G1HeapRegionSize

一般建议逐渐增大该值,随着 size 增加,垃圾的存活时间更长,GC 间隔更长,但每次 GC 的时间也会更长。

最大 GC 暂停时间 :-XX:MaxGCPauseMillis

运行过程

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

初始标记 (Initial Marking) :标记与 GC Roots 能关联到的对象,修改 TAMS 指针的值,这个过程是需要暂停用户线程的,但是耗时非常的短。

​ TAMS (Top at Mark Start):当进行下一步并发标记的时候用户线程是会产生新的对象的,而这些对象是被判定为可存活对象而非垃圾。这个时候就需要划分一小块区域来存放这这些对象。

并发标记 (Concurrent Marking):进行扫描标记所有课回收的对象。当扫描完成后,并发会有引用变化的对象,而这些对象会漏标这些漏标的对象会被 SATB 算法所解决。

​ SATB(snapshot-at-the-beginning):类似快照,对当前区域进行一个快照的保存,之后再最终标记的时候进行对比查看漏标的会被重新标记上(后面的文章会详解)。

最终标记 (Final Marking): 暂停所有的用户线程,对之前漏标的对象进行一个标记。

筛选回收( Live Data Counting and Evacuation):更新Region的统计数据,对各个 Region 的回收价值进行一个排序,根据用户所设置的停顿时间制定一个回收计划,自由选择任意个 Region 进行回收。将需要回收的Region 复制到空的 Region 区域中,再清除掉原来的整个Region区域。这块还涉及到对象的移动所以需要暂停所有的用户线程,有多条回收器线程进行完成。

特点:

并行与并发:G1 能充分利用多 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿的时间,部分其他收集器

原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。

分代收集:与其他收集器一样,分代概念在 G1 中依然得以保留。虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但它能够采用不同的方式

去处理新创建的对象和已经存活了一段时间、熬过多次 GC 的旧对象以获取更好的收集效果。

空间整合:与 CMS 的“标记—清理”算法不同,G1 从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。

追求停顿时间:-XX:MaxGCPauseMillis 指定目标的最大停顿时间,G1 尝试调整新生代和老年代的比例,堆大小,晋升年龄来达到这个目标时间。

​ 该垃圾回收器适合回收堆空间上百 G。一般在 G1 和 CMS 中间选择的话平衡点在 6~8G,只有内存比较大 G1 才能发挥优势

posted @ 2021-02-02 13:18  某人人莫  阅读(1216)  评论(0编辑  收藏  举报