垃圾收集算法

标记-革除算法

最根底的收集算法是“标记-革除”(Mark-Sweep)算法,分两个阶段:首先标记出所有须要回收的对象,在标记实现后对立回收所有被标记的对象。

有余:一个是效率问题,标记和革除两个过程的效率都不高;另一个是空间问题,标记革除之后会产生大量不间断的内存碎片,空间碎片太多可能导致当前在程序运行过程须要调配较大对象时,无奈找到足够的间断内存而不得不提前触发另一个的垃圾收集动作。

复制算法

为了解决效率问题,一种称为复制(Copying)的收集算法呈现了,它将可用内存按容量划分为大小相等的两块,每次只应用其中的一块。当这一块内存用完了,就将还存活着的对象复制到另外一块上,而后再把曾经应用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存调配时也就不必思考内存碎片等简单状况,只有挪动堆顶指针,按程序分配内存即可,实现简略,运行高效。代价是内存放大为原来的一半。

商业虚拟机用这个回收算法来回收新生代。IBM钻研表明98%的对象是“朝生夕死“,不须要依照1-1的比例来划分内存空间,而是将内存分为一块较大的”Eden“空间和两块较小的Survivor空间,每次应用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性复制到另外一个Survivor空间上,最初清理掉Eden和方才用过的Survivor空间。Hotspot虚拟机默认Eden和Survivor的比例是8-1.即每次可用整个新生代的90%, 只有一个survivor,即1/10被”节约“。当然,98%的对象回收只是个别场景下的数据,咱们没有方法保障每次回收都只有不多于10%的对象存活,当Survivor空间不够时,须要依赖其余内存(老年代)进行调配担保(Handle Promotion).

如果另外一块survivor空间没有足够空间寄存上一次新生代收集下来的存活对象时,这些对象将间接通过调配担保机制进入老年代。

eden survivor复制过程概述

Eden Space字面意思是伊甸园,对象被创立的时候首先放到这个区域,进行垃圾回收后,不能被回收的对象被放入到空的survivor区域。

Survivor Space幸存者区,用于保留在eden space内存区域中通过垃圾回收后没有被回收的对象。Survivor有两个,别离为To Survivor、 From Survivor,这个两个区域的空间大小是一样的。执行垃圾回收的时候Eden区域不能被回收的对象被放入到空的survivor(也就是To Survivor,同时Eden区域的内存会在垃圾回收的过程中全副开释),另一个survivor(即From Survivor)里不能被回收的对象也会被放入这个survivor(即To Survivor),而后To Survivor 和 From Survivor的标记会调换,始终保障一个survivor是空的。

为啥须要两个survivor?因为须要一个残缺的空间来复制过去。当满的时候降职。每次都往标记为to的外面放,而后调换,这时from曾经被清空,能够当作to了。

标记-整顿算法

复制收集算法在对象成活率较高时就要进行较多的复制操作,效率将会变低。更要害的是,如果不想节约50%的空间,就须要有额定的空间进行调配担保,以应答被应用的内存中所有对象都100%存活的极其状况,所以,老年代个别不能间接选用这种算法。

依据老年代的特点,有人提出一种”标记-整顿“Mark-Compact算法,标记过程依然和标记-革除一样,但后续步骤不是间接对可回收对象进行清理,而是让所有存活的对象都向一端挪动,而后间接清理端边界以外的内存.

分代收集算法

以后商业虚拟机的垃圾收集都采纳”分代收集“(Generational Collection)算法,这种算法依据对象存活周期的不同将内存划分为几块。个别把Java堆分为新生代和老年代,这样就能够依据各个年代的特点采纳最适当的收集算法。在新生代,每次垃圾收集时都发现少量对象死去,只有大量存活,那就选用复制算法,只须要付出大量存活对象的复制老本就能够实现收集。而老年代中因为对象存活率较高,没有额定的空间对它进行调配担保,就必须应用”标记-清理“和”标记-整顿“算法来进行回收。

HotSpot算法实现

