编译

  • 前端编译器: JDK的Javac 、EclipseJDT中的增量式编译器(ECJ)
  • 即时编译器:HotSpot虚拟机的C1\c2编译器、Graal编译器
  • 提前编译器:JDK的Jaotc、GNU Compiler for Java, Excelsisor JET.

前端编译器

编译过程分为1个准备过程和3个处理过程:
1)初始化插入式注解处理器
2)解析与填充符号表

  • 语法、词法分析
    词法分析是将源代码的字符流转变为标记集合的过程,单个字符是程序编写的最小元素,但标记才是编译时的最小元素。关键字、变量名、字面量、运算符都可以作为标记。

    语法分析时根据标记序列构造抽象语法树的过程,抽象语法树是用来描述程序代码语法结构的树形表示方式,抽象语法树的每一个节点都代表程序代码中的一个语法结构。例如包、类型、修饰符、运算符、接口、返回值、代码注释等。

    经过词法和语法分析生成语法树以后,编译器后续的操作都建立在抽象语法树之上。

  • 填充符号表
    符号表是一组符号地址和符号信息构成的数据结构,符号表中所登记的信息在编译的不同阶段都要被用到。如,在语义分析过程中,符号表所登记的内容将用于语义检查和产生中间代码,在目标代码生成阶段,对符号名进行地址分配时,符号表是地址解析分配的直接依据。

3)注解处理器
插入式注解处理器可以提前至编译期对代码中的特定注解进行处理,从而影响到前端编译器(javac)的工作过程。

如果在处理注解期间对语法树进行过修改,编译器将回到解析以及填充符号表的过程重新处理,知道所有插入式注解处理器都没有再对语法树进行修改位置,每次循环称为一个轮次。

4)语义分析与字节码生成

语义分析过程分为标注检查和数据以及控制流分析两个步骤。

在经过语法分析后,编译器获得了程序代码的抽象语法树表示,抽象语法树能够表示一个结构正确的源程序,但无法保证源程序的语义是符合逻辑的。而语义分析的主要任务则是对结构上正确的源程序进行上下文相关性质的检查,譬如进行类型检查、控制流检查、数据流检查等等。

  • 标注检查
    检查的内容包括变量是用钱是否已经被声明、变量与赋值之间的数据类型是否能够匹配等等。
    常量折叠:int a = 1 + 1 ; ==> int a = 2;

  • 数据流以及控制流分析
    数据流分析以及控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受检查异常都被正确处理了等问题。
    编译器的数据以及控制流分析与类加载时的数据以及控制流分析的目的基本哈桑一致,但是校验范围有所区别。

  • 解语法糖
    Java语言的语法糖包含泛型、变长参数、自动装箱拆箱等,Java虚拟机在运行时并不知节支持这些语法,它们在编译阶段被还原回原始的基础语法结构,这个过程称为解语法糖。

  • 字节码生成

    字节码生成阶段需要把前面哥哥步骤生成的信息转化成字节码指令写到磁盘中外,还要进行少量的代码添加和转换工作。
    如实例构造器<init>()方法和<cinit>()方法的添加。
    如果用户代码中没有提供任何构造函数,那么编译器将会添加一个没有参数的、可访问性与当前类型一致的默认构造函数,这个工作在填充符号表阶段已经完成。

    编译器会把语句块、变量变量初始化、调用父类的实例构造器方等操作收敛到<init>()和<cinit>()方法中,并且保证无论源码中出现的顺序如何,都一定是按先执行父类的实例构造器,然后初始化变量,最后执行语句块的顺序执行,

语法糖

泛型、自动装箱、拆箱、遍历循环、变长参数和条件编译、内部类、枚举类、断言、数值直面量、枚举和字符串的switch、try with resource, lambda等。

泛型

类型擦除

自动装箱、拆箱和循环遍历

1)大于127的值不会自动拆箱
2)没有遇到运算符的情况下不会自动拆箱
3)equals方法不处理数据转型

变长参数

条件编译

Java编译器将所有编译但愿的语法树顶级节点输入到待处理列表后再进行编译,因此各个文件之间可以互相提供符号信息。

根据条件中布尔值常量值的真假,编译器将把分支中不成立的代码块消除掉。

后端编译

当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即运行。
当程序运行环境中内存资源限制较大,可以使用解释执行节约内存。
当程序启动以后,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,减少解释器的中间损耗,获得更高的效率。

即时编译器

即时编译器将运行比较频繁的代码块或者方法编译成本地机器吗,并且对这段代码进行优化。

