JVM——垃圾回收(4)

本文部分图片摘抄于《深入理解java虚拟机》

0. 概述

我们本文主要关注一下几点:

一.如何判定对象为垃圾对象

1)引用计数法

2)可达性分析法

二.如何回收

1)回收策略:   ●标记-清除算法    ●复制算法    ●标记-整理算法     ●分代收集算法

2)垃圾回收器:    ●Serial    ●Parnew    ●Cms     ●G1

1. 如何判定对象为垃圾对象

1.1 引用计数法

引用计数法的逻辑是:在堆中存储对象时,在对象头处维护一个counter计数器,如果一个对象增加了一个引用与之相连,则将counter++。如果一个引用关系失效则counter–。如果一个对象的counter变为0,则说明该对象已经被废弃,不处于存活状态。

虽然这个方法简单灵活,但是目前这个方法用的不多:如果一个对象A持有对象B,而对象B也持有一个对象A,那发生了类似操作系统中死锁的循环持有,这种情况下A与B的counter恒大于1,会使得GC永远无法回收这两个对象。

扫描二维码关注公众号,回复: 2718994 查看本文章

看上图,堆中可能存在对象之间的相互引用,这时候就算没有栈对A对象的应用,A对象的计数值还是不为0,但此时包括A在内的三个对象都没有被引用,都是垃圾的,但是垃圾回收器也不会将他们回收。

1.2 可达性分析算法

在主流商用语言(如Java、C#)的主流实现中, 都是通过可达性分析算法来判定对象是否存活的: 通过一系列的称为 GC Roots 的对象作为起点, 然后向下搜索; 搜索所走过的路径称为引用链/Reference Chain, 当一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达, 也就说明此对象是不可用的, 如下图: Object5、6、7 虽然互有关联, 但它们到GC Roots是不可达的, 因此也会被判定为可回收的对象:

可作为GC Roots的对象包括:

      ●虚拟机栈(局部变量表)中引用的对象

      ●本地方法栈JNI(Native方法)中引用的对象

      ●方法区:类属性所引用的对象

      ●方法区:常量所引用的对象

注: 即使在可达性分析算法中不可达的对象, VM也并不是马上对其回收, 因为要真正宣告一个对象死亡, 至少要经历两次标记过程: 第一次是在可达性分析后发现没有与GC Roots相连接的引用链, 第二次是GC对在F-Queue执行队列中的对象进行的小规模标记(对象需要覆盖finalize()方法且没被调用过).

2. 如何回收

2.1 标记—清除算法

      我们回想一下上一章提到的根搜索算法,它可以解决我们应该回收哪些对象的问题,但是它显然还不能承担垃圾搜集的重任,因为我们在程序(程序也就是指我们运行在JVM上的JAVA程序)运行期间如果想进行垃圾回收,就必须让GC线程与程序当中的线程互相配合,才能在不影响程序运行的前提下,顺利的将垃圾进行回收

      为了达到这个目的,标记/清除算法就应运而生了。它的做法是当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被成为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。

标记:标记的过程其实就是,遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。

清除:清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。

上图中,红色的为root对象的可达对象,会标记为1,白色则是会标记为0,被清除的。当清除完后,所有的标记会重新清零。然后唤醒停止的程序线程,让程序继续运行即可。

有一点大家可能有疑问,为什么非要停止程序的运行呢?

假设我们的程序与GC线程是一起运行的,各位试想这样一种场景:我们刚标记完图中最右边的那个对象,暂且记为A,结果此时在程序当中又new了一个新对象B,且A对象可以到达B对象。但是由于此时A对象已经标记结束,B对象此时的标记位依然是0,因为它错过了标记阶段。因此当接下来轮到清除阶段的时候,新对象B将会被苦逼的清除掉。如此一来,不难想象结果,GC线程将会导致程序无法正常工作。

到此为止,标记/清除算法已经介绍完了,下面我们来看下它的缺点:

         1、首先,它的缺点就是效率比较低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲,尤其对于交互式的应用程序来说简直是无法接受。试想一下,如果你玩一个网站,这个网站一个小时就挂五分钟,你还玩吗?

         2、第二点主要的缺点,则是这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象都是随即的出现在内存的各个角落的,现在把它们清除之后,内存的布局自然会乱七八糟。而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。

图片copy于:https://my.oschina.net/winHerson/blog/114391

 

2.2 复制算法

这个算法就是为了解决标记清除算法的效率问题的,我们现在看看复制算法:

该算法的核心是将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完之后,就将还存活的对象复制到另外一块上面(放在一起),然后在把已使用过的内存空间一次理掉。这样使得每次都是对其中的一块进行内存回收,不会产生碎片等情况,只要移动堆订的指针,按顺序分配内存即可,实现简单,运行高效。

缺点:内存缩小为原来的一半。

2.3 标记—整理算法

标记操作和“标记-清除”算法一致,后续操作不只是直接清理对象,而是在清理无用对象完成后让所有存活的对象都向一端移动,并更新引用其对象的指针。

缺点:在标记-清除的基础上还需进行对象的移动,成本相对较高,好处则是不会产生内存碎片。

3  复制算法以及标记-整理算法在堆内存中的应用(分代收集算法)

       堆为了进行高效的垃圾回收,虚拟机把堆内存划分成新生代(Eden,Survivor0,Survivor1,Tenured Gen)、老年代和永久代(1.8中无永久代,使用metaspace实现)三块区域。 一般Eden与Survivor的内存大小比为8:1,

3.1 新生代与复制算法

       现代商用VM的新生代均采用复制算法, 由于新生代中的98%的对象都是生存周期极短的, 因此并不需完全按照1∶1的比例划分新生代空间, 而是将新生代划分为一块较大的Eden区和两块较小的Survivor区(HotSpot默认Eden和Survivor的大小比例为8∶1), 每次只用Eden和其中一块Survivor. 当发生MinorGC时, 将Eden和Survivor中还存活着的对象一次性地拷贝到另外一块Survivor上(只需要付出少量存活对象的复制成本就可以完成收集), 最后清理掉Eden和刚才用过的Survivor的空间. 当Survivor空间不够用(不足以保存尚存活的对象)时, 需要依赖老年代进行空间分配担保机制, 这部分内存直接进入老年代.

3.2 老年代与标记-整理算法

      老年代中因为对象存活率高、没有额外空间对他进行分配担保,就必须使用“标记-整理”算法进行回收,因为存活率高,所以也不需要对很多对象进行移动。

3.1+3.2也叫做分代收集算法

 

4 垃圾收集器

4.1  新生代

 

1) Serial收集器

       Serial收集器是Hotspot运行在Client模式下的默认新生代收集器, 它的特点是 只用一个CPU/一条收集线程去完成GC工作, 且在进行垃圾收集时必须暂停其他所有的工作线程(“Stop The World” -后面简称STW). 虽然是单线程收集, 但它却简单而高效, 在VM管理内存不大的情况下(收集几十M~一两百M的新生代), 停顿时间完全可以控制在几十毫秒~一百多毫秒内.

