一)垃圾回收器概述:
1.1)按照线程数来区分:
串行回收指的是在同一时间端内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾回收工作结束,在诸如单CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合,出行回收器的性能表现可以超过并行回收期和并发回收器,所以串行回收默认被应用在客户端的client模式下面的JVM中,但是在并发能力比较强的CPU上,并行回收器产生的停顿时间要大于串行回收器,和串行回收相反,并行收集可以运用多个CPU同时进行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然和串行回收一样,采用独占式,会造成STW;
1.2)按照工作模式来分,可以分为并发式垃圾回收器和独占式垃圾回收器:
并发式垃圾回收器可以和用户线程交替进行工作,尽可能的来减少应用线程的停顿时间
独占式垃圾回收器一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收器GC完成
吞吐量:运行用户代码的时间占总运行时间的比例,总运行时间=程序的运行时间(a)+GC垃圾回收的总时间(b)
暂停时间=STW时间
收集频率:回收的频率低,不代表一次GC的时间短,大学洗衣服
一次攒一快洗(时间比较长)VS经常洗(一天一洗,时间比较短),频率越高,STW时间短一点
1)吞吐量:吞吐量越大越好,就是用户线程所执行的时间在整个JVM生命周期中所占用的时间越长,那么垃圾频率就越低,但是每一次执行GC,那么用户线程停止,STW时间就越长(类比于洗衣服),一次性的暂停时间就很长,用户体验感就可能很差劲,但是高吞吐量单位时间内用户线程做的事情更多
2)低延迟:注重每一次的暂停时间变短,用户线程暂停时间短,那么垃圾回收GC的频率就越高,因为暂停时间短,每一次GC都收集不了多少垃圾,但是线程频繁切换也需要时间,每一次本来就注重低延迟,要求GC垃圾回收短,况且线程上下文切换还消耗时间,每一次GC垃圾又回收不了多少,那么最终一共的STW时间肯定会比吞吐量的STW时间长(类比于洗衣服)高吞吐量和低延迟是矛盾的
这就类似于洗衣服,从宿舍去水房的时间和从水房回到宿舍的时间就类似于线程切换
1)高吞吐量比较好是因为这会让应用程序的最终用户感觉只有应用线程在做生产性工作,直觉上,吞吐量越高程序运行越快
2)低暂停时间比较好是因为从嘴中用户的角度来看不管是GC还是其他原因来说导致一个应用被挂起始终是不好的,这取决于应用程序的类型,有的时候甚至于说短暂的200毫秒暂停都有可能直接打断终端用户的体验,因此具有低的较大的暂停时间是日常重要的,特别是一个交互性的应用程序
3)不幸的是高吞吐量和低暂停时间是一对相互竞争的目标或者说是矛盾,如果以高吞吐量优先,那么必然需要降低垃圾GC收集的时间频率,每一次垃圾收集的时间长一些,这也会导致GC需要更长时间的来执行GC
4)相反,如果以低延迟为主要目标,那么为了降低每一次内存回收时候的暂停时间,只能频繁的进行内存回收,但是这又引起了年轻代的内存的缩减和导致最终吞吐量的下降
在设计或者使用GC算法的时候,必须要确定目标,一个GC算法只可能针对于两个目标之一,就是只是关注于较大吞吐量和最小暂停时间或者找到一个二者的初衷,现在的标准是在最大吞吐量的有限的情况下,降低停顿时间
和用户交互的程序,延迟要短一些,争取在垃圾回收的过程中多线程回收
有的是服务器端,吞吐量要高一些
G1垃圾回收器就是可以保证在给定停顿时间的基础上,尽量的提高吞吐量
JDK7之前,实线,Serial OLD GC是CMS的后备方案
在JDK9中取消了红线组合
在JDK14中绿线会被删除
CMS和PSGC框架不同,不可以一起使用,PNGC和PSGC性能差不多
CMS:不能是在老年代空间满的时候进行使用,需要提前进行回收,因为CMS是并发的,在回收的时候用户线程还在执行,用户线程还有可能制造新的垃圾,所以需要提前进行回收,那如果说回收的比较晚,垃圾制造的速度比回收的速度还要快,可能CMS回收失败一旦失败,所以要使用SOGC作为备用方案,赶紧把用户线程停下来进行全部GC,应该达到一定阈值以后回收,单核CPU是单线程垃圾收集器比多线程垃圾收集器要高,因为防止进行大量的线程切换;
Serial和Serial old单线程垃圾回收器
ParNew针对于单线程的升级版本是多线程的垃圾回收器:
Parallel Scavenge/Parallel Old:吞吐量优先的垃圾回收期,以回收内存为主,速度比较低,这个垃圾回收器只是保证了吞吐量,但是实际程序是让用户有最少的等待时间
CMS:垃圾回收器,可以保证最小的等待时间,就是快,不影响用户久等,不需要将垃圾全部清除掉,多进行几次GC不就行了嘛,需要手动指定
为什么CMS和Parallel Scavenge不能一起用,设计理念不同
G1:可控垃圾回收时间的垃圾回收器(JDK 9以后HotSpot默认的垃圾的回收器)
分成多个区,为什么分区算法是可控的?因为分区算法里面有很多区,再进行垃圾回收的时候,假设一共有4个区,他不会保证在这一次GC将A B C D四个区域的垃圾全部回收,而是保证的是可控时间,但是会保证时间到了就罢工,如果时间允许的话,G1垃圾回收器会多回收几个区域,如果时间不允许,我少干一点活,到点就下班;
分代算法为什么时间不可控?
ZGC:停顿时间极短,不超过10ms情况下尽量提高垃圾回收吞吐量的垃圾回收器
二)Serial(新生代单线程垃圾回收器)+Serial Old(老年代单线程垃圾回收器)+单核CPU
-XX:+UseSerialGC -XX:+UseSerialOldGC
新生代使用serial的时候老年代默认使用Serial Old,在执行的时候必须停止所有的用户线程
Serial是最基本,历史最久远的垃圾回收器了,JDK1.3以前是回收新生代唯一的选择
Serial垃圾回收器是作为HotSpot虚拟机Client模式下默认的新生代垃圾回收器
Serial垃圾收集器采用复制算法,串行回收和STW机制的方式执行垃圾回收
除了年轻代以外,Serial垃圾收集器还提供了用于老年代垃圾收集的Serial Old垃圾回收器,Serial Old垃圾收集器同样也是采用了串行回收和STW机制,老年代使用的是标记整理算法
Serial Old是用于运行在客户端模式下面的默认的老年代的垃圾收集器
Serial Old在Server模式下面的主要有两个用途:
1)和新生代的Parallel Scavenge配合使用
2)作为CMS老年代收集器的后备垃圾收集方案
3)回收的内存区域不多
这俩收集器完全就是一个单线程的垃圾收集器,但是他的单线程的意义并不仅仅只是他只会使用一个CPU和一条收集线程来去完成垃圾收集工作,更重要的是在它进行垃圾收集的时候必须停止其他的工作线程,直到它垃圾收集结束
-XX:PrintCommandLineFlags
-XX:+UseSerialGC
表明新生代使用Serial GC,老年代使用Serial Old GC
然后可以通过jps验证一下,jinfo -flag UseSerialGC +进程的ID
总结:只是适合于单核CPU,对于交互性比较强的应用而言,这种垃圾收集器是不能接受的,一般在JAVA WEB应用程序中是不会使用这种串行垃圾收集器的
优点:简单而高效,和其他收集器的单线程相比,对于限定单个CPU的环境来说Serial收集器由于没有线程交互的开销,专心于做垃圾收集自然可以活得最高的单线程执行收集效率,运行在客户端模式下的虚拟机是一个不错的选择,在用户的桌面应用场景中,可用于内存不大,可以在较短时间内完成垃圾收集,只要不是频繁的发生,使用穿行回收器是可以接受的
在HotSpot虚拟机中,使用-XX:+UseSerialGC参数可以指定新生代和老年代都是用串行垃圾收集器等价于新生代使用Serial GC老年代使用Serial Old GC
缺点:串行垃圾回收器会导致STW
三)parNew新生代收集器(-XX:+UseParNewGC)
1)ParNew新生代并行垃圾回收器+和Serial Old单线程串行垃圾回收器或者是CMS(老年代并行垃圾回收器一起使用
2)如果说Serial GC是年轻代中的单线程垃圾收集器,那么ParNew收集器就是Serial收集器的多线程版本,Par是Parallel的缩写,New是回收新生代
3)ParNew垃圾回收器是新生代的多线程垃圾回收器和Serial 没啥区别,在年轻代也是使用复制算法,STW,它是很多JVM在Server模式下面的新生代的默认的垃圾回收器
1)对于新生代来说,回收次数频繁,使用并行方式比较高效,对于老年代,回收次数少,使用串行的方式高效,因为CPU并行需要切换资源,穿行可以省去切换线程资源
2)ParNew在服务器端模式下是多核CPU的场景,这个时候就不和客户端一样是一个单线程的垃圾回收器了,服务器端硬件更多一些,在老年代可以使用CMS或者是Serial Old,在JDK9中Serial Old不能再和ParewNew使用了,在JDK14CMS也被移除了,这个时候ParNew就比较尴尬了,对于新生代,使用多线程垃圾回收器,使得GC的时间更短,垃圾回收更高效STW时间更短,但是在老年代,标记整理算法效率比较差,涉及到内存碎片整理,所以说就是用单线程的了,单CPU:同一时刻只能由一个线程执行
3)设置线程数量不要超过CPU核数,防止多个线程抢夺CPU,和CPU核数相同越好
-XX:PrintCommandLineFlags -XX:+UseParNewSerialGC -XX:+UseConcMarkSweepGC
1)ParNew收集器运行在多CPU模式下,可以充分的利用多CPU,多核心等物理硬件资源优势,可以更快速的完成垃圾收集,来提升程序的吞吐量
2)但是在单个CPU的换进修改,ParNew收集器不必Serial收集器更高效,虽然Serial收集器是基于穿行回收,但是由于CPU不需要频繁的进行切换任务,因此可以有效地避免多线程交互过程中产生的一些额外开销,当前除了Serial以外,目前只有ParNew可以和CMS垃圾收集器配合工作
3)在程序中,开发人员可以通过选项-XX:+UseParNewGc来指定使用ParNew收集器来执行内存回收任务,他表示年轻代使用并行收集器并不影响老年代
4)使用-XX:ParallelGCThreads来限制线程数量,默认开启和CPU数据相同的线程数
1)ParNew垃圾收集器其实和Parallel收集器很相似,区别在于ParNew可以和CMS一起使用,新生代使用复制算法,老年代使用标记整理算法
2)ParNew垃圾收集器是运行在很多Server模式下面的虚拟机的主要选择,除了Serial收集器以外,只有ParNew才可以和CMS垃圾回收器一起使用;
四)Parallel Scavenge并行新生代垃圾回收器和Parallel Old老年代垃圾回收器:
(-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代)
1)吞吐量优先用户线程执行时间越长越好,JDK8默认GC,一开始来说Parallel Scavenge吞吐量的并行新生代垃圾收集器,一开始它是搭配Serial Old一起来做垃圾收集的,但是Parallel Scavenge本身在新生代是并行垃圾回收器,老年代用了一个串行的垃圾回收器,就不太好,所以最终Parallel Old老年代并行垃圾回收器出现了,Parallel Scavenge收集器在JDK1.6提供了用于执行老年代垃圾收集的Parallel Old垃圾收集器,用来代替来年代的Serial Old垃圾收集器,Parallel本身也是采用了标记压缩算法,STW;
2)Parallel Scavenge和ParNew垃圾回收器性能差不多,它们都是回收新生代的,但是底层使用的GC框架是不同的,是自成一派的,包括G1也是自成一派的;
3)HotSpot年轻代中除了拥有ParNew收集器是基于并行回收的以外,Parallel Scavenge收集器本身也采用了复制算法,并行回收和STW机制;
那么Parallel收集器的出现是否多此一举呢?
1)和ParNew收集器不同,Parallel Scavenge收集器的目标则是先打到一个可控制的吞吐量,他也是被称之为是吞吐量优先的垃圾回收器
2)自适应调节策略也是Parallel Scavenge的一个重要区别,就是在整个JVM运行的过程中,根据当前运行的情况,来做一个性能监控,来调整内存的分配情况,来达到最优的策略;
1)凡是提高吞吐量的:一定是和后台运行的,不和用户交互性场景强的,高吞吐量可以高效的利用CPU时间,尽快完成程序的运算任务,意味着一次STW暂停时间可能长一些主要适合那些在后台计算而不需要交互的任务(但是暂停时间优先就是适用于那些交互性比较强的任务)比如说那些执行批量处理,订单处理,工资支付,科学计算的程序,在这种模式下,年轻代的大小,Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点,自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别
2)自适应调节策略:会根据当前JVM的运行情况进行性能监控
动态地去调整内存分配情况意识和达到最优的策略,内存分配情况来到到低延迟或者是吞吐量优先的策略,和用户交互性强的:保证低延迟,暂停时间要短,保证在服务后台进行数据运算的,要保证高吞吐量;
为什么在JDK1.6的时候要使用Parallel Old收集器代替Serial Old收集器呢?
在Parallel Old出来之前肯定是只能使用Serial Old收集器
意义非常大,因为此时如果没有Parallel Old的时候新生代垃圾回收器使用Parallel,而老年代使用Serial Old,此时使用Serial Old会存在一定的问题,因为既然新生代使用了Parallel,那么后台肯定是不和用户做大量交互的服务器端(交互性比较弱的)进行使用,通常服务器端硬件的配置比较高,肯定是多核CPU,如果服务器配置比较低的话,单核CPU,那么新生代老年代直接都是用串行垃圾回收不就行了吗,在一个高性能的场景下硬件配置比较高,多核CPU,新生代使用并行垃圾回收器效率比较高,充分利用CPU多核资源,老年代使用串行垃圾回收器,此时的性能肯定得不到最大的提升更好地发挥,拖累服务器性能的效果了,达不到最大吞吐量的一个效果了,更好的对硬件性能做发挥;
在吞吐量优先的应用场景中,也就是服务器完成任务的数量越多越好,Parallel收集器和Parallel Old收集器的组合在服务器模式下的内存回收性能比较不错
参数设置:就是-XX:+UseParallelGC和-XX:+UseParallelOldGC当一个参数开启以后,另一个参数也会默认开启
最好CPU核数等于垃圾回收线程数,但是还是可以空闲出一些资源留给其他任务来执行
-XX:+PrintCommandLineFlags -XX:+ParallelGC
根据下面参数来了解自适应调节策略:
1)-XX:MaxGCPauseMillis:设置垃圾收集器最大停顿时间,就是STW的时间,默认是毫秒,此参数设置需要谨慎,为了尽可能地把停顿时间控制在MaxGCPauseMills以内,收集器在工作的时候会调整堆和其他的一些参数,对于用户来说,停顿时间越短,体验感越好,但是在服务器端,我们注重与整体的高并发,整体的吞吐量,所以服务器端适合于Parallel进行控制
2)-XX:GCTTimeRatio:垃圾收集时间占用的总时间的比例,用来衡量吞吐量的大小,默认是垃圾回收时间不超过1%,与前一个-XX:MaxGCPauseMillis参数具有一定的矛盾性,暂停时间越长,Radio参数就容易超过设定的比例
垃圾收集时间和吞吐量是互补的,吞吐量是对于服务器端来说执行任务越多越好
3)假设此时要是设置最大停顿时间是10ms,再进行垃圾回收的时候会尽可能的想办法把时间控制在10ms以内,要想实现垃圾回收的时间比较短,每一次垃圾回收比较短,那么就只能控制堆的大小,想让停顿时间短,那么垃圾回收的时间就比较短,垃圾回收器把堆控制的小一些,每一次GC时间就比较短,但是堆空间比较小,堆空间容易满,但是可能经常发生GC,GC频率增高,这样子吞吐量反而降低了,就会导致用户线程执行的总时长比较短,所以第一个参数使用需要谨慎;
4)自适应调节策略:尽量开发中的满足吞吐量和停顿时间,具有自动调节功能
5)-XX:+UseAdaptiveSizePolicy设置Parallel Scavenge的比例,京生老年代的对象的年龄等参数会被自动调整,已达堆大小,吞吐量和停顿时间之间的平衡点,再手动调优比较困难的场合,可以直接使用这种自适应的方式,进指定虚拟机的最大堆,目标的吞吐量和停顿时间,让虚拟机自己完成调优工作;
1)Parallel其实就是Serial的多线程版本, 除了使用多线程进行垃圾收集以外,其余行为控制参数,收集算法,回收策略等等和Serial收集器类似,默认的收集线程数和CPU核数相等,当然也是可以使用参数(-XX:ParallelThreadsGC)来指定收集的线程数,但是不建议修改;
2)Parallel Scavenge垃圾收集器的关注点就是吞吐量,就是高效地利用CPU,CMS等垃圾收集器的关注点在于用户线程的停顿时间,就是为了提高用户线程的体验,所谓的吞吐量就是CPU中用于执行运行用户的代码的时间/CPU总消耗时间的比值
3)Scavenge垃圾收集器提供了很多参数来供用户找到最合适的停顿时间和最大吞吐量,
4)ParallelOld收集器是ParallelScavenge收集器的老年代版本,使用多线程和“标记-整理”算法,在注重吞吐量以及CPU资源的场合,都可以优先考虑ParallelScavenge收集器和ParallelOld收集器(JDK8默认的新生代和老年代收集器)
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可用高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务
1)这组垃圾回收器如果堆的内存比较大,虽然是并发垃圾收集,但是STWGC时间还是比较长,如果堆中内存比较少,使用并发垃圾收集器Parallel NEW也是可以的
2)Parallel和ParalleOld注重于吞吐量优先,组合起来Server模式性能不错,但是在系统响应时间比较高的场景中,希望系统能够较快的响应,和用户交互不愿意看到过多的延迟,对于像响应时间要求比较高的场景下,使用Parallel就不太合适了
五)CMS垃圾回收器:-XX:+UseConcMarkSweepGC(old)
从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿
1)CMS STW的总时间会少一些,但是GC总共的垃圾收集还有可能更长一些,就是CMS在其他垃圾收集器占用时间较长的并发清理和并发标记过程中采取和用户线程并发执行,使得用户感知不到STW,但是其他垃圾收集器这两个步骤是完全停止用户线程的会造成严重的STW,主要是为了提供用户体验,牺牲了GC垃圾回收的总时间,但是STW时间会变短,因为本身STW如果并发执行GC线程很多可以更快速的进行垃圾收集,集中所有的线程全部做垃圾收集,但是此时采用并发执行,就会导致垃圾回收线程部分和CPU资源给用户线程来执行,导致总的GC线程的执行时间变长,垃圾收集器的效率不如Parallel NEW;
2)Parallel NEW总的GC总时间更少,STW时间更长;
1)初始标记:暂停其他所有的用户线程并且进行记录下GCroots直接引用的对象,速度非常快,几乎可以忽略不记,如果不进行STW的话,那么初始标记根本做不完,因为如果用户线程也在运行,那么不断有新的局部变量引用新的对象;
2)并发标记:和用户线程并发执行,从GC直接关联的对象进行深度优先遍历对象树,这个过程执行时间过长但是可以几乎不停顿用户线程,可以和垃圾收集线程一起执行,因为用户线程也在执行,可能会导致一些对象的状态发生改变,一些可达的对象变成不可达的(浮动垃圾下次垃圾回收清理),一些不可达的对象变成可达的了;
3)重新标记:重新标记阶段就是为了修正并发过程期间因为用户程序继续和GC线程并发执行而导致的标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间比初始标记的时间较长,比并发标记的过程要短,主要是用到三色标记过程中的增量更新算法来处理漏标的情况来做重新标记;
4)并发清理:开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理
5)标记重置:重置本次GC的标记数据
1)假设在并发清理的过程中,JVM线程是和用户线程同时执行的,假设此时用户线程New了一个大对象放到老年代,但是这个并发清理的过程中GC线程不会再来进行重新标记了,这个时候新new出来的对象不会被重新标记,而新对象在并发清理的过程中被GC回收了怎么办?
解决方案:根据三色标记,针对于并发清理和并发标记此时产生的新对象,通常的做法是全部标记成黑色,本轮GC不会直接清除掉,这部分对象可能也会变成垃圾,也是浮动垃圾
2)浮动垃圾:但是在并发标记的过程中还有一些其它的数据,一开始人家本身不是垃圾是可达的,但是在用户线程执行过程中变成垃圾了,但是还有一些对象本身不是垃圾,初始标记的过程中不是垃圾,但是并发标记的过程中变成垃圾了,这就很明显了啊,就当作这只有标记,但是没有清除标记的功能,第一次标记了,并发标记的时候失去引用变成垃圾,但是他身上的标记不会去掉的,所以说此时CMS不能清除第一次标记成不是垃圾,但是在并发标记过程中变成垃圾的那些对象,这就是用户线程和垃圾回收线程并发执行带来的问题;
在并发标记过程中,如果由于方法运行结束导致部分局部变量(gcroot)被销毁,这个gcroot引用的对象之前又被扫描过(被标记为非垃圾对象),那么本轮GC不会回收这部分内存,这部分本应该回收但是没有回收到的内存,被称之为浮动垃圾,浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除;
3)一个非常重要的参数:-XX:+CMSScavengeBeforeRemark,在重新标记过程中,重新标记的过程是扫描整个堆,包括新生代和老年代,为啥要扫描新生代呢?因为对于老年代的对象,如果被新生代的对象引用,也会被视为存活对象,因此对于老年代来说引用了老年代中的对象的新生代的对象,也会被老年代视为是GCROOTS,这个参数的作用就是在重新标记之前对新生代中的对象做一次GC,这样新生代中待标记的对象数量相比于之前少很多,被老年代视为GCROOTS的对象数量会骤减,如此Remark的工作量就少很多,重新标记的时间开销也会减少;
-X代表该功能稳定,-XX代表该功能不稳定,将来可能JVM更高版本可能被废弃掉
三色标记和漏标误删除操作:
1)在并发标记过程中,因为在标记过程期间用户线程还在继续跑,对象之间的引用关系还在发生着变化,多标(浮动垃圾,下一次在被回收)和漏标的情况下可能就会频繁的发生
多标:可能出现多标了,但是如果GCroots被回收了,这些由GCROOTS引用链上面的垃圾就不再使用了,垃圾就变成浮动垃圾了,但是问题不大,下一次垃圾回收的时候进行回收
并发标记会发生漏标
2)这里面就引入三色标记算法,就是将GCROOTS可达性分析遍历过程中遇到的对象,按照是否访问过标记成三种颜色:
2.1)黑色:代表着对象已经被垃圾收集器访问过,况且这个对象的所有的引用的对象都被找到过,假设A对象引用了B D对象,B对象引用了C D对象,那么如果B C对象全部GCROOTS被找到了,那么A对象就是黑色,黑色对象代表已经被扫描过,它是安全存活的,如果有其他引用指向了黑色对象,无需重新扫描一遍,黑色的对象一定是非垃圾对象;
2.2)灰色:代表该对象至少还存在着一个引用对象没有被GCROOTS找到,假设B这个对象引用了C和D对象,如果GCROOTS找到了C但是还没有找到D,如果C此时没有任何引用变量,会被顺便标记成黑色,那么此时B就是灰色的,D就是白色的,代表已经被垃圾收集器访问过,但是还至少还存在一个引用没有被扫描过;
2.3)白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达,最终垃圾回收器回收的就是白色的对象,下面这个图片只是一个扫描的过程,但是如果整个GCROOT标记完成以后还有对象是白色的,说明此对象应该被回收
假设就是在上面的这个阶段,B引用的对象扫描到了一半,此事发生了这种情况:
1)初始标记直接标记A,假设此时执行到2步骤的时候开始执行并发标记
2)如果并发标记过程中,如果这个对象成员变量所引用到的对象都被访问过了,那么这个节点就变成黑色了,如果A访问过了B和D(null),那么A在并发标记过程中就变成黑色了,凡是标记成黑色的对象在后续进行垃圾回收的时候不会做任何处理,因为这个对象肯定是一个非垃圾对象;
3)灰色对象:在代码中,B有两个成员变量,B中的C和D是存在引用的,C 和D没有任何的引用也就意味着C D不在引用任何成员变量了,如果在进行扫描B的时候,B已经访问过C了,但是此时B还没有来得及访问到D,此时B就会标记成灰色,因为此时B中的成员变量D对象还没有来得及扫描,如果在可达性分析算法中如果扫描B的过程中还没有扫描D,D就是白色,C因为已经被D扫描过了,但是C还没有任何成员变量,也相当于是黑色此时C会标记成黑色
开始出现漏标:先把从B到D上面的引用链置为空,然后再让已经标记成黑色的A的成员变量的d指向B上面的D,B此时继续扫描找不到D,但是A已经扫描过了,A已经变成黑色了,此时A不会再扫描,但是此时B还扫描不到D,此时D就永远是白色,在并发清理阶段就被当做是垃圾回收了,按照三色标记算法,D会被清理掉,这就叫做误删除,也叫做多标;
1)进行扫描B的所有引用成员变量的时候,已经扫描到了C,但是B此时还没有完全扫描完,B是灰色,C就是黑色
2)此时B.d=null,此时B和D的引用链断开,此时在让A.d=D
3)此时B的所有引用对象扫描完,B,C也变成黑色,此时B扫描不到D,D就是白色
4)因为A已经被重新扫描过了,不会再继续重新扫描,D又是白色,所以并发清理阶段D被回收,但是D不是垃圾;
如何解决漏标操作:漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决
1)增量更新:增量更新会把赋值新增的引用关系,会创建一个集合会存放起来存放引用关系(引用的源头和被引用的对象),在重新标记的过程中解决并发标记过程中漏标的情况,重新标记的过程中时会造成STW的,漏标是一定会处理,重新标记的过程中会在数据集合中去寻找新增的这些引用,重新去扫描,记录的是a.d=d(新增的引用,赋值操作多了一条引用链)
在重新标记的扫描过程中会把A重新标记成灰色,这个时候还是会重新扫描A的所有的成员变量,最终将D变成黑色的对象,A也会变成黑色的对象,此时是不会回收D的,因为重新标记是STW的,整个所有的对象图是不会发生变化的,再写之后将引用关系放到集合中,是写后屏障,就是当黑色对象新插入到白色对象的引用关系的时候,就将这个新插入的引用记录下来,等到并发扫描结束以后,再见这些新记录过的引用关系的灰色的对象记录成根,重新再来扫描一次,就是黑色对象一旦建立了新插入白色对象的引用以后,就有变成灰色了;
1.1)并发标记过程中用户线程将A对象指向D对象,使用一个数据结构把A记录下来;
1.2)并发标记完成后将A对象置为灰色;
1.3)重新标记过程中再来扫描A,程序会STW;
2)原始快照:SATB,把删除的引用记录下来,在并发标记过程B把D置为空,不会关心A引用是否指向D,于是就把D放在一个集合中,以后都不会再动D,就把D当成一个浮动垃圾,那么在重新标记的过程中把这个集合中的所有引用全部标记成黑色,下一次垃圾回收的时候再来进行处理,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根写之前将删除的引用放到集合中,是写前屏障;
写屏障:新增引用和赋值减少删除引用都是通过赋值进行操作的,增量更新是写屏障在赋值之后做把引用丢到集合里面,原始快照写屏障就是在写前屏障把引用关系记录到集合里面,也就是赋值操作的前面和后面做一些操作,把引用收集到集合里面,集合在C++底层就是使用的是内存队列,这个写屏障的写操作是将引用放到队列里面,然后再开启一个线程将队列中的引用写入到真正的数据结构里面,这就是一个典型的异步处理,绝对不可以用同步的操作,如果将引用直接写入到数据结构里面会影响指针改变指向的性能,万一写数据结构的过程中出现了问题怎们办呢?比如说数据结构满了,这就是提升性能的一种方式;
卡表和记忆集:解决快带引用大规模扫描的问题
1)如果回收新生代做minorGC,某一个或者是某一些新生代对象被老年代对象引用,这个新生代对象不是垃圾,这些新生代对象不是被GCroots也就是一些局部变量静态变量给引用着,这个或者这些新生代对象而是被老年代的对象引用着,这个新生代对象不是垃圾,这些对象无法通过GCroots来直接来到年轻代区域找到这些对象,必须先通过GCroots找到老年代(老年代的某些对象将新生代引用的对象引用着)来找到判断被老年代引用的新生代的对象是否是垃圾,但是如果通过GCROOT先扫描老年代对象,所以先把老年代扫描一遍,才可以找到年轻代的非垃圾对象,那么此时效率非常低;
2)JVM在新生代开辟了一块空间,空间里面维护者所有老年代对于年轻代的引用,叫做记忆集,再去扫描新生代的对象的时候除了进行扫描被GCROOTS引用的年轻代对象之外,还去扫描跨代引用,还要将记忆集中的对象拿出来加入到GCROOTS关联的对象一起扫描,就是还需要扫描记忆集中老年代对年轻代有跨代引用的那些对象,但是JVM跨代引用不是特别多,也就是老年代对于年轻代的引用不是很多;
3)卡表:就是记忆集,把老年代的内存分割成固定大小512M的卡页也叫做页内存,页内存中有着很多很多的对象,只要说最终只要卡页有一个对象引用新生代的对象,就会把这个卡页当成脏卡表,0表示老年代没有任何的对象引用年轻代,1表示这个卡页有对象引用年轻代
那以后进行扫描年轻代的那些GCROOTS之外,还可以扫描卡表中卡页中是1老年代的对象
在赋值的过程中要维护卡表,老年代有一个成员变量的对象引用到了年轻代,假设现在有一个老年代的对象指向了年轻代,在记忆集中要更新卡页的状态,也就是脏卡表,所以说这个字节数组中的1的修改就是在写屏障中进行修改的;
记忆集:底层就是字节数组,在新生代有一块空间记录记录着,以及对应卡页的二进制位来维护卡页的状态,在二进制数据周围还维护着老年代的卡页的起始地址,然后在进行新生代扫描的时候会将老年代卡页中的对象加入到新生代GCROOTS的扫描集合里面,卡表和记忆集在新生代里面,卡页是在老年代里面;
使用到STW的时间只有初始标记和重新标记,但是这两个时间是整个CMS执行时间最短的,因此STW的时间也是最短的,虽然并发标记和并发清理都是最耗时的,但是确实可以和用户线程并发执行的,所以整体是低停顿的
CMS的优缺点:
concurrent mode failer在并发标记的过程中,用户线程还在不断的执行,而用户线程再进行不断的执行过程中,就有可能占用使用更多的内存,像以前的垃圾回收器,用户线程正在执行突然说内存不够了,不够用户在进行去分配新的对象了,就是内存占用率已经接近100%了,这个时候垃圾回收器开始运行,直接STW,直接回收垃圾了;但是CMS再进行回收垃圾的时候用户线程还在执行,用户线程花的时间还很长,这个时候JVM就不可以在等到内存空间不够的时候再去进行垃圾回收,可以设置老年代空间达到固定比例以后触发FullGC,为了避免并发收集失败,如果系统中的大对象比较多,那么该参数要设置的小一些,早早地进行碎片整理,但是这样的配置可能会导致老年代部分空间不可用
假设用户线程不断产生大量垃圾,这个时候就可能OOM了,用户线程产生垃圾的速度远远超过CMS垃圾回收的速度,这个时候只能使用Serial Old,STW时间就比较长了
就是说CMS垃圾收集器本身是回收老年代对象的,因为CMS开始进行执行,肯定是老年代内存空间不足才产生的,但是此时CMS垃圾收集器和用户线程并发执行,CMS又是使用的是标记清除算法,很有可能出现用户线程产生对象的速度大于CMS垃圾回收的速度,这时候并发清理失败,此时直接使用Serial Old垃圾收集器所有整个GC过程全部STW,专心做垃圾收集,存活的对象越少,效率越高,存活对象越多,效率越慢,因为很多对象的引用地址是妖更换的,CMS垃圾收集器采用的是标记清除算法,这就意味着每一次执行完成内存回收以后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的内存块,不可避免地将会产生一些内存碎片,那么CMS在为新对象分配连续内存空间的时候,将无法使用指针碰撞技术只能使用空闲列表执行内存分配
1)内存碎片:GC不能等待内存耗尽的时候才去执行GC,是有一个阈值,本身还有碎片,内存不太规整,有可能老年代空间还剩余很多,业务高峰期,但是由于碎片化很严重来了一个大对象,这个时候发现老年代还空闲很多,但是还无法分配内存,所以只能频繁发生FullGC,可能会导致并发清楚以后,用户线程的可用空间不足,在无法分配大对象的情况下,不得不提前出发Full GC
2)对于CPU敏感性比较强:并发程序高,会影响用户线程的执行,在并发阶段,她虽然不会导致用户卡顿,但是会因为占用了一部分线程而导致应用程序变慢,总的吞吐量会降低;
3)浮动垃圾:在重新标记的环节,由于之前并发标记过程中用户线程和GC线程是在并发执行的,需要做修正,就是修正在并发标记环节有一些没有办法确认是垃圾的一些情况会做一些修改(不可达对象),你怀疑他是垃圾(因为在并发标记过程中是不可达的),把怀疑的垃圾进行确认一下(在并发标记过程中不可达的对象再进行一次确认,到底是不是垃圾),重新标记的过程中JVM修正的是那些怀疑是垃圾不可达,但是不确定到底是不是垃圾
4)可能会出现串行收集:假设后台线程正在执行,此时突然来了一波业务的高峰,可能产生FullGC,又把CMS假设挂了,使用Serial Old可能造成严重的STW