在Hotspot中内置了2个即时编译器 Client Compiler、 Server Compiler。 虚拟机会根据自身版本与宿主机的硬件性能自动选择运行模式,也可以使用-client 或者 -Server 来强制指定虚拟机运行在客户端模式或者服务端模式。

解释模式(Interperted Mode):-Xint
编译模式(Compiled Mode): -Xcomp
混合模式(Mixed Mode): -Xmixed (默认)

编译器编译本地代码需要占用程序运行时间,位了打到响应速度与运行效率之间打到最佳平衡,HotSpot在编译子系统中加入了分层编译的功能。分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次:
第0层,程序春解释执行,并且解释器不开启性能监控。
第一层:使用客户端编译器将字节码编译为本地代码,进行简单可靠的稳定优化,不开启性能监控功能。
第二层:使用客户端编译器执行,仅开启方法以及回边次数统计等优先的性能监控功能。
第三层:使用客户端编译器执行,开启全部性能监控,除了第2层的痛就之外,还会收集如分支跳转、虚方法调用版本等全部的统计信息。
第四层:使用服务端编译器将字节码编译为本地代码,服务端编译器会启用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化。

实施分层编译后, 解释器、客户端编译器和服务端编译器就会同时工作,热点代码都可能会被多次编译,用客户端编译器获得更高的编译速度,用服务端编译器来获取更好的编译质量,在解释执行的时候也无需额外承担手机性能监控信息的任务,而在服务端编译器采用高复杂度的优化算法时, 客户端编译器可先采用简单优化来为它争取更多的编译时间。

编译对象与触发条件

被编译的热点代码有:被多次调用的方法和被多次执行的循环体。这两种情况编译的目标对象都是整个方法体,而不是单独的循环体。

基于采样的热点探测

周期性的检查线程的调用栈顶,如果发现某个方法经常出现在栈顶,那么这个方法就是热点方法。
优点:简单高效,容易获取方法的调用关系
缺点:不能精确确认方法的忍睹,容易受到线程阻塞或者外界因素影响扰乱热点探测。

基于计数器的热点探测

为每个方法建立计数器,统计方法的执行次数,如果超过一定的阈值就认为它是热点方法。
优点:结果精确严谨
缺点:每个方法建立并维护计数器,不能直接获取到方法的调用关系。
HotSpot为每个方法准备了两类计数器:方法调用计数器和回边计数器(在循环边界往回跳),当调用次数超过设定的阈值,就会触发即时编译。
-XX:CompileThreshold 来设定触发即时编译的阈值。
默认设置下,方法调用计数器统计的是相对执行瓶绿,当一个时间段内调用次数不足以提交即时编时,这个方法的调用计数器就会被减半(衰减),可以使用-XX:-UseCounterDecay关闭热度衰减。可以设置-XX:CounterHalfLifeTime设置半衰周期的时间,单位是秒。

当方法被调用时,首先检查该方法是否被即使编译器编译过,是则优先使用编译后的版本执行,否则将方法调用的计数器加一,然后判断方法调用计数器与回边计数器值之和是否超过阈值,超过则像即时编译器提交该方法的代码编译请求。
如果没有做过任何设置,执行引擎默认不会同步等待编译请求完成,而是继续进入解释器按照解释方式执行字节码,直到提交的请求被即时编译器编译完成。当编译工作完成后,这个方法的调用入口地址会被系统自动改写成新值,下次调用该方法时就会使用已编译版本。

方法调用计数器 触发即时编译

回边计数器:统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”,回边计数器的目的是为了触发栈上的替换编译。回边计数器没有热度衰减过程。

编译过程

一般情况下,方法调用产生的标准编译请求和栈上替换编译请求,在虚拟机编译器未完成编译之前,都将按照解释方式执行代码,编译动作在后台的编译线程完成。
设置-XX:-BackgroundCompilation禁止后台编译,禁止后触发即时编译条件时,执行线程向虚拟机提交编译请求后一致阻塞等待,直到编译过程完成后再开始执行编译器输出的本地代码。

服务端编译器效率较低,但是编译速度远超传统的静态优化编译器,相对于客户端编译器编译输出的代码质量有很大的提交,可以大幅降低本地代码的执行时间,从而抵消掉额外的编译水间开销。

即时编译的缺点

1)占用程序运行时间
2)占用程序运算资源

提前编译器

