天天看点

垃圾回收机制3 相关问题

尽管虚拟机内存的动态分配与内存回收技术很成熟,可万一出现了这样那样的内存溢出问题,那么将难以定位错误的原因所在。

为了高效的回收,jvm将堆分为三个区域:1.新生代(Young Generation)2.老年代(Old Generation);3.永久代(Permanent Generation)(jdk1.8之前)

1 判断对象是否存活算法

1.1 引用计数算法

这种算法判断很简单,简单来说就是给对象添加一个引用计数器,每当对象被引用一次就加1,引用失效时就减1。当为0的时候就判断对象不会再被引用。

优点:简单效率

实现简单效率高,被广泛使用与如python何游戏脚本语言上。

缺点:循环引用

难以解决用循环引的问题,就是假如两个对象互相引用已经不会再被其它其它引用,导致一直不会为0就无法进行回收。

1.2 可达性分析算法

通过一个称为“GC Roots”的对象为起始点,搜索所经过的路径称为引用链,当一个对象到GC Roots没有任何引用跟它连接则证明对象是不可用的,有效解决了循环引用的问题

1.2.1 GC Roots(根节点)

可作为GC Roots的对象有四种。

局部变量表:栈(栈桢中的局部变量表)中的引用的对象,就是平时所指的java对象,存放在堆中。

static:方法区中的类静态属性引用的对象,一般指被static修饰引用的对象,加载类的时候就加载到内存中

常量:方法区中的常量引用的对象

JNI:本地方法栈中JNI(native方法)引用的对象
           

1.2.2 枚举根节点算法

GC Roos主要是在一些全局引用【如常量或静态属性】、执行上下文【如栈帧中本地变量表】中。如何在这么多全局变量和本地变量表找到根节点将是个问题。
           
基本思路
垃圾收集时,收集线程会对栈上的内存进行扫描,看看哪些位置存储了 Reference 类型,如果发现引用就意味着它所引用的对象这一次不能被回收
           
存在的问题

栈上的本地变量表里面的非 Reference 类型的数据对我们而言毫无用处,但我们还是不得不对整个栈全部扫描一遍,这是对时间和资源的一种浪费,另外可达性分析的时候要保证期间不发生引用关系的变化,所有执行线程要停顿等待,称为“Stop The World”

OopMap
在某个时候把栈上代表引用的位置全部记录下来,这样到真正 gc 的时候就可以直接读取,而不用再一点一点的扫描了。

Ordinary Object Pointer Map(普通对象指针映射),类型的映射表,存放栈上本地变量到堆上对象的引用关系。

类加载完时,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来;JIT过程中,也会在特定位置记录下栈和寄存器中哪些位置是引用,存在OopMap

gc 发生时,程序首先运行到最近的一个安全点停下来,然后更新自己的 OopMap ,记下栈上哪些位置代表着引用

使用 OopMap 可以避免全栈扫描,加快枚举根节点的速度,但这并不是它的全部用意。它的另外一个更根本的作用是,可以帮助实现准确式 GC
           
Safe Poin
程序运行期间,引用的变化在不断发生,如果每一条指令都生成OopMap,那占用空间就太大了,所以有了安全点。

安全点的选定是以“是否具有让程序长时间执行的特征为标准”进行选定。长时间执行的的最明显特征是指令序列复用,防止大循环的时候一直不进入safepoint,而其他线程在等待它进入safepoint,如
           

1:循环的末尾; 2:方法临返回前/调用方法的call指令后; 3:可能抛异常的位置。

safepoint不能太少,否则GC等待的时间会很久,也不能太多,否则将增加运行GC的负担。
           
Safe Region
在一段代码片段中,引用关系不会发生变化,在该区域的任何地方发生GC都是安全的

用来处理没有分配CPU时间的程序,比如线程处于Sleep状态,这些线程没办法响应JVM的暂停要求,对于这种状况,单独设置了一个安全区域。

当线程执行到安全区域中的代码时,标识自己进入了安全区域

当线程准备离开安全区域的时候,检查垃圾收集是否完成,如果结束了,线程继续执行;如果没结束,就等到结束之后再离开安全区域。
           
中断方式

拟机有两种中断方式。

抢先式中断:由虚拟机发起,所有线程全部中断,不在安全点上的线程,恢复运行至安全点上。

主动式中断:由线程去轮询是否中断的标志位,发现标识,就自己将线程暂停挂起。
           

2 垃圾收集器

Java垃圾收集器分为年轻代收集器(Serial、ParNew、Parallel、Scavenge),老年代垃圾收集器(Serial Old、Parallel Old、CMS收集器),还有比较特殊的G1。

2.1 Serial

最基本、发展最久的收集器,在jdk3以前是gc新生代收集器的唯一选择。

搭配:Serial Old、CMS。

单线程、复制算法:最基本、发展最久的收集器,在jdk3以前是gc新生代收集器的唯一选择。

