您好,登录后才能下订单哦!
本篇内容主要讲解“GC过程中需要stop the world的原因是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“GC过程中需要stop the world的原因是什么”吧!
标记-清除 ( mark-sweep )
优点:开销低,速度快 缺点: 原地清理所以无法避免碎片问题
标记-复制 ( mark-copy )
优点:GC后的内存空间是连续的缺点: 可用内存空间减半
标记-整理 ( mark-compact)
优点:无碎片问题,内存空间可用大小不减半 缺点:效率低,开销大
引用-计数 (reference counting)
前三种垃圾回收算法都是间接式的,它们都需要从已知的根集合出发对存活对象图进行遍历,进而才能确定所有的存活对象。 在引用计数中,对象的存活性可以通过引用关系的创建或删除直接判定,从而无须像追踪式回收器那样先通过堆遍历找出所有的存活对象,然后再反向确定出未遍历的垃圾对象。 优点:直接遍历,速度快 缺点:无法解决环形引用问题
对传统的、基本的GC实现来说,由于它们在GC的整个工作过程中都要“stop-the-world”,如果能想办法缩短GC一次工作的时间长度就是件重要的事情。如果说收集整个GC堆耗时太长,那不如只收集其中的一部分?
在对象数量较少的情况下,追踪式垃圾回收器(特别是复制式垃圾回收器)能够最高效地进行垃圾回收。但长寿对象的存在却会影响回收效率,因为回收器不是反复地对其进行标记、追踪,就是反复地把它们从一个半区复制到另一个半区。
根据程序实际运行的情况,jvm关于垃圾回收有2条假说(即2条经验法则)
1)弱分代假说:绝大多数对象的生命周期都很短,绝大多数的对象都是朝生夕灭的。 2)强分代假说:熬过越多次垃圾收集过程的对象越难以消亡。
现在绝大多数的jvm都遵循了分代回收理论来设计。jvm的堆被分成至少两个区域,新生代(young)和 老年代(old)。新生代存放才创建的对象,老年代存放少量存活的对象。
在新生代,对象“朝生夕死”。gc一般采取复制算法,因为此算法的突出特点就是只关心哪些需要被复制,可达性分析只用标记和复制很少的存活对象。不用遍历整个堆,因为大部分都是要丢弃的。但是其缺点也很明显,需要浪费一半的内存空间
所以针对老年代对象的特点,一般采用标记-清理(有的算法会带压缩)策略的算法。这部分如果采用复制算法的话,一方面没有额外空间给其担保,另一方面由于存活率高,复制的开销显著增大 。
一个对象产生之后首先进行栈上分配,栈上如果分配不下会进入伊甸区,伊甸区经过一次垃圾回收之后进入surivivor区,survivor区在经过一次垃圾回收之后又进入另外一个survivor,与此同时伊甸区的某些对象也跟着进入另外一个survivot,什么时候年龄够了就会进入old区,这是整个对象的一个逻辑上的移动过程。
那什么时候会在栈上分配,什么时候会在伊甸区分配?
栈上分配:
线程私有小对象:小对象、线程私有的
无逃逸:就在某一段代码中使用,除了这段代码就没有人认识它了
支持标量替换:意思是用普通的属性、把普通的类型代替对象就叫标量替换
栈上分配会比在堆上分配快一点,如果在栈上分配不下,会优先进行本地分配,也就是 线程本地分配TLAB(Thread local Allocation Buffer): 在伊甸区很多线程都会往里面分配对象,但是分配对象的时候我们一定会进行空间的征用,谁抢到算谁的,多线程的同步,效率就会降低,所以设计了TLAB机制
占用eden,默认为1%,在伊甸区取用百分之一的空间,这块空间叫做线程独有,分配对象的时候首先往线程独有的这块空间进行分配
多线程的时候不用竞争eden就可以申请空间,提高效率
对象什么时候进入老年代?
回收了多少次进入老年代?
超过 XX:MaxTenuringThreshold
指定次数(YGC)
Parallel Scavenge 15次进入老年代
CMS 6次进入老年代
G1 15次进入老年代
网上有说可以次数往上调大,这个是不可能的
为了能够适用不同程序的内存状况,虚拟机并不是永远的要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Surivivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
两个Survivor之间拷贝来拷贝去只要超过百分之50的时候把年龄最大的直接放入到old区,也就是不一定非得到15岁。
在s1里面有这么多对象拷贝到了s2里面超过百分之50的话,s1里面在加上伊甸区里面,整个一个对象一下子拷贝到s2里面,经过一次垃圾回收,过去之后,这个时候整个加起来对象已经超过s2的一半了,这里面年龄最大的一些对象直接进入老年区,这个就叫做动态年轻判断
大对象直接进入老年代
所谓的大对象是指,需要连续大量内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组,经常出现大对象容易导致内存还有不少空间的时候就提前触发了垃圾收集来获得足够的连续内存空间
start 先是new一个对象,然后在栈上进行分配,如果在栈上能够分配,就分配到栈上,栈直接弹出,弹出结束,如果在栈上分配不下,判断对象是否为大对象,如果是大对象,直接进入老年代,FGC后结束如果不是,进入线程本地分配(TLAB),不管怎么样都会到伊甸区进行GC清除,如果清除完毕,直接结束,如果没有清除完毕,进入S1,S1继续GC清除,如果年龄到了进入old区,如果年龄不够进入S2,然后S2再继续GC的清除,要么年龄到了,要么动态年龄达到
MinorGC/YGC: 年轻代空间耗尽时触发
MajorGC/FullGC: 在老年代无法继续分配空间时触发,新生代老年代同时进行回收
年轻代中的对象可能会引用老年代的对象,那么年轻代垃圾回收的时候怎么避免老年代全部扫描?
解决办法:记忆集 (remembered set)对应实现 卡表(CardTable)
GC Roots = 新生代 GCRoot + RememberedSet
新生代收集器: Serial、ParNew、Parallel Scavenge
老年代收集器: Serial Old、CMS、Parallel Old
新生代和老年代收集器: G1、ZGC、Shenandoah
每种垃圾回收器之间不是独立操作的,下图表示垃圾回收器之间有连线表示,可以协作使用:
Serial
单线程串行 复制算法
Serial Old
老年代的收集器,与Serial一样是单线程,不同的是算法用的是标记-整理(Mark-Compact)
Parallel Scavenge
Serial 收集器的多线程版本,并行收集器。 复制算法
Parallel Old
老年代的收集器,是Parallel Scavenge老年代的版本。其中的算法替换成 Mark-Compact。
ParNew
跟Parallel类似,专门为了配合cms使用。 复制算法。 新生代并行收集器。
CMS
concurrent mark sweep 并发标记清除,以获取最短回收停顿时间为目标。 老年代并发回收器。
CMS(Concurrent Mark Sweep)是一款里程碑式的垃圾收集器,为什么这么说呢?因为在它之前,GC线程和用户线程是无法同时工作的,即使是Parallel Scavenge,也不过是GC时开启多个线程并行回收而已,GC的整个过程依然要暂停用户线程,即Stop The World。这带来的后果就是Java程序运行一段时间就会卡顿一会,降低应用的响应速度,这对于运行在服务端的程序是不能被接收的。
GC时为什么要暂停用户线程?
首先,如果不暂停用户线程,就意味着期间会不断有垃圾产生,永远也清理不干净。
其次,用户线程的运行必然会导致对象的引用关系发生改变,这就会导致两种情况:漏标和错标。
漏标
原本不是垃圾,但是GC的过程中,用户线程将其引用关系修改,导致GC Roots不可达,成为了垃圾。 这种情况还好一点,无非就是产生了一些浮动垃圾,下次GC再清理就好了。
错标
原本是垃圾,但是GC的过程中,用户线程将引用重新指向了它,这时如果GC一旦将其回收,将会导致程序运行错误。
针对这些问题,CMS是如何解决的呢?它是如何做到GC线程和用户线程并发工作的呢???
初始标记
只标记GC Root 直接关联的对象,时间很短, STW
并发标记
从GC Root 直接关联的对象开始遍历整个对象图,与用户线程并行
重新标记
修正并发标记,三色标记。 STW
并发清理
采用标记清除算法,存在碎片问题
GC时为什么要暂停用户线程?
首先,如果不暂停用户线程,就意味着期间会不断有垃圾产生,永远也清理不干净。
其次,用户线程的运行必然会导致对象的引用关系发生改变,这就会导致两种情况:漏标和错标。
漏标
原本不是垃圾,但是GC的过程中,用户线程将其引用关系修改,导致GC Roots不可达,成为了垃圾。这种情况还好一点,无非就是产生了一些浮动垃圾,下次GC再清理就好了。
错标
原本是垃圾,但是GC的过程中,用户线程将引用重新指向了它,这时如果GC一旦将其回收,将会导致程序运行错误。
为什么CMS的GC线程可以和用户线程一起工作 ? 采用三色标记解决
标记的过程大致如下:
刚开始,所有的对象都是白色,没有被访问。
将GC Roots直接关联的对象置为灰色。
遍历灰色对象的所有引用,灰色对象本身置为黑色,引用置为灰色。
重复步骤3,直到没有灰色对象为止。
结束时,黑色对象存活,白色对象回收。
这个过程正确执行的前提是没有其他线程改变对象间的引用关系,然而,并发标记的过程中,用户线程仍在运行,因此就会产生漏标和错标的情况。
假设GC已经在遍历对象B了,而此时用户线程执行了A.B=null
的操作,切断了A到B的引用。
本来执行了A.B=null
之后,B、D、E都可以被回收了,但是由于B已经变为灰色,它仍会被当做存活对象,继续遍历下去。
最终的结果就是本轮GC不会回收B、D、E,留到下次GC时回收,也算是浮动垃圾的一部分。
实际上,这个问题依然可以通过「写屏障」来解决,只要在A写B的时候加入写屏障,记录下B被切断的记录,重新标记时可以再把他们标为白色即可。
假设GC线程已经遍历到B了,此时用户线程执行了以下操作:
B.D=null;//B到D的引用被切断A.xx=D;//A到D的引用被建立
这种情况什么时候成立? 一个对象失去了引用,还可以再被其它对象引用?
他这个的是可达性分析的时候,从 B.D = null , 但是 别的线程里面还用着 D , 所以还能进行引用D
B到D的引用被切断,且A到D的引用被建立。
此时GC线程继续工作,由于B不再引用D了,尽管A又引用了D,但是因为A已经标记为黑色,GC不会再遍历A了,所以D会被标记为白色,最后被当做垃圾回收。
可以看到错标的结果比漏表严重的多,浮动垃圾可以下次GC清理,而把不该回收的对象回收掉,将会造成程序运行错误。
错标只有在满足下面两种情况下才会发生:
只要打破任一条件,就可以解决错标的问题。
原始快照打破的是第一个条件:当灰色对象指向白色对象的引用被断开时,就将这条引用关系记录下来。当扫描结束后,再以这些灰色对象为根,重新扫描一次。相当于无论引用关系是否删除,都会按照刚开始扫描时那一瞬间的对象图快照来扫描。
增量更新打破的是第二个条件:当黑色指向白色的引用被建立时,就将这个新的引用关系记录下来,等扫描结束后,再以这些记录中的黑色对象为根,重新扫描一次。相当于黑色对象一旦建立了指向白色对象的引用,就会变为灰色对象。
这个写屏障指的可不是并发编程里的写屏障哦!这里的写屏障指的是属性赋值的前后加入一些处理,类似于AOP。
CMS采用的方案就是:写屏障+增量更新来实现的,打破的是第二个条件。
当黑色指向白色的引用被建立时,通过写屏障来记录引用关系,等扫描结束后,再以引用关系里的黑色对象为根重新扫描一次即可。
GC时为什么要暂停用户线程?
有没有办法,彻底消除STW ? 看样子是重新标记的时候,也避免不了标错,所以才必须 STW
|- 黑色指向白色的引用被建立 增量更新 |
错标--> | | --> 写屏障
|- 色指向白色的引用全部被破坏 原始快照 |
弱三色不变式:所有被黑色对象引用的白色对象都处于灰色保护状态。
强三色不变式:不存在从黑色对象执行白色对象的指针。
基于原始快照的解决方案:弱三色不变式,将A的目标引用对象标位灰色。
基于增量更新的解决方案:强三色不变式,将A的目标引用对象标位黑色。
尽管CMS是一款里程碑式的垃圾收集器,开启了GC线程和用户线程同时工作的先河,但是不管是哪个JDK版本,CMS从来都不是默认的垃圾收集器,究其原因,还是因为CMS不太完美,存在一些缺点。
CMS碎片化问题,没有可用空间时,用SeralOld单线程整理空间,为什么不用Parallel Old ?
串行收集器:serial + serial old
并行收集器:Paraller Scanvenge + Paraller Old
并发标记清除收集器组合:ParNew + CMS + serial old
Java 7 update4 之后引入,Jdk9默认
逻辑分代,物理不分代
G1 将Java 堆划分为多个大小相等的独立区域,JVM最多可以有2048个Region。一般Region大小等于堆大小除以2048。 比如堆大小为4096M,则Region大小为2M
G1收集器的运行过程:
初始标记(Initial Marking): 标记GC Roots 能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确在可用的Region中分配新对象,需要耗时较短的停顿线程,但是是借用Minor GC的时候同步完成的,所以在这个阶段实际没有额外的停顿
并发标记(Concurrent Marking): 从GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆里面的对象图,找出要回收的对象,这个阶段耗时较长,但可以和用户程序并发执行。
最终标记(Final Marking): 对用户线程做另一个短暂的暂停,用户处理并发阶段结束后仍遗留下来的最后那少量的SATB记录
筛选回收(Live Data Counting and Evacuation): 负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户锁期望的停顿时间来制定回收计划,可以只有选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象赋值到空的Region中,再清理整个Region的全部空间。
根据期望的停顿时间,进行预估比例的回收,可能到时回收跟不上对象的分配。进而触发Full GC
一张图让你看懂JVM之垃圾回收算法详解
到此,相信大家对“GC过程中需要stop the world的原因是什么”有了更深的了解,不妨来实际操作一番吧!这里是亿速云网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。