文章目录
GC是什么
GC英文全称为Garbage Collection,即垃圾回收。
Java中的GC就是对内存的GC。
Java的内存管理实际上就是对象的管理,其中包括对象的分配和释放。
我们知道对象的分配,程序员可以通过new关键字,Class的new-Instance方法等来显示的分配;而对象的释放,程序员不能实时的进行释放,这就需要GC来完成
为什么要GC
- 因为内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃。因此,Java提供了GC功能。该功能可以自动监测对象是否超过作用域从而判断是否需要回收内存
- GC有效的防止了内存泄漏,可以有效的使用可以使用的内存
- 对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的".当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间
- 注:判断对象"不可达",一般有两种方法,即引用计数法和可达性分析法
GC算法总体概述
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代
因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(major GC or Full GC)
-
普通GC(minor GC)
:只针对新生代区域的GC -
全局GC(major GC or Full GC)
:针对年老代的GC,偶尔伴随对新生代的GC以及对永久代的GC
引用计数法
复制算法(Copying)
年轻代中使用的是Minor GC,这种GC算法采用的是复制算法(Copying)。
- 原理
Minor GC会把Eden中的所有活的对象都移到Survivor区域中,如果Survivor区中放不下,那么剩下的活的对象就被移到Old generation中,也即一旦收集后,Eden是就变成空的了
当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,再经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,通过-XX:MaxTenuringThreshold 来设定参数),这些对象就会成为老年代
算法分析
年轻代中的GC,主要是复制算法(Copying)
HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1:1,一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。因为年轻代中的对象基本都是朝生夕死的(90%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中
因为Eden区对象一般存活率较低,一般的,使用两块10%的内存作为空闲和活动区间,而另外80%的内存,则是用来给新建对象分配内存的。一旦发生GC,将10%的from活动区间与另外80%中存活的eden对象转移到10%的to空闲区间,接下来,将之前90%的内存全部释放,以此类推
- 劣势
复制算法它的缺点也是相当明显的
- 它浪费了一半的内存,这太要命了
- 如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变得不可忽视。 所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费
标记清除(Mark-Sweep)
老年代一般是由标记清除或者是标记清除与标记整理的混合实现
- 原理
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除
标记:从引用根节点开始标记所有被引用的对象。标记的过程其实就是遍历所有的GC Roots,然后将所有GC Roots可达的对象 标记为存活的对象
清除:遍历整个堆,把未标记的对象清除
缺点:此算法需要暂停整个应用,会产生内存碎片
用通俗的话解释一下标记/清除算法,就是当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将依旧存活的对象标记一遍,最终再将堆中所有没被标记的对象全部清除掉,接下来便让程序恢复运行
- 劣势
-
首先,它的缺点就是效率比较低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲
-
其次,主要的缺点则是这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象都是随即的出现在内存的各个角落的,现在把它们清除之后,内存的布局自然会乱七八糟。而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找
标记清除(Mark-Sweep)
老年代一般是由标记清除或者是标记清除与标记整理的混合实现
- 原理
在整理压缩阶段,不再对标记的对象做回收,而是通过所有存活对象都向一端移动,然后直接清除边界以外的内存
可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销
标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价
- 劣势
标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法
标记清除压缩(Mark-Sweep-Compact)
标记清除、标记压缩的结合使用
- 原理
算法总结
内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)
内存整齐度:复制算法=标记整理算法>标记清除算法
内存利用率:标记整理算法=标记清除算法>复制算法
可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程
没有最好的算法,只有最合适的算法
分代收集算法。
-
年轻代(Young Gen)
-
年轻代特点是区域相对老年代较小,对像存活率低。
-
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对像大小有关,因而很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
-
-
老年代(Tenure Gen)
-
老年代的特点是区域较大,对像存活率高
-
这种情况,存在大量存活率高的对像,复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现
-
Mark阶段的开销与存活对像的数量成正比,这点上说来,对于老年代,标记清除或者标记整理有一些不符,但可以通过多核/线程利用,对并发、并行的形式提标记效率
-
Sweep阶段的开销与所管理区域的大小形正相关,但Sweep“就地处决”的特点,回收的过程没有对像的移动。使其相对其它有对像移动步骤的回收算法,仍然是效率最好的。但是需要解决内存碎片问题
-
Compact阶段的开销与存活对像的数据成开比,如上一条所描述,对于大量对像的移动是很大开销的,做为老年代的第一选择并不合适
-
基于上面的考虑,老年代一般是由标记清除或者是标记清除与标记整理的混合实现。以hotspot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对像的回收效率很高,而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器做为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理
-
垃圾回收器
垃圾回收器分类
- 按线程数分,可以分为串行垃圾回收器和并行垃圾回收器
串行垃圾回收
: 指的是在同一个时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束
并行垃圾回收
: 是和串行垃圾回收相反的,并行运算可以拥有多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用“Stop The World”的机制。在回收的时候,需要暂停所有的线程
- 按照工作模式分,可以分为并发式垃圾回收器和独占式回收器
并发式垃圾回收器
: 与应用程序交替工作,以尽可能减少应用程序的停顿时间。
独占式垃圾回收器
: 就是Stop - The - World。一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。
- 按照工作的内存区间,又可分为年轻代垃圾回收器和老年代垃圾回收器
年轻代垃圾回收器
: 效率高,采用复制算法,但对内存的占用控制不精确,容易造成内存溢出。
老年代垃圾回收器
: 效率低,在执行的时候,会STW,但一般启动次数少。
GC的性能指标
-
吞吐量:运行用户代码的时间栈总运行时间的比例。
(总运行时间:程序的运行时间+内存回收时间) -
垃圾收集开销:垃圾收集所用时间与总运行时间的比例。
-
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
-
收集频率:相对于应用程序的执行,收集操作发生的频率。
-
内存占用:Java堆区所占的内存大小。
-
快速:一个对象从诞生到被回收所经历的时间。
HotSpot虚拟机中的垃圾收集器
因为HotSpot的虚拟机是现在主流的商用虚拟机,也是非常经典的一款虚拟机所以在这我就自己学了一下这款虚拟机的垃圾收集。
这些经典的收集器尽管已经算不上最先进的技术,但它们曾在实践中千锤百炼,足够成熟,基本上可认为是现在能够在商用生产环境中放心使用的全部垃圾收集器了。
各款经典收集器之间的关系如图。
如果两个收集器中间存在连线,就说明他们可以搭配使用。图中收集器所处的区域,则表示它是属于新生代收集器或者老年代收集器。
串行回收器:Serial,Serial old;
并行回收器:ParNew,Parallel scavenge,Parallel old;
并发回收器:CMS、G1;
新生代收集器:Serial,ParNew,Parallel scavenge;
老年代收集器:Serial old,Parallel old.cMS;
整堆收集器:G1;
虽然我们会对各个收集器进行比较,但并非为了挑选一个最好的收集器出来,虽然垃圾收集器的技术在不断进步,但是直到现在还没有最好的收集器出来,更加不存在“万能”的收集器,所以我们选择的只是对具体应用最合适的收集器。
如果有一种放之四海,任何场景都使用的完美收集器存在,那么HotSpot虚拟机就完全没有必要实现那么多种不同的收集器了
在说收集器的时候,会接触到“并发”和“并行”
并行: (Parallel)描述的是多条垃圾收集器线程之间的关系。说明同一时间有多条这样的线程协同工作,通常默认此时用户线程是处于等待状态。
并发: (Concurrent): 并发描述的是垃圾收集器线程与用户线程之间的关系。
说明在同一时间垃圾收集器线程与用户线程都在运行。由于用户线程并非被冻结,所以程序仍然是能够响应服务请求的,但由于垃圾收集器线程占有了一部分的系统资源,此时应用程序的处理的吞吐量将会受到一定的影响。
Serial 垃圾收集器(单线程)
Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择。它是一个“单线程”的收集器,但是它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集的时候,必须停止其他所有的工作线程,直到结束。
运行示意图:
特点:单线程,简单高效,采用的是复制算法,对于单CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集器自然可以获得最高的单线程收集效率
。收集器进行垃圾回收时,必须暂停其他所有的线程(Stop The World),直到它结束。
虽然它是最早产生的,但是它与其他收集器的单线程相比是最简单而高效的。所以在用户桌面的应用场景以及近几年来流行的部分微服务器应用中是一个很好的选择
应用场景:适用Client模式下的虚拟机。
Serial Old 垃圾收集器(单线程)
Serial Old是Serial收集器的老年代的版本,单线程,使用标记 - 整理算法。
应用场景:主要也是使用在 Client 模式下的虚拟机中。也可在 Server 模式下使用。
ParNew 垃圾收集器
ParNew收集器实质上是Serial收集器的多线程版本,除了同时使用多线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数(例如: -XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、STW、对象分配规则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码
特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可用使用 -XX:ParallelGCThreads参数来限制垃圾收集的线程。和Serial收集器一样存在Stop The World 问题。
应用场景:ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为它是除了Serial收集器外,唯一一个能与CMS收集器配合工作的。
Parallel Scavenge 垃圾收集器
Parallel Scavenge 收集器也是一款新生代的收集器,它同样是基于 标记 - 复制算法实现的收集器,也是能够并行收集的多线程收集器
特点: 它的关注点与其他收集器是不同的,CMS等收集器的关注点是尽肯地缩短垃圾收集时用户线程的停顿使劲,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)
。
所谓吞吐量就是处理器用于远程用户代码的时间与处理器总消耗时间的比值,即:
高吞吐量则可以最高效率地利用处理器资源,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的分许任务。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的 -XX:GCTimeRatio参数
。
-XX:MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定的值。(但是设置太小的话,停顿时间的确在下降,但吞吐量也降下来了)
-XX:GCTimeRatio参数的值为一个正整数,表示用户期望虚拟机消耗在GC上的时间不超过程序运行时间的 1/(1+N)。默认值为99.含义是尽可能保证应用程序执行的时间为收集器执行时间的99倍,也即收集器的时间消耗不超过总运行时间的1%。
由于与吞吐量关系密切,Parallel Scavenge收集器也经常被称作“吞吐量优先收集器”
Parallel Old垃圾收集器
是Parallel Scavenge收集器的老年版本。
特点:支持多线程并发,采用标记-整理算法。
这个收集器是在JDK 6时才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直都处于一个相当尴尬的场面,原因是如果新生代选择了Parallel Acavenge收集器,老年代除了Serial Old收集器以外别无选择,其他表现良好的老年代收集器,比如CMS是无法配合它工作的。由于老年代Serial Old的“拖累”,使得Sarallel Scavenge收集器是没有办法在整体上获得吞吐量最大化的效果。
直到Parallel Old 收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合。
应用场景:在注重吞吐量或者处理器资源较为稀缺的场合。都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合
CMS垃圾收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求。
它的运作过程相对于前面几种收集器来说要更为复杂一些,整个过程分为四个步骤:
-
初始标记(CMS initial mark)
- 这个阶段仅仅只是标记一下GC Roots能直接关联到的对象,速度快。
-
并发标记(CMS concurrent mark)
- 这个阶段就是从GC Roots的直接关系对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
-
重新标记(CMS remark)
- 这个阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发阶段的时间短。
-
并发清除(CMS concurrent sweep)
- 这个阶段,就是清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
由于在整个过程中耗时最长的并发标记和并行清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以在总体上来看,CMS收集器的内存回收过程是与线程一起并行执行的。
优点:并发收集、低停顿。
缺点:
-
对处理器资源非常敏感。
- 在并发阶段虽然不会导致用户线程停顿,但却会因为占用一部分线程而导致应用程序变慢,降低总吞吐量。
-
无法处理“浮动垃圾”。
- 因为无法处理“浮动垃圾”,就有可能出现“Concurrent Mode Failure”失败而导致另一次完全“Stop The Stop”的Full GC的产生。
在CMS的并发标记和并发清理阶段,用户线程还是继续运行的,程序在运行自然就还有伴随着有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程以后的,CMS无法在当次收集中处理掉它们,就只好期待下一次垃圾收集时再清理掉。这就是“浮动垃圾”。
- 因为无法处理“浮动垃圾”,就有可能出现“Concurrent Mode Failure”失败而导致另一次完全“Stop The Stop”的Full GC的产生。
-
会产生内存碎片。
- 因为CMS是基于“标记 - 清除”算法来实现的收集器。从垃圾回收中就了解到垃圾回收中的“标记 - 清除”算法就会产生内存碎片。内存碎片过多的话,就会导致给大对象分配带来了很大的麻烦。就会不得不提前触发Full GC。
三色标记算法
为了提高JVM的垃圾回收性能,从CMS垃圾收集器开始,引入了并发标记的感念。引入并发标记的过程就会带来一个问题,在业务执行的过程中,会对现有的引用关系链出现改变。
三色标记中的三色:
黑色
:表示该对象已经被标记过了,且该对象下的属性也全部都被标记过了,例如:GCRoots对象
灰色
:对象已经被垃圾收集器扫描过了,但是对象中还存在没有扫描的引用(GC需要从此对象中去寻找垃圾)
白色
:表示该对象没有被垃圾收集器访问过,即表示不可达。
三色标记的过程
1.初始时,全部对象都是白色的
2. GC Roots直接引用的对象变为灰色
3. 从灰色集合中获取元素;将本对象直接引用的对象标记为灰色;然后将当前的对象标记为黑色。
4. 重复步骤3,直到灰色的对象集合全部变为空
5. 结束后,仍然被标记为白色的对象就是不可达对象,就视为垃圾对象。
当Stop The Word时,对象间的引用是不会发生变化的,因为此时用户线程是中断的,可以轻松完成标记。但是在并发标记的时候,标记期间用户线程还在跑,对象间的引用可能发生变化,多标和漏标的情况就可能会发生。
多标(又叫浮动垃圾)
看图,A为GC Roots的对象,然后向下进行遍历。把A标记为黑色,将B和C标记为灰色。
此时A取消了对B的引用。 A->B = null
这个时候A->B之间的引用没有了,B应该为白色,但是因为之前在对A的时候,已经把D标为灰色了了,所以B对象任然会被当做存活对象遍历下去。
最终结果:这部分对象仍然会被标记为存活对象,本轮GC是不会回收他们的内存。这部分因为并发而造成的本应该回收但是没有回收的对象就称为“浮动垃圾”,我们稍微一想,浮动垃圾是不会影响应用程序的重要性,只需要等到下一轮GC到来就会被回收了
。
另外一点: 针对并发标记开始后产生的新对象,通常做法是直接标记为黑色,本轮不进行清除。即使这些对象会变成垃圾对象,这也算浮动垃圾的一部分。
漏标(错杀)
假设GC线程遍历到了B,发生了以下操作:
断开B和D之间的引用关系,新增了A和D之间的引用关系了。
此时B到D之间的引用消失,A生成了新的对D的引用。但是此时GC线程已经走过了A,因为B已经没有了对D的引用,所以不会遍历到D,D也就不会标志为灰色,同时A已经标志为黑色了,不会再被遍历,那么也就导致D一直是白色的,最后被当成垃圾处理,这显然与事实不符。这是绝对不被允许的。
这个问题是比较致命的,如果错杀了,就会出现运行结果不符合预期的情况。这个是绝对不能发生的。
解决漏标(错杀)问题
先分析一下发生漏标的具体原因有二:
- 灰色指向白色的引用全部断开(在这就行一个原始快照)
- 黑色指向白色的引用被建立(增量更新)
那么仔细想一想,只要打破上面任何一个问题,就可以解决漏标(错杀)的问题。
写屏障:
-
写屏障 + SATB
- 当对象B的引用发生变化时,利用写屏障,将B原来的引用对象记录下来,这样可以尝试保留开始时的对象图,保证标记依然按照原本的路线走
-
写屏障 + 增量更新
- 当对象A的引用发生变化时,利用写屏障,将A新的引用对象D记录下来
即当有新的引用插入进来时,记录下新的引用 - 这种思路不要求保留原始对象图,而是针对新的引用记录下来等待遍历,即增量更新
- 当对象A的引用发生变化时,利用写屏障,将A新的引用对象D记录下来
读屏障:
读屏障针对第一步,当读取引用对象的时候,一律记录下来,显然这种方法非常保守,但是安全。
将记录下的再引用遍历就是了。
在现代的垃圾回收器当中可达性分析算法的垃圾回收器几乎都借鉴了三色标记法的思想
在Java HotSpot VM中
CMS采用的是:写屏障 + 增量更新
G1采用的是:写屏障 + SATB
G1(Garbage First)收集器
Garbage First(简称G1)收集器是垃圾收集器计数发展历史上的里程碑式的成果。G1垃圾回收器是在Java7 update 4 之后引入的一个新的垃圾回收器,是当今收集器计数发展的最前沿成果。
它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。是一款面向服务端应用的垃圾收集器
。
因为在后世发展中,所对应的业务越来越庞大、复杂、用户越来越多,没有GC就不能保证应用程序正常进行,而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。
与此同时,为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时为了兼顾良好的吞吐量。
官方给G1设定的目标就是在延迟可控的情况下获得尽可能高的吞吐量,所以才会担当起“全功能收集器”的重任与期望
。
虽然G1任然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单词回收的最小单位,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
在Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为大小超过一个Region容量的一半的对象即可以判定为大对象。而对于那些超过整个Region容量的超级大对象,就会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。
这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。
因为 G1 是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)、(物理上不连续的)。使用不同的 Region 来表示 Eden、幸存者0区,幸存者1区,老年代等。
G1 GC有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个 Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给G1还有一个名字:垃圾优先(Garbage First)。
G1(Garbage-First)是一款面向服务端应用的垃圾收集器,主要针对配备现在的多核 CPU 及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征
。
G1收集器可以 “ 建立可预测的停顿时间模型 ”,它维护了一个列表用于记录每个 Region 回收的价值大小(回收后获得的空间大小以及回收所需时间的经验值),这样可以保证G1收集器在有限的时间内可以获得最大的回收效率。
如果我们不去计算用户线程运行过程中的动作(比如使用写屏障维护记忆集的操作),G1收集器的运行过程大致可划分为以及4个步骤
-
初始标记(Initial Marking)
仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一个阶段用户线程并发运行的时候,能正确地在可用的Region中分配新对象。
这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成时,所以G1收集器在这个阶段实际并没有额外的停顿。 -
并发标记(Concurrent Marking)
从GC Root开始对堆中的对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。
当对象图扫描完成之后,还有重新处理SATB记录下的并发时有引用变动的对象。 -
最终标记(Final Marking)
对用户线程做另一个短暂的暂停,用于处理并发阶段结束后人遗留下的最后那少量的SATE记录。 -
筛选回收(Live Data Counting and Evacuation)
负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收那一部分Region的存活对象复制到空的Region中,再清理掉整个旧的Region的全部空间。这里的操作涉及存回对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的
从上述阶段完全可以看出,G1收集器除了并发标记以外,其余阶段也要完全暂停用户线程的,换言之,它并非纯粹地追求低延迟,官方给它设立的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才坦当器“全功能收集器”的重任和期望。
适用场景:
要求尽可能可控GC停顿时间;
内存占用较大的应用。可以用 -XX:+UseG1GC 使用 G1 收集器
jdk9 默认使用 G1 收集器。