优点:适用于单核CPU,因为少了多线程切换的开销,相较于其他收集器能够更加专注于垃圾回收。

缺点:收集时要暂停其它线程,多核下显得有点浪费资源。

2.2 ParNew

可以认为是Serial的升级版,因为它支持多线程[GC线程],而且收集算法、Stop The World、回收策略和Serial一样,就是可以有多个GC线程并发运行。

搭配:CMS。

多线程并发、复制算法:它是HotSpot第一个真正意义实现并发的收集器。默认开启线程数和当前cpu数量相同。

优点:多核CPU下可以充分的利用CPU资源,运行在Server模式下新生代首选的收集器【新生代收集器里只有它和Serial可以配合CMS收集器一起使用】。

缺点:在单核下表现不会比Serial好,在线程收集过程中可能会出现频繁上下文切换,导致额外的开销。

-XX:ParallelGCThreads:如果cpu核数很多不想用那么多,可以通过这个配置来控制垃圾收集线程的数量。

2.3 Parallel Scavenge

多线程并发、复制算法:采用复制算法的收集器,和ParNew一样支持多线程,但是该收集器重点关心的是吞吐量【吞吐量 = 代码运行时间 / (代码运行时间 + 垃圾收集时间) 。

搭配:Serial Old 、Parallel Old。

2.4 Serial Old

单线程、标记-整理:和新生代的Serial一样为单线程,Serial的老年代版本,不过它采用"标记-整理算法",这个模式主要是给Client模式下的JVM使用。

搭配:Serial、Parallel Scavenge、CMS。

用途:jdk5前和Parallel Scavenge搭配使用,作为CMS收集器的后备。

2.5 Parallel Old

多线程并发、标记-整理:支持多线程,Parallel Scavenge的老年版本,jdk6开始出现, 采用"标记-整理算法"。

搭配: Parallel Scavenge。

2.6 CMS

CMS收集器(Concurrent Mark Sweep)是以一种获取最短回收停顿时间为目标的收集器。

2.6.1 多线程并行、标记-清除

正如其名,CMS采用的是"标记-清除"(Mark Sweep)算法,而且是支持并发的,而且CMS收集器的部分内存回收工作是可以和用户线程一起并行执行。

2.6.2 运作阶段

初始标记 :在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)。这个过程从垃圾回收的"根对象"开始,只扫描到能够和"根对象"直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。该阶段需要扫描新生代。

并发标记 :这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。

并发预清理 :并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段"重新标记"的工作,因为下一个阶段会Stop The World。

重新标记 :这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从"跟对象"开始向下追溯,并处理对象关联。该阶段也需要扫描新生代。

并发清理 :清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。

并发重置 :这个阶段,重置CMS收集器的数据结构,等待下一次垃圾回收。

2.6.3 缺点

2.6.3.1 对cpu数量敏感

默认配置启动的时候垃圾线程数为 (cpu数量+3)/4,它的性能很容易受cpu核数影响,当cpu的数目少的时候可能会很大程度的影响到计算机性能

2.6.3.2 会产生浮动垃圾

由于cms支持运行的时候用户线程也在运行,程序运行的时候会产生新的垃圾,这里产生的垃圾就是浮动垃圾,cms无法当次处理,得等下次才可以。

值得一提的是,重新标记的作用只是增加之前并发标记所获得的可达对象,但是不会去检查已有的可达对象(是否依然可达),所以是没有办法处理浮动垃圾的。

cms无法处理浮动垃圾,可能导致Concurrent Mode Failure(并发模式故障)而触发full GC。

2.6.3.3 垃圾碎片

CMS回收器采用的基础算法是Mark-Sweep。所有CMS不会整理、压缩堆空间。这样就会有一个问题:经过CMS收集的堆会产生空间碎片。 CMS不对堆空间整理压缩节约了垃圾回收的停顿时间,但也带来的堆空间的浪费。为了解决堆空间浪费问题,CMS回收器不再采用简单的指针指向一块可用堆空 间来为下次对象分配使用。而是把一些未分配的空间汇总成一个列表,当JVM分配对象空间的时候,会搜索这个列表找到足够大的空间来hold住这个对象

2.6.3.4 Concurrent Model Failure

原因一 promotion failed – concurrent mode failure

Minor GC后, Survivor空间容纳不了剩余对象,将要放入老年代,老年代有碎片或者不能容纳这些对象,就产生了concurrent mode failure, 然后进行stop-the-world的Serial Old收集器。

解决这个问题的办法就是可以让CMS在进行一定次数的Full GC(标记清除)的时候进行一次标记整理算法。

解决办法:-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5 或者调大新生代或者Survivor空间( -Xmn=600M -XX:NewRatio=4 -XX -XX:Surviorratio=4)

原因二 concurrent mode failure

