四、JVM(HotSpot)垃圾收集器与内存分配策略

注:本博文主要是基于JDK1.7会适当加入1.8内容。

1、对象生死判断

方法一:引用计数算法(JVM不采用)
对象添加一个引用计数器,当对象被引用则计数器+1,当对象引用失效则计数器-1,任何时刻计数器为0则判断对象不可能再被使用,判定为死亡。

JVM(HotSpot)为什么没有使用引用计数器进行判断对象的生死呢?Java对象中存在对象之间的循环引用,如obja.instance = objb; objb.instance = obja; 当obja = null; objb = null; 对象引用计数器不为0,但是对象确实已经不再使用了,这样无法回收,造成内存泄漏。

方法二:可达性分析算法
通过一些列的称之为“GC Roots”的对象作为起始点,从这些节点向下搜索成为引用链,当一个对象到GC Roots没有任何引用链,则判定死亡。Java语言中可作为GC Roots的对象包括以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中静态属性引用的对象(JDK1.8Metaspace取代)
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(Native方法)引用的对象

引用分类

  1. 强引用:Object obj = new Object(); 即强引用,只要引用在,垃圾收集器永远不会将其回收
  2. 软引用:SoftReference,在发生内存溢出错误(OutOfMemoryError)前,将会对这一部分对象进行二次回收,如果回收引用这部分对象还没有足够内存则报内存溢出错误(OutOfMemoryError)
  3. 弱引用:WeakReference,垃圾收集器进行垃圾回收时,无论这部分对象是否被使用,都会将其回收。
  4. 虚引用:PhantomReference,一个对象是否有虚引用的存在,完全不会对其生存空间产生影响,也无法通过虚引用获取一个对象的实例对象。设立虚引用的目的就是为了在垃圾收集器进行垃圾回收时收到一个系统通知。

finalize方法详解(类似于C++中析构函数)
当一个对象判定为不可达对象时,并不是意味着这个对象完全已经死亡,它需要经过一些再判断进行确定。
(1)对象经过可达性算法判断后,与GC Roots不相连;
(2)第一次进行标记并进行筛选,看是否有必要执行finalize方法。如果对象没有覆盖finalize方法,或者虚拟机中被调用过(finalize方法只能被调用一次),则认为没必要执行finalize方法,判定死亡;
(3)如果覆盖finalize方法,并且虚拟机中没有调用过,则会将对象放入F-Queue队列中,稍后GC会对F-Queue进行第二次标记,如果对象在finalize方法中再次被引用,则判断对象生存。

方法区回收(JDK移除,Metaspace代替)
主要回收废弃常量(JDK1.7移除方法区)和无用类。
废弃变量:即系统中不存在对该变量的引用,即可判断为废弃常量。
无用类:(1)该类中所有实例已经回收;(2)加载该类的ClassLoader已经回收;(3)该类对象的java.lang.Class对象没有在任何地方引用,即不存在任何地方可以通过反射获取该类对象实例。

2、垃圾收集算法

(1)标记清除(Mark-Sweep):第一步对需要回收的对象进行标记操作;第二步将标记的对象回收。

两个不足点:效率问题,标记和清除的效率都不高;空间问题,标记清除会产生大量的碎片空间,如果大型对象实例化将不好分配内存空间。

(2)复制算法(解决标记清除中效率问题):将内存空间分为两块,所有对象分配到其中一个内存空间中,回收操作后将所有生存下来的对象复制到另一个内存空间中。

不足点:将内存空间分成两块,相当于回收时只能使用一半的内存空间,这未免太过于浪费内存空间。

HotSpot对复制算法内存空间分配进行了可配置化,默认为SurvivorRatio = 8,即Eden:From Survivor:To Survivor = 8:1:1,在Eden和From Survivor进行回收操作,然后将其赋值到To Survivor,这样只会浪费仅仅10%的内存空间,新生代生存率很低的特性下,采用的垃圾回收常常为赋值算法。

(3)标记整理(Mark-Compact):第一步对回收对象进行标记并清除,第二步为了防止碎片化问题,将存活的对象进行整理使其在内存空间中连续地址存放。老年代中常使用该方法策略。

(4)分代收集算法(JVM常用算法):新生代和老年代有一个重要的区分,新生代大多死亡率很高,使用完之后即可死亡,存活率低,适用复制算法;老年代大多数死亡率低,存活率高,适用于标记整理算法。

3、HotSpot算法实现

(1)枚举根节点:可以作为可达性GC Roots的节点全局性引用(常量或类静态属性)和执行上下文(栈帧中本地方法变量表)。很多应用仅仅方法区就几百兆,逐个去检查里面的引用需要花费大量的时间。另外,可达性分析对执行时间的敏感还体现在GC停顿上,因为这项工作必须在一个能够确保一致性的快照中执行,一致性则要求整个分析过程处于一个相对静止(不变化)的状态中,这也会导致Stop The World现象。

主流的Java虚拟机当系统停顿下来(STW)并不需要一个不漏的检查全局性引用和执行上下文,虚拟将将对象引用村OopMap这一特定位置,提高检查效率。

