一、垃圾收集算法
分代收集理论:
堆内存分为新生代和老年代,我们可以根据各个年代的特点选择合适的垃圾收集算法。
标记-复制算法:
将内存分为大小相同的两块,每次使用其中一块,当这块内存使用完后,就将还存活的对象复制到另一块,然后将这块内存全部清空。这种算法速度快,但是浪费空间,多用于年轻代。
标记-清除算法:
分为“标记”和“清除”两个阶段,先标记后清除,可以标记存活对象,然后回收未标记的,也可以标记垃圾对象,回收标记对象。但是标记对象过多的话效率不高,并且回收垃圾对象后会产生大量的内存碎片,多用于老年代。
标记-整理算法:
和标记-清除类似,但标记好存活对象后会把这些存活对象向内存的一端移动,然后清理掉边界外的内存,同时内存地址也会随之变化,多用于老年代。
二、垃圾收集器
1、 Serial收集器
是单线程的,最原始的垃圾收集器,有年轻代和老年代两个版本,新生代采用复制算法,老年代采用标记-整理算法,可通过参数-XX:+UseSerialGC -XX:+UseSerialOldGC开启。
2、Parallel收集器 JDK1.8默认使用此收集器(新生代、老年代)
Parallel收集器其实就是Serial收集器的多线程版本,也有年轻代和老年代两个版本,算法也和Serial相同,默认的收集线程数跟cpu核数相同,可通过参数-XX:+UseParallelGC、-XX:+UseParallelOldGC开启,-XX:ParallelGCThreads指定线程数。
3、ParNew收集器
和Parallel类似,但除了Serial收集器外,只有它能与CMS收集器配合使用,所以出现了PraNew。可通过参数-XX:+UseParNewGC开启。
4、CMS收集器
可以让垃圾收集线程与用户线程同时工作的,采用标记-清除算法,工作流程分为四个阶段:
初始标记: 需要STW,只标记GC Roots直接能引用的对象,速度很快。
并发标记: 不需要STW,从GC Roots的直接关联对象开始遍历整个内存, 这个过程耗时较长,并且因为用户线程同时运行,可能会有导致已经标记过的对象状态发生改变。
重新标记: 需要STW,修正并发标记中因用户线程继续运行导致标记变动的部分对象的标记(主要是处理漏标问题),一般会比初始标记阶段的时间稍长,但远比并发标记阶段时间短,主要用到三色标记里的增量更新算法。
并发清理: 不需要STW,GC线程对未标记的区域做清除,这个阶段如果有新增对象会被标记为黑色不做任何处理,清理完会并发重置本次GC过程中的标记。
CMS缺点:
1、会和服务抢资源
2、无法处理浮动垃圾(并发标记和并发清理阶段用户线程新产生的垃圾)
3、标记-清除算法会产生大量内存碎片,但可通过参数让jvm在执行完标记清除后再做整理
4、会有并发失败问题(concurrent mode failure),就是本次Full Gc还没执行完,然后垃圾回收又被触发的情况。因为在并发标记和并发清理阶段不会STW,用户线程可能会有大对象直接放进老年代,但是这时候老年代的剩余内存不够,就会再一次出发Full Gc,这时就会STW,并用serial old垃圾收集器来回收,单线程执行,所以效率很低
CMS核心参数
-XX:+UseConcMarkSweepGC:启用cms
-XX:ConcGCThreads:并发时的GC线程数
-XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理
-XX:CMSFullGCsBeforeCompaction:指定多少次FullGC之后压缩一次
-XX:CMSInitiatingOccupancyFraction:老年代使用率达到该比例触发FullGC(默认是92%):
-XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收比例,不用该参数时,JVM仅在第一次使用设定值,后续会自动调整
-XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc
-XX:+CMSParallellnitialMarkEnabled:在初始标记的时候多线程执行
-XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行
5、G1收集器
G1内存模型
G1和以前的收集器内存结构不同,如上图:它把堆内存分成了许多大小相等的内存块(Region),默认2048个,每个Region的大小就是堆内存除以2048,也可以通过参数指定大小,G1也是分代回收的算法,但是不是物理层面上的年轻代和老年代,每个Region再GC后都可能变成其他的年龄代,默认年轻代的比例为5%,Eden和Surivior区的比例也是8:1:1,所有年龄代都采用标记-复制算法。G1的大对象会存放在有自己特有的大对象区(Humongous)而不是老年代,当新增对象大小超过一个Region的50%时,就会把这个对象放进Humongous区,如果一个Region放不下,会横跨多个Region来存放,Full GC的时候除了收集年轻代和老年代,也会将Humongous区回收。
G1还可以设置最大STW时间,就是整个GC时每个流程STW时间总和的最大值,默认为200ms,可以通过参数调整,底层会通过一些算法来保证STW时间保持在这个值以内,从而可控制STW时间,对用户体验来说更为友好。
G1工作流程分为四个阶段:
初始标记: 需要STW,只标记GC Roots直接能引用的对象,速度很快。
并发标记: 不需要STW,从GC Roots的直接关联对象开始遍历整个内存, 这个过程耗时较长,并且因为用户线程同时运行,可能会有导致已经标记过的对象状态发生改变。
最终标记: 需要STW,修正并发标记中因用户线程继续运行导致标记变动的部分对象的标记(主要是处理漏标问题),一般会比初始标记阶段的时间稍长,但远比并发标记阶段时间短,主要用到三色标记里的增量更新算法。
筛选回收: 这是和CMS不同的地方,由于G1可以设置最大STW时间,所以在回收前会对每个Region的回收性价比做排序,比如有A、B两个Region,回收10M的空间在A中需要10ms,在B中需要5ms,所以B就会排在A之前去回,如果需要回收的对象较多,达到了最大STW时间,剩下的对象就会等待下次GC时回收。
G1垃圾收集分类
YoungGC: G1的YoungGC不会在Eden区存满之后立即触发,会计算现在Eden区回收的大概时间,如果远低于自定义的最大STW时间,就会把对象存入空白Region中使其变成Eden区,以此类推,直到回收时间接近最大STW时间才会出发YoungGC。
MixedGC: 混合回收,G1可以设置老年代的堆占有率达,到达这个值后,会回收所有的年轻代垃圾对象和部分老年代(不超过最大STW时间就能够清理的对象)以及大对象区。
Full GC: MixedGC时,如果没有足够的Region去存放老年代对象,就会触发Full Gc,会停止所有用户线程,采用单线程进行标记-整理
G1收集器参数:
-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的线程数量
-XX:G1HeapRegionSize:指定单个Region大小(1MB~32MB,必须是2的N次幂)
-XX:MaxGCPauseMillis:最大STW时间(默认200ms)
-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%,值配置整数,默认就是百分比)
-XX:G1MaxNewSizePercent:新生代内存最大空间
-XX:MaxTenuringThreshold:最大分代年龄(默认15)
-XX:InitiatingHeapOccupancyPercent:老年代的堆占有率达(默认45%)
-XX:G1MixedGCLiveThresholdPercent:(默认85%) region中的存活对象低于这个值时才会回收该region
-XX:G1MixedGCCountTarget:筛选回收次数(默认8次),筛选回收阶段可以回收一会,然后暂停回收,恢复用户线程,一会再开始回收,这样可以让系统不会单次停顿时间过长。
-XX:G1HeapWastePercent: 设置空闲Region阈值(默认5%),在MixedGC过程中,基于复制算法,堆内存中会不断空出来新的Region,一旦空闲出来的Region数量达到了这个值,此时就会立即结束混合回收,防止因为老年代复制内存空间不够触发Full Gc。
G1应用场景:
例如部署了Kafka的机器,在高并发情况下,每秒几万的消息会很常见,如果使用32G内存机器的话可以给堆内存分配20G,但这些消息通常都是消费之后就变成了垃圾对象,Young Gc这时候需要清理的对象可能就有十多个G,清理十几个G的垃圾对象会使STW时间变长,影响用户体验,这种场景下就可以使用G1,比如设置最大STW时间为100ms,假设100ms可以回收3G的垃圾对象,虽然每次GC清理的垃圾可能会变少,但是STW时间会缩短很多,对于用户体验来说很好。
总结来说,G1适用于大内存机器,并且停顿时间过长,业务场景需要设置8GB以上的堆内存就可以选用G1。
如何选择垃圾收集器:
看服务器类型:
如果是单核,并且没有STW时间的要求,可以使用串行收集器Serial
如果是多核,但STW允许1秒以上,可以使用Parallel,JDK1.8默认就是这个
如果是多核,但STW不能超过1秒,可以使用ParNew+CMS组合或者G1,JDK1.9默认是G1
看堆分配的内存大小:
4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1
下图中互相连线的收集器可搭配使用
三、三色标记算法
就是在可达性分析扫描过程中,把对象按照是否扫描过,标记成黑、灰、白三种颜色,用来解决多标和漏标的情况
黑色: 表示该对象已经被扫描过, 并且这个对象的所有引用都已经扫描过,黑色对象不可能是垃圾对象,并且黑色对象不可能直接指向某个白色对象。
灰色: 表示该对象已经被扫描过, 但这个对象上至少存在一个引用还没有被扫描,会在重新标记阶段重新扫描。
白色: 表示对象未被扫描过, 在扫描结束的阶段,仍然是白色的对象, 代表不可达,会被回收。
浮动垃圾: 并发标记过程中,由于方法运行结束导致部分GcRoot被销毁,这些GcRoot引用的对象之前又被标记为非垃圾对象,那么此次GC不会回收这部分内存。这部分本该回收但是没有回收的内存,就叫做浮动垃圾。还有并发清理开始后产生的新对象,也会被标记成黑色,留到下一轮再进行清理,这些对象中也可能存在浮动垃圾。
多标: 浮动垃圾就是多标
漏标: 就是被引用的对象被当成垃圾误删,比如下图中:GC过程中,C对象还没有被扫描到,此时用户线程代码中B对象中把C对象赋为Null,此时B-C的引用就会被取消,然后代码中又把A对象中的某个成员变量赋值为C,C又会被重新引用,但此时C的引用已经被取消了,所以就会出现漏标的情况
两种解决方案:增量更新和原始快照 ,这两种方法都是通过写屏障实现的。
增量更新(Incremental Update): CMS
就是黑色对象一旦新增了指向白色对象的引用之后, 就把这个新的引用记录下来, 在重新标记阶段, 把这些记录过的引用关系中的黑色对象为根, 重新扫描一次,可以简单理解为:黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了。
原始快照(Snapshot At The Beginning,SATB): G1
就是当灰色对象要删除指向白色对象的引用关系时, 就把这个要删除的引用记录下来, 扫描结束后, 再把这些记录过的引用关系中的灰色对象为根, 重新扫描一次,将扫描到的白色对象直接标记为黑色,等下一轮gc的时候重新扫描。
写屏障:
就是指在赋值操作前后,加入一些处理逻辑(类似Spring的AOP),增量更新是写后屏障、SATB是写前屏障
void oop_field_store(oop* field, oop new_value) {
pre_write_barrier(field); // 写前屏障
*field = new_value;
post_write_barrier(field, value); // 写后屏障
}
记忆集与卡表
记忆集(Remember Set): 新生代可达性扫描过程中可能会碰到跨代引用的对象,记忆集就是用来存储这些跨代对象指针集合的一种数据结构, 不止新生代有,所有分区域收集的垃圾收集器中都会也存在这种问题,但是跨代引用的情况是很少出现的。
卡表(Cardtable): 记忆集是一种抽象的概念,卡表是JVM的实现方式,年轻代的卡表使用一个字节数组实现,其中每个card占1字节。在老年代会按512字节大小划分成很多内存空间,其中每个空间就是一个卡页,一个卡页中包含多个对象,卡页的状态和起始内存地址会维护在卡表中,如果有卡页中有引用到年轻代的对象,就会在年轻代的卡表里把对应的card更新成Dirty状态,在可达性分析的时候,会连带这个卡页中所有的对象一起扫描,卡表更新状态的赋值操作是写后屏障。
安全点与安全区域
安全点:
就是用户线程执行到某些特定的代码位置时,会去轮训一个标志位,如果确认可以继续执行才会继续执行,这样JVM就可以安全的进行一些操作。
比如GC不是在任意时间就可以立即触发的,需要等待所有线程运行到安全点后才能触发,因为有一些核心代码是绝对不能STW的,例如CAS,还有每次执行完代码JVM还需要修改程序计数器,这种有原子性操作代码是绝对不能随意暂停的。
这些特定的安全点位置主要有以下几种:
方法返回之前、调用某个方法之后、抛出异常的位置、循环的末尾
安全区域:
如果一个线程处于Sleep或中断状态,它本身就不能响应JVM的中断请求,引用关系也不会发生变化,在这个区域内的任意地方开始 GC 都是安全的,所以这样的代码区域叫做安全区域。