java 内存以及GC
java 

java 内存区域

java 运行时数据区如下: Alt text

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器

为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常

如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过 Java 虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。

本地方法栈

本地方法栈则为虚拟机使用到的Native方法服务,本地方法栈区域也会抛出 StackOverflowErrorOutOfMemoryError 异常

方法区

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

运行时常量区

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

直接内存

jdk1.4 引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

内存溢出

除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生 OutOfMemoryError

Java堆溢出:Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。

虚拟机栈和本地方法栈溢出:如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

方法区和运行时常量池溢出:方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对于这些区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。

方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判定条件是比较苛刻的。在经常动态生成大量Class的应用中,需要特别注意类的回收状况。这类场景除了上面提到的程序使用了CGLib字节码增强和动态语言之外,常见的还有:大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)、基于OSGi的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。

java 堆内存

根据 Generation 算法,Java 的堆内存被划分为新生代、老年代和持久代。新生代又进一步划分为 Eden 和 Survivor 区,最后 Survivor 由 FromSpace(Survivor0) 和 ToSpace(Survivo1) 组成。

Alt text

所有通过 new 创建的对象都会被分配到堆内存中,堆内存的大小可以通过 -Xmx-Xms 来控制。

分代收集是基于 不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,并采用不同的回收算法进行回收。

Java 的堆内存以外的部分:

  • 栈,每个线程执行方法时都会在栈中申请一个栈帧,每个栈帧包括局部变量区和操作数栈,用于存放此次方法调用过程中的临时变量、参数和中间结果。
  • 本地方法栈,用于支持 native 方法的执行
  • 方法区,存放了要加载的类的信息、静态变量、final类型的常亮、属性和方法信息。JVM 用持久代存放方法区,通过 -XX:PermSize-XX:MaxPermSize 来指定最小和最大值

堆内存分配

新生代

新生代的内存是按照 8:1:1 的比例分为一个 Eden 区和两个 Survivor 区。

大部分对象在 Eden 区生成,当新对象生成时,会在 Eden 区申请内存,如果申请失败,则会发起一次新生代的 GC。

回收时现将 Eden 区存活对象复制到 Survivor0 区,然后清空 Eden 区。当 Survivor0 区也存放满了,则将 Eden 区和 Survivor0 区存活的对象复制到 Survivor1 区,然后清空 Eden 和 Survivor0 区,然后将 Survivor0 和 Survivor1 区交换,保持 Survivor1 区为空。

如果 Survivor1 区不足以存放 Eden 和 Survivor0 的存活对象时,则将存活对象直接放到老年代。

当对象在 Survivor0 区没有被 GC 的话,则其年龄加1。默认情况下,当对象年龄达到15时,就会用到过到老年代中。

老年代

在新生代经历 N 次回收仍然存活的对象,就会被放到老年代中。

老年代一般都是生命周期较长的对象,内存的比例也比新生代大,大概是2:1。

当老年代内存满时触发 Full GC,Full GC 的频率比较低,如果频繁 Full GC 说明内存不够或者有内存溢出。

老年代的对象存活时间比较长。一般来说,大对象会直接被分配到老年代。大对象指的是需要大量连续存储空间的对象。

持久代

用于存放静态文件(class类、方法等)和常量。持久代对垃圾回收没有显著影响,但是有些应用(例如Hibernate)可能动态生成或者调用一些class,此时就需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。

对永久代回收主要是两部分内容:废弃常量和无用的类。

内存不够时,java8以前会抛出:java.lang.OutOfMemoryError: PermGen error 错误。

java8 将永久代移除,用元空间(MetaSpace)代替。

堆内存分配策略

  • 对象优先在 Eden 分配
  • 大对象直接进入老年代
  • 长期存活的对象将进入老年代

垃圾收集器与内存分配策略

对象存活算法

在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。

引用计数法

堆中的每个对象都有一个引用计数器。每当有地方引用时,计数器的值就加 1. 当引用失效时,计数器的值就减 1. 任何引用计数为 0 的对象可以被回收,当一个对象被回收时,它引用的任何对象计数减 1.

优点:引用计数收集器执行简单,判定效率高,对程序不被长时间打断的实时环境有利 缺点:难以检测对象之间的循环引用,同时增加了程序执行开销

可达性分析算法

通过一系列的称为 GC Roots 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

可达性分析对执行时间的敏感还体现在GC停顿上

该算法的基本思路:

  1. 通过一系列名为 GC Roots 的对象作为起点,寻找对应的引用节点
  2. 找到节点后,从这些节点开始向下搜寻它们的引用节点
  3. 重复 2
  4. 搜索所走过的路径成为引用链,当一个对象到 GC Roots 没有任何引用链相连时,那么该对象是不可用的

