咱们接着下面一篇持续学习JVM的基本知识。

对象存活判断

上篇中咱们介绍过JVM垃圾回收综述中说过一次垃圾回收之后会有一些对象存活。这节咱们介绍两个判断对象存活的算法。

判断对象存活有援用计数算法和可达性剖析算法。

1、援用计数算法

给每一个对象增加一个援用计数器,每当有一个中央援用它时,计数器值加1;每当有一个中央不再援用它时,计数器值减1,这样只有计数器的值不为0,就阐明还有中央援用它,它就不是无用的对象。

这种办法看起来非常简单,但目前许多支流的虚拟机都没有选用这种算法来治理内存,起因就是当某些对象之间相互援用时,无奈判断出这些对象是否已死。如下图,对象1和对象2都没有被堆外的变量援用,而是被对方相互援用,这时他们尽管没有用途了,然而援用计数器的值依然是1,无奈判断他们是死对象,垃圾回收器也就无奈回收。

2、可达性剖析算法

理解可达性剖析算法之前先理解一个概念——GC Roots,垃圾收集的终点,能够作为GC Roots的有虚拟机栈中本地变量表中援用的对象、办法区中动态属性援用的对象、办法区中常量援用的对象、本地办法栈中JNI(Native办法)援用的对象。 当一个对象到GC Roots没有任何援用链相连(GC Roots到这个对象不可达)时,就阐明此对象是不可用的,是死对象。如下图:object1、object2、object3、object4和GC Roots之间有可达门路,这些对象不会被回收,但object5、object6、object7到GC Roots之间没有可达门路,这些对象就是死对象。

下面被断定为非存活的死对象(object5、object6、object7)并不是必死无疑,还有解救的余地。进行可达性剖析后对象和GC Roots之间没有援用链相连时,对象将会被进行一次标记,接着会判断如果对象没有笼罩Object的finalize()办法或者finalize()办法曾经被虚拟机调用过,那么它们就会革除;如果对象笼罩了finalize()办法且还没有被调用,则会执行finalize()办法中的内容,所以在finalize()办法中如果从新与GC Roots援用链上的对象关联就能够援救本人。当然,理论中个别不会这么做。

GC算法

接下来讲GC的算法,次要有标记-革除算法、复制算法、标记-整顿算法、分代收集算法。

1、标记-革除算法

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

长处:不须要进行对象的挪动,并且仅对不存活的对象进行解决,在存活对象比拟多的状况极为无效。

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

上面两张图从两个角度说明了标记-分明算法:

2、复制算法