提前编译器的分支:
与传统C、C++编译器类似,在程序运行之前把程序代码编译成机器码的静态翻译工作;
把原本即时编译器在运行时要做的编译工作提前做好并保存下来,下次运行到这些代码时直接加载进来使用。

编译器优化技术

方法内联

最重要的优化技术之一。

逃逸分析

最前沿的优化技术之一。

  • 栈上分配
    支持方法逃逸,但不支持线程逃逸

  • 标量替换
    如果一个数据已经无法在分解为更小的数据表示时(原始数据类型),这些数据就可以称为标量。
    标量替换就是将用到的成员变量恢复为原始类型来访问的过程。
    加入逃逸分析可以证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候可能不去创建这个对象,而改为直接创建它的若干个被这个的方法使用的成员变量来替代,成员变量存储在栈上。

    同步消除:如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定不会有竞争,对这个变量的同步措施也可以安全地消除掉。

公共子表达式消除

语言无关的经典优化技术。如果一个表达式E之前已经被计算过,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就称为公共子表达式。

数组边界检查消除

语言相关的经典优化技术。

Java技术体系的自动内存管理最根本目标是自动化地给对象分配内存以及自动回收分配给对象的内存。
《Java虚拟机规范》并未规定新对象的创建和存储细节,这取决于虚拟机当前使用的是哪种垃圾收集器,以及虚拟机中与内存相关的参数设定。

HotSpot虚拟机中对象的分配原则

对象优先在Eden分配

在大多数情况下,对象在新生代Eden区中分配。 当Eden区没有足够空间进行分配时,虚拟机将发起一次minorGC。
当survivor区的空间无法存放Eden区的对象时,就会通过担保机制提前转移到老年代去。

-Xms:20M 堆内存初始值20M
-Xmx:20M 堆内存最大20M
-Xmn10M 新生代占用10M(剩下的10M留给老年代)
-XX:SurvivorRatio=8 决定了新生代Eden区与一个Survivor区的空间比例是8:1:1

大对象直接进入老年代

大对象是需要大量连续内存空间的Java对象,如很长的字符串或者元素数量很多的数组。

在分配空间时,如果对象比较大,就会提前触发垃圾收集,以获取足够的连续空间才能存放该对象, 当复制对象时大对象意味着高额的内存复制开销。

-XX:PretenureSizeThreshold=3145728 (大于3M的对象会直接分配到老年代中)
该参数只对Serial和ParNew两款收集器有效。

长期存活的对象将进入老年代

内存回收时必须决策那些存活对象应该存放在新生代,哪些存活对象存放在老年代。
虚拟机为每个对象定义了一个对象年龄(Age)存储在对象头重。

对象通常在Eden区诞生,如果经过一次MinorGC后仍然存活,并且能被Survivor容纳的话,会被移动到Survivor空间,并且将对象的年龄设置为1岁。对象在Survivor中每经过一次MinorGC,Age增加1, 当年龄增加到一定程度的时候,该对象会被移动到老年代中。
可以通过-XX:MaxTenuringThreshold设置(默认为15次)。

动态对象年龄判定

如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无需等到-XX:MaxTenuringThreshold中要求的年龄。

也就是当前Survivor空间大小为1M,有两个Age=1且大小为256k的对象时,这两个对象就会直接进入老年代。

空间分配担保

在发生MinorGC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。
如果条件成立,那么这次MinorGC时安全的;;
如果不成立,虚拟机会先查看-XX:HandlerPromotionFailure参数的设置值是否允许担保失败
如果允许担保失败,继续检查老年代最大可用的连续空间是否大于历次今生到老年代对象的平均大小
大于,尝试进行一次MinorGC;
小于,或者-XX:HandlePromotionFailure不允许冒险,则变更为FullGC。

在JDK6 Update24之后的规则变为只要老年代的连续空间大于新生代总大小或者历次晋升的平均大小,就会进行MinorGC,否则就进行FullGc。-XX:HandlePromotionFailure参数不在生效。

垃圾的回收

  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

如何判断对象已死

  • 引用计数器
  • 可达性分析算法(固定可作为GCRoot的对象有)
    • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如每个线程背调用的方法堆栈中使用的参数、局部变量、临时变量
    • 在方法区中类静态属性引用的对象,如Java类的引用类型静态变量
    • 在方法区中常量引用的对象,如字符串常量池中的引用
    • 在本地方法栈中JNI引用的对象
    • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象,系统类加载器
    • 所有背同步锁持有的对象
    • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

