四、分代垃圾回收机制及垃圾回收算法

学习垃圾回收的意义

Java 与 C++等语言最大的技术区别:自动化的垃圾回收机制(GC)

为什么要了解 GC 和内存分配策略

1、面试需要

2、GC 对应用的性能是有影响的;

3、写代码有好处

栈:栈中的生命周期是跟随线程,所以一般不需要关注

堆:堆中的对象是垃圾回收的重点

方法区/元空间:这一块也会发生垃圾回收,不过这块的效率比较低,一般不是我们关注的重点

一、什么是GC

自动化的垃圾回收机制

回收区域:

堆:堆中的对象是垃圾回收的重点

方法区/元空间:这一块也会发生垃圾回收,不过这块的效率比较低,回收 class,常量,静态常量,字符串常量池

二、分代回收理论及GC分类

1、堆内存为什么分新生代和老年代

原因如下:

1.绝大部分(98%)对象都是朝生夕死

2.熬过多次垃圾回收的对象就越难回收

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

新生代占堆内存的1/3,又分为Eden区,From区,To区,比例为8:1:1

2、GC(Garbage Collection)分类

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

特点:发生在新生代上,发生的较频繁,执行速度较快

触发条:Eden 区空间不足

2、老年代回收(Major GC/Old GC):指的是进行老年代回收。(目前只有CMS垃圾回收器会有这个单独回收老年代的行为)

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

特点: 主要发生在老年代上(新生代也会回收),较少发生,执行速度较慢

触发条件:调用 System.gc()、老年代区域空间不足、空间分配担保失败、JDK 1.7 及以前的永久代(方法区)空间不足。CMS GC 处理浮动垃圾时,如果新生代空间不足,则采用空间分配担保机制,如果老年代空间不足,则触发 Full GC

3、新生区的垃圾回收机制

新生区分为:Eden区、Survivor0区、Survivor1区(也称为from区和to区)

其中Eden区占80%的内存空间,每块Survivor各占用10%的内存空间(如:Eden占800M,每个Survivor占100M)

1.开始时创建的对象都是分配在Eden区域中,当Eden区快满了,就会触发垃圾回收Minor GC(使用复制算法进行垃圾回收)

2.Minor GC处理后,首先会把Eden区中还存活着的对象一次性转入其中一块空闲着的Survivor区。然后清空Eden区,之后创建的对象就继续放入Eden区中了,直至下次Eden又被填满。

3.Eden再次被填满时,就会再次出发Minor GC,清理后(Minor会清理Eden区和Survivor区的内存),Eden区和存在对象的Survivor区(此时的from区)中存活的对象转移到另一块空着的Survivor区中(此时的to区),并清空Eden区和之前存在对象的Survivor区(此时变为to区了,"From"和"To"会交换他们的角色,也就是新的"To"就是上次GC前的"From",新的"From"就是上次GC前的"To"。)

这就是复制算法的流程。

一直要保持一个Survivor区是空的以提供复制算法垃圾回收,而这块区域的内存只占整块的10%,其他90%内存都可以被使用,课件内存利用率还是相当高的。

三、垃圾回收算法

  • 复制算法
  • 标记清除
  • 标记整理

1、复制算法(Copying)----新生代多用此算法

将可用内存按容量划分为大小相等的两块,比如A块,B块,每次只使用其中的一块。假如A块快用完了,就把A块还存活着的对象复制到B块上面,然后再把A块空间一次性清理掉,反之一样。这样每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可。实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。

但是要注意:内存移动是必须实打实的移动(复制),所以对应的引用(直接指针)需要调整。

复制回收算法适合于新生代,因为大部分对象朝生夕死,那么复制过去的对象比较少,效率自然就高,另外一半的一次性清理是很快的。

优点:实现简单、运行高效,没有内存碎片,只需扫一遍

缺点:利用率只有一半,所有引用(直接指针)需要调整(因为整体内存都移动了,内存地址变了)。存活对象较多时效率明显会降低

  • Appel式回收

因为复制算法缺点利用率只有一半,就出现了eden区和两块较小的Survivor空间,from区和to区 ,这样只用浪费10%。

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

新生代的内存空间比例是Eden : From : To = 8 : 1 : 1

研究表明,新生代中的对象98%是"朝生夕死"的,所以并不需要按照1:1的比例划分,而是将新生代划分为一块较大的Eden区和两块较小的Survivor空间,每次使用Eden区和其中一块Survivor,当回收的时候,将Eden区和Survivor中还存活着的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才说用过的Survivor空间,这样新生代的浪费空间只有10%。

HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10%的对象存活,当10%的 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)

-XX:SurvivorRatio 8 8:1:1

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

2、标记-清除算法----老年代多用此算法

标记清除算法分为"标记"和"清除"两个阶段,第一次扫描所有的对象标记出需要回收的对象。第二次扫描回收被标记的对象。

优点:内存利用率100%

缺点:比复制算法效率略低,因为两遍扫描,内存位置不连续,产生碎片。因为空间碎片太多可能导致分配大对象时无法找到足够的连续内存而不得提前触发另一次垃圾回收动作。

回收的时候如果需要回收的对象越多,需要做的标记和清除的工作越多,所以标记清除算法适用于老年代。

3、标记-整理算法----年代多用此算法

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

优点:没有碎片,利用率100%

缺点:效率偏低,两遍扫描,对象移动导致指针需要调整(整理的时候需要移动),对象移动不单会加重系统负担,同时需要全程暂停用户线程(stw)才能进行,同时所有引用对象的地方都需要更新

四、常见的垃圾回收器

1、垃圾回收器基础

新生代中,每次垃圾回收时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成回收。

老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用"标记---清理"或者"标记---整理"算法来进行回收。

    1. 单线程垃圾回收器 Serial、Serial Old
    2. 多线程并行垃圾回收器 Parallel Scavenge、ParNew、Parallel Old
    3. 多线程并发垃圾回收器 CMS、G1

并行:垃圾收集器的多线程同时进行。

并发:垃圾收集的多线程和应用的多线程同时进行。

注:吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间) 虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%。

垃圾收集时间= 垃圾回收频率 * 单次垃圾回收时间

Stop The World(STW)指的是停止所有的用户线程

单线程进行垃圾回收时,必须暂停所有的工作线程,直到它回收结束。这个暂停称之为"Stop The World", 但是这种 STW 带来了恶劣的用户体验

例如:应用每运行一个小时就需要暂停响应 5 分。这个也是早期 JVM 和 java 被 C/C++语言诟病性能差的一个重要原因。所以 JVM 开发团队一直努力消除或降低 STW的时间。

GC 收集器和我们 GC 调优的目标就是尽可能的减少 STW 的时间和次数。

各种垃圾收集器组合示意图(黑色的线表示组合)如下:

Oracle 官方也有对应英文解释 https://docs.oracle.com/en/java/javase/13/gctuning/ergonomics.html#GUID-DB4CAE94-2041-4A16-90EC-6AE3D91EC1F1![](https://file.jishuzhan.net/article/1734273072673853442/fa28a32d41d0e36d1779dbf2f42db768.webp)

2、单线程垃圾回收器

(1)Serial (使用复制算法)和 Serial Old (使用标记整理算法)

最古老的,单线程,独占式,成熟,适合单 CPU,一般用在客户端模式下。

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

过程:用户线程进行-->用户线程停止,进行新生代回收-->用户线程继续-->用户线程停止,进行老年代回收-->用户线程继续

参数设置

-XX:+UseSerialGC 新生代和老年代都用串行收集器,Serial,Serial Old

3、并行多线程垃圾回收器(重点)

  • Parallel Scavenge (使用复制算法)
  • ParNew (使用复制算法)
  • ParOLD (使用标记整理算法)

过程:用户线程进行-->用户线程暂停,进行新生代回收-->用户线程继续-->用户线程暂停,新型老年代回收-->用户线程继续

(1)ParNew

多线程垃圾回收器,与 CMS 进行配合,对于 CMS(CMS 只回收老年代),新生代垃圾回收器只有 Serial 与 ParNew 可以选。

CMS是追求STW,而Parallel Scavenge是追求吞吐量的,方向不一样,所以CMS和ParaNew组合

与Serial 基本没区别;唯一的区别:多线程,多 CPU 的,停顿(stw)时间比 Serial 少(在 JDK9 以后,把 ParNew 合并到了 CMS 了)

-XX:+UseParNewGC 新生代使用 ParNew,老年代使用 Serial Old

(2)Parallel Scavenge(ParallerGC)/ Parallel Old(jdk1.8默认使用)

关注吞吐量的垃圾收集器

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

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

参数设置

JDK1.8 默认就是以下组合

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

收集器提供了两个参数用于精确控制吞吐量

-XX:MaxGCPauseMillis 控制的停顿的时间

-XX:GCTimeRatio 设置吞吐量大小

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

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

例如:把此参数设置为 19,那允许的最大垃圾收集时占用总时间的 5% (即 1/(1+19)), 默认值为 99,即允许最大 1% (即 1/(1+99))的垃圾收集时间