(2)安全点

  • 安全点选择。HotSpot并没有为每一条指令都生成OopMap,不然的话,内存消耗将会非常严重。所以HotSpot会选定特殊的点儿进行OopMap记录。如何选择这些安全点需要考虑两个问题:安全点不能选择太少,太少会让一次GC等待时间过长;安全点不能太多,太多会增加运行时执行的负担。一般情况下,以“是否具有让程序长时间运行”为标准设置安全点,长时间运行的一个显著特征是指令序列复用,例如方法滴啊用,循环跳转,异常跳转,条件跳转等,所有具备这些指令的才会产生安全点。
  • GC发生时,如何让所有运行中线程达到安全点。两类形式:抢占式中断和主动式中断。抢占式中断(不推荐使用)是指当发生GC停顿时,没有到达安全点的线程恢复执行,到达安全点后停止线程;主动式中断即不直接对线程进行操作,设置一个标志位,各个线程主动轮询这个标志,发现中断标志位true时,中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
  • 安全区域即程序执行时,安全点较完美解决了在不长时间内即可进入GC。然而,如果程序不在执行的时候呢?线程处于Sleep或Blocked状态,无法响应JVM的中断请求。虚拟机显然不太愿意等待线程重新分配CPU时间,这种情况下需要设置安全区域。安全区域是指一段代码片段中,引用关系不会发生变化,在这个区域开始的GC都是安全的。过程为线程执行到安全区域中的代码时,首先标志自己进入安全区域;此刻发生GC时,不用标识自己为安全区域的线程;当线程离开时,需要检查系统是否完成枚举根节点,如果完成则可以继续执行,如果没有完成则必须等收到可以离开安全区域的标识为止。

4、垃圾收集器

  • 年轻代:Serial、ParNew、Parallel Scavenge
  • 老年代:CMS、Serial Old、Parallel Old
  • 年轻代+老年代: G1

配对使用规则:
Serial —> CMS、Serial Old
ParNew —> CMS、Serial Old
Parallel Scavenge —>Serial Old、Parallel Old

CMS —>Serial、ParNew、Serial Old
Serial Old—>Serial、ParNew、Parallel Scavenge
Parallel Old—>Parallel Scavenge

(1)Serial收集器—新生代
使用服务器CPU个数少,单线程执行。

(2)ParNew收集器—新生代
使用服务器CPU个数多,Serial收集器的多线程版本。

参数设置:-XX:SurvivorRatio(Eden与Survivor内存划分比例),-XX:HandlePromotionFailure(是否允许牺牲带收集担保,也就是说执行了一次Monitor GC后,另一块Survivor空间不足时,直接放入老年代)
并行:多条垃圾收集线程秉性工作,用户线程仍然处于等待状态。
并发:用户线程和垃圾手机线程同时执行(并不一定是并行的,有可能是交替执行),用户程序继续执行,而垃圾收集线程则在另一个CPU上执行。

(3)Parallel Scavenge收集器—新生代
吞吐量=CPU用于执行用户代码消耗的时间 / (CPU用于执行用户代码消耗时间+垃圾收集时间)
Parallel Scavenge提供两个精确控制吞吐量的参数,分别是-XX:MaxGCPauseMills和-XX:GCTimeRatio
-XX:MaxGCPauseMills:内存回收实际那不超过设定值。设定值设定越小,回收对象达到内存空间就越小,GC越频繁。反之,内存回收时间设定越大,回收对象达到的空间就越大,GC越稀少。
-XX:GCTimeRatio:垃圾收集时间占总时间比率。例如,参数为19,则GC占用时间为5%,1/(1+19)依次类推。
另外,-XX:UseAdaptiveSizePolicy:GC自适应调整策略,即不用手动调整-Xms、-Xmx、-Xss等参数。

(4)Serial Old收集器—老年代
适用服务器CPU个数少,单线程执行,一般与Serial组合使用。

(5)Parallel Old收集器—老年代
适用服务器CPU个数多,多线程执行,一般与Parallel Scavenge组合使用。

(6)CMS收集器—老年代
过程:初始化标记—>并发标记—>重新标记—>并发清除
初始化标记(STW):标记GC Roots能关联的对象。
并发标记:与用户程序线程一同执行,标记GC Roots Tracking对象。
重新标记(STW):并发标记过程中产生变动的一部分对象,停顿时间比初始化标记时间长,较并发标记时间短。
并发清除:与用户线程一同执行。