CMS是和业务线程并发运行的,在执行CMS的过程中有业务对象需要在老年代直接分配,例如大对象,但是老年代没有足够的空间来分配,所以导致concurrent mode failure, 然后需要进行stop-the-world的Serial Old收集器。

解决这个问题的通用方法是调低触发CMS GC执行的阀值,CMS GC触发主要由CMSInitiatingOccupancyFraction值决定,默认情况是当旧生代已用空间为68%时,即触发CMS GC,在出现concurrent mode failure的情况下,可考虑调小这个值。

解决办法:+XX:CMSInitiatingOccupancyFraction,调大老年代的空间(-Xms -Xmx),+XX:CMSMaxAbortablePrecleanTime。

解决方法总结

总结一句话:调整年轻代或老年代空间,使用标记整理清除碎片和提早进行CMS操作。

2.7 G1收集器

2.7.1 关注降低延迟,解决cms碎片等缺陷

G1(garbage first:先收集存活数据最少的区域(垃圾优先),尽可能多收垃圾,避免full gc)收集器是当前最为前沿的收集器之一(1.7以后才开始有),同cms一样也是关注降低延迟,是用于替代cms功能更为强大的新型收集器,因为它解决了cms产生空间碎片等一系列缺陷。

2.7.2强化分区,弱化分代,区域化、增量式

g1的特别之处在于它强化了分区,弱化了分代的概念,是区域化、增量式的收集器,它不属于新生代也不属于老年代收集器。

g1是区域化的,它将java堆内存划分为若干个大小相同的区域,jvm可以根据堆得大小合理分配每个region的大小(1-32m,2的幂),g1将一组或多组区域中存活对象以增量并行的方式复制到不同区域进行压缩,从而减少堆碎片,目标是尽可能多回收堆空间【垃圾优先】且尽可能不超出暂停目标以达到低延迟的目的。

2.7.3 适用场景

1.像cms能与应用程序并发执行,GC停顿短【短而且可控】,用户体验好的场景。

2.面向服务端,大内存,高cpu的应用机器。【6g或更大】

3.应用在运行过程中经常会产生大量内存碎片,需要压缩空间

3 相关问题

3.1 GC种类

Minor GC:在年轻代Young space(包括Eden区和Survivor区)中的垃圾回收称之为 Minor GC,Minor GC只会清理年轻代。

Major GC:Major GC清理老年代(old GC),但是通常也可以指和Full GC是等价,因为收集老年代的时候往往也会伴随着升级年轻代,收集整个Java堆。

Full GC:full gc是对新生代、老年代、永久代【jdk1.8后没有这个概念了】统一的回收。

mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式。

3.2 什么时候触发GC

3.2.1 minor GC(young GC)

当年轻代中eden区分配满的时候触发[因为young GC后部分存活的对象会已到老年代(比如对象熬过15轮),所以过后old gen的占用量通常会变高]。

3.2.2 full GC

1)System.gc()

手动调用System.gc()方法 [增加了full GC频率,不建议使用,可以设置-XX:+ DisableExplicitGC来禁止RMI调用System.gc]。

2)永久代空间不足

发现perm gen(如果存在永久代的话)需分配空间但已经没有足够空间。

3)老年代空间不足

老年代空间不足,比如说新生代的大对象大数组晋升到老年代就可能导致老年代空间不足。

4)Promotion Faield

CMS GC时出现Promotion Faield。

5)平均大小大于剩余空间

统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间。

6)什么是空间分配担保

在minor gc前,jvm会先检查老年代最大可用空间是否大于新生代所有对象总空间,如果是的话,则minor gc可以确保是安全的,即在极端情况下新生代中的所有存活对象都可以在不触发fullGC的情况下迁移到老年代。

如果小于则会触发分配担保机制,首先会检查一个配置(HandlePromotionFailire),即是否允许担保失败。这个担保机制即意味着要承担担保失败(minorGC失败之后再进行一次FullGC)的风险。

如果允许担保失败,继续检查老年代最大可用连续空间是否大于之前晋升的平均大小(比如说剩10m,之前每次都有9m左右),如果是将尝试一次minor gc,这里也就是风险所在了。

如果不允许担保失败,或者最大可用连续空间小于之前晋升的平均大小,则会触发full gc(此时就不用承担minorGC失败的风险了)。

7)新生代什么情况会晋升为老年代

长期存活:长期存活的对象进入老年代,对象每熬过一次GC年龄+1(默认年龄阈值15,可配置)。

对象太大:对象太大新生代无法容纳则会分配到老年代。

分配担保: eden区满了,进行minor gc后,eden和一个survivor区仍然存活的对象无法放到(to survivor区)则会通过分配担保机制放到老年代。

动态年龄判定:为了使内存分配更灵活,jvm不一定要求对象年龄达到MaxTenuringThreshold(15)才晋升为老年代。若survior区从0累加年龄为n的对象总大小大于survior区空间的一半(TargetSurvivorRatio),则大于等于这个年龄的对象将会在minor gc时移到老年代。