在Java语言中,可作为GC Roots的对象包含上面几种:

  • 虚拟机栈(栈帧中的本地变量表)中援用的对象
  • 办法去中类动态属性援用的对象
  • 办法区中常量援用的对象
  • 本地办法栈中JNI(即个别说的Native办法)援用的对象

从可达性剖析中从GC Roots节点找援用链这个操作为例,可作为GC Roots的节点次要在全局性的援用(例如常量或类动态属性)与执行上下文(例如栈帧中的本地变量表)中,当初很多利用仅仅办法区就有数百兆,如果要一一查看外面的援用,必然耗费很多工夫。

可达性剖析对执行工夫的敏感还体现在GC进展上,因为这项剖析工作必须在一个能确保一致性的快照中进行--这里”一致性“的意思是指整个剖析期间整个执行零碎看起来就像被解冻在某个工夫点,不能够呈现剖析过程中对象援用关系还在一直变动的状况,该点不满足的话剖析后果准确性就无奈失去保障。这点是导致GC进行时必须进展所有Java执行线程(Sun公司将这件事件称为”Stop The World“)的一个重要起因,即便是在号称(简直)不会产生进展的CMS收集器中,枚举根节点时也必须进展的。

平安点,Safepoint

垃圾收集器

Serial 收集器

标记-复制。

单线程,一个CPU或一条收集线程去实现垃圾收集工作,收集时必须暂停其余所有的工作线程,直到它完结。

尽管如此,它仍然是虚拟机运行在Client模式下的默认新生代收集器。简略而高效。

ParNew 收集器

ParNew是Serial收集器的多线程版本。Server模式下默认新生代收集器,除了Serial收集器之外,只有它能与CMS收集器配合工作。

并行 Parallel

指多条垃圾收集线程并行工作,但此时用户线程依然处于期待状态。

并发 Concurrent

指用户线程与垃圾收集线程同时执行(但不肯定是并行的,可能会交替执行),用户程序再持续运行,而垃圾收集程序运行于另一个CPU上。

Parallel Scavenge 收集器

Parallel Scavenge 收集器是一个新生代收集器,它也是应用复制算法的收集器。看上去来ParNew一样,有什么特地?

Parallel Scavenge 收集器的特点是它的关注点与其余收集器不同,CMS等收集器关注点是尽可能缩短垃圾收集时用户线程的进展工夫。而Parallel Scavenge收集器的指标则是达到一个可管制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的工夫和CPU总小号工夫的比值,即吞吐量 = 运行用户代码工夫 / (运行用户代码工夫+垃圾收集工夫),虚拟机总共运行了100min,其中垃圾收集破费了1min,那吞吐量就是99%.

进展工夫越短就越适宜须要与用户交互的程序,良好的响应速度能晋升用户体验,而高吞吐量则能够高效地利用CPU工夫,次要适宜在后盾运算而不须要太多交互的工作。

Parallel Scavenge收集器提供了两个参数用于准确管制吞吐量,别离是管制最大垃圾收集进展工夫 -XX:MaxGCPauseMillis以及间接设置吞吐量大小的-XX:GCTimeRatio

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器。给Client模式下的虚拟机应用。

新生代采纳复制算法,暂停所有用户线程;

老年代采纳标记-整顿算法,暂停所有用户线程;

Parallel Old 收集器

这里留神,Parallel Scavage 收集器架构中自身有PS MarkSweep收集器来收集老年代,并非间接应用了Serial Old,但二者靠近。自己win10 64位零碎,jdk1.8.0_102,测试默认垃圾收集器为:PS MarkSweep 和 PS Scavenge。 也就是说Java8的默认并不是G1。

这是”吞吐量优先“,重视吞吐量以及CPU资源敏感的场合都能够优先思考Parallel Scavenge和Parallel Old(PS Mark Sweep)。Java8 默认就是这个。

CMS 收集器

CMS(Concurrent Mark Sweep) 收集器是一种以获取最短回收进展工夫为指标的收集器。目前很大一部分的Java利用集中在互联网站或者B/S零碎的服务端上,这类尤其器重服务的响应速度,心愿零碎进展工夫最短。CMS收集器就十分合乎这类利用的需要。

