上篇文章咱们主要围绕对象的建立过程展开描述,本篇文章咱们把思路切换到对象的回收,对于JVM的整个知识点而言,对象的回收才是咱们真正要关心的。本篇涉及到的一些JVM参数比较多,详细的能够参考官方的解释:docs.oracle.com/javase/8/do…html
在目前Hotspot内部所实现的垃圾回收策略中,主要用到了3种垃圾回收算法:标记复制
、标记清除
、标记整理
。java
好比有一大块内存空间,经过GC Roots 找到可用对象,将垃圾对象清除掉,把垃圾对象占用的内存腾出来。假设在清理以前对象在内存中的占用是这样子的,黑色表示垃圾对象,蓝色表示存活对象:算法
在使用标记清除算法进行一次GC以后,内存就变成了这样子:
图中,白色部分表示垃圾对象被清理后留下来的可用内存,蓝色表示存活对象。markdown
对于标记清除作了一次优化,将有用的对象挪动到一边,将另外一边的垃圾对象清除掉。好比,清理以前是这样子的,黑色是垃圾对象,蓝色是有用对象: GC以后的结果:
多线程
好比将内存空间分为A,B两部分,A用来存放对象,B空着,回收的时候将A空间的根可达对象进行标记,将有用对象复制到B,再把A里面的垃圾对象清理掉。 并发
从上面的几种垃圾回收算法中不难发现,每种方式都各有利弊,那么JVM该经过什么方式去权衡,选择合适的回收算法呢:根据反复的测试结果来看,Oracle官方发现98%的对象都是朝生夕死的,不须要占用内存过久,存活率低;而剩余的10%能够理解为顽固对象,要在内存中占用好久,存活率比较高,基于这个因素,JVM把对象分开进行管理,也就是分代收集 ,将内存划分为年轻代和老年代;由于年轻代在执行几分钟或者几秒后剩余的对象不多,这种状况下该用那种回收器呢,咱们一一分析:oracle
展开一通分析以后咱们发现,年轻代使用复制算法比较合适一些,可是上面咱们也说过他的弊端,空间利用率过小了,针对这种状况JVM特地作了一种优化,引入了eden区域用来存放大量的对象,剩余出来两块很小的空间用来存放复制算法后的存活对象,这样咱们既能够高效的回收,又能够下降空间利用率小带来的影响。然而老年代是通过几回回收都存活的对象,很难再次被回收,基于这种垃圾对象不多的状况下,清除和整理算法均可以作。因此如今出现了这样的结果: oop
垃圾回收算法是一套回收理念,不一样的垃圾回收器针对这套理念实现的时候会作一些优化,目前,Hotspot所实现的垃圾回收器有:Serial、Parallel、ParNew、CMS、G一、ZGC。不管是什么样的垃圾回收器,在回收垃圾的时候都会中止全部的业务线程,单独让GC线程进行垃圾回收(这个过程也被成为STW),由于若是业务线程还在执行的话可能会打乱对象之间的引用关系,GC在进行标记的时候会混乱,因此必需要STW。 在JVM调优中,不管是年轻代仍是老年代,咱们的调优目的有两个:1是避免OOM;2是减小STW的时间,让用户卡顿的感知减小。性能
为了知足分代收集理念,Serial收集器分别在年轻代和老年代各实现了一个版本,老年代是Serial Old,使用标记整理算法。Serial在回收垃圾的整个过程当中都是采用单线程的方式,因此STW的时间会很长,内存越大,STW时间越长,用户的卡顿时间就很长,如今咱们生产环境堆分配的通常都是几个G以上的,因此Serial收集器必定会很慢很慢,在很早以前的jdk版本中有使用,如今都不主动用这个了,只有在使用CMS回收器并发清理失败的状况下系统会默认回退到这种方式。 测试
-XX:+UseSerialGC
:使用Serial回收器。
多线程的垃圾收集器,默认状况下启用的线程数是和CPU核心数相同。老年代是Parallel Old,使用标记整理算法,也是jdk1.8中使用的默认垃圾回收器。由于是使用多线程进行回收,因此STW的时间相对Serial来讲会更短。这里会涉及到一个吞吐量的概念:吞吐量 = 用户应用程序运行的时间 / (应用程序运行的时间 + 垃圾回收的时间)。因此在Parallel收集器中,吞吐量是很高的,适用于追求吞吐量的系统。
-XX:+UseParallelGC
:开启ParallelGC。
-XX:+UseParallelOldGC
:开启老年代的ParallelGC,和上面的任意开启一个就行。
-XX:ParallelGCThreads
:指定线程数。
咱们能够经过参数:-XX:+PrintCommandLineFlags
将JVM的已经设置好的参数打印出来,发现他默认的GC就是Parallel:
和Parallel的实现基本同样,惟一不一样的是它能够和CMS搭配使用,而Parallel不能够,当设置了回收器是cms的时候,JVM则会默认开启ParNew做为年轻代的回收器且没法关闭,对于Parallel的一些参数也能够在ParNew里面用。
尽管垃圾回收器从单线程发展到多线程,可是STW很长的问题始终是存在的,虽说Parllel的STW时长可能会短一点,但仍是没有作到极致,在CMS中STW的停顿时间获得了很好的解决:CMS在回收的过程当中容许和GC线程和用户线程同时执行(并行)且将标记对象的过程延长,每次只标记一点点,以获取最短回收停顿时间为目标。同时,CMS也是垃圾收集器发展过程当中的转折点,从CMS开始以后的垃圾回收器都是基于并行作GC的。这里要注意:CMS是使用标记清除算法进行垃圾回收的。
整个CMS的回收过程,能够用一张图来更清晰的看一下: 在初始标记触发STW的时候它的标记方式仍是原始的更改对象头MarkWord的GC标记字段,可是在并发标记阶段,由于是用户线程和GC线程同时在跑,因此这里采用的是三色标记的方式进行垃圾标记:
将对象的标记过程分为三种颜色:白色、灰色、黑色。
可是,这种方式会存在漏标与多标的问题:
好比如今有ABCD四个对象,A依赖了B和C,C依赖了D;初始标记完以后A对象已经被扫描过了因此是灰色,其余对象是白色:
继续往下执行扫描B和C,当B和C扫描完以后,A变成了黑色,B变成了灰色,C是黑色,D仍是白色:
此时若是用户线程把B和D的引用去掉,让C依赖D,创建起C和D的关系以后B变成了黑色:
那么问题来了,C已是黑色就不回再对其依赖对象扫描了,但事实上C还有一个依赖对象D没有被扫描。此时若是进行垃圾回收的话D会被回收掉,这就是所谓的漏标问题。
还用上面的例子说,好比如今AB是黑色,C是灰色,D是白色,当GC正在扫描D的时候,B被置空了,从逻辑上来说B是垃圾,理应被回收,可是由于GC不会对黑色对象作重复扫描因此B仍是黑色,在垃圾清理的时候B不会被回收,只能等到下次GC的时候再从新进行标记扫描。这种状况相对于漏标来讲还行,起码不会致使系统出BUG。
将新增的引用维护到一个集合里面,将引用的源头变为灰色,等待从新标记阶段在从新进行一次扫描。 好比:当D的引用指向了C,则会将C变为灰色,并将C放到一个新增引用的集合里面;在从新标记阶段会将C做为根节开始继续向下扫描。
CMS的垃圾回收阶段是并发回收的,若是使用标记整理的话,对象的内存地址会进行移动,由于用户线程还在执行,为了不因内存地址移动而带来的bug,还须要对用户线程的对象指针进行维护,在这个过程当中确定会STW,这样作就提升了垃圾清理的时长,停顿时间也变长了,不符合CMS以获取最短回收停顿时间为目标的设计初衷。
CMS的回收周期很长,可是他的STW时间是分开的,好比总的STW要100ms,可能他会在初始标记消耗20ms,从新标记消耗80ms,对于用户来讲能感知的到停顿时长可能只有80ms,也就是说CMS的设计初衷是为了提升用户体验,减小停顿时间。这是和Parallel最大的不一样。正由于CMS的回收周期很长,因此在垃圾不少的状况下可能出现上次的GC周期还没执行完就又触发了GC,被称为”concurrent mode failure“;对于这种状况会回退到Serial的方式进行回收,全程STW。由于是采用标记清除算法,因此会存在内存碎片的问题,经过参数-XX:+UseCMSCompactAtFullCollection
能够设置清除以后再作一次整理。
-XX:+UseConcMarkSweepGC
:使用CMS垃圾收集器,当设置这个参数后,年轻代默认会开启ParNew。
-XX:ConcGCThreads
:并发的GC线程数,默认是CPU的核数。
-XX:+UseCMSCompactAtFullCollection
:至关于标记整理。
-XX:CMSFullGCsBeforeCompaction
:多少次FullGC以后压缩一次,默认是0。
-XX:CMSInitiatingOccupancyFraction
: 当老年代使用达到该比例时会触发FullGC,默认是92。
-XX:+UseCMSInitiatingOccupancyOnly
:这个参数搭配上面那个用,表示是否是要一直使用上面的比例触发FullGC,若是设置则只会在第一次FullGC的时候使用-XX:CMSInitiatingOccupancyFraction的值,以后会进行自动调整。
-XX:+CMSScavengeBeforeRemark
:在FullGC前启动一次MinorGC,目的在于减小老年代对年轻代的引用,下降CMS GC的标记阶段时的开销,通常CMS的GC耗时80%都在标记阶段。 -XX:+CMSParallellnitialMarkEnabled
:默认状况下初始标记是单线程的,这个参数可让他多线程执行,能够减小STW。
-XX:+CMSParallelRemarkEnabled
:使用多线程进行从新标记,目的也是为了减小STW。
日日行,不怕千万里;经常作,不怕千万事。