OopMap
我们都知道在GC之前要做一次GC Roots来查找对象的存活情况,一边在GC时候正确的回收。那么每次GC时候遍历所有的引用是不现实的,那么这之后就引入了OopMap,它里面记录了一些类加载时候的类型与偏移量地址等信息生成一张映射表放在OopMap中。GC开始的时候,就通过OopMap这样的一个映射表知道,在对象内的什么偏移量上是什么类型的数据,而且特定的位置记录下栈和寄存器中哪些位置是引用。
安全点/安全区域(Safepoint/Safe Region)
上面为了快速的分析可达性,使用了一个引用类型映射表,可以快速的知道对象内或者栈和寄存器中哪些位置引用了。那么在方法执行过程中,这些引用关系可能会随时发生变化,那么OopMap是不是也要跟着变呢?如果没出引用变化就更新OopMap那么也是不现实的,这时候就引入了安全点的概念。OopMap的作用就是在每次GC前保证是最新的就可以了。OopMap只需要在预先选定的一些位置上记录变化的OopMap就行了。在这个状态下虚拟机堆栈不在发生变化。而安全点的选定是以程序‘是否具有让程序长时间执行的特征’为标准选定的。‘长时间执行’的明显特征就是指令序列复用,例如:方法调用(方法临返回前/调用方法的call指令后),循环跳转(循环的末尾),异常跳转(可能抛异常的位置)等,具有这些功能的指令才再回产生安全点。大白话就是在程序中寻找一个安全点,当GC触发时,为了线程状态和数据的一致性,让线程都跑到这个安全点停顿下来后再执行GC。至于安全区域你可以认为在这个区域的任何位置都可以GC,即点.线,面的关系。基于安全点中断GC的方式有两种:
- 抢先式中断(Preemptive Suspension):抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机采用这种方式来暂停线程从而响应GC事件。
- 主动式中断(Voluntary Suspension):主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
卡表(Card Table)
有个场景,老年代的对象可能引用新生代的对象,由于新生代的垃圾收集通常很频繁,那标记存活对象的时候,需要扫描从老年代到新生代的所有引用对象。因为该对象拥有对新生代对象的引用,那么这个引用也会被称为GC Roots。那不是每次YGC时又得做全堆扫描?显然不是,对于HotSpot JVM,使用了卡标记(Card Marking)技术来解决老年代到新生代的引用问题。具体是,使用卡表(Card Table)和写屏障(Write Barrier)来进行标记并加快对GC Roots的扫描。卡表的设计师将堆内存平均分成2的N次方大小(默认512字节)个卡,并且维护一个卡表,用来储存每个卡的标识位。当对一个对象引用进行写操作时(对象引用改变),写屏障逻辑将会标记对象所在的卡页为脏页。在YGC只需要扫描卡表中的脏卡,将脏中的对象加入到YGC的GC Roots里面。当完成所有脏卡扫描时候,虚拟机会将卡表的脏卡标志位清空。
在高并发环境下,每次对引用的更新,无论是否更新了老年代对新生代对象的引用,都会进行一次写屏障操作,频繁的写屏障很容易发生虚共享(false sharing),从而带来性能开销。举个例子:假设CPU缓存行大小为64字节,由于一个卡表项占1个字节,这意味着,64个卡表项将共享同一个缓存行。HotSpot每个卡页为512字节,那么一个缓存行将对应64个卡页一共 64*512=32KB。如果不同线程对对象引用的更新操作,恰好位于同一个32KB区域内,这将导致同时更新卡表的同一个缓存行,从而造成缓存行的写回、无效化或者同步操作,间接影响程序 性能。
在JDK 7中引入了VM参数-XX:+UseCondCardMark
,意思就是现在不采用无条件写屏障,而是先检查此卡是否已经是脏页,如果是将不再标记。这样就减少了并发下的虚共享问题。但是这样却不能避免对未标记的页进行并发标记。
新生代回收器:Serial、ParNew、parallel
老年代回收器:Serial Old、CMS、Parallel Old
新生代和老年代回收器:G1
JVM GC垃圾回收器参数设置
JVM给出了3种选择:串行收集器、并行收集器、并发收集器。
串行收集器只适用于小数据量的情况,所以生产环境的选择主要是并行收集器和并发收集器。默认情况下JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行智能判断。
串行收集器
-XX:+UseSerialGC
:设置串行收集器。
并行收集器(吞吐量优先):
-XX:+UseParallelGC
:设置为并行收集器。此配置仅对年轻代有效。即年轻代使用并行收集,而年老代仍使用串行收集。
-XX:ParallelGCThreads=20
:配置并行收集器的线程数,即:同时有多少个线程一起进行垃圾回收。此值建议配置与CPU数目相等。
-XX:+UseParallelOldGC
:配置年老代垃圾收集方式为并行收集。JDK6.0开始支持对年老代并行收集。
-XX:MaxGCPauseMillis=100
:设置每次年轻代垃圾回收的最长时间(单位毫秒)。如果无法满足此时间,JVM会自动调整年轻代大小,以满足此时间。
-XX:+UseAdaptiveSizePolicy
:设置此选项后,并行收集器会自动调整年轻代Eden区大小和Survivor区大小的比例,以达成目标系统规定的最低响应时间或者收集频率等指标。此参数建议在使用并行收集器时,一直打开。并发收集器(响应时间优先)
并发收集器
-XX:+UseConcMarkSweepGC
:即CMS收集,设置年老代为并发收集。CMS收集是JDK1.4后期版本开始引入的新GC算法。它的主要适合场景是对响应时间的重要性需求大于对吞吐量的需求,能够承受垃圾回收线程和应用线程共享CPU资源,并且应用中存在比较多的长生命周期对象。CMS收集的目标是尽量减少应用的暂停时间,减少Full GC发生的几率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代内存。
-XX:+UseParNewGC
:设置年轻代为并发收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此参数。
-XX:CMSFullGCsBeforeCompaction=0
:由于并发收集器不对内存空间进行压缩和整理,所以运行一段时间并行收集以后会产生内存碎片,内存使用效率降低。此参数设置运行0次Full GC后对内存空间进行压缩和整理,即每次Full GC后立刻开始压缩和整理内存。
-XX:+UseCMSCompactAtFullCollection
:打开内存空间的压缩和整理,在Full GC后执行。可能会影响性能,但可以消除内存碎片。
-XX:+CMSIncrementalMode
:设置为增量收集模式。一般适用于单CPU情况。
-XX:CMSInitiatingOccupancyFraction=70
:表示年老代内存空间使用到70%时就开始执行CMS收集,以确保年老代有足够的空间接纳来自年轻代的对象,避免Full GC的发生。
其它垃圾回收参数
-XX:+ScavengeBeforeFullGC
:年轻代GC优于Full GC执行。
-XX:-DisableExplicitGC
:不响应 System.gc() 代码。
-XX:+UseThreadPriorities
:启用本地线程优先级API。即使 java.lang.Thread.setPriority() 生效,不启用则无效。
-XX:SoftRefLRUPolicyMSPerMB=0
:软引用对象在最后一次被访问后能存活0毫秒(JVM默认为1000毫秒)。
-XX:TargetSurvivorRatio=90
:允许90%的Survivor区被占用(JVM默认为50%)。提高对于Survivor区的使用率。
各个垃圾收集器详解:
Serial
特点
JDK1.3.1前,Serial是HotSpot新生代收集的唯一选择。
有如下特点:
- 针对新生代;
- 采用复制算法;
- 单线程收集;
- 进行垃圾收集时,必须暂停所有工作线程,直到完成;
优势
简单高效,由于采用的是单线程的方法,因此与其他类型的收集器相比,对单个cpu来说没有了上下文之间的的切换,效率比较高。
劣势
会在用户不知道的情况下停止所有工作线程。
使用场景
-
Client 模式(桌面应用)
在用户的桌面应用场景中,可用内存一般不大,可以在较短时间内完成垃圾收集,只要不频繁发生,这是可以接受的 -
单核服务器
对于限定单个CPU的环境来说,Serial收集器没有线程切换开销,可以获得最高的单线程收集效率
参数设置
-XX:+UseSerialGC
:添加该参数来显式的使用串行垃圾收集器
ParNew
特点
ParNew收集器是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余均和Serial 收集器一致。
优势
多线程版本的Serial,可以更加有效的利用系统资源。
劣势
同Serial,会在用户不知道的情况下停止所有工作线程。
使用场景
Server模式下使用,亮点是除Serial外,目前只有它能与CMS收集器配合工作,是一个非常重要的垃圾回收器。
参数配置
-XX:+UseConcMarkSweepGC
:指定使用CMS后,会默认使用ParNew作为新生代收集器;-XX:+UseParNewGC
:强制指定使用ParNew;-XX:ParallelGCThreads
:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;
parallel
特点
Parallel Scavenge也是一款用于新生代的多线程收集器,也是采用复制算法。与ParNew的不同之处在于Parallel Scavenge收集器的目的是达到一个可控制的吞吐量,而ParNew收集器关注点在于尽可能的缩短垃圾收集时用户线程的停顿时间。
有如下特点:
- 新生代收集器;
- 采用复制算法;
- 多线程收集;
- 关注点与其他收集器不同:
- CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;
- Parallel Scavenge收集器的目标是达一个可控制的吞吐量;
关于吞吐量和暂停时间的区别参考 GC的性能衡量指标:吞吐量 VS 暂停时间
优势
追求高吞吐量,高效利用CPU,是吞吐量优先,且能进行精确控制。
劣势
应该说是特点,追求高吞吐量必然要牺牲一些其他方面的优势。例如单个GC周期的停顿时间会变得更长。
使用场景
根据相关特性,我们很容易想到它的使用场景,即:当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,程序主要在后台进行计算,而不需要与用户进行太多交互等就特别适合parallel收集器。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序等。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
参数设置
-XX:MaxGCPauseMillis
:控制最大垃圾收集停顿时间,大于0的毫秒数;
-XX:GCTimeRatio
:设置垃圾收集时间占总时间的比率,0<n<100的整数;
MaxGCPauseMillis
参数允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值。不过大家不要认为如果把这个参数的值设置得稍小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1 /(1+19)),默认值为99,就是允许最大1%(即1 /(1+99))的垃圾收集时间。
Serial Old
特点
Serial Old是Serial收集器的老年代版本,同样是一个单线程收集器,使用标记-整理算法。
有如下特点:
- 针对老年代;
- 采用"标记-整理"算法(还有压缩,Mark-Sweep-Compact);
- 单线程收集;
- 优劣势基本和Serial无异,它是和Serial收集器配合使用的老年代收集器。
使用场景
Client模式;
单核服务器;
与Parallel Scavenge收集器搭配;
作为CMS收集器的后备方案,在并发收集发生Concurrent Mode Failure时使用
CMS
特点
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。采用的算法是“标记-清除”,运作过程分为四个步骤:
- 初始标记,标记GC Roots 能够直接关联到达对象
- 并发标记,进行GC Roots Tracing 的过程
- 重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录
- 并发清除,用标记清除算法清除对象。
有如下特点:
针对老年代;
- 基于"标记-清除"算法(不进行压缩操作,产生内存碎片);
- 以获取最短回收停顿时间为目标;
- 并发收集、低停顿;
- 需要更多的内存(看后面的缺点);
优势
停顿时间短;
吞吐量大;
并发收集
劣势
对CPU资源非常敏感
无法收集浮动垃圾
容易产生大量内存碎片
使用场景
与用户交互较多的场景;
希望系统停顿时间最短,注重服务的响应速度;
以给用户带来较好的体验;
如常见WEB、B/S系统的服务器上的应用。
参数设置
-XX:+UseConcMarkSweepGC
:指定使用CMS收集器
Parallel Old
特点
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法,可以充分利用多核CPU的计算能力。
有如下特点:
- 针对老年代;
- 采用"标记-整理"算法;
- 多线程收集;
- 优劣势参考Parallel Scavenge收集器。
使用场景
JDK1.6及之后用来代替老年代的Serial Old收集器;
特别是在Server模式,多CPU的情况下;
这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge(新生代)加Parallel Old(老年代)收集器的"给力"应用组合;
参数设置
-XX:+UseParallelOldGC
:指定使用Parallel Old收集器
G1
G1(Garbage-First)是JDK7-u4才推出商用的收集器
- 并行与并发:G1能充分利用多CPU,多核环境下的硬件优势。
- 分代收集:能够采用不同的方式去处理新创建的对象和已经存活了一段时间的对象,不需要与其他收集器进行合作。
- 空间整合:G1从整体上来看基于“标记-整理”算法实现的收集器,从局部上看是基于复制算法实现的,因此G1运行期间不会产生空间碎片。
- 可预测的停顿:G1能建立可预测的时间停顿模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
有如下特点:
- 并行与并发
- 分代收集,收集范围包括新生代和老年代
- 结合多种垃圾收集算法,空间整合,不产生碎片
- 可预测的停顿:低停顿的同时实现高吞吐量
- 面向服务端应用,将来替换CMS
优势
- 能充分利用多CPU、多核环境下的硬件优势;
- 能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;
- 不会产生内存碎片,有利于长时间运行;
- 除了追求低停顿处,还能建立可预测的停顿时间模型;
G1收集器是当今收集器技术发展的最前沿成果。
劣势
G1 需要记忆集 (具体来说是卡表)来记录新生代和老年代之间的引用关系,这种数据结构在 G1 中需要占用大量的内存,可能达到整个堆内存容量的 20% 甚至更多。而且 G1 中维护记忆集的成本较高,带来了更高的执行负载,影响效率。
按照《深入理解Java虚拟机》作者的说法,CMS 在小内存应用上的表现要优于 G1,而大内存应用上 G1 更有优势,大小内存的界限是6GB到8GB。
使用场景
个人以为G1已经基本全面压制cms、parallel等回收器,缺点见上面的劣势。但如果不是追求极致的性能,基本可以无脑G1。
参数设置
-XX:+UseG1GC
:指定使用G1收集器;
-XX:InitiatingHeapOccupancyPercent
:当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;
-XX:MaxGCPauseMillis
:为G1设置暂停时间目标,默认值为200毫秒;
-XX:G1HeapRegionSize
:设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region;
CMS和G1的比较
比较点:
- CMS回收垃圾的4个阶段
- CMS的总结和优缺点
- G1回收器特点
- G1回收垃圾的4个阶段
- 什么情况下应该考虑使用G1
- G1设置参数
区别一: 使用范围不一样
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用
区别二: STW的时间
CMS收集器以最小的停顿时间为目标的收集器。
G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)
区别三: 垃圾碎片
CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。(G1从整体上看是“标记-整理”算法,从局部(两个Region之间)看是“标记-复制”算法,不会产生内存碎片)
区别四: 垃圾回收的过程不一样
CMS回收垃圾的4个阶段
- 初始标记:会让线程全部停止,也就是 Stop the World 状态
- 并发标记:对所有的对象进行追踪,这个阶段最耗费时。但这个阶段是和系统并发运行的,所以不会对系统运行造成影响
- 重新标记:由于第二阶段是并发执行的,一边标记垃圾对象,一边创建新对象,老对象会变成垃圾对象。 所以第三阶段也会进入 Stop the World 状态,并且重新标记,标记的是第二阶段中变动过的少数对象,所以运行速度很快
- 并发清理:这个阶段也是会耗费很多时间,但由于是并发运行的,所以对系统不会造成很大的影响
CMS的总结和优缺点
CMS采用 标记-清理 的算法,标记出垃圾对象,清除垃圾对象。算法是基于老年代执行的,因为新生代产生无法接受该算法产生的碎片垃圾。
优点:并发收集,低停顿
缺点:
- 无法处理浮动垃圾,并发收集会造成内存碎片过多
- 由于并发标记和并发清理阶段都是并发执行,所以会额外消耗CPU资源
G1回收器的特点
G1的出现就是为了替换jdk1.5种出现的CMS,这一点已经在jdk9的时候实现了,jdk9默认使用了G1回收器,移除了所有CMS相关的内容。G1和CMS相比,有几个特点:
- 控制回收垃圾的时间:这个是G1的优势,可以控制回收垃圾的时间,还可以建立停顿的时间模型,选择一组合适的Regions作为回收目标,达到实时收集的目的
- 空间整理:和CMS一样采用标记-清理的算法,但是G1不会产生空间碎片,这样就有效的使用了连续空间,不会导致连续空间不足提前造成GC的触发
G1把Java内存拆分成多等份,多个域(Region),逻辑上存在新生代和老年代的概念,但是没有严格区分。
依旧存在新生代老年代的概念,但是没有严格区分。Region最多分为2048个
大对象的处理
除了上面优点之外,还有一个优点,那就是对大对象的处理。在CMS内存中,如果一个对象过大,进入S1、S2区域的时候大于改分配的区域,对象会直接进入老年代。G1处理大对象时会判断对象是否大于一个Region大小的50%,如果大于50%就会横跨多个Region进行存放
G1回收垃圾的4个阶段
- 初始标记:标记GC Roots 可以直接关联的对象,该阶段需要线程停顿但是耗时短
- 并发标记:寻找存活的对象,可以与其他程序并发执行,耗时较长
- 最终标记:并发标记期间用户程序会导致标记记录产生变动(好比一个阿姨一边清理垃圾,另一个人一边扔垃圾)虚拟机会将这段时间的变化记录在Remembered Set Logs 中。最终标记阶段会向Remembered Set合并并发标记阶段的变化。这个阶段需要线程停顿,也可以并发执行。
- 筛选回收:对每个Region的回收成本进行排序,按照用户自定义的回收时间来制定回收计划
什么情况下应该考虑使用G1
- 实时数据占用超过一半的堆空间
- 对象分配或者晋升的速度变化大
- 希望消除长时间的GC停顿(超过0.5-1秒)
G1设置参数
-XX:+UseG1GC
//使用G1收集器
-XX:MaxGCPauseMillis=200
//控制G1回收垃圾的时间(默认200ms)
列表为 G1 GC垃圾收集器常用配置参数:
可选项及默认值 | 描述 |
---|---|
-XX:+UseG1GC | 采用 Garbage First (G1) 收集器 |
-XX:MaxGCPauseMillis=n | 设置最大GC 暂停时间。这是一个大概值,JVM 会尽可能的满足此值 |
-XX:InitiatingHeapOccupancyPercent=n | 设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。默认值 45. |
-XX:NewRatio=n | new/old 年代的大小比例. 默认值 2. |
-XX:SurvivorRatio=n | eden/survivor 空间的大小比例. 默认值 8. |
-XX:MaxTenuringThreshold=n | 对象晋升年代的最大阀值。默认值 15.这个参数需要注意的是:最大值是15,不要超过这个数啊,要不然会被人笑话。原因为:JVM内部使用 4 bit (1111)来表示这个数。 |
-XX:ParallelGCThreads=n | 设置在垃圾回收器的并行阶段使用的线程数。默认值因与 JVM 运行的平台而不同。 |
-XX:ConcGCThreads=n | 并发垃圾收集器使用的线程数。默认值因与 JVM 运行的平台而不同。 |
-XX:G1ReservePercent=n | 设置作为空闲空间的预留内存百分比以降低晋升失败的可能性。默认值10 |
-XX:G1HeapRegionSize=n | 使用G1,Java堆被划分为大小均匀的区域。这个参数配置各个子区域的大小。此参数的默认值根据堆大小的人工进行确定。最小值为 1Mb 且最大值为 32Mb。 |
-XX:G1PrintRegionLivenessInfo | 默认值false, 在情理阶段的并发标记环节,输出堆中的所有 regions 的活跃度信息 |
-XX:G1PrintHeapRegions | 默认值false, G1 将输出那些 regions 被分配和回收的信息 |
-XX:+PrintSafepointStatistics | 输出具体的停顿原因 |
-XX: PrintSafepointStatisticsCount=1 | 输出具体的停顿原因 |
-XX:+PrintGCApplicationStoppedTime | 停顿时间输出到GC日志中 |
-XX:-UseBiasedLocking | 取消偏向锁 |
-XX:+UseGCLogFileRotation | 开启滚动日志输出,避免内存被浪费 |
-XX:+PerfDisableSharedMem | 关闭 jstat 性能统计输出特性,使用 jmx 代替 |
-XX:InitiatingHeapOccupancyPercent | 设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45% |