CMS基于 标记-革除算法实现。整个过程分为4个步骤:

  1. 初始标记(CMS initial mark) -stop the world
  2. 并发标记(CMS concurrent mark)
  3. 从新标记(CMS remark) -stop the world
  4. 并发革除(CMS concurrent sweep)

初始标记,从新标记这两个步骤依然须要Stop The World, 初始标记仅仅标记以下GC Roots能间接关联的对象,速度很快。

并发标记就是进行GC Roots Tracing的过程;

而从新标记阶段则是为了修改并发标记期间因为用户程序持续运作而导致标记产生变动的那一部分对象的标记记录。这个阶段进展比初始标记略微长,但远比并发标记的工夫短。

整个过程耗时最长的并发标记和并发革除过程,收集器都能够与用户线程一起工作。总体上来说,CMS收集器的内存回收过程与用户线程一起并发执行的。

CMS特点:并发收集,低进展。

毛病

1.CMS收集器对CPU资源十分敏感。默认启动的回收线程数是(CPU+3)/4. 当CPU 4个以上时,并发回收垃圾收集线程不少于25%的CPU资源。

2.CMS收集器无奈解决浮动垃圾(Floating Garbage), 可能呈现”Concurrent Mode Failure“失败而导致另一次Full GC的产生。因为CMS并发清理时,用户线程还在运行,随同产生新垃圾,而这一部分呈现在标记之后,只能下次GC时再清理。这一部分垃圾就称为”浮动垃圾“。

因为CMS运行时还须要给用户空间持续运行,则不能等老年代简直被填满再进行收集,须要预留一部分空间提供并发收集时,用户程序运行。JDK1.6中,CMS启动阈值为92%. 若预留内存不够用户应用,则呈现一次Concurent Mode Failure失败。这时虚拟机启动后备预案,长期启用Serial Old收集老年代,这样进展工夫很长。

3.CMS基于”标记-革除“算法实现的,则会产生大量空间碎片,空间碎片过多时,没有间断空间调配给大对象,不得不提前触发一次FUll GC。当然能够开启-XX:+UseCMSCompactAtFullCollection(默认开),在CMS顶不住要FullGC时开启内存碎片合并整顿过程。内存整理过程是无奈并发的,空间碎片问题没了,但进展工夫变长。

面试题:CMS一共会有几次STW

首先,答复两次,初始标记和从新标记须要。

而后,CMS并发的代价是预留空间给用户,预留有余的时候触发FUllGC,这时Serail Old会STW.

而后,CMS是标记-革除算法,导致空间碎片,则没有间断空间调配大对象时,FUllGC, 而FUllGC会开始碎片整顿, STW.

即2次或屡次。

CMS什么时候FUll GC

除间接调用System.gc外,触发Full GC执行的状况有如下四种。

1. 旧生代空间有余

旧生代空间只有在新生代对象转入及创立为大对象、大数组时才会呈现有余的景象,当执行Full GC后空间依然有余,则抛出如下谬误:
java.lang.OutOfMemoryError: Java heap space
为防止以上两种情况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创立过大的对象及数组。

2. Permanet Generation空间满

PermanetGeneration中寄存的为一些class的信息等,当零碎中要加载的类、反射的类和调用的办法较多时,Permanet Generation可能会被占满,在未配置为采纳CMS GC的状况下会执行Full GC。如果通过Full GC依然回收不了,那么JVM会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space
为防止Perm Gen占满造成Full GC景象,可采纳的办法为增大Perm Gen空间或转为应用CMS GC。

3. CMS GC时呈现promotion failed和concurrent mode failure

对于采纳CMS进行旧生代GC的程序而言,尤其要留神GC日志中是否有promotion failed和concurrent mode failure两种情况,当这两种情况呈现时可能会触发Full GC。
promotionfailed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间有余造成的。
应答措施为:增大survivorspace、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会因为JDK的bug29导致CMS在remark结束后很久才触发sweeping动作。对于这种情况,可通过设置-XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来防止。

4. 统计失去的Minor GC降职到旧生代的均匀大小大于旧生代的残余空间

