jvm垃圾回收算法和垃圾回收器

垃圾回收算法

jvm垃圾回收算法包括复制算法、标记清楚算法和标记整理算法,它们都基于分代收集理论。所谓分代收集理论,可以理解为jvm根据对象的生命年龄将他们分在不同的内存模块,也就是熟知的新生代和老年代。由于新生代存储的对象大部分都是朝生夕死的对象,一般使用复制算法,只需要付出少量的复制成本就能满足了。而老年代的对象生命周期都比较长,一般会选择标记-清楚或者标记-整理算法。复制算法是比其他两种算法快10倍以上的。

标记-复制算法

复制算法是将内存区域划分为两个模块,如下图,在内存被整理前,左边的内存区局用于当前存放新生的对象,右半部分是未使用的空白内存区域。当内存需要复制算法重新整理时,会标记存活对象,将它们规整的复制到右半部分,左半部分就空闲出来,这种算法常常用于jvm的新生代。

黑色-垃圾对象,灰色-存活对象,空白-可用内存

jvm垃圾回收算法和垃圾回收器_第1张图片

标记-清除算法

标记-清除算法即将内存所有对象通过某种算法(如GCRoots可达性分析算法)进行标记,查找出所有的存活对象,将剩余对象进行清除。它最大的弊端就是清除完所有的垃圾对象以后,内存碎片化非常严重。 

jvm垃圾回收算法和垃圾回收器_第2张图片

标记-整理算法

标记-整理也是通过算法(可达性分析算法)标记存活的对象,将剩余垃圾对象进行清除,最后将内存进行整理,存活对象放在一端,剩余空间都是可用内存空间。标记-整理结合了以上两种算法的优点进行了整合。

jvm垃圾回收算法和垃圾回收器_第3张图片

垃圾回收器

垃圾回收算法是理论,而垃圾回收器是这些算法的具体实现。

Serial收集器

jvm参数:-XX:+UseSerialGC -XX:+UseSerialOldGC

Serial顾名思义串行,它使用单线程对内存中垃圾对象进行回收,也就意味着工作线程在此期间会全部停止工作(stop the world),弊端显而易见用户体验差,另外回收的内存越大可能花费的时间越长。相反,由于单线程工作,避免了上下文的切换所以它简单效率高。

Serial Old收集器是老年代的版本收集器,它也是使用单线程工作。

新生代使用的是复制算法,老年代使用的是标记-整理算法

Parallel收集器

jvm参数:-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代) 

Parallel收集器可以理解为Serial的多线程版本,多线程并发清除垃圾对象,减少stw的时间,使用户体验比Serial收集器更好。

parallel old是parallel收集器的老年代版本,使用多线程和标记-整理算法可以提高系统的吞吐量。

新生代使用的是复制算法,老年代使用的是标记-整理算法

ParNew收集器

jvm参数:-XX:+UseParNewGC

ParNew收集器跟Parallel收集器很类似,最主要的区别就是可以配合CMS收集器一起使用。

CMS(Concurrent Mark Sweep)收集器-并发标记清除

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程 (基本上)同时工作。  并发标记大概占用整个垃圾回收时间的80%

流程: 

jvm垃圾回收算法和垃圾回收器_第4张图片

初始标记:初始标记会触发stw,但是这个时间非常短,因为这一步骤只会标记那些GCRoots直接引用的对象。

并发标记:并发标记和业务线程并发执行,它继续向下标记GCRoots直接引用对象的所引用的对象,这个过程会比较长但不需要stop the world。但此时某些被标记的对象可能已经变为垃圾对象(多标),而业务线程又会产生新的非垃圾对象(漏标)。

重新标记:重新标记会stop the world,它会重新处理并发标记那些发生变动的对象引用。主要用到三色标记里的增量更新算法做重新标记。

并发清理:工作线程可以继续处理业务,同时CMS线程开始清理未标记的垃圾对象。这个过程如果有新增对象会被标记为黑色,不会处理。