由于与吞吐量关系密切,Parallel Scavenge 是"吞吐量优先垃圾回收器"。

4、并发垃圾回收器-Concurrent Mark Sweep(CMS)和Garbage First(G1)

  • CMS --追求减少STW
  • G1 ---追求可预测的STW

(1)Concurrent Mark Sweep(CMS))并发的多线程标记清除垃圾回收器

CMS是追求STW,而Parallel Scavenge是追求吞吐量的,方向不一样,所以CMS和ParaNew组合

过程:

用户线程进行-->用户线程暂停,初始标记-->并发标记(用户线程继续)--->暂停用户线程,重新标记--->并发清理(用户线程继续)--->用户线程继续

cms收集器是一种以获取最短回收停顿时间(stw)为目标的收集器。

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

从名字(包含"Mark Sweep")上就可以看出,CMS 收集器是基于"标记---清除"算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些。

垃圾回收过程分为 4 个步骤

  • 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿(STW -Stop the world)。
  • 并发标记:从 GC Root 开始对堆中对象进行可达性分析,找到存活对象,它在整个回收过程中耗时最长,不需要停顿。三色标记算法
  • 重新标记:为了修正并发标记时因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿(STW)。停顿时间一般会比初始标记稍长一些,但远比并发标记的时间短。
  • 并发清除:不需要停顿。

优点:

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

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

缺点:

CPU 资源敏感:因为并发阶段多线程占据 CPU 资源,如果 CPU 资源不足,效率会明显降低。

浮动垃圾:由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为"浮动垃圾"。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。需要提前,在 1.6 的版本中老年代空间使用率阈值(92%)。我理解的是新生代晋级老年代

如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。

内存碎片:标记 - 清除算法会导致产生不连续的空间碎片

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

-XX:+UseCMSCompactAtFullCollection,内存碎片整理,一般是开启的,如果分配不了大对象,就进行内存碎片的整理过程。这个地方一般会使用 Serial Old ,因为 Serial Old 是一个单线程,所以如果内存空间很大、且对象较多时,CMS 发生这样情况会很卡。

浮动垃圾和内存碎片造成的最大的弊端就是需要重启,可能需要人工每天晚上重启。重启总比serialod快

CMS 总结

CMS 问题比较多,所以现在没有一个版本默认是 CMS,只能手工指定。但是它毕竟是第一个并发垃圾回收器,对于了解并发垃圾回收具有一定意义,所以我们必须了解。

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

在 JDK1.8 中,配置参数:

(2)Garbage First(G1)

设计思想-追求可预测的停顿时间

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

Region只是一小块内存,可能是Eden区,也可能是Survivor区,也可能是Old区,另外还有一类特殊的Humongous区域,专门来存储大对象。G1认为只要大小超过一个Region容量一半的对象即可判定为大对象。

每个 Region 的大小可以通过参数-XX:G1HeapRegionSize 设定,取值范围为 1MB~32MB,且应为 2 的 N 次幂。

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

内部布局改变

G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。

算法:标记---整理 (humongous) 和复制回收算法(survivor)。

参数设置

-XX:+UseG1GC 使用 G1 垃圾回收器 >6G,且GC延迟要求有限(稳定且可预测的暂停时间低于0.5秒)的应用程序

-XX:+G1HeapRegionSize 分区大小, 取值范围为 1MB~32MB,且应为 2 的 N 次幂

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

最大 GC 暂停时间 MaxGCPauseMillis,因为进行选择性的回收所以可以追求可预测的stw

过程:

分四步:初始标记,并发标记,最终标记,筛选回收

1、初始标记( Initial Marking) -stw

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

2、并发标记( Concurrent Marking)

TAMS(Top at Mark Start)

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

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

3、最终标记( Final Marking),三色标记发生的阶段 -stw

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

4、筛选回收( Live Data Counting and Evacuation) -stw

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

特点

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

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

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

可预测的停顿:G1 收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region(这也就是 Garbage-First 名称的来由)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率。

追求停顿时间:

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

怎么玩?

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

G1 GC 模式

Young GC

选定所有年轻代里的 Region。通过控制年轻代的 region 个数,即年轻代内存大小,来控制 young GC 的时间开销。(复制回收算法)

Mixed GC

选定所有年轻代里的 Region,外加根据全局并发标记( global concurrent marking ,上述四个过程)统计得出收集收益高的若干老年代 Region。在用户指定的开销目标范围内尽可能选择收益高的老年代 Region。mixed GC 不是 full GC,它只能回收部分老年代的 Region。如果 mixed GC 实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行 Mixed GC,就会使用 serial old GC(full GC)来收集整个 GC heap。所以我们可以知道,G1 是不提供 full GC 的。