这是一个较为简单的触发状况,Hotspot为了防止因为新生代对象降职到旧生代导致旧生代空间有余的景象,在进行Minor GC时,做了一个判断,如果之前统计所失去的Minor GC降职到旧生代的均匀大小大于旧生代的残余空间,那么就间接触发Full GC。
例如程序第一次触发MinorGC后,有6MB的对象降职到旧生代,那么当下一次Minor GC产生时,首先查看旧生代的残余空间是否大于6MB,如果小于6MB,则执行Full GC。
当新生代采纳PSGC时,形式稍有不同,PS GC是在Minor GC后也会查看,例如下面的例子中第一次Minor GC后,PS GC会查看此时旧生代的残余空间是否大于6MB,如小于,则触发对旧生代的回收。
除了以上4种情况外,对于应用RMI来进行RPC或治理的Sun JDK利用而言,默认状况下会一小时执行一次Full GC。可通过在启动时通过- java-Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

G1

什么是垃圾回收

首先,在理解G1之前,咱们须要分明的晓得,垃圾回收是什么?简略的说垃圾回收就是回收内存中不再应用的对象。

垃圾回收的根本步骤

回收的步骤有2步:

1.查找内存中不再应用的对象

2.开释这些对象占用的内存

1,查找内存中不再应用的对象

那么问题来了,如何判断哪些对象不再被应用呢?咱们也有2个办法:

1.援用计数法
援用计数法就是如果一个对象没有被任何援用指向,则可视之为垃圾。这种办法的毛病就是不能检测到环的存在。

2.根搜索算法

根搜索算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜寻,搜寻所走过的门路称为援用链(Reference Chain),当一个对象到GC Roots没有任何援用链相连时,则证实此对象是不可用的。

当初咱们曾经晓得如何找出垃圾对象了,如何把这些对象清理掉呢?

2. 开释这些对象占用的内存

常见的形式有复制或者间接清理,然而间接清理会存在内存碎片,于是就会产生了清理再压缩的形式。

总得来说就产生了三种类型的回收算法。

1.标记-复制

2.标记-清理

3.标记-整顿

基于分代的假如

因为对象的存活工夫有长有短,所以对于存活工夫长的对象,缩小被gc的次数能够防止不必要的开销。这样咱们就把内存分成新生代和老年代,新生代寄存刚创立的和存活工夫比拟短的对象,老年代寄存存活工夫比拟长的对象。这样每次仅仅清理年老代,老年代仅在必要时时再做清理能够极大的进步GC效率,节俭GC工夫。

Java垃圾收集器的历史

第一阶段,Serial(串行)收集器

在jdk1.3.1之前,java虚拟机仅仅能应用Serial收集器。 Serial收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅是阐明它只会应用一个CPU或一条收集线程去实现垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其余所有的工作线程,直到它收集完结。

PS:开启Serial收集器的形式

-XX:+UseSerialGC

第二阶段,Parallel(并行)收集器

Parallel收集器也称吞吐量收集器,相比Serial收集器,Parallel最次要的劣势在于应用多线程去实现垃圾清理工作,这样能够充分利用多核的个性,大幅升高gc工夫。

PS:开启Parallel收集器的形式

-XX:+UseParallelGC -XX:+UseParallelOldGC

第三阶段,CMS(并发)收集器

CMS收集器在Minor GC时会暂停所有的利用线程,并以多线程的形式进行垃圾回收。在Full GC时不再暂停利用线程,而是应用若干个后盾线程定期的对老年代空间进行扫描,及时回收其中不再应用的对象。

PS:开启CMS收集器的形式

-XX:+UseParNewGC -XX:+UseConcMarkSweepGC

第四阶段,G1(并发)收集器

G1收集器(或者垃圾优先收集器)的设计初衷是为了尽量缩短解决超大堆(大于4GB)时产生的进展。绝对于CMS的劣势而言是内存碎片的产生率大大降低。

PS:开启G1收集器的形式

-XX:+UseG1GC

理解G1