一般 GC Roots 包含如下对象:

  • 虚拟机栈中引用的对象
  • 方法区中的常量引用对象
  • 方法区中的类静态属性引用的对象
  • 本地方法栈中 JNI (Native 方法) 引用的对象
  • 活跃线程

Alt text

存活对象在上图中被标记为蓝色。标记阶段完成后,所有存活对象都已被标记,剩余的灰色是不可达对象,可以回收掉。

垃圾收集算法

标记-清除算法

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象

主要有两个不足:

  1. 效率问题,标记和清除两个过程的效率都不高;
  2. 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

复制算法

每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。种算法将内存缩小为了原来的一半,代价很高

新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor

没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

标记-整理算法

让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

分代收集算法

把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

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

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

垃圾收集器

如果两个收集器之间存在连线,就说明它们可以搭配使用

Alt text

直到现在为止还没有最好的收集器出现,更加没有万能的收集器,所以我们选择的只是对具体应用最合适的收集器

Serial 收集器

采用复制算法

这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是 在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束

因为其简单高效,Serial收集器对于运行在Client模式下(桌面程序)的虚拟机来说是一个很好的选择。

ParNew 收集器

ParNew 收集器除了多线程收集之外,其他与Serial收集器相比并没有太多创新之处,但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作

ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证能超越Serial收集器。当然,随着可以使用的CPU的数量的增加,它对于GC时系统资源的利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多(譬如32个,现在CPU动辄就4核加超线程,服务器超过32个逻辑CPU的情况越来越多了)的环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数

Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。

如果在Server模式下,作为CMS收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用

Parallel Old收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法.

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。

CMS收集器是基于“标记—清除”算法实现的,它的运作过程分为4个步骤,包括:

  1. 初始标记(CMS initial mark) STW
  2. 并发标记(CMS concurrent mark)
  3. 重新标记(CMS remark) STW
  4. 并发清除(CMS concurrent sweep)

明显缺点:

  1. CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低
  2. CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生(临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了)
  3. CMS是一款基于“标记—清除”算法实现的收集器,收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC

Initial Mark

CMS 垃圾回收有两次 SWT (stop the world),其中一次是 Initial Mark。这个阶段的目标是:标记那些直接被 GC Roots 引用或者被年轻代存活的对象所引用的对象。

在日志中对应的信息是:

2019-03-05T09:13:03.124+0800: 59223.296: [GC (CMS Initial Mark) [1 CMS-initial-mark: 6051135K(8437760K)] 6279638K(10258240K), 0.0252641 secs] [Times: user=0.16 sys=0.00, real=0.02 secs]
  • 2019-03-05T09:13:03.124+0800: 59223.296:GC 开始的时间,以及相对于 JVM 启动的相对时间(单位是秒),与前面 ParNew 类似;
  • CMS-initial-mark:初始标记阶段,它会收集所有 GC Roots 以及其直接引用的对象;
  • 6051135K:当前老年代使用的容量,这里是 5.77G;
  • (8437760K):老年代可用的最大容量,这里是 8G;
  • 6279638K:整个堆目前使用的容量,这里是 5.99G;
  • (10258240K):堆可用的容量,这里是 9.78G;
  • 0.0252641 secs:这个阶段的持续时间;
  • [Times: user=0.16 sys=0.00, real=0.02 secs]:与前面的类似,这里是相应 user、system and real 的时间统计。

Concurrent Mark

在这个阶段会根据上一个阶段的 GC Roots 遍历老年代,并标记所有存活的对象。

并发标记阶段与用户的应用程序并发运行,但并不是所有的老年代存活对象都会被标记,因为在标记期间用户的应用程序可能会改变一些引用。

2019-03-05T09:13:03.149+0800: 59223.321: [CMS-concurrent-mark-start]
2019-03-05T09:13:04.627+0800: 59224.799: [CMS-concurrent-mark: 1.403/1.478 secs] [Times: user=4.45 sys=0.10, real=1.47 secs]

这里详细对上面的日志解释,如下所示:

  • CMS-concurrent-mark:并发收集阶段,这个阶段会遍历老年代,并标记所有存活的对象;
  • 1.403/1.478 secs:这个阶段的持续时间与时钟时间;
  • [Times: user=4.45 sys=0.10, real=1.47 secs]:如前面所示,但是这部的时间,其实意义不大,因为它是从并发标记的开始时间开始计算,这期间因为是并发进行,不仅仅包含 GC 线程的工作。

Concurrent Preclean

该阶段也是一个并发阶段,不会 STW。在并发运行的过程中,一些对象的引用就会发生变化。这种情况发生时,JVM 会将包含这个对象的区域标记为 Dirty,也就是 Card Marking。

