前言
上一篇咱们介绍了对象在堆内的内存布局曾经占用空间的大小,同时剖析了堆内能够分为Young区和Old区,而且Young区能够分为Eden区和Survivor区,Survivor区又拆分成了两个大小一样的区S0和S1区域,其实这么拆分的理由和GC是密切相关的,那么这一篇文章就让咱们深刻理解一下Java中的垃圾收集机制。
如何确定有效对象
在垃圾收集的时候第一件事就是怎么确定一个对象是垃圾,那么该如何确定一个对象曾经能够被回收了呢?支流的算法有两种:援用计数法和可达性剖析算法
援用计数法(Reference Counting)
这个算法很简略,效率也十分高。就是给每个对象增加一个援用计数器,每当有一个中央援用它时,计数器的值就加1,当援用生效时,计数器的值就减1,当计数器的值减为0时就表名这个对象不会再被应用,成为了无用对象,能够被回收。
这种算法尽管实现简略,效率也高,然而存在一个问题,咱们看上面一个场景:
上图中4个对象互相援用,然而并没有其余对象去援用他们,这种对象实际上也是有效对象,然而他们的援用计数器都是1而不是0,所以援用计数法没方法解决这种“一坨垃圾”的场景。
可达性剖析算法(Reachability Analysis)
可达性剖析算法就是抉择一些对象作为起始点,这些对象称之为:GC Root。而后从GC Root开始向下搜寻,搜寻门路称之为援用链(Reference Chain),当一个对象不在任何一条援用链上时,就阐明此对象是有效对象,能够被回收。
比如说上面这幅图,左边那一串相互援用的对象因为没有不在GC Root的援用链上,所以就是有效对象,可达性剖析算法无效的解决了相互援用对象无奈回收问题。
GC Root
在Java中,能够作为GC Root对象的包含上面几种:
- Java虚拟机栈内栈帧中的局部变量表中的变量
- 办法区中类动态属性
- 办法区中常量
- 本地办法栈中JNI(即Native办法)中的变量
留神:在剖析对象的过程中,为了确保后果的准确性,须要保障剖析过程中对象援用关系不会发生变化,而为了达到这个目标,就须要暂停用户线程,这种操作也叫:Stop The World(STW)。
援用的分类
下面两种算法其实都是一个目标,判断对象有没有被援用,而援用也不仅仅都是一样的援用,JDK1.2开始,Java中将援用进行了分类,划分成了四种援用,别离是:强援用,软援用,弱援用,虚援用。这四种援用关系的强度为:强援用>软援用>弱援用>虚援用。
强援用(Strong Reference)
咱们写的代码中个别都是用的强援用,如:Object obj = new Object()这种就属于强援用,强援用只有还存在,肯定不会被回收,空间不够就间接抛出OOM异样
软援用(Soft Reference)
软援用是通过SoftReference类来实现的。软援用能够用来示意一些还有用但又是非必须的对象,零碎在行将溢出之前,如果发现有软援用的对象存在,会对其进行二次回收,回收之后内存还是不够,就会抛出OOM异样。
弱援用(Weak Reference)
弱援用是通过WeakReference类来实现的。弱援用也是用来示意非必须对象的,然而相比拟软援用,弱援用的对象会在第一次垃圾回收的时候就被回收掉。
虚援用(Phantom Reference)
虚援用是通过PhantomReference类来实现的,也被成为幽灵援用或者幻影援用。这是最弱的一种援用关系。一个对象是否有虚援用的存在,齐全不对其生存工夫形成影响。也无奈通过虚援用来获得一个对象实例。设置为虚援用的惟一用途可能就是当这个对象被回收的时候能够收到一个零碎告诉。
垃圾收集算法
下面剖析了如何确定一个对象属于可回收对象的两种算法,那么当一个对象被确定为垃圾之后,就须要对其进行回收,回收也有不同的算法,上面就来看一下罕用的垃圾收集算法
标记-革除(Mark-Sweep)算法
标记-革除算法次要分为两步,标记(Mark)和革除(Sweep)。
比如说有上面一块内存区域(红色-未应用,灰色-无援用,蓝色-有援用):
而后标记-革除算法会进行如下两个步骤:
- 1、将堆内存扫描一遍,而后会把灰色的区域(无援用对象,可悲回收)对象标记一下。
- 2、持续扫描,扫描的同时将被标记的对象进行对立回收。
标记革除之后失去如下图所示:
能够很显著看到,回收之后内存空间是不间断的,产生了大量的内存空间碎片。过多内存碎片最间接的就是能够导致当前在程序运行过程中须要调配较大对象时,无奈找到足够的间断内存而不得不提前触发另一次垃圾收集动作。
标记-革除算法的毛病
1、标记和革除两个过程都比拟耗时,效率不高
2、会产生大量不间断的内存碎片。
为了解决这两个问题,所以就有了复制算法。
复制(Copying)算法
复制算法的思维就是把内存区域一分为二,两块内存放弃一样的大小,每次只应用其中的一块,当其中一块内存应用完了之后,将依然存活的对象复制到另一块内存区域,而后把已应用的一半内存全副一次性清理掉。
如下图(绿色示意临时不放对象的一半空间):
回收之后:
复制算法的毛病
复制算法的毛病就是就义了一半的内存空间,有点过于节约。
复制算法在Java虚拟机的落地模式
Java堆内存中做了好几次划分,最初是将Survivor辨别成了2个区域S0和S1来进行复制算法,这种做法就是为了补救原始复制算法间接将一半的空间作为闲暇空间形式的补救。
IBM公司的钻研表明,Young区(新生代)中98%的对象都是“朝生夕死”的,生命周期极短,所以说在一次GC之后能存活下来的对象很少,齐全没必要划分一半的区间来进行复制算法。Hot Spot虚拟机中Eden区和Survivor区域的比例为:Eden:S0:S1=8:1:1,也就是说其实只有10%的空间被节约掉,齐全是能够承受的。
标记-整顿(Mark-Compact)算法
咱们想一下,如果Young区(新生代)的对象在一次GC之后,根本所有对象都存活下来了,那就须要复制大量的对象,效率也会变低。而堆中的old区(老年代)的特点就是对象生命周期极为倔强,因为默认要进行第16次垃圾回收的时候还能存活下来的对象才会放到老年代,所以对老年代中对象的回收个别不会抉择标记-复制算法。
标记-整顿算法就是为了老年代而设计的一种算法,标记-整顿算法和标记革除算法的区别就是在最初一步,标记-整顿算法不会对对象进行清理,而是进行挪动,将存活的对象全副向一端挪动,而后清理掉端边界以外的对象。如下图所示:
回收前:
回收后:
分代收集算法(Generational Collection)
目前支流的商业虚拟机都是采纳的分代收集算法,这种算法实质上就是下面介绍的算法的结合体。新生代采纳标记-革除算法,老年代采纳标记-革除或者标记-整顿算法。
垃圾收集器
下面介绍了确定对象的算法以及回收对象的算法,而后具体要怎么落地却并没有一个规定,而垃圾收集器就是实现了对算法的落地,而因为落地模式不同,天然也产生了很多不同的收集器。上面是一张收集器的汇总图:
下面一半示意新生代收集器,上面一半示意老年代收集器,横跨两头的示意都能够用。
依据这个图形有了整体认知之后,咱们再来一个个看看这些垃圾收集器的工作原理吧。
Serial和Serial Old收集器
Serial收集器是根本、倒退历史悠久的收集器,在JDK1.3.1之前是虚拟机新生代收集的唯 一抉择。
Serial收集器是一种单线程收集器,而且是在进行垃圾收集的时候须要暂停所有其余线程,也就是说触发了GC的时候,用户线程是暂停的,如果GC工夫过长,用户是能够显著感知到卡顿的。
Serial Old是Serial的一个老年代版本,也是一种单线程收集器。
能够用上面一个图形来示意一下Serial和Serial Old收集器的工作原理:
长处:简略高效,领有很高的单线程收集效率
毛病:收集过程须要暂停所有线程
算法:Serial采纳复制算法,Serial Old采纳标记-整顿算法
适用范围:Serial用于新生代,Serial Old用于老年代
利用:Client模式下的默认的收集器
ParNew收集器
ParNew收集器是Serial收集器的多线程版本,实现了并行执行,其余工作原理都和Serial统一。能够应用参数:-XX:+UseParNewGC来指定应用。
留神:这里的并行指的是多个GC线程并行,然而其余线程还是暂停,而并发指的是用户线程和GC线程同时执行。
ParNew收集器默认开启和CPU个数雷同的线程数来进行回收,能够应用参数:-XX:ParallelGCThreads来限度线程数
ParNew收集器工作原理如下图:
长处:在多CPU时,比Serial效率高。
毛病:收集过程暂停所有应用程序线程,单CPU时比Serial效率差
算法:复制算法
适用范围:新生代
利用:运行在Server模式下的虚拟机中首选的新生代收集器
Parallel Scavenge收集器
Parallel Scavenge收集器是一个新生代收集器,它也是应用复制算法的收集器,和ParNew一样也是一个并行的多线程收集器,Parallel Scanvenge收集器相比拟于ParNew收集器,更关注与晋升零碎的吞吐量。
吞吐量指的是CPU用于运行用户代码的而工夫于CPU总耗费工夫的比值。
即:吞吐量=运行用户代码工夫/(运行用户代码工夫+GC工夫)
Parallel Scavenge收集器提供了两个参数用于准确管制吞吐量:
`-XX:MaxGCPauseMillis//GC最大进展毫秒数,必须大于0-XX:GCTimeRatio//设置吞吐量大小,大于0小于100,默认值为99` * 1* 2
咱们思考一个问题,如果咱们通过参数把容许最大进展毫秒数设置的绝对较小会怎么样?是不是GC速度就会变快了?
答案是否定的。如果设置的工夫过短,Parallel Scavenge收集器会就义吞吐量和新生代空间来替换。
比方新生代400Mb须要GC工夫为100ms,而后手动设置为50ms,那么就会把新生代调小为200Mb,这样必定工夫就降下来了,然而这种操作可能会升高吞吐量,如果说原先是10s触发一次GC,每次100ms,批改工夫后编程5s触发一次GC,每次70ms,那么10s触发两次GC工夫就变成了140ms,吞吐量反而升高。
如果不晓得如何设置,那么还能够通过参数:-XX:+UseAdaptiveSizePolicy开启自适应策略(GC Ergonomics),这样咱们就不须要手动设置吞吐量和GC进展工夫了,虚构机会依据运行状况手机监控信息来动静调整。
Paralled Old收集器
Paralled Old收集器是Parallel Scavenge收集器的老年代版本,然而这个收集器是jdk1.6之后才呈现的,所以导致了在Paralled Old收集器呈现之前Parallel Scavenge收集器始终找不到适合的“搭档”。因为Parallel Scavenge收集器没方法和CMS收集器配合应用(前面会介绍起因),所以在Paralled Old收集器呈现之前,如果新生代抉择了Parallel Scavenge收集器,那么老年代就只能抉择Serial Old收集器,而Serial Old收集器是单线程的,所以单单只是新生代替换成了多线程的吞吐量收集器Parallel Scavenge,在性能上并不一定有多少晋升。
在重视吞吐量的业务零碎中,能够思考Parallel Scavenge+Paralled Old收集器配合应用,联合应用后的工作原理如下图所示:
PS:在jdk1.8中,默认收集器就是Parallel Scavenge+Parallel Old组合
CMS(Concurrent Mark Sweep)收集器
这是一种以实现GC时最短进展工夫为指标的收集器,也是一款真正实现了并发回收的收集器。当然,尽管是并发的,然而依然须要Stop The World,只是尽可能将这个工夫缩到最短。
对于任何暂停工夫要求较低的应用程序,都应该思考应用此收集器。CMS收集器能够通过参数:-XX:+UseConcMarkSweepGC启用。
CMS收集器是基于算法标记-革除来实现的,整个过程分为4步:
- 1、初始标记(inital mark)
须要Stop The World。标记GC Roots对象,因为GC Root对象并不会很多,所以这个过程十分快。 - 2、并发标记(concurrent mark)
这个阶段能够和用户线程同时进行,也能够分为三步:
(1)并发标记(CMS-concurrent-mark):次要是进行GC Roots Tracing。就是说依据第1步中找到的GC Root对象,开始搜寻,这个过程相比阶段1是比较慢的。
(2)预清理(CMS-concurrent-preclean),这个阶段是为了解决并发标记之后产生了变动的对象
(3)可被终止的预清理(CMS-concurrent-abortable-preclean),这个预清理差不多,然而是能够被终止的,次要是分了尽可能分担上面第3步的工作,这个阶段会有一个abort触发条件,该阶段存在的目标是心愿能产生一次Young GC,这样就能够缩小Young区对象的数量,升高从新标记的工作量,因为从新标记会扫描整个堆内空间。能够通过参数-XX:+CMSScavengeBeforeRemark参数管制在从新标记前产生一次Young GC,默认为false。这个阶段产生的最大工夫由-XX:CMSMaxAbortablePrecleanTime管制,默认5s - 3、从新标记(remark)
须要Stop The World,这个阶段是为了修改在阶段2标记之后产生了变动的对象 - 4、并发革除(concurrent sweep)
和用户线程同时进行,开始正式革除垃圾,在此阶段也会产生垃圾,产生垃圾后无奈革除,只能留待下一次GC。
CMS收集过程如下图所示:
CMS优缺点
- 长处:并发收集、低进展。
其实最次要的是CMS把收集过程中步骤拆分了,而最耗时的操作都是并发执行,天然就会低进展了。 - 毛病:产生大量空间碎片、并发阶段会升高吞吐量。
CMS采纳的是标记-革除算法,所以会产生大量的空间碎片。在阶段2和阶段4并发执行的时候,会占用CPU资源,就会导致应用程序变慢,升高了吞吐量。
Floating Garbage(浮动垃圾)
下面的步骤中,步骤2是并发标记,所以在标记过程中,可能会有新的垃圾产生而没有被标记到。比如说对象A,刚扫描的时候是无效对象,而后持续扫描的时候,对象A又变成不可用了,而后还有并发革除的阶段,也可能会有新的垃圾产生,这种就称之为浮动垃圾(Floating Garbage)。CMS并不能收集浮动垃圾,只能等到下一次GC时再回收。
Concurrent Mode Failure(并发模式失败)
CMS收集器不能和其余收集器一样等到空间满了才开始触发GC,因为CMS收集的时候是并发的,并发的过程必定会继续产生对象,如果因为在垃圾收集期间内存不足而导致了GC失败,就称之为Concurrent Mode Failure。呈现这种状况之后,Java虚拟机就会启动准备计划,启用Serial Old收集器替换CMS收集器,这时候整个GC过程都会Stop The World。
CMS收集器的触发阈值能够通过参数:-XX:CMSInitiatingOccupancyFraction=来进行设置,N为(0,100)之间,在jdk1.6中默认是92,即老年代空间使用率达到92%就会触发CMS收集器开始进行垃圾回收。
G1(Garbage-First)收集器
G1也是以实现GC时最短进展工夫为指标并发回收的收集器,它尝试以高概率满足垃圾收集(GC)暂停工夫指标,同时实现高吞吐量。
在G1之前的其余收集器都是属于分代收集器,也就是说一个收集器要不然用于新生代,要不然就是用于老年代,而G1中,将堆的整个内存布局做了很大的批改,在G1中,将整个Java堆划分为多个大小相等的独立区域(Region),尽管在逻辑上还保留了新生代和老年代的概念,然而物理上曾经没有隔离了。
G1收集器中堆内布局如下图所示:
上图中堆被划分为一组大小雷同的Region,每个Region都是间断的虚拟内存范畴。
G1能够晓得哪个Region区域内大部分都是空的,这样就能够在每次容许的收集工夫内去优先回收价值最大的Region区域(依据回收所取得的空间大小以及回收所须要的工夫综合思考),所以这也就是G1为什么叫做Garbage-First的起因。
PS:G1是JDK1.9的默认垃圾收集器
G1特点
通过下面的简略介绍,能够得出G1次要有以下特点:
- 1、实现了并行与并发,尽可能的缩短了Stop The World工夫。
- 2、分代收集:逻辑上仍然保留了分代概念
- 3、空间整合:整体来看是基于“标记-整顿”算法来实现的(如果冲Region来看,是基于“复制”算法),所以不会产生大量内存空间碎片。
- 4、反对可预测的进展工夫:能够通过参数来设置每次GC最大工夫
- 5、非实时收集:因为能够人为设置进展工夫,所以在指定工夫范畴内会进行优先选择收集,而不会收集所有被标记好的垃圾。
G1工作流程
G1收集器在工作流程上和CMS比拟类似,只是在最初的步骤有所区别,次要通过了如下4个步骤:
- 1、初始标记(Initial Marking):须要Stop The World。标记一下GC Roots可能关联的对象,并且批改TAMS(Next Top at Mark Start)的值,使得下一阶段并发运行时,能在正确可用的Region中创建对象。
- 2、并发标记(Concurrent Marking):和CMS一样,次要是进行GC Roots Tracing,找出存活对象进行标记。
- 3、最终标记(Final Marking):须要Stop The World。和CMS一样,这个阶段次要是为了修改在并发标记期间因用户程序持续运行而导致产生变动的对象。
- 4、筛选回收(Live Data Counting and Evacuation):对各个Region的回收价值和老本进行排序,依据 用户所冀望的GC进展工夫制订回收打算进行回收。
工作流程图如下所示:
G1利用场景
G1的第一个重点是为运行须要大堆且GC提早无限的应用程序的用户提供解决方案。这意味着堆大小大概为6GB或更大,并且稳固且可预测的暂停工夫低于0.5秒。
如果咱们的应用程序具备以下一个或多个个性,那么能够思考切换到G1收集器。
- 1、超过50%的Java堆被实时数据占用。
- 2、对象分配率或晋升率差别很大。
- 3、以后应用程序GC进展工夫超过0.5到1秒,而又想缩短进展工夫的利用。
其余收集器
- ZGC收集器:是Java11中提供的一种垃圾收集器。
- Shenandoah:OpenJDK中蕴含的收集器,最开始是由RedHat公司开发,起初奉献给了OpenJDK。
- Epsilon(A No-Op Garbage Collector):一款管制内存调配,然而不执行任何垃圾回收工作的收集器。一旦java的堆被耗尽,jvm就间接敞开。
如何抉择垃圾回收器
垃圾收集器次要能够分为如下三大类:
- 串行收集器:Serial和Serial Old
只能有一个垃圾回收线程执行,用户线程暂停。 实用于内存比拟小的嵌入式设施 。 - 并行收集器[吞吐量优先]:Parallel Scanvenge和Parallel Old
多条垃圾收集线程并行工作,但此时用户线程依然处于期待状态。 实用于科学计算、后盾解决等若交互场景 。 - 并发收集器[进展工夫优先]:CMS和G1。
用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的时候不会进展用户线程的运行。 实用于对工夫有要求的场景,比方Web利用。
总结
本文次要介绍了确定有效对象的两种算法,并且联合垃圾收集算法介绍了不同类型的落地模式而产生的不同垃圾收集器,本文将对比拟偏差于实践,下一篇开始,JVM系列文章将会联合JVM系列前5篇文章来进一步结合实际场景以及相干监控工具的应用来进行理论场景剖析。