G1的第一篇paper(附录1)发表于2004年,在2012年才在jdk1.7u4中可用。oracle官网打算在jdk9中将G1变成默认的垃圾收集器,以代替CMS。为何oracle要极力推荐G1呢,G1有哪些长处

首先,G1的设计准则就是简略可行的性能调优

开发人员仅仅须要申明以下参数即可:

-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200

其中-XX:+UseG1GC为开启G1垃圾收集器,-Xmx32g 设计堆内存的最大内存为32G,-XX:MaxGCPauseMillis=200设置GC的最大暂停工夫为200ms。如果咱们须要调优,在内存大小肯定的状况下,咱们只须要批改最大暂停工夫即可。

其次,G1将新生代,老年代的物理空间划分勾销了。

这样咱们再也不必独自的空间对每个代进行设置了,不必放心每个代内存是否足够。

取而代之的是,G1算法将堆划分为若干个区域(Region),它依然属于分代收集器。不过,这些区域的一部分蕴含新生代,新生代的垃圾收集仍然采纳暂停所有利用线程的形式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,实现了清理工作。这就意味着,在失常的处理过程中,G1实现了堆的压缩(至多是局部堆的压缩),这样也就不会有cms内存碎片问题的存在了。

在G1中,还有一种非凡的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认间接会被调配在年轻代,然而如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门寄存巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找间断的H分区来存储。为了能找到间断的H区,有时候不得不启动Full GC。

PS:在java 8中,长久代也挪动到了一般的堆内存空间中,改为元空间。

对象调配策略

说起大对象的调配,咱们不得不谈谈对象的调配策略。它分为3个阶段:

1.TLAB(Thread Local Allocation Buffer)线程本地调配缓冲区
2.Eden区中调配
3.Humongous区调配

TLAB为线程本地调配缓冲区,它的目标为了使对象尽可能快的调配进去。如果对象在一个共享的空间中调配,咱们须要采纳一些同步机制来治理这些空间内的闲暇空间指针。在Eden空间中,每一个线程都有一个固定的分区用于调配对象,即一个TLAB。调配对象时,线程之间不再须要进行任何的同步。

对TLAB空间中无奈调配的对象,JVM会尝试在Eden空间中进行调配。如果Eden空间无奈包容该对象,就只能在老年代中进行调配空间。

最初,G1提供了两种GC模式,Young GC和Mixed GC,两种都是Stop The World(STW)的。上面咱们将别离介绍一下这2种模式。

G1 Young GC

Young GC次要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种状况下,Eden空间的数据挪动到Survivor空间中,如果Survivor空间不够,Eden空间的局部数据会间接降职到年轻代空间。Survivor区的数据挪动到新的Survivor区中,也有局部数据降职到老年代空间中。最终Eden空间的数据为空,GC进行工作,利用线程继续执行。

这时,咱们须要思考一个问题,如果仅仅GC 新生代对象,咱们如何找到所有的根对象呢? 老年代的所有对象都是根么?那这样扫描下来会消耗大量的工夫。于是,G1引进了RSet的概念。它的全称是Remembered Set,作用是跟踪指向某个heap区内的对象援用。

在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的援用。这是一种point-out,在进行Young GC时,扫描根时,仅仅须要扫描这一块区域,而不须要扫描整个老年代。

但在G1中,并没有应用point-out,这是因为一个分区太小,分区数量太多,如果是用point-out的话,会造成大量的扫描节约,有些基本不须要GC的分区援用也扫描了。于是G1中应用point-in来解决。point-in的意思是哪些分区援用了以后分区中的对象。这样,仅仅将这些对象当做根来扫描就防止了有效的扫描。因为新生代有多个,那么咱们须要在新生代之间记录援用吗?这是不必要的,起因在于每次GC时,所有新生代都会被扫描,所以只须要记录老年代到新生代之间的援用即可。

须要留神的是,如果援用的对象很多,赋值器须要对每个援用做解决,赋值器开销会很大,为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)。一个Card Table将一个分区在逻辑上划分为固定大小的间断区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。Card Table通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认状况下,每个卡都未被援用。当一个地址空间被援用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被援用,此外RSet也将这个数组下标记录下来。个别状况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个汇合,外面的元素是Card Table的Index。