2) ParNew收集器

      ParNew收集器其实是前面Serial的多线程版本, 除使用多条线程进行GC外, 包括Serial可用的所有控制参数、收集算法、STW、对象分配规则、回收策略等都与Serial完全一样(也是VM启用CMS收集器-XX: +UseConcMarkSweepGC的默认新生代收集器). 

      由于存在线程切换的开销, ParNew在单CPU的环境中比不上Serial, 且在通过超线程技术实现的两个CPU的环境中也不能100%保证能超越Serial. 但随着可用的CPU数量的增加, 收集效率肯定也会大大增加(ParNew收集线程数与CPU的数量相同, 因此在CPU数量过大的环境中, 可用-XX:ParallelGCThreads参数控制GC线程数).

3)Parallel Scavenge收集器

       与ParNew类似, Parallel Scavenge也是使用复制算法, 也是并行多线程收集器. 但与其他收集器关注尽可能缩短垃圾收集时间不同, Parallel Scavenge更关注系统吞吐量: 系统吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)

       停顿时间越短就越适用于用户交互的程序——良好的响应速度能提升用户的体验;而高吞吐量则适用于后台运算而不需要太多交互的任务(例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序)——-可以最高效率地利用CPU时间,尽快地完成程序的运算任务.

Parallel Scavenge提供了如下参数设置系统吞吐量:

Parallel Scavenge参数 描述
MaxGCPauseMillis (毫秒数) 收集器将尽力保证内存回收花费的时间不超过设定值, 但如果太小将会导致GC的频率增加.
GCTimeRatio (整数:0 < GCTimeRatio < 100) 是垃圾收集时间占总时间的比率
-XX:+UseAdaptiveSizePolicy 启用GC自适应的调节策略: 不再需要手工指定-Xmn-XX:SurvivorRatio-XX:PretenureSizeThreshold等细节参数, VM会根据当前系统的运行情况收集性能监控信息, 动态调整这些参数以提供最合适的停顿时间或最大的吞吐量

 