G1 GC 主要的参数

-XX:G1HeapRegionSize=n 设置 Region 大小,并非最终值

-XX:MaxGCPauseMillis 设置 G1 收集过程目标时间,默认值 200ms,不是硬性条件

-XX:G1NewSizePercent 新生代最小值,默认值 5%

-XX:G1MaxNewSizePercent 新生代最大值,默认值 60%

-XX:ParallelGCThreads STW 期间,并行 GC 线程数

-XX:ConcGCThreads=n 并发标记阶段,并行执行的线程数

-XX:InitiatingHeapOccupancyPercent 设置触发标记周期的 Java 堆占用率阈值。默认值是 45%。这里的 java 堆占比指的是non_young_capacity_bytes,包括 old+humongous

5、CMS 及 G1 的细节

(1)CMS

CMS 垃圾收集器是基于标记清除算法实现的,主要用于老年代垃圾回收。CMS 收集器的 GC 周期主要由 7 个阶段组成,其中有两个阶段会发生stop-the-world(初始标记、重新标记),其它阶段都是并发执行的。

(2)G1

G1 垃圾收集器是基于标记整理算法实现的,是一个跨越新生代和老年代的垃圾收集器,既负责年轻代,也负责老年代的垃圾回收。

G1 的垃圾回收分为 Young GC、Mix GC 。

G1 Young GC 主要是在 Eden 区进行,当 Eden 区空间不足时,则会触发一次 Young GC。

将 Eden 区数据移到 Survivor 空间时,如果 Survivor 空间不足,则会直接晋升到老年代。

此时 Survivor 的数据也会晋升到老年代。Young GC 的执行是并行的,期间会发生 STW。

当堆空间的占用率达到一定阈值后会触发 G1 Mix GC(阈值由命令参数 -XX:InitiatingHeapOccupancyPercent 设定,默认值 45),

Mix GC 主要包括了四个阶段,其中只有并发标记阶段不会发生 STW,其它阶段均会发生 STW。

(3)G1技术细节

G1的内存区域不固定

G1的每一个Region随着对象的变化而Eden区->survivor区->old区,后者直接为大对象区

(4)G1和CMS跨代引用

老年代引用新生代

由于新生代的垃圾收集通常很频繁,如果老年代对象引用了新生代的对象,老年代的对象就是 GC ROOTS,那么回收新生代的话,需要跟踪从老年代到新生代的所有引用,所以要避免每次 Young GC 时扫描整个老年代,减少开销。

解决方法 :RSet记忆集(记忆跨代引用的表)与 CardTable卡表数组(记录跨代引用内存地址的表)提高效率

  • Rset(记忆集)

记录了其他Region中的对象到本Region的引用,RSet 本身就是一个 Hash 表。

如果是在 G1 的话,则是在一个 Region 区里面一个。如果在cms就只有一个。

RSet 的价值在于使得垃圾收集器不需要扫描整个堆,找到谁引用了当前分区中的对象,只需要扫描 RSet 即可。

  • CardTable(卡表)

由于做新生代 GC 时,需要扫描整个 OLD 区,效率非常低,所以 JVM 设计了 CardTable。如果一个 OLD 区 CardTable 中有对象指向 Young 区, 就将它设为 Dirty(标志位 1), 下次扫描时,只需要扫描 CardTable 上是 Dirty 的内存区域即可。

字节数组 CardTable 的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作"卡页"(Card Page)。 一般来说,卡页大小都是以 2 的 N 次幂的字节数,假设使用的卡页是 2 的 10 次幂,即 1K,内存区域的起始地址是 0x0000 的话,数组 CARD_TABLE 的第 0、1、2 号元素,分别对应了地址范围为 0x0000~0x03FF、0x0400 ~ 0x07FF、0x0800~0x011FF 的卡页内存。

CMS中的CardTable是一个很长很长的数组

总结

这里描述的是 G1 处理跨代引用的细节,其实在 CMS 中也有类似的处理方式,比如 CardTable,也需要记录一个 RSet 来记录,我们对比一下,在 G1 中是每一个 Region 都需要一个 RSet 的内存区域,导致有 G1 的 RSet 可能会占据整个堆容量的 20%乃至更多。但是 CMS 只需要一份,所以就内存占用来说,G1占用的内存需求更大,虽然 G1 的优点很多,但是我们不推荐在堆空间比较小的情况下使用 G1,尤其小于 6 个 G。