Young GC 阶段

阶段1:根扫描

动态和本地对象被扫描

阶段2:更新RS

解决dirty card队列更新RS

阶段3:解决RS

检测从年老代指向年轻代的对象

阶段4:对象拷贝

拷贝存活的对象到survivor/old区域

阶段5:解决援用队列

软援用,弱援用,虚援用解决

G1 Mix GC

Mix GC不仅进行失常的新生代垃圾收集,同时也回收局部后盾扫描线程标记的老年代分区。

它的GC步骤分2步:

1.全局并发标记(global concurrent marking)
2.拷贝存活对象(evacuation)

在进行Mix GC之前,会先进行global concurrent marking(全局并发标记)。 global concurrent marking的执行过程是怎么的呢?

在G1 GC中,它次要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为五个步骤:

初始标记(initial mark,STW)

在此阶段,G1 GC 对根进行标记。该阶段与惯例的 (STW) 年老代垃圾回收密切相关。

根区域扫描(root region scan

G1 GC 在初始标记的存活区扫描对老年代的援用,并标记被援用的对象。该阶段与应用程序(非 STW)同时运行,并且只有实现该阶段后,能力开始下一次 STW 年老代垃圾回收。

并发标记(Concurrent Marking)

G1 GC 在整个堆中查找可拜访的(存活的)对象。该阶段与应用程序同时运行,能够被 STW 年老代垃圾回收中断

最终标记(Remark,STW)

该阶段是 STW 回收,帮忙实现标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被拜访的存活对象,并执行援用解决。

革除垃圾(Cleanup,STW)

在这个最初阶段,G1 GC 执行统计和 RSet 污染的 STW 操作。在统计期间,G1 GC 会辨认齐全闲暇的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到闲暇列表时为局部并发。

三色标记算法

提到并发标记,咱们不得不理解并发标记的三色标记算法。它是形容追踪式回收器的一种有用的办法,利用它能够推演回收器的正确性。 首先,咱们将对象分成三种类型的。

彩色:根对象,或者该对象与它的子对象都被扫描

灰色:对象自身被扫描,但还没扫描完该对象中的子对象

红色:未被扫描对象,扫描实现所有对象之后,最终为红色的为不可达对象,即垃圾对象

当GC开始扫描对象时,依照如下图步骤进行对象的扫描:

根对象被置为彩色,子对象被置为灰色。

持续由灰色遍历,将已扫描了子对象的对象置为彩色。

遍历了所有可达的对象后,所有可达的对象都变成了彩色。不可达的对象即为红色,须要被清理。

这看起来很美妙,然而如果在标记过程中,应用程序也在运行,那么对象的指针就有可能扭转。这样的话,咱们就会遇到一个问题:对象失落问题

咱们看上面一种状况,当垃圾收集器扫描到上面状况时:

这时候应用程序执行了以下操作:

A.c=CB.c=null

这样,对象的状态图变成如下情景:

这时候垃圾收集器再标记扫描的时候就会下图成这样:

很显然,此时C是红色,被认为是垃圾须要清理掉,显然这是不合理的。那么咱们如何保障应用程序在运行的时候,GC标记的对象不失落呢?有如下2中可行的形式:

1.在插入的时候记录对象
2.在删除的时候记录对象

刚好这对应CMS和G1的2种不同实现形式:

在CMS采纳的是增量更新(Incremental update),只有在写屏障(write barrier)里发现要有一个白对象的援用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。

在G1中,应用的是STAB(snapshot-at-the-beginning)的形式,删除的时候记录所有的对象,它有3个步骤:

1.在开始标记的时候生成一个快照图标记存活对象

2.在并发标记的时候所有被扭转的对象入队(在write barrier里把所有旧的援用所指向的对象都变成非白的)

3.可能存在游离的垃圾,将在下次被收集

这样,G1到当初能够晓得哪些老的分区可回收垃圾最多。 当全局并发标记实现后,在某个时刻,就开始了Mix GC。这些垃圾回收被称作“混合式”是因为他们不仅仅进行失常的新生代垃圾收集,同时也回收局部后盾扫描线程标记的分区。混合式垃圾收集如下图:

混合式GC也是采纳的复制的清理策略,当GC实现后,会从新开释空间。

调优实际

MaxGCPauseMillis调优

后面介绍过应用GC的最根本的参数:

-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200

后面2个参数都好了解,前面这个MaxGCPauseMillis参数该怎么配置呢?这个参数从字面的意思上看,就是容许的GC最大的暂停工夫。G1尽量确保每次GC暂停的工夫都在设置的MaxGCPauseMillis范畴内。 那G1是如何做到最大暂停工夫的呢?这波及到另一个概念,CSet(collection set)。它的意思是在一次垃圾收集器中被收集的区域汇合。

Young GC:选定所有新生代里的region。通过管制新生代的region个数来管制young GC的开销。

Mixed GC:选定所有新生代里的region,外加依据global concurrent marking统计得出收集收益高的若干老年代region。在用户指定的开销指标范畴内尽可能抉择收益高的老年代region。

在了解了这些后,咱们再设置最大暂停工夫就好办了。 首先,咱们能容忍的最大暂停工夫是有一个限度的,咱们须要在这个限度范畴内设置。然而应该设置的值是多少呢?咱们须要在吞吐量跟MaxGCPauseMillis之间做一个均衡。如果MaxGCPauseMillis设置的过小,那么GC就会频繁,吞吐量就会降落。如果MaxGCPauseMillis设置的过大,应用程序暂停工夫就会变长。G1的默认暂停工夫是200毫秒,咱们能够从这里动手,调整适合的工夫。

其余调优参数

-XX:G1HeapRegionSize=n

设置的 G1 区域的大小。值是 2 的幂,范畴是 1 MB 到 32 MB 之间。指标是依据最小的 Java 堆大小划分出约 2048 个区域。

-XX:ParallelGCThreads=n

设置 STW 工作线程数的值。将 n 的值设置为逻辑处理器的数量。n 的值与逻辑处理器的数量雷同,最多为 8。

如果逻辑处理器不止八个,则将 n 的值设置为逻辑处理器数的 5/8 左右。这实用于大多数状况,除非是较大的 SPARC 零碎,其中 n 的值能够是逻辑处理器数的 5/16 左右。

-XX:ConcGCThreads=n

设置并行标记的线程数。将 n 设置为并行垃圾回收线程数 (ParallelGCThreads) 的 1/4 左右。

-XX:InitiatingHeapOccupancyPercent=45 

设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。

防止应用以下参数:

防止应用 -Xmn 选项或 -XX:NewRatio 等其余相干选项显式设置年老代大小。固定年老代的大小会笼罩暂停工夫指标。

触发Full GC

在某些状况下,G1触发了Full GC,这时G1会进化应用Serial收集器来实现垃圾的清理工作,它仅仅应用单线程来实现GC工作,GC暂停工夫将达到秒级别的。整个利用处于假死状态,不能解决任何申请,咱们的程序当然不心愿看到这些。那么产生Full GC的状况有哪些呢?

并发模式失败

G1启动标记周期,但在Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。这种情景下,须要减少堆大小,或者调整周期(例如减少线程数-XX:ConcGCThreads等)。

降职失败或者疏散失败

G1在进行GC的时候没有足够的内存供存活对象或降职对象应用,由此触发了Full GC。能够在日志中看到(to-space exhausted)或者(to-space overflow)。解决这种问题的形式是:

a. 减少 -XX:G1ReservePercent 选项的值(并相应减少总的堆大小),为“指标空间”减少预留内存量。

b. 通过缩小-XX:InitiatingHeapOccupancyPercent 提前启动标记周期。

c. 也能够通过减少 -XX:ConcGCThreads 选项的值来减少并行标记线程的数目。

巨型对象调配失败

当巨型对象找不到适合的空间进行调配时,就会启动Full GC,来开释空间。这种状况下,应该防止调配大量的巨型对象,减少内存或者增大-XX:G1HeapRegionSize,使巨型对象不再是巨型对象。