对象的引用

  • 强引用,如Object obj = new Object(); 无论任何情况下,只要强引用关系存在,垃圾收集器就永远不会回收掉背引用的对象。
  • 软引用,描述有用但非必须的对象,被软引用关联的对象在系统将要发生内存溢出前会把这些对象例如回收范围内进行二次回收。(SoftReference)
  • 弱引用,描述非必须对象,被弱引用的对象只能生存到下一次垃圾收集发生位置。无论内存是否足够,都会回收掉只被弱引用关联的对象。
  • 虚引用,一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的时为了能在这个对象被收集器回收时受到一个系统通知(NIO用的RdirectMemory)

垃圾收集器算法

分代收集理论

建立在两个分代假说上:
1)弱分代假说,绝大数对象都是朝生夕灭的。
2)强分代假说,经过越多次收集过程的对象月难消亡。
3)(推理假说)跨代引用假说,跨代引用相对于同代来说仅占极少数。

分代收集区域
  • 部分收集(Partial GC)
    • 新生代收集(Minor GC/ Yong GC)
    • 老年代收集(Major GC / Old GC)(只有CMS会单独回收老年代)
    • 混合收集(Mixed GC),目前只有GC存在混合回收行为。
  • 整堆收集(Full GC)

标记清除算法

标记需要被回收的对象后,回收掉所有被标记的对象;
或者标记不需要被回收的对象,同一回收未被标记的对象。

缺点:
不稳定,标记清除的性能随着对象的增多而降低;
空间碎片化问题,肯能会导致在分配较大对象时无法找到足够的连续空间内存不得不提前触发另一次垃圾收集动作。

mark-sweep

标记复制算法

为了解决标记清除算法面对大量可回收对象时执行效率低的问题。

优点:只需要复制少数存活对象,简单高效
缺点:浪费内存空间。

mark-copy

根据弱分代假说,以及实际情况,大部分的对象在第一次垃圾回收时就会被回收,因此并不需要按照1:1的比例来划分新生代的内存空间。
内存被分为较大的Eden区和较小的两个Survivor区域,HotSpot默认比例是8:1:1。
打给你Survivor空间不足容纳一次MinorGC后存活的对戏那个时,就需要依赖其他内存区域进行分配担保。

标记整理算法

根据老年代对象的存亡特征以及强分代假说提出的算法。
将所有存活的对象向内存空间的一端移动,然后直接清理掉边界以外的内存。

mark-compact

吞吐量:赋值器和收集器的效率总和。
如果在清理时移动,必然会影响到对象回收的效率。
如果清理时不移动,必然会影响到对象分配的效率。

常见的垃圾收集器

Serial / SerialOld

Serial收集器是最古老的收集器,使用标记拷贝算法,在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
优点:在内存较小的桌面应用或者Client模式下,额外内存消耗最小,简单高效。

SerialOld同样是单线程收集起,使用标记整理算法。当CMS收集起发生失败时会转为SerialOld工作。

Serial GC

PS / PO

Paralle Scavenge/ Paralle Old 的目标是达到一个可控的吞吐量。

ParalleScavenge 同样是基于标记-复制算法,支持多线程并发收集,
吞吐量 = (运行用户代码时间)/(运行用户代码时间+运行垃圾收集时间)
-XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间
-XX:GCTimeRatio 控制垃圾收集时间占总时间的比率
-XX:+UseAdaptiveSizePolicy 就不需要人工指定新生代的大小,Eden于Survivor的比例,以及晋升老年代的大小,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

ParalleOld 收集老年代,基于标记整理算法。

Paralle Scavenge / Paralle Old

ParNew / CMS

ParNew是Serial收集器的多线程并行版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集起完全一致。

ParNew / SerialOld

CMD是一种以获取最短回收停顿时间为目的的收集器。符合希望系统停顿时间尽可能短,以给用户带来良好交互体验的需求。他是一种标记-清除算法实现的。
包含四个步骤:
1)初始标记
2)并发标记
3)重新标记
4)并发清除

其中初始标记和重新标记这两个步骤仍需要STW。
初始标记仅仅只是标记一下GC Roots能直接关联到的对象、速度很快;
并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时长,但不需要停顿用户线程,可以与垃圾收集线程一起并发运行;
重新标记为了修正并发标记期间因为用户程序继续运行导致标记产生变动的部分,这个阶段比初始标记耗时稍长;
并发清除,清理删除掉标记阶段判断已经死亡的对象,不需要移动哦存活对象,因此这个阶段也是与用户线程同时并发。