五、各个收集器对比

100M serial

几百兆几个G parallel scavenge /parallel old

1g~几十个G CMS

6个G~几百G G1

垃圾回收器的重要参数(使用-XX:)

jinfo -flags pid

参数 描述

UseSerialGC 虚拟机运行在 Client 模式下的默认值,打开此开关后,使用 Serial+Serial Old 的收集器组合进行内存回收

UseParNewGC 打开此开关后,使用 ParNew + Serial Old 的收集器组合进行内存回收

UseConcMarkSweepGC 打开此开关后,使用 ParNew + CMS + Serial Old 的收集器组合进行内存回收。Serial Old 收集器将作为 CMS 收集器出现 Concurrent Mode Failure 失败后的后备收集器使用

UseParallelGC 虚拟机运行在 Server 模式下的默认值,打开此开关后,使用 Parallel Scavenge + Serial Old(PS MarkSweep) 的收集器组合进行内存回收

UseParallelOldGC 打开此开关后,使用 Parallel Scavenge + Parallel Old 的收集器组合进行内存回收

SurvivorRatio 新生代中 Eden 区域与 Survivor 区域的容量比值,默认为 8,代表 Eden : Survivor = 8 : 1

PretenureSizeThreshold 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配

MaxTenuringThreshold 晋升到老年代的对象年龄,每个对象在坚持过一次 Minor GC 之后,年龄就增加 1,当超过这个参数值时就进入老年代

UseAdaptiveSizePolicy 动态调整 Java 堆中各个区域的大小以及进入老年代的年龄

HandlePromotionFailure 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个 Eden 和 Survivor 区的所有对象都存活的极端情况

ParallelGCThreads 设置并行 GC 时进行内存回收的线程数

GCTimeRatio GC 时间占总时间的比率,默认值为 99,即允许 1% 的 GC 时间,仅在使用 Parallel Scavenge 收集器生效

MaxGCPauseMillis 设置 GC 的最大停顿时间,仅在使用 Parallel Scavenge 收集器时生效

CMSInitiatingOccupancyFraction 设置 CMS 收集器在老年代空间被使用多少后触发垃圾收集,默认值为 68%,仅在使用 CMS 收集器时生效

UseCMSCompactAtFullCollection 设置 CMS 收集器在完成垃圾收集后是否要进行一次内存碎片整理,仅在使用 CMS 收集器时生效

CMSFullGCsBeforeCompaction 设置 CMS 收集器在进行若干次垃圾收集后再启动一次内存碎片整理,仅在使用 CMS 收集器时生效

-XX:+UseSerialGC 新生代和老年代都用串行收集器,Serial,Serial Old

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

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

-XX:+UseG1GC 使用 G1 垃圾回收器 >6G,且GC延迟要求有限(稳定且可预测的暂停时间低于0.5秒)的应用程序

-Xms 设置堆的最小值和初始大小

-Xmx 设置堆堆区内存可被分配的最大上限

-Xmn 设置堆的初始和最大大小

-XX:+PrintGCDetails 打印 GC 详情

-XX:+HeapDumpOnOutOfMemoryError 当堆内存空间溢出时输出堆的内存快照新生代大小配置参数的优先级:

-XX:SurvivorRatio 设置eden区和survior区的比例 8 81:1

java -XX:+PrintCommandLineFlags -version 查看jdk使用的垃圾收集器信息

java -XX:+PrintGCDetails -version 看看jvm内存信息

相关推荐
爬山算法2 分钟前
Maven(28)如何使用Maven进行依赖解析?
java·maven
杜杜的man13 分钟前
【go从零单排】迭代器(Iterators)
开发语言·算法·golang
2401_8574396926 分钟前
SpringBoot框架在资产管理中的应用
java·spring boot·后端
怀旧66627 分钟前
spring boot 项目配置https服务
java·spring boot·后端·学习·个人开发·1024程序员节
李老头探索29 分钟前
Java面试之Java中实现多线程有几种方法
java·开发语言·面试
小沈熬夜秃头中୧⍤⃝29 分钟前
【贪心算法】No.1---贪心算法(1)
算法·贪心算法
芒果披萨34 分钟前
Filter和Listener
java·filter
qq_49244844639 分钟前
Java实现App自动化(Appium Demo)
java
阿华的代码王国1 小时前
【SpringMVC】——Cookie和Session机制
java·后端·spring·cookie·session·会话
木向1 小时前
leetcode92:反转链表||
数据结构·c++·算法·leetcode·链表