在 PreClean 阶段,那些能够从 Dirty 对象到达的对象也会被标记。标记做完后,Dirty card 标记就会被清除。

日志信息如下:

2019-03-05T09:13:04.627+0800: 59224.799: [CMS-concurrent-preclean-start]
2019-03-05T09:13:04.653+0800: 59224.825: [CMS-concurrent-preclean: 0.025/0.026 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]

含义为:

  • CMS-concurrent-preclean:Concurrent Preclean 阶段,对在前面并发标记阶段中引用发生变化的对象进行标记;
  • 0.025/0.026 secs:这个阶段的持续时间与时钟时间;
  • [Times: user=0.03 sys=0.00, real=0.03 secs]:同并发标记阶段中的含义。

Concurrent Abortable Preclean

这个阶段是为了尽量承担 STW 中最终标记阶段的工作。这个阶段是在重复做很多相同的工作,直至满足一些条件,如:重复迭代的次数、完成的工作量或者时钟时间等。

日志信息如下:

2019-03-05T09:13:04.653+0800: 59224.825: [CMS-concurrent-abortable-preclean-start]
2019-03-05T09:13:08.918+0800: 59229.089: [CMS-concurrent-abortable-preclean: 3.843/4.264 secs] [Times: user=9.57 sys=0.19, real=4.26 secs]

含义为:

  • CMS-concurrent-abortable-preclean:Concurrent Abortable Preclean 阶段;
  • 3.843/4.264 secs:这个阶段的持续时间与时钟时间,本质上,这里的 gc 线程会在 STW 之前做更多的工作,通常会持续 5s 左右;
  • [Times: user=9.57 sys=0.19, real=4.26 secs]:同前面。

Final Remark

这是第二个 STW 的阶段,这个阶段是标记所有老年代存活的对象。因为之前的阶段是并发执行的,gc 线程可能跟不上应用程序的变化,为了完成标记,STW 就非常必要了。

通常 CMS 的 Final Remark 阶段会在年轻代尽可能干净的时候运行,目的是为了减少连续 STW 发生的可能性(年轻代存活对象过多的话,也会导致老年代涉及的存活对象会很多)。这个阶段会比前面的几个阶段更复杂一些,相关日志如下:

2019-03-05T09:13:08.923+0800: 59229.094: [GC (CMS Final Remark) [YG occupancy: 1319324 K (1820480 K)]2019-03-05T09:13:08.923+0800: 59229.095: [Rescan (parallel) , 0.0853762 secs]2019-03-05T09:13:09.008+0800: 59229.180: [weak refs processing, 0.0046217 secs]2019-03-05T09:13:09.013+0800: 59229.185: [class unloading, 0.0673909 secs]2019-03-05T09:13:09.080+0800: 59229.252: [scrub symbol table, 0.0129999 secs]2019-03-05T09:13:09.093+0800: 59229.265: [scrub string table, 0.0029732 secs][1 CMS-remark: 6161078K(8437760K)] 7480403K(10258240K), 0.1744124 secs] [Times: user=0.69 sys=0.00, real=0.17 secs]
  • YG occupancy: 1319324 K (1820480 K):年轻代当前占用量及容量,这里分别是 1.26G 和 1.74G;
  • ParNew:…:触发了一次 young GC,这里触发的原因是为了减少年轻代的存活对象,尽量使年轻代更干净一些;
  • [Rescan (parallel) , 0.0853762 secs]:这个 Rescan 是当应用暂停的情况下完成对所有存活对象的标记,这个阶段是并行处理的,这里花费了 0.0853762s;
  • [weak refs processing, 0.0046217 secs]:第一个子阶段,它的工作是处理弱引用;
  • [class unloading, 0.0673909 secs]:第二个子阶段,它的工作是:unloading the unused classes;
  • [scrub symbol table, 0.0129999 secs] … [scrub string table, 0.0029732 secs]:最后一个子阶段,它的目的是:cleaning up symbol and string tables which hold class-level metadata and internalized string respectively,时钟的暂停也包含在这里;
  • 6161078K(8437760K):这个阶段之后,老年代的使用量与总量,这里分别是 5.88G 和 8.05G;
  • 7480403K(10258240K):这个阶段之后,堆的使用量与总量(包括年轻代,年轻代在前面发生过 GC),这里分别是 7.13G 和 9.78G;
  • 0.1744124 secs:这个阶段的持续时间;
  • [Times: user=1.24 sys=0.00, real=0.14 secs]:对应的时间信息。

Concurrent Sweep

在经过以上五个阶段后,老年代所有存活的对象都被标记了,现在可以通过清除算法清理老年代。

这个阶段不用 STW。对应的日志如下:

2019-03-05T09:13:09.097+0800: 59229.269: [CMS-concurrent-sweep-start]
2019-03-04T16:46:44.118+0800: 44.290: [CMS-concurrent-sweep: 0.561/0.590 secs] [Times: user=1.75 sys=0.05, real=0.59 secs]
  • CMS-concurrent-sweep:这个阶段主要是清除那些没有被标记的对象,回收它们的占用空间;
  • 0.561/0.590 secs:这个阶段的持续时间与时钟时间;
  • [Times: user=30.34 sys=16.44, real=8.28 secs]:同前面;

Concurrent Reset

这个阶段也是并发执行的,它会重设 CMS 内部的数据结构,为下次 GC 做准备,日志信息如下:

2019-03-04T16:46:44.118+0800: 44.290: [CMS-concurrent-reset-start]
2019-03-04T16:46:44.137+0800: 44.309: [CMS-concurrent-reset: 0.018/0.018 secs] [Times: user=0.08 sys=0.00, real=0.02 secs]
  • CMS-concurrent-reset:这个阶段的开始,目的如前面所述;
  • 0.018/0.018 secs:这个阶段的持续时间与时钟时间;
  • [Times: user=0.15 sys=0.10, real=0.04 secs]:同前面。

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

G1 收集器

G1是一款面向服务端应用的垃圾收集器,具备如下特点:

  1. 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  2. 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
  3. 空间整合:与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC
  4. 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

G1 将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

G1跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

如果你的应用追求低停顿,那G1现在已经可以作为一个可尝试的选择,如果你的应用追求吞吐量,那G1并不会为你带来什么特别的好处

G1 GC(垃圾优先型回收器)是适用于 Java HotSpot VM 的低暂停、服务器风格的分代式垃圾回收器。

G1 使用并发和并行阶段实现其目标暂停时间,并保持良好的吞吐量。当有必要进行垃圾回收时,会优先收集存活数据最少的区域。

G1 是区域化、分代式垃圾回收器,它将 Java 堆划分成大小相同的若干区域。启动时,JVM 会根据堆的大小,划分出不超过2048个区域,每个区域的大小从 1M 到 32M 不等。这样 Eden、Survivo 和老年代是一系列不连续的逻辑区域。

新生代的垃圾收集依然采用 STW 方式,将存活的对象拷贝到老年代或者 Survivor 空间。因为老年代也分很多区域,所以回收时将老年代对象从一个区域复制到另一个区域,完成清理工作,这样也就不会有 CMS 的内存碎片问题。

上图中的 H 代表 Humongous,表示这些区域存储的是巨型对象。一般巨型对象的大小大于等于区域大小的一半。

巨型对象有如下特征:

  • 直接被分配到 H 区,防止反复拷贝
  • 在 Concurrent Marking 阶段的 cleanup 和 Full GC 阶段回收

RSet

RSet 全称 Remembered Set,是一种空间换时间的工具。RSet 是其他区域中的对象引用该区域中的对象的集合,属于 points-into 结构,记录谁引用了我的对象。G1 使用 RSet 跟踪区域的引用,独立的 RSet 可以并行、独立地回收区域,只需要对区域的 RSet 进行区域引用进行扫描。

G1 将一组或多组区域(回收集 CSet)中的存活对象以增量、并行的方式复制到不同的新区域来实现压缩。 一般来说,每个区域都有对一个的 RSet,

在 GC 的时候,对于 old->yong 和 old->old 的跨代对象引用,只要扫描对应的 CSet 中的 RSet。

年轻代回收

G1 可以满足添加到 eden 区域集的大多数分配请求。在年轻代垃圾回收期间,G1 会同时回收 Eden 区域和上次垃圾回收的存活区域。Eden 和存活区的对象将被复制或疏散到新的区域集。足够老的对象疏散到老年代,否则疏散到存活区,并将包含在下一次年轻代或混合垃圾回收的 CSet 中

混合垃圾回收

G1 在完成并发标记周期后,从执行年轻代垃圾回收切换为执行混合垃圾回收。 在混合垃圾回收期间,G1 将一些旧的区域添加到 Eden 和存活区来供将来回收。在回收足够的旧区域后,G1 将恢复执行年轻代的垃圾回收。

参数设定

参数内容取值含义
-XX:G1HeapRegionSize1M - 32M (2的指数)设定区域的大小

对比 CMSA

相比 CMS,G1 有以下特色:

  • G1有一个整理内存的过程,不会产生很多内存碎片
  • G1的 STW 更可控,它在停顿时间上添加了预测机制,用户可以指定期望停顿时间

如果你现在采用的收集器没有出现问题,那就没有任何理由现在去选择G1,如果你的应用追求低停顿,那G1现在已经可以作为一个可尝试的选择,如果你的应用追求吞吐量,那G1并不会为你带来什么特别的好处

参考

https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html

local_offer #java