CMS收集器有明显三个缺点:

  1. CPU资源敏感。并发标记和并发清除都需要服务器划分CPU供其执行,导致兵法执行的用户程序变慢,吞吐量降低。
  2. 无法处理浮动垃圾。并发清除过程中,程序还在运行状态就可能产生新的垃圾对象,出现Concurrent Mode Failure导致一次Full GC产生。-XX:CMSInitiatingOccupancyFraction为老年代中占用百分比时触发CMS收集器执行,如果老年代中预留的内存空间不足以支撑CMS并发清理时产生的浮动垃圾就报Concurrent Mode Failure。
  3. 并发清除是基于标记清除算法,这意味着老年代中会产生很多碎片空间,当碎片空间过多时,对对大对象分配到老年代中产生影响,不得不触发一次Full GC。为此,CMS提供了参数为-XX:UseCMSCompactAtFullConllection,默认开启,用于CMS收集器顶不住要进行Full GC时,进行一次内存合并整理。另一个参数为-XX:CMSFullGCsBeforeCompaction,指执行多少次不压缩的Full GC后,来一次带压缩Full GC,默认值为0,即每次Full GC都进行一次压缩处理。

(7)G1收集器—年轻代+老年代
特征:并发与并行、分代收集、空间整理、可预测停顿。
G1将整个Java堆划分成多个大小相等的独立区域Region,虽然还保留着新生代和老年代的概念,但是它们不再是物理隔离,而是一部分Region(不需要连续)的集合。G1之所以能够建立可预测的停顿时间模型,在于Java堆进行全区域的垃圾收集,G1跟踪各个Region里面垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。这种使用Region划分内存空间以及优先级的内存回收方式,保证G1收集器在有限时间内获取尽可能高的手机销量。
一个问题,分成了多个Region,但是Region内对象之间存在引用,所以G1采用了Remembered Set来避免全堆扫描,G1每一个Region对应一个Remembered Set,Remembered Set里面记录Region对象的引用关系。当虚拟机发现程序对Reference类型的数据进行写操作时,会产生一个Write Barrier暂停中断写操作,检查Reference引用的对象是否处于不同的Region中。如果是,通过CardTable把相关引用信息记录到被饮用对象的Region的Remembered Set中。这样,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆进行扫描也不会有遗漏。
G1除去Remembered Set维护,还有步骤为初始化标记、并发标记、最终标记、筛选回收。

5、理解GC日志

案例:
33.125:[GC [DefNew:3324k -> 152k(3712k), 0.0024925 secs] 3324k->152k(11904), 0.0031680 secs]
100.667:[Full GC [Tenured: 0k -> 210k(10240k), 0.0149142 secs] 4603k -> 210k(19456k), [Perm: 2999k -> 2999k(21248k)], 0.0150007 secs] [Times: user=0.01 sys=0.00 real=0.02secs]
解释:
33.125100.667指的是GC发生的时间,这个时间为虚拟机启动后经过的秒数;[GC[Full GC说明垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC,Full GC是发生了STW,如果调用System.gc()方法触发垃圾收集,那么显示[Full GC(System)];[DefNew[Tenured[Perm表示垃圾手机发生的区域;3324k->152k(3712k)表示垃圾回收前使用的内存,垃圾回收后使用的内存,区域总内存;3324k->152k(11904k)表示Java堆已使用的内存,垃圾回收后使用的内存,Java堆总内存。
[Perm 在JDK1.8已替换为[Metaspace

6、垃圾收集器参数总结

参数 描述
UseSerialGC 虚拟机运行在Client模式下的默认值,打开此开关使用Serial+Serial Old收集器组合进行内存回收
UseParNewGC ParNew+Serial Old
UseCon从MarkSweepGC ParNew+CMS+Serial Old,Serial Old作为CMS收集器出现Concurrent Mode Failure失败后的备用收集器
UseParallelGC Server模式下默认值,Parallel Scavenge + Serial Old
UseParallelOldGC Parallel Scavenge + Parallel Old
SurvivorRatio 新生代中Eden与Survivor容量比值,默认为8,代表Eden:Survivor=8:1
PretenureSizeThreshold 直接晋级老年代对象的大小,设置这个参数后,大于这个参数的对象将直接在老年代中分配,使用ParNew+Serial Old收集器
MaxTenuringThreshold 晋级到老年代中对象年龄,每个对象坚持过一次Minor GC之后年龄+1,当超过这个参数数值后直接在老年代中分配,默认值为15
UseAdaptiveSizePolicy 动态调整Java堆中各个区域的大小以及进入老年代的年龄
HandlePromotionFailre 是否允许分配担保失败,即老年代的剩余空间不足以分配新生代整个Eden和Survivor区域所有对象存活的情况
ParallelGCThreads 设置并行GC时进行内存回收的线程数
GCTimeRatio GC时间占总时间的比率,默认值为99,即允许1%的GC时间,Parallel Scavenge收集器时生效
MaxGCPauseMills 设置GC最大停顿时间,Parallel Scavenge收集器时生效
CMSInitiatingOccupancyFraction 设置CMS收集器老年代空间被使用多少后触发垃圾收集,默认值为68%,JDK1.7为92%
UseCMSCompactAtFullCollection 设置CMS收集器完成垃圾收集后是否要进行一次内存碎片整理
CMSFullGCsBeforeCompaction 设置CMS收集器进行若干次垃圾收集后再启动一次内存碎片整理

猜你喜欢

转载自blog.csdn.net/zhangwei408089826/article/details/81631714
今日推荐