JVM是经过分代收集理论进行垃圾回收的,即新生代和老年代选择的垃圾回收算法是不一样的:java
下面来看每一个算法的理论和应用:
算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不一样将内存分为几块。通常将java堆分为新生代和老年代,这样咱们就能够根据各个年代的特色选择合适的垃圾收集算法。这就是分代收集理论:安全
为何要分代收集:服务器
由于对象的存活周期不同,因此使用分代收集,不一样的代收集不一样存活周期的对象!
多线程
“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后再把已使用过的内存空间一次清理掉。由于会复制并清理已使用的通常内存,因此也就不用考虑内存碎片等复杂状况,只要移动堆顶指针,按顺序分配内存便可,实现简单,运行高效,但却要牺牲通常的内存空间。并发
标记-复制 算法通常用在新生代,由于标记-复制算法只使用一半的内存空间,由于新生代对象朝生夕死的缘故,只须要付出少许的复制成本就能够完成垃圾收集。而老年代对象存活概率高,复制的成本很大,并且内存只能使用通常,因此不适用于老年代。jvm
如图所示:ide
算法分为 “标记“ 和 “清除” 两个阶段。标记存活的对象,清除未被标记的对象。高并发
标记-清除算法带来的两个问题:oop
内存碎片的危害是什么?
空间碎片太多可能会致使,当程序在之后的运行过程当中须要分配较大对象时没法找到足够的连续内存而不得不提早触发另外一次垃圾收集动做。
因为复制算法不适用于老年代,根据老年代的特色,有人提出了另一种“标记-整理”(Mark-Compact)算法。该算法是在标记-清除的基础上,增长了整理的操做,把碎片化的空间整理为隔离的。后续步骤不是直接对可回收对象回收,而是让全部存活的对象向一端移动,而后直接清理掉端边界之外的内存。
这种算法克服了复制算法的空间浪费问题,同时克服了标记清除算法的内存碎片化的问题;
垃圾回收算法是jvm内存回收过程当中具体的、通用的方法。而垃圾收集器是jvm内存回收过程当中具体的执行者,即各类GC算法的具体实现。
目前为止尚未万能的垃圾收集器,咱们只能根据具体场景来选择合适的垃圾收集器。这也是目前垃圾收集器种类繁多的缘由!!各类垃圾收集器的组合使用以下图:
Epsilon、Shenandoah
:这两个收集器是redHat
开发的,其中Shenandoah
是G1
的加强版本,因为他们不是Oracle
公司开发的,且使用的极少,本文暂不介绍!
JVM参数设置: -XX:+UseSerialGC -XX:+UseSerialOldGC
单线程收集器,他不只只有一条GC线程,在GC时还必须中止其余全部的工做线程(STW),不多使用。
注意:
JVM参数设置:-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代)
JDK 1.8默认使用 Parallel
垃圾收集器(年轻代和老年代都是),这个垃圾收集器没法与CMS
垃圾收集器配合使用!对于堆内存2-3个G的状况,使用Parallel Scavenge
收集器足够应对!
多线程收集器,是Serial收集器的多线程版本,默认的收集线程数跟cpu核数相同,固然也能够用参数- XX:ParallelGCThreads
指定收集线程数,可是通常不推荐修改。
注意:
①:Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。GC总时间相对于CMS收集器较短!
②:Parallel Scavenge收集器新生代采用复制算法,老年代采用标记-整理算法。
JVM参数设置:-XX:+UseParNewGC
ParNew
收集器主要做用和parallel
收集器相似,区别主要在于ParNew
收集器能够配合CMS
收集器使用。除了Serial
收集器外,只有它能与CMS
收集器配合工做。配合工做时,通常ParNew
负责年轻代垃圾收集,CMS
负责老年代垃圾收集!这种组合是不少公司都在用的一种垃圾收集组合
JVM参数设置:-XX:+UseConcMarkSweepGC(old)
CMS相对于parallel 收集器的区别?
CMS
(Concurrent Mark Sweep)收集器是只有老年代才能用的垃圾收集器!CMS
收集器使用的是 标记-清除 算法,parallel
收集器新生代使用 复制 算法,老年代采用 标记-整理 算法parallel
收集器时,GC时须要较长时间进行 标记-整理 ,在此期间,用户线程是stw
的,很大程度上下降了用户体验;而CMS
把parallel
的多线程GC过程分为多个阶段,在最耗时的标记阶段使用并发标记,让用户线程和GC线程同时执行。因此在应对大内存的jvm
时,明显CMS
收集器使得用户体验更好Parallel
收集器,CMS使用较短期的STW
,换取用户的体验,由于他把最耗时的标记过程,改为了GC线程和用户线程并行,但因为CMS
拆分了GC过程,因此总体GC时间要长于Parallel
,但stw
时间更短。因此cms
主要是提高用户体验的,其实gc
效率不如Parallel
!工做流程以下
gc roots
直接引用的对象,速度很快!由于初始标记并不标记gc root
的全部引用。STW
,保证了用户体验,这点也是cms
收集器饱受青睐的缘由之一。但正由于并发标记,用户线程也在执行,就可能会出现多标或漏标的问题。CMS收集器的优缺点
- XX:+UseCMSCompactAtFullCollection
可让jvm在执行完标记清除后再作整理,整理是也会stw
,但时间较短!"concurrent mode failure"
(并发修改失败),此时会stop the world
全部用户线程,专心作垃圾收集,可是用的是serial old
串行垃圾收集器来回收,这个串行垃圾收集器效率至关低!代价比较大,尽可能避免!CMS的相关核心参数
-
、-xx
、-xx
三种jvm
参数前缀有什么不一样:x
的个数越多,表明这个参数的版本支持变数越高,有可能jdk8适用,jdk9就废除掉了!
-XX:+UseConcMarkSweepGC
:启用cms-XX:ConcGCThreads
:并发的GC线程数-XX:+UseCMSCompactAtFullCollection
:FullGC以后作压缩整理(减小碎片)-XX:CMSFullGCsBeforeCompaction
:多少次FullGC以后压缩一次,默认是0,表明每次FullGC后都会压缩一次-XX:CMSInitiatingOccupancyFraction
: 当老年代使用达到该比例时会触发FullGC(默认是92%
,这个参数能够防止concurrent mode failure
)-XX:+UseCMSInitiatingOccupancyOnly
:只使用设定的回收百分比(-XX:CMSInitiatingOccupancyFraction
设定的值),若是不配置此参数,-XX:CMSInitiatingOccupancyFraction
设定的值无效!由于jvm默认会根据gc
状况动态调整回收的百分比,相似于元空间的自动扩容、缩容!-XX:+CMSScavengeBeforeRemark
:在CMS GC前启动一次minor gc,下降CMS GC标记阶段(也会对年轻代一块儿作标记,若是在minor gc就干掉了不少对垃圾对象,标记阶段就会减小一些标记时间)时的开销,通常CMS的GC耗时 80%都在标记阶段-XX:+CMSParallellnitialMarkEnabled
:表示在初始标记的时候多线程执行,缩短STW-XX:+CMSParallelRemarkEnabled
:在从新标记的时候多线程执行,缩短STW;问题一:“concurrent mode failure”(并发修改失败)怎么预防?
因为默认老年代空间达到92% 就会full GC
,固然这个值是能够经过参数调的。在并发标记或并发清理阶段,若是不断有大对象进入老年代,老年代剩余的8%空间很快会被填满,此时就会出现"concurrent mode failure
"。咱们能够经过 -XX:CMSInitiatingOccupancyFraction=80
参数来调整老年代的full GC发生时机为80%,让老年代发生GC时还有更多空间存储新生代存活的大对象!
问题二:"Parallel 和CMS收集器使用场景
JDK8默认的垃圾回收器是-XX:+UseParallelGC
(年轻代)和-XX:+UseParallelOldGC
(老年代)。
若是内存较大(超过4个G,8个G之内,只是经验值),系统对停顿时间比较敏感,咱们可使用ParNew+CMS(-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
)这两个垃圾收集器配合使用!
在并发标记的过程当中,由于标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的状况就有可能发生,可使用三色标记来解决。
三色标记原理
三色标记把可达性分析遍历对象过程当中遇到的对象, 按照“是否访问过
”这个条件标记成如下三种颜色:
如图所示
三色标记过程分析:假如:A类中包含了B ,B类中包含了C和D。
刚开始默认都是白色对象,扫描标记完成后,黑色和灰色对象不会被回收,白色会回收。明白了三色标记原理后,来看一下具体是如何解决漏标问题的!
问题三:并发标记阶段的多标和漏标怎么解决?
多标:会产生浮动垃圾。因为并发运行的用户线程结束,会改变某些已标记过的对象的状态,好比gc root被销毁,那么会有部分GC线程已扫描过的黑色对象转变为白色对象,那么本轮GC不会回收这些浮动垃圾,留着下一次GC进行回收,浮动垃圾并不影响垃圾回收的正确性。
漏标:漏标会致使被引用的对象被当成垃圾误删除,这是严重bug,必须解决。产生缘由:并发执行中,用户线程把某些白色对象的引用指向了GC已扫描过的黑色对象,那么最初的白色对象也变成黑色对象了,而GC线程并不知道这个过程,会删除有用的对象。
漏标有两种解决方案:
GC期间新增了对象引用
。增量更新就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的增量记录下来到保存一个集合里边。stw
状态,只有GC
线程并发执行,因此不会再次产生漏标,且速度较快。GC期间引用关系被删除
的操做。就是当灰色对象要删除指向白色对象的引用关系时, 就将这个引用关系记录到一个容器里边。增量更新
和原始快照
两种方案的区别在于:
写屏障
以上不管是增量更新仍是原始快照, 虚拟机的记录操做都是经过写屏障实现的。由于想要增长引用或者删除引用,必有引用赋值操做这一步,写屏障就是利用AOP
的理念,在引用赋值操做先后,加入一些记录处理,收集这些将要赋值的引用,并保存起来!
给某个对象的成员变量赋值时,其底层代码大概长这样:
/** * @param field 某对象的成员变量,如 a.b.d * @param new_value 新值,如 null */ void oop_field_store(oop* field, oop new_value) { *field = new_value; // 赋值操做 }
所谓的写屏障,其实就是指在赋值操做先后,加入一些处理(能够参考AOP的概念):
void oop_field_store(oop* field, oop new_value) { pre_write_barrier(field); // 写屏障-写前操做 *field = new_value; // 赋值操做 post_write_barrier(field, value); // 写屏障-写后操做 }
a.d = d
,咱们能够利用写屏障,在增量更新以后,将A新的成员变量引用对象d
记录下来remark_set.add(new_value); // 在增量更新以后,记录新引用的对象
a.b.d = null
,咱们能够利用写屏障,在引用删除以前,将B原来成员变量的引用对象d
记录下来remark_set.add(old_value); // 在引用删除以前,记录原来的引用对象
对于读写屏障,以Java HotSpot VM
为例,其并发标记时对漏标的处理方案以下:
JVM参数设置:-XX:+UseG1GC
JDK 1.9默认使用 G1
G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高几率知足GC停顿时间要求的同时,还具有高吞吐量性能特征.G1
垃圾收集器摒弃了分代收集理念,只保留了年轻代和老年代的概念,但物理上已经不存在了,而是以(能够不连续)Region
的形式来存储对象。
Forget
分代收集:
以Region
的形式来存储对象:每个小块能够看作是一个Region
!
G1垃圾收集器的特色?
200ms
)用户可控(经过参数"- XX:MaxGCPauseMillis
"指定),以极高的几率知足GC停顿的同时,也保证了高吞吐量的特征。G1垃圾收集器在逻辑上保留了年轻代、老年代的概念,但在物理上已经抛弃了这些,年轻代和老年代区域能够任意转换。5%
(能够经过-XX:G1NewSizePercent
设置新生代初始占比),在系统运行中,JVM会不停的给年轻代增长更多的Region
,可是最多新生代的占比不会超过60%
(能够经过-XX:G1MaxNewSizePercent
进行调整),这也是与其余垃圾收集器的不一样之处!好比:堆大小为4096M
,那么年轻代默认占据200MB
左右的内存,对应大概是100
个Region,每一个Region
大小为2MRegin
),jvm最多存在2048
个Regin
,通常Region大小等于堆大小除以2048
,若是堆内存大小是4096M
,那每一个Region
大小默认为2M。可以使用-XX:G1HeapRegionSize
手动指定Region大小。年轻代中的Eden
和Survivor
对应的region
也跟以前同样,默认8:1:1
,假设年轻代如今有1000个region,eden区对应800个,s0对应100个,s1对应100个。一个Region可能以前是年轻代,若是Region进行了垃圾回收,以后可能又会变成老年代,也就是说Region的区域功能可能会动态变化。Humongous
区。若是一个对象超过了一个Regin大小的50%
,则会进入Humongous
区,一个Humongous
放不下,会横跨多个Humongous
放置这个对象!Full GC
的时候除了收集年轻代和老年代以外,也会将Humongous区一并回收。region
中的对象。由于G1
中年轻代和老年代都是以region
进行存储的,因此年轻代和老年代均可以使用复制算法! 这种不会像CMS那样回收完由于有不少内存碎片还须要整理一次,G1采用复制算法回收几乎不会有太多内存碎片
G1的垃圾回收过程
G1由于在物理上已经不区分年轻代、老年代,因此逻辑上的年轻代,老年代都用的同一个垃圾收集器G1。
G1
使用原始快照解决漏标问题,而CMS
使用增量更新解决漏标问题
G1的垃圾收集分类
5%
,YoungGC
并非说Eden区满了就马上触发,G1
会计算如今回收Eden须要多长时间,若是时间远小于用户设定的指望时间(使用-XX:MaxGCPauseMills
设定),就会给Eden
区扩容,直到扩容后的Eden
区再次放满,再次计算。。。直到回收须要时长约等于用户设定的指望停顿时间,此时才会触发YoungGC
!MixedGC
并非FullGC
,MixedGC的发生条件:经过-XX:InitiatingHeapOccupancyPercent
设置老年代的占用比,默认是45%
,若是达到这个比例就触发MixedGC,会回收Young、部分Old、Humongous区的对象。好比:堆默认有2048
个region,若是有接近1000
个region都是老年代的region,则可能就要触发MixedGC
了,MixedGc
使用复制算法。须要把各个region中存活的对象拷贝到别的region里去,拷贝过程当中若是发现没有足够的空region可以承载拷贝对象就会触发一次真正的Full GC
!Region
来供下一次MixedGC
使用,这个过程是很是耗时的。(Shenandoah
优化成多线程收集了)G1收集器参数设置
-XX:+UseG1GC
:使用G1收集器-XX:ParallelGCThreads
:指定GC工做的线程数量-XX:G1HeapRegionSize
:指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区,默认2M
-XX:MaxGCPauseMillis
:目标暂停时间(默认200ms
)-XX:G1NewSizePercent
:新生代内存初始空间(默认整堆5%,值配置整数,默认就是百分比)-XX:G1MaxNewSizePercent
:新生代内存最大空间-XX:TargetSurvivorRatio
:Survivor区的填充容量(默认50%),其实就是以前说的动态年龄判断。Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代-XX:MaxTenuringThreshold
:最大年龄阈值(默认15)-XX:InitiatingHeapOccupancyPercent
:老年代占用空间达到整堆内存阈值(默认45%
),则执行新生代和老年代的混合收集(MixedGC),好比咱们以前说的堆默认有2048
个region,若是有接近1000
个region都是老年代的region,则可能就要触发MixedGC了-XX:G1MixedGCLiveThresholdPercent
:region中的存活对象低于这个值时才会回收该region(默认85%
) ,若是超过这个值,存活对象过多,回收的的意义不大。-XX:G1MixedGCCountTarget
:在一次回收过程当中指定作几回筛选回收(默认8次),在最后一个筛选回收阶段能够回收一会,而后暂停回收,恢复系统运行,一会再开始回收,这样可让系统不至于单次停顿时间过长。这个过程至关于把筛选回收阶段切分为 GC线程 – 用户线程 – GC线程,注意这过程不是并发,而是串行-XX:G1HeapWastePercent(默认5%)
:gc过程当中空出来的region是否充足阈值,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其余Region,而后这个Region中的垃圾对象所有清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会当即中止混合回收,意味着本次混合回收就结束了。问题一:为何g1筛选回收阶段不作成和CMS用户线程和GC线程并发呢?
CMS用户线程和GC线程并发的最主要做用就是防止STW的时间过长而设计。但由于g1垃圾收集器的STW时间是用户可控的,就解决了CMS并发收集存在的问题。
在问题已解决的同时,关闭用户线程将大幅度提升GC效率,即知足了GC停顿,还保证了GC的高吞吐量!
问题二:用户能够随意设置stw停顿时间吗?为何?
问题三:什么场景适合使用G1收集器?
G1
收集器的底层算法是比CMS
要复杂的。若是在低内存中使用G1
,原本垃圾也不是不少,算法还要占用必定时间。可能得不偿失,因此g1
要物尽其用,尽可能在大内存中使用! 好比像kafka
这种支持高并发的系统,每秒处理几万甚至几十万消息时很正常的,通常来讲部署kafka
须要用大内存机器(好比64G),那么年轻代就有40多个G,普通的Young GC 须要扫描40G空间花费的时间是很是多的,可能最快也要几秒钟。
按kafka这个并发量,放满三四十G的eden
区可能也就一两分钟吧,那么意味着整个系统每运行一两分钟就会由于young gc
卡顿几秒钟无法处理新消息,显然是不行的 ,那么对于这种状况如何优化呢?
咱们可使用G1收集器,设置 -XX:MaxGCPauseMills
为50ms,假设50ms可以回收三到四个G内存,而后50ms的卡顿其实彻底可以接受,用户几乎无感知,那么整个系统就能够在卡顿几乎无感知的状况下一边处理业务一边收集垃圾。
G1天生就适合这种大内存机器的JVM运行,能够比较完美的解决大内存垃圾回收时间过长的问题。
问题四:在并发标记产生的漏标中,为何G1用(原始快照)SATB?CMS用增量更新?
在解决漏标问题时,增量更新须要以黑色对象为根,在经过gc root
作一次深度扫描,这其中还可能包括跨代引用等状况,这个过程是挺耗费时间的。而原始快照则只须要把集合中的白色对象引用置为黑色,默认这个对象是有用的,不能被回收,即便它多是浮动垃圾。这种简单粗暴的方式,虽然可能产生多的浮动垃圾,但不须要深度扫描。
G1的不少对象都位于不一样的regin
中,这个regin
是有不少个的,若是使用增量更新要从不少个regin
中找gc root
的引用关系,很是耗时。而使用原始快照不须要在从新标记阶段再次深度扫描对象,只是简单标记,等到下一轮GC 再深度扫描。因此G1
使用原始快照相对于增量更新效率会高。而CMS
使用增量更新,由于CMS
就一块老年代区域,深度扫描的话影响也不是很大!
ZGC是一款JDK 11
中新加入的具备实验性质的低延迟垃圾收集器,在目前的jdk8
中并不适用!
ZGC的特色
10
ms,且不随堆内存增大而增大!由于ZGC
中全部的垃圾收集阶段几乎都是并发执行!15%
,这个就很厉害了,G一、CMS都是经过延长回收时间来增长用户体验的!
ZGC的运做过程
并发重分配过程
问题:ZGC和G1在清理垃圾阶段的区别是什么?
zgc和g1的最大区别是在筛选回收阶段,G1是GC线程并发执行清理,此时STW,修改对象引用很方便。ZGC是GC执行清理时和用户线程并发操做,没有stw,复杂度很高
颜色指针
以下图所示,ZGC的核心设计之一。之前的垃圾回收器的GC信息都保存在对象头中, 而ZGC的GC信息保存在指针中。
颜色指针的三大优点:
安全点
就是指代码中一些特定的位置,当线程运行到这些位置时它的状态是肯定的,这样JVM就能够安全的进行一些操做,好比GC等,因此GC不是想何时作就当即触发的,是须要等待全部线程运行到安全点后才能触发。若是马上挂起全部用户线程,可能会破坏某些用户线程的原子性,好比:i++、jvm底层程序计数器的跳转等。
大致实现思想是当垃圾收集须要中断线程的时候, 不直接对线程操做, 仅仅简单地设置一个标志位, 各个线程执行过程 时会不停地主动去轮询这个标志, 一旦发现中断标志为真时就本身在最近的安全点上主动中断挂起。 轮询标志的地方和 安全点是重合的。
这些特定的安全点位置主要有如下几种:
安全区域
若是一个线程处于 Sleep 或中断状态,它就不能扫描安全点,响应 JVM 的中断请求。那么他周围的一片区域都是称为安全区域,这个区域的引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。