垃圾收集算法
分代收集理论
分代收集就是根据对象在各个年代内存分配的策略和对象的存活周期来选择对应年代的收集算法,如年轻代中的对象朝生夕死,我们则可以选择复制算法以空间换时间去收集,老年代中的对象大多是老顽固,收集一次可能并没有多少对象要被回收,那这种情况下采用复制算法有点浪费空间且效率低,再者老年代没有别的年代为他进行分配担保,所以我们采用标记清楚或标记整理算法。
-
复制算法
-
标记清除算法
-
标记整理算法
垃圾收集器
-
serial收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)
串行收集器,收集时开启单线程收集,且会STW,新生代采用复制算法,老年代采用标记整理算法。
-
Parallel Scavenge收集器(-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代))
是serial的多线程版本,默认收集线程数与cpu核数相同,可以使用- XX:ParallelGCThreads)指定收集线程数。新生代采用复制算法,老年代采用标记整理算法。
-
parNew收集器(-XX:+UseParNewGC)
ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。新生代采用复制算法,老年代采用标记-整理算法。
-
CMS收集器(-XX:+UseConcMarkSweepGC(old))
CMS收集器是以获取最短停顿时间为目标的收集器。使用标记清除算法。它是第一款真正意义上的并发收集器,第一次实现了垃圾收集线程和用户线程同时工作。它工作的整个过程分为以下步骤:
缺点:
CMS的相关核心参数
-
对CPU资源敏感,会和服务抢资源。
-
在并发标记和并发清理阶段产生的浮动垃圾只能等下一次gc处理。
-
使用标记清除算法会产生大量的空间碎片。
-
会发生concurrent mode failure,上一次垃圾回收还没执行完,新的垃圾回收又被触发。此时会stw,serial old垃圾收集器来回收。
-
初始标记:暂停用户线程(stw),标记所有存活的对象,包括老年代中所有gc roots对象和年轻代中引用到老年代中的对象。其实就是标记所有能被gc roots引用到的对象。这个过程虽然会造成stw,但是速度很快。
-
并发标记:从步骤一中的对象出发找所有存活的对象,这个过程与用户线程一起并发进行,所以会标记不准确,将新生代晋升到老年代的对象、直接在老年代分配内存的对象或更新老年代对象引用关系的对象标记,这些都需要重新标记,以防漏标。这些对象所在的card会被标识为dirty,后续扫描的时候以防扫描整个老年代。这个过程可能会发生concurrent mode failure。
-
重新标记:这个过程就是为了修正并发标记期间因为用户程序继续运行而产生变动的对象的标记记录,会发生第二次stw,且速度比第一次长,比并发标记短,会扫描整个堆。主要用到三色标记里的增量更新算法。
-
并发清理:开启用户线程,垃圾收集线程对未标记的区域做清扫,如果有新增对象会被标记为黑色不做任何处理。
-
并发重置:重置本次gc过程中的标记数据。
-
-XX:+UseConcMarkSweepGC:启用cms
-
-XX:ConcGCThreads:并发的GC线程数
-
-XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
-
-XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一 次
-
-XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
-
-XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设 定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
-
-XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引 用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
-
-XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
-
-XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;
-
三色标记
三色标记是在GC期间异步执行垃圾收集算法的底层原理。三色是指黑色、白色、灰色。
黑色:表示对象被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。
灰色:表示对象已经被垃圾收集器访问过,但是这个对象至少还存在一个引用没被扫描过。
白色:表示对象未被垃圾收集器访问过。如果扫描完所有对象之后,最终未白色的为不可达对象,即垃圾对象。
因为插入了一条或多条从黑色对象到白色对象的新引用或删除了全部从灰色对象到白色对象的直接或间接引用,而造成严重的bug——漏标。有两种方案解决这个问题,CMS 基于 增量更新(Increamental Update)来做并发标记,G1 基于 原始快照(Snap shot At The Begining, SATB)来实现的。
-
增量更新(Increamental Update):当一个白色对象被黑色对象引用,将黑色对象重新标记为灰色,让垃圾回收器重新扫描。
-
原始快照(Snap shot At The Begining, SATB):原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。总而言之就是:无论引用关系删除与否,都会按照刚刚开始扫描的那一刻的对象图快照来进行搜索。
为啥G1不使用增量更新算法呢?因为使用增量更新算法,那变成灰色的对象还要重新扫描一遍,效率太低了,所以G1在处理并发标记的过程比CMS效率要高,这个主要是解决漏标的算法决定的。
https://segmentfault.com/a/1190000021820577 这篇文章写的太好了
记忆集
为了解决跨代引用的问题,在新生代引入的记录集的数据结构(记录从非收集区到收集区的指针集合),避免把整个老年代加入GCRoots扫描范围。垃圾收集场景中,收集器只需通过记忆集判断出某一块非收集区域是否存在指向收集区域的指针即可,无需了解跨代引用指针的全部细节。
记忆集的实现
- 字长精度:每个记录精确到一个机器字长(处理器的寻址位数,如常见的32位或64位),该字包含跨代指针
- 对象精度:每个记录精确到一个对象,该对象中有字段包含跨代指针
- 卡精度:每个记录精确到一块内存区域,该区域中有对象包含跨代指针
卡表
第三种卡精度是使用一种叫做“卡表”的方式实现记忆集,也是目前最常用的一种方式
记忆集是一种抽象概念,卡表是它的实现方式。它记录了记忆集的记录精度、与堆内存的映射关系等。
卡表是使用一个字节数组实现:CARD_TABLE[this addredd >>9]=0,每个元素对应着其标识的内存区域一块特定大小的内存块,称为“卡页”。
hotSpot使用的卡页是2^9大小,即512字节
![]()
img
一个卡页中可包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,表示该元素变脏,否则为0.
GC时,只要筛选卡表中变脏的元素加入GCRoots。卡表的维护卡表变脏上面已经说了,但是需要知道如何让卡表变脏,即发生引用字段赋值时,如何更新卡表对应的标识为1.
hotSpot使用写屏障维护卡表状态。
可看做在虚拟机层面对“引用类型字段赋值”动作的AOP切面,在赋值时产生一个环形通知。赋值前后都属于写屏障,赋值前称为“写前屏障(Pre-Write Barrier)”,赋值后称为“写后屏障(Post-Write Barrier)”。
写屏障的问题虚拟机会为所有赋值操作生成相应的指令,一旦收集器在写屏障中增加了更新卡表操作,无论更新的是不是老年代对新生代的引用,每次只要对引用进行更新,就会产生额外的开销。
伪共享问题
并发场景下,当多个互相独立的变量被读取到一个缓存行时,会影响性能。伪共享参考:https://blog.csdn.net/weixin_43696529/article/details/104884373
解决:
加条件
只有卡表元素未被标记时才将其标记为变脏。
JDK7以后,使用 -XX:+UseCondCardMark参数设定是否开启卡表更新时的条件判断 开启条件自然会增加一个判断开销,但能够避免伪共享问题。根据实际来。———————————————— 原文链接:https://blog.csdn.net/weixin_43696529/article/details/104884514
G1收集器(-XX:+UseG1GC)
G1收集器将堆划分成大小相同的一个个region,最多region有2048个,每个region就是总共的堆内存除以2048,当然我们可以使用-XX:G1HeapRegionSize设置region的大小。G1不再有物理上的分代概念,但是逻辑上还是按照不同region的集合划分成年轻代、老年代,年轻代默认为堆内存的5%,最大为60%,也可以根据-XX:G1NewSizePercent设置,根据-XX:G1MaxNewSizePercent设置最大占比。每个region可能是动态变化的,开始可能是年轻代,但是经过垃圾回收后可能变成老年代。G1中多了一个存放大对象的区域Humongous区,当对象的大小超过了region的一半,则进入H区,可以跨多个H区存放。
G1的收集过程:
-
初始标记:这个过程跟CMS很像,暂停用户线程然后记录gc roots能直接引用的对象,stw的速度很快。
-
并发标记:收集线程与用户线程并发进行,跟CMS的并发标记过程一样。
-
最终标记:跟CMS的重新标记一样,会STW。
-
筛选回收:这个过程采用复制算法,将region中存活的对象复制到另一个region中,几乎不会产生内存碎片,但是不一定会回收所有的region,而是根据用户指定的 -XX:MaxGCPauseMillis期望GC停顿时间来对每个region的回收价值和成本排序,排序后在停顿时间内收集
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字 Garbage-First的由来),比如一个Region花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,在回 收时间有限情况下,G1当然会优先选择后面这个Region回收。这种使用Region划分内存空间以及有优先级的区域回收 方式,保证了G1收集器在有限时间内可以尽可能高的收集效率。
G1的垃圾收集分类
-
YoungGC:YoungGC并不是现有Eden区放满了就会马上触发,G1会计算现在Eden区回收大概需要多长时间,回收时间远小于用户设置的最大期望回收时间,则增加年轻代的region,继续存放新对象,直到下一次Eden区放满,G1计算收集时间接近最大回收时间,就触发YoungGC
-
MixedGC:老年代的堆占有率达到参数-XX:InitiatingHeapOccupancyPercent设定的值则触发,回收所有的YoungGC和部分old以及大对象区。正常的情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次FullGC
-
FullGC:停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批region来供下一次MixedGC使用,这个过程是非常耗时的。
G1收集器参数设置
-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的线程数量
-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区
-XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)
-XX:G1MaxNewSizePercent:新生代内存最大空间
-XX:TargetSurvivorRatio:Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个 年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
-XX:MaxTenuringThreshold:最大年龄阈值(默认15)
-XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合 收集(MixedGC),比如我们之前说的堆默认有2048个region,如果有接近1000个region都是老年代的region,则可能 就要触发MixedGC了
-XX:G1MixedGCLiveThresholdPercent(默认85%) region中的存活对象低于这个值时才会回收该region,如果超过这个值,存活对象过多,回收的的意义不大。
-XX:G1MixedGCCountTarget:在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一 会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。
-XX:G1HeapWastePercent(默认5%): gc过程中空出来的region是否充足阈值,在混合回收的时候,对Region回收都 是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。
G1垃圾收集器优化建议
假设参数 -XX:MaxGCPauseMills 设置的值很大,导致系统运行很久,年轻代可能都占用了堆内存的60%了,此时才 触发年轻代gc。那么存活下来的对象可能就会很多,此时就会导致Survivor区域放不下那么多的对象,就会进入老年代中。或者是你年轻代gc过后,存活下来的对象过多,导致进入Survivor区域后触发了动态年龄判定规则,达到了Survivor 区域的50%,也会快速导致一些对象进入老年代中。所以这里核心还是在于调节 -XX:MaxGCPauseMills 这个参数的值,在保证他的年轻代gc别太频繁的同时,还得考虑 每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc.
什么场景适合使用G1
50%以上的堆被存活对象占用
对象分配和晋升的速度变化非常大
垃圾回收时间特别长,超过1秒
8GB以上的堆内存(建议值)
停顿时间是500ms以内
每秒几十万并发的系统如何优化JVM
Kafka类似的支撑高并发消息系统大家肯定不陌生,对于kafka来说,每秒处理几万甚至几十万消息时很正常的,一般 来说部署kafka需要用大内存机器(比如64G),也就是说可以给年轻代分配个三四十G的内存用来支撑高并发处理,这里就 涉及到一个问题了,我们以前常说的对于eden区的young gc是很快的,这种情况下它的执行还会很快吗?很显然,不可 能,因为内存太大,处理还是要花不少时间的,假设三四十G内存回收可能最快也要几秒钟,按kafka这个并发量放满三 四十G的eden区可能也就一两分钟吧,那么意味着整个系统每运行一两分钟就会因为young gc卡顿几秒钟没法处理新消 息,显然是不行的。那么对于这种情况如何优化了,我们可以使用G1收集器,设置 -XX:MaxGCPauseMills 为50ms,假 设50ms能够回收三到四个G内存,然后50ms的卡顿其实完全能够接受,用户几乎无感知,那么整个系统就可以在卡顿几 乎无感知的情况下一边处理业务一边收集垃圾。G1天生就适合这种大内存机器的JVM运行,可以比较完美的解决大内存垃圾回收时间过长的问题。
如何选择垃圾收集器
-
优先调整堆的大小让服务器自己来选择
-
如果内存小于100M,使用串行收集器
-
如果是单核,并且没有停顿时间的要求,串行或JVM自己选择
-
如果允许停顿时间超过1秒,选择并行或者JVM自己选
-
如果响应时间最重要,并且不能超过1秒,使用并发收集器
-
4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,几百G以上用ZGC
下图有连线的可以搭配使用 JDK 1.8默认使用 Parallel(年轻代和老年代都是) JDK 1.9默认使用 G1
安全点与安全区域
安全点就是指代码中一些特定的位置,当线程运行到这些位置时它的状态是确定的,这样JVM就可以安全的进行一些操作,比 如GC等,所以GC不是想什么时候做就立即触发的,是需要等待所有线程运行到安全点后才能触发。这些特定的安全点位置主要有以下几种:
-
方法返回之前
-
调用某个方法之后
-
抛出异常的位置
-
循环的末尾
大体实现思想是当垃圾收集需要中断线程的时候, 不直接对线程操作, 仅仅简单地设置一个标志位, 各个线程执行过程 时会不停地主动去轮询这个标志, 一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。轮询标志的地方和 安全点是重合的。
安全区域又是什么?
Safe Point 是对正在执行的线程设定的。如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。因此 JVM 引入了 Safe Region。Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。