并发重置:重置本次GC过程中标记的对象

优点:并发标记,停顿时间短。

缺点:

并发处理导致与服务器业务线程竞争CPU资源。

无法处理浮动垃圾,并发标记和并发清理期间产生的垃圾只能等到下一次gc清理

它使用的标记-清除算法会导致大量的内存碎片。但是可以通过调优参数在本次GC结束后进行清理(-XX:+UseCMSCompactAtFullCollection

CMS核心调优参数:

1. -XX:+UseConcMarkSweepGC:启用cms
2. -XX:ConcGCThreads:并发的GC线程数
3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设
定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引
用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;  

三色标记

三色分为黑色、灰色、白色,初始标记时jvm从GCRoots开始扫描直接引用对象,并发标记向下搜索那些被GCRoots直接引用对象的对象。当扫描完一个对象包含的所有引用后,就会将这个对象的对象头中标志位设置黑色标记(注意只是标志位设置了标记);如果某个对象并没有完全扫描完成,那么这个对象被置为灰色标记;还没有被扫描的对象不会标记(所有对象默认为白色)。白色表示GCRoot不可达,会被jvm回收

多标-浮动垃圾

在并发标记时,垃圾回收线程和业务线程并发执行,在方法运行结束后,方法内部存在栈中局部变量会被释放,此前初始标记时他们已经被标记为非垃圾对象,这些对象称为浮动垃圾,本轮Full GC不会回收他们,下一次full gc会清除他们。

另外并发标记过程产生的新对象会被标记为黑色,不会被jvm回收掉。

漏标-读写屏障(可以理解为AOP,在做某种操作时前后可以做额外处理)

在并发标记时,业务线程可能会修改某些对象的引用,比如对象引用被删除后,又重新被其他已经扫描过的对象(标记为黑色)所引用,而这个引用已经无法被扫描了,这就产生了漏标。如果不做特殊处理,这些漏标的对象会被当成垃圾对象回收掉。jvm有两种解决方式:增量更新和原始快照

增量更新:将新增的黑色对象指向白色对象的引用通过集合记录下来,等并发标记结束后,将这些新增引用的黑色对象重新扫描一次

原始快照:当灰色对象要删除指向白色对象的引用时,将这些要删除的引用记录下来,等并发标记结束后,灰色对象指向的白色对象设置为黑色对象,标记为非垃圾对象,本次gc不回收,等下一次回收。

记忆集和卡表 

新生代在做GCRoot可达性分析时,可能会碰到跨代引用的情况(如老年代引用新生代的对象),这种情况如果去扫描老年代效率会很低,也违背了新生代和老年代的设计初衷。通过在新生代引入记忆集的数据结构(从非收集区到收集去的指针集合) 解决跨代引用。

hotspot使用一种叫卡表的方式实现记忆集,卡表是一个字节数组的数据结构,每个元素对应某一内存区域,判断某一特定大小的内存区域是否存在跨代引用就是通过这个字节数组内某一元素的是否存指向收集区的指针即可。

G1(Garbage First)垃圾回收器

G1核心思想让我们忘记以前的分代模型(即将内存分为年轻代和老年代两块独立的内存),它重新定义一个新的模型,不在将内存分为年轻代和老年代两大块,取而代之的是将一整块内存分为很多个内存格子(称为region,默认为2048个region),可以想象为拥有2048个格子的Excel表,而region里面存储的对象分为四种类型:Eden、Survivor、Old和Humongous。其中Humongous是G1新概念,译为“巨大的”,在这里如果一个格子被标记为Humongous,代表这个格子里存储的都是大对象。如果一个对象很大,大到一个格子存储不下,那么jvm就会使用连续多个格子来存储这个大对象。其他三种和以前我们理解的内存区域概念一致。

垃圾回收过程

jvm垃圾回收算法和垃圾回收器_第5张图片

初始标记:和CMS的初始标记一样

并发标记:和CMS的并发标记一样

最终标记:和CMS的重新标记一样

筛选回收:筛选回收会stop the world停止业务线程,这是垃圾回收过程中和CMS最大的区别。筛选回收做到了智能回收,它会对每个region做回收成本和回收价值的排序,最中来决定回收哪些region能符合程序员设置的jvm参数-XX:MaxGCPauseMillis指定的stop the world停顿时间。不管是老年代还是年轻代用的回收算法都是复制算法,它会把一个region中非垃圾对象复制到另一个空的region中。G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Gegion(这也就是它的名字Garbage-First的由来),比如一个Region花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,在回 收时间有限情况下,G1当然会优先选择后面这个Region回收

G1垃圾回收方式分类:

YoungGC:YoungGC针对的region是Eden区。注意G1并不是Eden区放满了就立即开始gc,而是计算回收当前这么多的Eden区的内存所需时间是否达到程序员设置的jvm参数-XX:MaxGCPauseMillis指定的时间,如果没有则不会进行GC;否则继续新增Eden区直到下一次放满,接近程序员指定的停顿时间,才开始进行gc。

MixedGC:针对所有的young、old和大对象。当老年代的对象占有率达到了程序员设置的-XX:InitiatingHeapOccupancyPercent值,就会触发MixedGC,他会回收所有的young和部分old以及大对象,当然会根据设置的停顿时间选择合适回收region。主要用复制算法回收垃圾,如果在回收的过程中发现没有足够的空region来存储非垃圾对象,那么会触发FullGC。

FullGC:停止业务线程,采用单线程进行标记,清理和压缩整理,以此空闲出来一批region来给下一次MixedGC使用。这个过程非常耗时。

超大并发系统jvm参数调优

web系统 

对于业务复杂并发高的系统,往往比较占用内存和CPU资源,尤其内存。当前jdk8开发web系统通常使用ParNew配合CMS的垃圾回收器,对于jvm来说如何配比年轻代和老年代的内存才能减少FullGC的次数,是一个需要评估业务和并发量才能进行调优的技术难题。拿点外卖来举例,在成都2000万的人口,假设每天中午点外卖占30%,每天集中在11:00-12:00点外卖,在而且没有竞争对手,有4核8G内存的机器若干。对于这样一个系统我们如何优化jvm参数?

600万的系统订单在一个小时集中处理,也就是接近1700qps,按每台机器能处理300qps,至少需要6台机器才能抗住。对于单台处理300qps的订单,即每秒会创建300个订单对象,假设每个订单对象有100个字段,每个字段按最大算占用8个字节,一个订单对象大概在800B接近1KB的大小,我们按1KB计算,300个订单就是300KB。但是订单会伴随着其他业务的触发,如通知商家,骑手配送,生成红包,系统奖励等等业务,如此生成的对象就不仅仅是300个订单,还有触发的其他业务,我们将它扩大10倍,即生成一个订单占用3M大小内存。但是用户不可能一来就直接下订单,会先逛逛然后选择符合自己喜欢的口味菜品,这期间跟订单关系不大,但是查询数据也需要占用内存,我将它跟着订单一起算,再将其扩大10倍,那么生成一个订单占用30M大小内存。

每秒生成30M对象,而这些对象一秒后都成为垃圾,设置使用ParNew+CMS垃圾回收器,分机器一半内存给jvm,使用之前内存模型的jvm调优参数:‐Xms3072M ‐Xmx3072M ‐Xmn1024M ‐Xss512K ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M,年轻代1G,按照8:1:1的配比,Eden区域800M,两个Survivor区各有100M,Eden区内存在二三十秒后触发minorGC,由于Survivor区域动态年龄判断机制,每minorGC一次后,就有可能一批对象进入老年代,这会造成频繁的FullGC,最终可能导致系统卡顿,影响用户体验。jvm调优将年轻代内存加大,避免触发动态年龄判断机制,有效的将朝生夕死的订单对象全部回收。如:‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss512K ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M。

你可能感兴趣的:(jvm,java)