为了解决效率问题,一种称为复制(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了。

3、标记-整顿算法

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

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

上面两张图讲了这个算法的过程:

4、分代收集算法

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

这种算法就是咱们在后面JVM垃圾回收综述中讲述的内容。其本质是更为灵便的应用”标记-清理“和”标记-整顿“算法。

常见的GC回收器

当初常见的垃圾收集器有如下几种

新生代收集器:Serial、ParNew、Parallel Scavenge

老年代收集器:Serial Old、CMS、Parallel Old

堆内存垃圾收集器:G1

如图所示:

0、垃圾收集工夫

当程序运行时,各种数据、对象、线程、内存等都时刻在发生变化,当下达垃圾收集命令后垃圾收集器并不会立即执行垃圾收集。为了搞明确垃圾收集器的工作原理,咱们须要讲两个名词:平安点(safepoint)和安全区(safe region)。

平安点:从线程角度看,平安点能够了解为是在代码执行过程中的一些非凡地位,当线程执行到平安点的时候,阐明虚拟机以后的状态是平安的,如果有须要,能够在这里暂停用户线程。当垃圾收集时,如果须要暂停以后的用户线程,但用户线程过后没在平安点上,则应该期待这些线程执行到平安点再暂停。

安全区:平安点是绝对于运行中的线程来说的,对于如sleep或blocked等状态的线程,收集器不会期待这些线程被调配CPU工夫,这时候只有线程处于安全区中,就能够算是平安的。安全区就是在一段代码片段中,援用关系不会发生变化,能够看作是被扩大、拉长了的平安点。

GC过程肯定会产生STW(Stop The World),而一旦产生STW必然会影响用户应用,所以GC的倒退都是在围绕缩小STW工夫这一目标。

1、Serial 收集器

Serial是一款用于新生代的单线程收集器,采纳复制算法进行垃圾收集。Serial进行垃圾收集时,不仅只用一条线程执行垃圾收集工作,它在收集的同时,所有的用户线程必须暂停(Stop The World)。 如下是Serial收集器和Serial Old收集器联合进行垃圾收集的示意图,当用户线程都执行到平安点时,所有线程暂停执行,Serial收集器以单线程,采纳复制算法进行垃圾收集工作,收集完之后,用户线程持续开始执行。

实用场景:Client模式(桌面利用);单核服务器。能够用-XX:+UserSerialGC来抉择Serial作为新生代收集器。

2、ParNew 收集器

ParNew就是一个Serial的多线程版本,其它与Serial并无区别。ParNew在单核CPU环境并不会比Serial收集器达到更好的成果,它默认开启的收集线程数和CPU数量统一,能够通过-XX:ParallelGCThreads来设置垃圾收集的线程数。 如下是ParNew收集器和Serial Old收集器联合进行垃圾收集的示意图,当用户线程都执行到平安点时,所有线程暂停执行,ParNew收集器以多线程,采纳复制算法进行垃圾收集工作,收集完之后,用户线程持续开始执行。

实用场景:多核服务器;与CMS收集器搭配应用。当应用-XX:+UserConcMarkSweepGC来抉择CMS作为老年代收集器时,新生代收集器默认就是ParNew,也能够用-XX:+UseParNewGC来指定应用ParNew作为新生代收集器。

3、Parallel Scavenge 收集器

Parallel Scavenge也是一款用于新生代的多线程收集器,与ParNew的不同之处是,ParNew的指标是尽可能缩短垃圾收集时用户线程的进展工夫,Parallel Scavenge的指标是达到一个可管制的吞吐量。吞吐量就是CPU执行用户线程的的工夫与CPU执行总工夫的比值【吞吐量=运行用户代代码工夫/(运行用户代码工夫+垃圾收集工夫)】,比方虚拟机一共运行了100分钟,其中垃圾收集破费了1分钟,那吞吐量就是99% 。比方上面两个场景,垃圾收集器每100秒收集一次,每次进展10秒,和垃圾收集器每50秒收集一次,每次进展工夫7秒,尽管后者每次进展工夫变短了,然而总体吞吐量变低了,CPU总体利用率变低了。

收集频率

每次进展工夫

吞吐量

每100秒收集一次

10秒

91%

每50秒收集一次

7秒

88%

能够通过-XX:MaxGCPauseMillis来设置收集器尽可能在多长时间内实现内存回收,能够通过-XX:GCTimeRatio来准确管制吞吐量。

如下是Parallel收集器和Parallel Old收集器联合进行垃圾收集的示意图,在新生代,当用户线程都执行到平安点时,所有线程暂停执行,ParNew收集器以多线程,采纳复制算法进行垃圾收集工作,收集完之后,用户线程持续开始执行;在老年代,当用户线程都执行到平安点时,所有线程暂停执行,Parallel Old收集器以多线程,采纳标记整顿算法进行垃圾收集工作。

实用场景:重视吞吐量,高效利用CPU,须要高效运算且不须要太多交互。能够应用-XX:+UseParallelGC来抉择Parallel Scavenge作为新生代收集器,jdk7、jdk8默认应用Parallel Scavenge作为新生代收集器。

4、Serial Old收集器

Serial Old收集器是Serial的老年代版本,同样是一个单线程收集器,采纳标记-整顿算法。

如下图是Serial收集器和Serial Old收集器联合进行垃圾收集的示意图:

实用场景:Client模式(桌面利用);单核服务器;与Parallel Scavenge收集器搭配;作为CMS收集器的后备预案。

5、CMS(Concurrent Mark Sweep) 收集器

CMS收集器是一种以最短回收进展工夫为指标的收集器,以“最短用户线程进展工夫”著称。整个垃圾收集过程分为4个步骤:

  1. 初始标记:标记一下GC Roots能间接关联到的对象,速度较快
  2. 并发标记:进行GC Roots Tracing,标记出全副的垃圾对象,耗时较长
  3. 从新标记:修改并发标记阶段引用户程序持续运行而导致变动的对象的标记记录,耗时较短
  4. 并发革除:用标记-革除算法革除垃圾对象,耗时较长

整个过程耗时最长的并发标记和并发革除都是和用户线程一起工作,所以从总体上来说,CMS收集器垃圾收集能够看做是和用户线程并发执行的。

CMS收集器也存在一些毛病:

  • 对CPU资源敏感:默认调配的垃圾收集线程数为(CPU数+3)/4,随着CPU数量降落,占用CPU资源越多,吞吐量越小
  • 无奈解决浮动垃圾:在并发清理阶段,因为用户线程还在运行,还会一直产生新的垃圾,CMS收集器无奈在当次收集中革除这部分垃圾。同时因为在垃圾收集阶段用户线程也在并发执行,CMS收集器不能像其余收集器那样等老年代被填满时再进行收集,须要预留一部分空间提供用户线程运行应用。当CMS运行时,预留的内存空间无奈满足用户线程的须要,就会呈现“Concurrent Mode Failure”的谬误,这时将会启动后备预案,长期用Serial Old来从新进行老年代的垃圾收集。
  • 因为CMS是基于标记-革除算法,所以垃圾回收后会产生空间碎片,能够通过-XX:UserCMSCompactAtFullCollection开启碎片整顿(默认开启),在CMS进行Full GC之前,会进行内存碎片的整顿。还能够用-XX:CMSFullGCsBeforeCompaction设置执行多少次不压缩(不进行碎片整顿)的Full GC之后,跟着来一次带压缩(碎片整顿)的Full GC。

实用场景:器重服务器响应速度,要求零碎进展工夫最短。能够应用-XX:+UserConMarkSweepGC来抉择CMS作为老年代收集器。

6、Parallel Old 收集器

Parallel Old收集器是Parallel Scavenge的老年代版本,是一个多线程收集器,采纳标记-整顿算法。能够与Parallel Scavenge收集器搭配,能够充分利用多核CPU的计算能力。如Parallel Scavenge中的两个垃圾收集器的搭配应用图。

实用场景:与Parallel Scavenge收集器搭配应用;重视吞吐量。jdk7、jdk8默认应用该收集器作为老年代收集器,应用 -XX:+UseParallelOldGC来指定应用Paralle Old收集器。

7、G1 收集器

上述的一些GC收集器通过并行与并发曾经极大的缩小了STW的工夫,然而STW的工夫还是会因为各种起因不可控,而G1提供的一个最大性能就是可控的STW工夫。

G1通过把Java堆分成大小相等的多个独立区域,回收时计算出每个区域回收所取得的空间以及所需工夫的经验值,依据记录两个值来判断哪个区域最具备回收价值,所以叫Garbage First(垃圾优先)。

这里有几个重要的概念:

  • Region(区域):G1采纳了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次调配对象空间将逐段地应用内存。因而,在堆的应用上,G1并不要求对象的存储肯定是物理上间断的,只有逻辑上间断即可;每个分区也不会确定地为某个代服务,能够按需在年老代和老年代之间切换。启动时能够通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。
  • Card(卡片):在每个分区外部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,调配的对象会占用物理上间断的若干个卡片,当查找对分区内对象的援用时便可通过记录卡片来查找该援用对象(见RSet)。每次对内存的回收,都是对指定分区的卡片进行解决。
  • CSet(收集汇合):GC过程记录的可被回收的Region的汇合。在CSet中存活的数据会在GC过程中被挪动到另一个可用分区,CSet中的分区能够来自eden空间、survivor空间、或者老年代。
  • RSet(Remembered Set 记忆汇合):记录了其余Region中的对象援用本Region中对象的关系,属于points-into构造 (谁援用了我的对象)。作用是不须要扫描整个堆找到谁援用了以后分区中的对象,只须要扫描RSet即可。
  • Humongous regions:用来寄存大于规范的Region内存50%的大对象区域,如果有些对象大于整个Region就会去找间断的Region保留,如果没有就会触发GC。

G1收集器与之前的收集器最大的不同就在于堆内存的划分,之前的收集器只辨别新生代与老年代,而G1收集器则是把堆内存划分成多个独立的Region。

在上图中G1的Java堆中每个Region都有一个身份,每个Region有可能是eden、survivor、old,然而他们的身份仅仅是逻辑上的,是能够变动的,G1能够依据状况动静的调整各种Region的数量,通过管制回收的Region数量来管制STW的工夫,以达到STW工夫的可管制。

尽管G1收集器把Java堆化整为零成一个个Region,然而也不会进行所有Region进行收集,G1也分成了两种收集模式,两种模式如下:

Young GC: CSet就是所有年老代外面的Region;

Mixed GC: CSet是所有年老代里的Region加上在全局并发标记阶段标记进去的收益高的老年代Region;

Young GC过程:

阶段1:根扫描,动态和本地对象被扫描;

阶段2:更新RS,解决dirty card队列更新RS;

阶段3:解决RS,检测从年老代指向老年代的对象;

阶段4:对象拷贝,拷贝存活的对象到survivorl/old区域;

阶段5:解决援用队列,软援用,弱援用,虚援用解决;

Mixed GC过程:

1、全局并发标记(global concurrent marking)

2、拷贝存活对象(evacuation)

全局并发标记包含5个步骤:

1、初始标记(initial mark,STW):标记了从GCRoot开始间接可达的对象。

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

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

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

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

实用场景:要求尽可能可控GC进展工夫;内存占用较大的利用。能够用-XX:+UseG1GC应用G1收集器,jdk9默认应用G1收集器。

GC日志

每一种回收器的日志格局都是由其本身的实现决定的,换而言之,每种回收器的日志格局都能够不一样。但虚拟机设计者为了不便用户浏览,将各个回收器的日志都维持肯定的共性。

GC日志是学GC调优之前的必备前置条件,所以咱们必须学会。上面放两张网图,大家能够从中看到日志的每个节点:

young gc 日志:

Full GC日志:

文章首发于:
九神带你入门JVM(下)