垃圾回收的基本步骤,回收的步骤有2步:java
引用计数法就是若是一个对象没有被任何引用指向,则可视之为垃圾。这种方法的缺点就是不能检测到环的存在。算法
根搜索算法的基本思路就是经过一系列名为”GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证实此对象是不可用的。数组
它将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完以后,就将还存活的对象复制到另一块上面,而后在把已使用过的内存空间一次理掉。它的优势是实现简单,效率高,不会存在内存碎片。缺点就是须要2倍的内存来管理。并发
标记清除算法分为“标记”和“清除”两个阶段:首先标记出须要回收的对象,标记完成以后统一清除对象。它的优势是效率高,缺点是容易产生内存碎片。性能
标记操做和“标记-清理”算法一致,后续操做不仅是直接清理对象,而是在清理无用对象完成后让全部 存活的对象都向一端移动,并更新引用其对象的指针。由于要移动对象,因此它的效率要比“标记-清理”效率低,可是不会产生内存碎片。spa
开发人员仅仅须要声明如下参数便可:线程
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200
其中-XX:+UseG1GC为开启G1垃圾收集器,-Xmx32g 设计堆内存的最大内存为32G,-XX:MaxGCPauseMillis=200设置GC的最大暂停时间为200ms。若是咱们须要调优,在内存大小必定的状况下,咱们只须要修改最大暂停时间便可。设计
G1将新生代,老年代的物理空间划分取消了。3d
这样咱们不再用单独的空间对每一个代进行设置了,不用担忧每一个代内存是否足够。 指针
取而代之的是,G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停全部应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分红不少区域,G1收集器经过将对象从一个区域复制到另一个区域,完成了清理工做。这就意味着,在正常的处理过程当中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。
在G1中,还有一种特殊的区域,叫Humongous区域。 若是一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,可是若是它是一个短时间存在的巨型对象,就会对垃圾收集器形成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。若是一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
PS:在java 8中,持久代也移动到了普通的堆内存空间中,改成元空间。
对象分配策略
提及大对象的分配,咱们不得不谈谈对象的分配策略。它分为3个阶段:
TLAB为线程本地分配缓冲区,它的目的为了使对象尽量快的分配出来。若是对象在一个共享的空间中分配,咱们须要采用一些同步机制来管理这些空间内的空闲空间指针。在Eden空间中,每个线程都有一个固定的分区用于分配对象,即一个TLAB。分配对象时,线程之间再也不须要进行任何的同步。
对TLAB空间中没法分配的对象,JVM会尝试在Eden空间中进行分配。若是Eden空间没法容纳该对象,就只能在老年代中进行分配空间。
最后,G1提供了两种GC模式,Young GC和Mixed GC,两种都是Stop The World(STW)的。下面咱们将分别介绍一下这2种模式。
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 阶段:
Mix GC不只进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。 它的GC步骤分2步:
在G1 GC中,它主要是为Mixed GC提供标记服务的,并非一次GC过程的一个必须环节。global concurrent marking的执行过程分为五个步骤:
提到并发标记,咱们不得不了解并发标记的三色标记算法。它是描述追踪式回收器的一种有用的方法,利用它能够推演回收器的正确性。 首先,咱们将对象分红三种类型的。
当GC开始扫描对象时,按照以下图步骤进行对象的扫描:
根对象被置为黑色,子对象被置为灰色。
继续由灰色遍历,将已扫描了子对象的对象置为黑色。
遍历了全部可达的对象后,全部可达的对象都变成了黑色。不可达的对象即为白色,须要被清理。
这看起来很美好,可是若是在标记过程当中,应用程序也在运行,那么对象的指针就有可能改变。这样的话,咱们就会遇到一个问题:对象丢失问题
咱们看下面一种状况,当垃圾收集器扫描到下面状况时
这时候应用程序执行了如下操做:
A.c=C
B.c=null
这样,对象的状态图变成以下情形:
这时候垃圾收集器再标记扫描的时候就会下图成这样:
很显然,此时C是白色,被认为是垃圾须要清理掉,显然这是不合理的。那么咱们如何保证应用程序在运行的时候,GC标记的对象不丢失呢?有以下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,使巨型对象再也不是巨型对象。
因为篇幅有限,G1还有不少调优实践,在此就不一一列出了,你们在日常的实践中能够慢慢探索。最后,期待java 9能正式发布,默认使用G1为垃圾收集器的java性能会不会又提升呢?