4.2  老年代

1) Serial Old收集器

Serial Old是Serial收集器的老年代版本, 同样是单线程收集器,使用“标记-整理”算法:

Serial Old应用场景如下: 

   ● JDK1.5之前与Parallel Scavenge收集器搭配使用

   ● 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时启用(见下:CMS收集器)

2) Parallel old收集器

Parallel Old是Parallel Scavenge收老年代版本, 使用多线程和“标记-整理”算法, 吞吐量优先, 主要与Parallel Scavenge配合在 注重吞吐量 及 CPU资源敏感 系统内使用: 


 

3)CMS收集器

CMS(Concurrent Mark Sweep)收集器是一款具有划时代意义的收集器, 一款真正意义上的并发收集器, 虽然现在已经有了理论意义上表现更好的G1收集器, 但现在主流互联网企业线上选用的仍是CMS(如Taobao、微店). 
CMS是一种以获取最短回收停顿时间为目标的收集器(CMS又称多并发低暂停的收集器), 基于”标记-清除”算法实现, 整个GC过程分为以下4个步骤: 
1. 初始标记(CMS initial mark) 
2. 并发标记(CMS concurrent mark: GC Roots Tracing过程) 
3. 重新标记(CMS remark) 
4. 并发清除(CMS concurrent sweep: 已死象将会就地释放, 注意: 此处没有压缩
其中两个加粗的步骤(初始标记重新标记)仍需STW. 但初始标记仅只标记一下GC Roots能直接关联到的对象, 速度很快; 而重新标记则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录, 虽然一般比初始标记阶段稍长, 但要远小于并发标记时间. (由于整个GC过程耗时最长的并发标记和并发清除阶段的GC线程可与用户线程一起工作, 所以总体上CMS的GC过程是与用户线程一起并发地执行的.)

由于CMS收集器将整个GC过程进行了更细粒度的划分, 因此可以实现并发收集、低停顿的优势, 但它也并非十分完美, 其存在缺点及解决策略如下:

1. 对CPU资源非常敏感:

CMS默认启动的回收线程数 = (CPU数目 + 3) / 4 ;当CPU数>4时, GC线程最多占用不超过25%的CPU资源, 但是当CPU数<=4时, GC线程可能就会过多的占用用户CPU资源, 从而导致应用程序变慢, 总吞吐量降低.

2. 无法处理浮动垃圾, 可能出现Promotion FailureConcurrent Mode Failure而导致另一次Full GC的产生:

浮动垃圾是指在CMS并发清理阶段用户线程运行而产生的新垃圾. 由于在GC阶段用户线程还需运行, 因此还需要预留足够的内存空间给用户线程使用, 导致CMS不能像其他收集器那样等到老年代几乎填满了再进行收集. 因此CMS提供了-XX:CMSInitiatingOccupancyFraction参数来设置GC的触发百分比(以及-XX:+UseCMSInitiatingOccupancyOnly来启用该触发百分比), 当老年代的使用空间超过该比例后CMS就会被触发(JDK 1.6之后默认92%). 但当CMS运行期间预留的内存无法满足程序需要, 就会出现上述Promotion Failure等失败, 这时VM将启动后备预案: 临时启用Serial Old收集器来重新执行Full GC(CMS通常配合大内存使用, 一旦大内存转入串行的Serial GC, 那停顿的时间就是大家都不愿看到的了).

3. 最后, 由于CMS采用”标记-清除”算法实现, 可能会产生大量内存碎片:

内存碎片过多可能会导致无法分配大对象而提前触发Full GC. 因此CMS提供了-XX:+UseCMSCompactAtFullCollection开关参数, 用于在Full GC后再执行一个碎片整理过程. 但内存整理是无法并发的, 内存碎片问题虽然没有了, 但停顿时间也因此变长了, 因此CMS还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction用于设置在执行N次不进行内存整理的Full GC后, 跟着来一次带整理的(默认为0: 每次进入Full GC时都进行碎片整理).

g1垃圾收集器

重点看下这篇博文:http://blog.jobbole.com/109170/

猜你喜欢

转载自blog.csdn.net/qq_36582604/article/details/81586822