CMS

CMS缺点:
1) 耗CPU资源
2) 无法处理浮动垃圾,由于预留的内存无法分配新对象时,有可能出现回收失败而导致退化到Serial Old的模式收集。
3)分配占用大空间的对象时,会出现老年代还有很多剩余空间,但是无法找到足够大的连续空间分配对象,不得不提前触发FullGC。
提供了两个参数优化,但是JDK1.9之后已经废弃:
-XX:UseCMS-CompactAtFullCollection 不得不进行FullGC时开启内存碎片的合并整理。
-XX:CMSFullGCsBefore-Compaction,要求CMS收集器在执行过若干次不整理空间的FullGC之后,下一次进入FullGC前进行碎片整理。

Garbage First

G1时一款主要面向服务端应用的垃圾收集器。

在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标。

G1面向堆内存任何部分来组成回收集(CollectionSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的MixedGC模式。

G1把连续的Java堆内存划分为多个大小相等的独立区域(Region),每个Region都可以根据需要扮演 新生代的Eden空间Survivor空间,或者老年代空间。收集器能够对不同角色的Region采用不同的=策略取处理,这样无论是新创建的对象还是存活了一段时间、经过多次收集的旧对象都能获得很好的收集再熬过。

Region中有一类Humongous区域专门存放大对象。 G1认为只要大小超过了一个Region容量的一般的对象就可以判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围1MB~32MB,且应为2的N次幂。那些超过了整个Region容量的超级大对象,会被存放在N个连续的HumongousRegion之中,G1的大多数行为都把HumongousRegion作为老年代的一部分进行看待。

G因为将Region作为单词回收的最小单元,所以可以建立可预测的停顿时间模型。
每次根据用户设置的停顿时间-XX:MaxGCPauseMillis, 优先处理回收价值收益最大的那些Region。
这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在悠闲的时间内获取尽可能高的收集效率。

G1 Region

需要解决的问题

1)跨Region引用的对象: 每个Region都维护自己的记忆集避免全堆作为GCRoots扫描, 这些记忆集会记录下别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。
G1的记忆集是一种Hash表,Key时别的Region的起始地址,Value时一个集合,里面存储的元素时卡表的索引号。这种双向的卡表结构记录了我指向谁和谁指向了我,因此G1只少需要耗费大约10%~20%的额外内存来维持收集工作。

2)在并发阶段如何保证收集线程与用户线程互不干扰地运行?
用户线程在改变对象引用关系的时候,必须保证不能打破原本的对象图结构,导致比阿吉结果出现错误,
CMS使用增量更新算法,G1使用原始快照算法来实现。

垃圾收集对用户线程的影响还体现在回收过程中心窗卷对象的内存分配上,程序要继续运行就肯定会持续有新对象呗创建,G1位每个Region设计了两个名为TAMS(TOp at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时心分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象时被隐式标记过的,不纳入回收范围。如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行,导致FullGC而产生长时间的STW。

3)如何建立可靠的停顿模型
G1的停顿预测模型时以衰减均值为理论基础实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可以测量的步骤话费的成本,并分析得出平均值,标准偏差、可行度等统计信息,衰减平均值时比普通的平均值更容易受到新数据的影响,平均值更容易受到新数据的影响,平均值代表整体平均状态,衰减均值代表最近的平均状态。 Region的统计状态越新越能决定器回收的价值。通过这些信息预测现在开始回收的话,哪些region组成回收集采可以在不超过期望停顿时间的约束下获得最高的收益。

G1的回收步骤

初始标记

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

并发标记

从GCRoots开始对对子红对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这个阶段耗时较长,但是可以和用户程序并发执行,当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。

最终标记

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

筛选回收

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

G1

G1从整体上来看是基于标记整理算法,但从局部来看时基于标记复制算法。无论如何,这两种算法意味着G1再与性期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发限一次收集。

G1位了垃圾收集产生的内存占用以及额外的执行副在都比CMS搞。
内存占用是由于双向卡表占用空间大。CMs只记录了老年代到新生代的卡表记录。

负载:CMS用写后屏障维护更新卡表;
G1除了使用写后屏障维护卡表外,位了实现原始快照搜索(SATB)算法,还需要使用写前屏障来跟踪并发前的指针变化。 原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免CMS那样在最终标记阶段停顿时间过长的缺点。

CMS的写屏障是同步操作,G1使用的是消息队列,把写前屏障和写后屏障中要做的事情都放在队列里,再异步处理。