Java GC是在什么时候,对什么东西,做了什么事情?”
什么位置:大部分在堆中,还有方法区!!方法区的垃圾收集主要回收两部分内容:废弃常量和无用的类,当满了之后同样触发FullGC, HotSpot1.8之前由永久代实现,1.8已经移到元空间,元空间并不在虚拟机中,而是使用本地内存。
什么时候:程序员不能控制具体时间,系统在不可预测的时间调用System.gc()函数的时候;当然可以通过调优,用NewRatio控制newObject和oldObject的比例,用MaxTenuringThreshold 控制进入oldObject的次数,使得oldObject 存储空间延迟达到full gc,从而使得计时器引发gc时间延迟OOM的时间延迟,以延长对象生存期。
什么东西:超出了作用域或引用计数为空的对象;从gc root开始搜索找不到的对象,而且经过一次标记、清理,仍然没有复活的对象。
什么事情:删除不使用的对象,回收内存空间;运行默认的finalize,当然程序员想立刻调用就用dipose调用以释放资源如文件句柄,JVM用from survivor、to survivor对它进行标记清理,对象序列化后也可以使它复活。
引用计数法和可达性分析算法
引用计数法(ReferenceCounting):给对象中添加一个引用计数器,每当它被引用到一个地方时,计数器值就+1,;当引用失效时,计数器值就-1;任何时刻计数器为0的对象就是不可能在被使用。
1)、优点
判定效率很高
(2)、缺点
不会完全准确,因为如果出现两个对象相互引用的问题就不行了。
就像JVM问A可以被回收不,A说我被B引用去问B;
JVM问B可以被回收不,B说我被A引用去问A。
现在虚拟机都不采用引用计数法。
可达性分析法:该方法的基本思想是通过一系列的“GC Roots”对象(局部变量,栈等)作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象;被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。
GC Roots的对象
- 虚拟机栈(帧栈中的本地变量表)中引用的对象。
- 方法区中静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中 JNI(Java Native Interface) 引用的对象。
Stop The World
可达性分析期间需要保证整个执行系统的一致性,对象的引用关系不能发生变化,所以需要将用户的正常的工作线程全部停掉,避免对象的引用关系变化,与可达性分析不一致;
导致GC进行时必须停顿所有Java执行线程(称为"Stop The World");(几乎不会发生停顿的CMS收集器中,枚举GC ROOTS时也是必须要停顿的)
是JVM在后台自动发起和自动完成的;
在用户不可见的情况下,把用户正常的工作线程全部停掉;
垃圾回收算法:
标记-清除(Mark-Sweep)算法:两个阶段:标记阶段和清除阶段。标记阶段的任务是根据GC ROOTS标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间;标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
复制算法:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
标记-整理算法:应用在老年代。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。在清理的时候,把所有 存活 对象扎堆到同一个地方,让它们待在一起,这样就没有内存碎片了。
分代收集算法(目前常用):应用在年轻代。根据对象存活的生命周期将内存划分为若干个不同的区域,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。
新生代一个Eden、两个survivor区,对Eden和其中一个survivor Minor GC,再复制另一个Survivor区,目的是减少内存碎片;
老年代只有在 Major GC 的时候才会进行清理,每次 GC 都会触发STW(Stop-The-World),使用标记-整理算法。
分代收集算法总结:
(1)在年轻代中,Eden区提供堆内存如果满了,Eden进行MinorGC,将存活的对象→Survivor A中,Eden区清空;
(2)Eden区再次满, Eden 区和 Survivor A 区同时进行 Minor GC,把存活对象放入 Survivor B 区,Eden和Survivor A同时清空;
(3)重复(2)的操作,如果当某个 Survivor 区被填满,且仍有对象未被复制完毕时,或者某些对象在反复 Survive 15 次左右时,或者大对象,则把这部分剩余对象放到Old 区(老年代);
(4)当 Old 区也被填满时,进行 Full GC,对 Old 区进行垃圾回收。
[注意,在真实的 JVM 环境里,可以通过参数 SurvivorRatio 手动配置 Eden 区和单个 Survivor 区的比例,默认为 8:1:1。可以通过参数–XX:SurvivorRatio 来设定,即将堆内存中年轻代划分为8:1:1]
收集器
接下里会介绍在HotSpot虚拟机中常用的几种垃圾收集器,垃圾收集器是垃圾回收算法的具体实现,不同的商家、不同版本的JVM所提供的垃圾收集器可能会存在差异。
这几种收集器分别是Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1。在了解垃圾收集器之前,我们先来区分几个概念:
(重点)并发收集器VS并行收集器
并行:指多条收集线程同时进行收集工作,但此时用户线程处于等待状态。如ParNew、Parallel Scavenge、Parallel Old。
并发:指用户线程与垃圾收集线程同时执行(并不一定是并行,可能会交替执行)。如CMS、G1。
YoungGC VS OldGC VS MinorGC VS MajorGC VS FullGC
Minor GC、YoungGC:Minor GC又称为新生代GC,所以等价于Young GC,在新生代的Eden区分配满的时候触发。在Young GC后新生代中有部分存活对象会晋升到老年代,有可能是年龄达到阈值(默认为15岁,在JVM里面15岁就步入老年生活了,O(∩_∩)O哈哈~)了,也可能是Survivor区域满了,如果是Survivor区域被填满,会将所有新生代中存活的对象移动到老年代中!
Major GC、Old GC、Full GC:Old GC从字面能理解是老年代的GC,但是对Major GC和Full GC存在多种说法,有的认为Major GC等价于Old GC只是针对老年代的GC,有的认为Major GC和Full GC是等价的。但是我个人认为Major是指老年代GC,而Full GC针对新生代、老年代、方法区整个的回收。
由于老年代的GC都会伴随一次新生代的GC,所以习惯性的把Major GC和Full GC划上了等号。前面Young GC时候说到“在Young GC后新生代中有部分存活对象会晋升到老年代”,万一老年代的空间不够存放新生代晋升的对象怎么办呢?所以当准备要触发一次Young GC时,如果发现统计数据之前Young GC的平均晋升大小比目前老年代剩余的空间大,则不会单独触发Young GC,而是转为触发Full GC,也就是堆和方法区的收集!
常见垃圾回收器:
串行收集器
串行垃圾收集器是最基本、发展历史最悠久的收集器。
主要包含Serial和Serrial Old两种收集器,分别用来收集新生代和老年代。
串行收集器由于是单线程收集,在进行垃圾收集时,必须暂停(Stop The World)所有的工作线程,直到GC线程工作完成。运行示意图如下:
Serial 收集器:主要针对新生代回收,采用复制算法,单线程收集。
Serial Old收集器:主要针对老年代回收,采用“标记-整理”算法,单线程收集。
串行收集器在单CPU的环境下,没有线程切换的开销,可以获得最高的单线程收集效率,但是由于现在普遍都是多CPU(或者多核)环境,所以除了在桌面应用中仍然将串行收集器作为默认的收集器,其他场景已经很少(很少不代表没有,后面CMS会讲到)使用。
在上面我们谈到一个词,需要暂停(Stop The World)所有的工作线程,这个概念在后面也会多次提到,为什么需要暂停呢?一是为了方便GC动作,不然在GC过程中又会额外产生新的垃圾,或者分配新的对象。二是因为GC过程中对象的地址会发生变化,如果不暂停线程,可能会导致引用出现问题。
并行收集器
并行收集器是串行收集器的多线程版本,除了多线程外,其余的行为、特点和串行收集器一样。主要包含ParNew收集器、Parallel Scavenge收集器、Parallel Old收集器。
JDK1.6~1.8默认使用了Parallel Scavenge(年轻代收集器),Parallel Old(老年代回收器);
运行示意图如下:
ParNew收集器:主要针对新生代回收,采用复制算法,多线程收集。一般老年代如果使用CMS收集器,则默认会使用ParNew作为新生代收集器。
Parallel Scavenge收集器:该收集器与ParNew收集器类似,也是新生代收集器,采用复制算法,多线程收集。其他收集器关注点是尽可能地缩短垃圾收集时用户线程停顿的时间,但是Parallel Scavenge收集器的目标则是达到一个可控的吞吐量(吞吐量=CPU运行用户代码时间/(CPU运行用户代码时间+CPU垃圾收集时间)),所以该收集器也成为吞吐量收集器。由于该收集器没有使用传统的GC收集器代码框架,是另外独立实现的,所以无法和CMS收集器配合工作。
Parallel Old收集器:主要针对老年代回收,采用“标记-整理”算法,多线程收集。该收集器是Parallel Scavenge收集器的老年代版本。在JDK1.6之后用来替代老年的Serial Old收集器。在注重吞吐量以及CPU资源敏感的场景,一般会选择Parallel Scavenge+Parallel Old的组合进行垃圾收集。
并发型收集器
CMS
CMS(Concurrent Mark-Sweep Collector)收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,CPU在CMS并发标记和并发清除阶段会与用户线程切换执行,采用的是Mark-Sweep算法。
一种以获取最短回收停顿时间为目标的收集器 “标记-清除”,有 4 个过程:
初始标记(查找直接与 gc roots 链接的对象),需要“Stop The World”;
并发标记(GC Roots Tracing 过程:查找与gc roots非直接相连的对象,以GCRoots的对象作为起始点,从这个节点向下搜索,搜索走过的路径称为ReferenceChain,当一个对象到GCRoots没有任何ReferenceChain相连时,这个对象不可到达,则证明这个对象不可用);
重新标记(因 为并发标记时有用户线程在执行,标记结果可能有变化),需要“Stop The World” ;
并发清除(并发清除阶段会清除对象)。
其中初始标记和重新标记阶段,要“stop the world”(停止工作线程)。
(优缺点都很重要)
优点:并发收集,低停顿,所以CMS收集器适合与用户交互较多的场景,注重服务的响应速度,能给用户带来较好的体验!所以我们在做WEB开发的时候,经常会使用CMS收集器作为老年代的收集器!
缺点:
1)不能处理浮动垃圾 (在最后一步并发清理阶段,用户线程还在运行,这时候可能就会又有新的垃圾产生,而无法在此次GC过程中被回收,这成为浮动垃圾)
2)对 cpu 资源敏感,占用CPU资源较大。CMS默认启动的回收线程数是(CPU数量+3)/ 4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大。
3)产生大量内存碎片(因为使用的是标记-清除算法)
CMS收集器采用“标记-清除”算法,在清除后不会进行压缩操作,这样会导致产生大量不连续的内存碎片,在分配大对象时,无法找到足够的连续内存,从而需要提前触发一次FullGC的动作。针对该问题,提供了两个参数来设置是否开启碎片整理。
1)、“-XX:+UseCMSCompactAtFullCollection”参数
从名字能看出来,在收集的时候是否开启压缩。这个参数默认是开启的,但是是否开启压缩还需要结合下面的参数!
2)、“-XX:+CMSFullGCsBeforeCompaction”参数
该参数设置执行多少次不压缩的Full GC后,来一次压缩整理。这个参数默认为0,也就是说每次都执行Full GC,不会进行压缩整理。
要根据实际情况合理设计该参数,因为CMS要的就是低停顿,如果CMS多线程一起整理垃圾对象需要Stop the world所需时间过长,反而会导致高停顿,违背了设计初衷低停顿的特点。
如果开启了压缩(整理对象),则在清理阶段需要“Stop the world”,不能进行并发!
4)“Concurrent Mode Failure”失败
不知道大家在开发过程中有没有遇到过“Concurrent Mode Failure”失败的信息,这个异常是什么原因导致的呢。
在并发标记和并发清除阶段,用户线程与GC线程并发工作,这会导致在清理的时候又会有用户的线程在拼命的创建对象,本身垃圾回收时候肯定是可用内存不够了,可万一这时候用户线程创建了大量的对象怎么办呢?所以一般CMS收集器的垃圾回收的动作不会在完全无法分配内存的时候进行,可以通过“-XX:CMSInitiatingOccupancyFraction”参数来设置CMS预留的内存空间!
如果预留的空间无法满足CMS的需要,就会出现 “Concurrent Mode Failure”失败。
这时候JVM会启用后备方案,也就是前面介绍过的Serial Old收集器,这样会导致另一次的Full GC的产生,这样的代价是很大的,所以CMSInitiatingOccupancyFraction这个参数设置需要根据程序合理设置!
G1
JDK1.9开始默认使用G1回收器,全量回收器,当今JAVA最好的回收器,充分利用CPU并且不存在内存碎片,原理是将内存切分成多个相同大小的区域,标记-整理时也是按区域进行,所以当回收时会采用分区回收最大程度的降低了STOP-THE-WORLD情况。
G1收集器是面向服务端应用的收集器,它能充分利用多CPU、多核环境。
因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。
对垃圾回收进行了划分优先级的操作,这种有优先级的区域回收方式保证了它的高效率;最大的优点是结合了空间整合,不会产生大量的碎片,也降低了进行gc的频率,让使用者明确指定停顿时间。
根据用户设定的GC停顿时间的多少,在最后"筛选回收"阶段,按照区域优先级回收,对优先级高的阶段优先进行回收,停顿时间不够,则只回收一部分区域。
初始标记(Initial Marking)
初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
并发标记(Concurrent Marking)
并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
最终标记(Final Marking)
最终标记阶段是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
筛选回收(Live Data Counting and Evacuation)
筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率(分代收集算法)。
参考来源
https://www.baeldung.com/jvm-garbage-collectors
https://baijiahao.baidu.com/s?id=1583441733083989684&wfr=spider&for=pc
http://blog.csdn.net/cy609329119/article/details/51771953
https://blog.csdn.net/xiaomingdetianxia/article/details/77446762
https://juejin.im/post/5ad5c0216fb9a028e014fb63