【JVM】垃圾回收算法及垃圾回收器

1、对象状态判断算法

垃圾回收的第一步是要判断堆中存放的对象之中哪些还“存活”着,哪些已经“死去”(即不可能再被任何途径使用的对象)。

1.1、引用计数算法

引用计数算法的思想是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1;当引用失效的时候,计数器的值就减1。任何时刻,计数器值为0的对象就是不可能再被使用的。

引用计数算法(Reference Counting)的实现简单,判断效率也高,也有较为广泛的引用。但是,java虚拟机里面没有选用引用计数算法来进行内存管理,其主要原因是它很难解决对象之间循环引用的问题

1.2、可达性分析

在主流的商用程序语言的主流实现中,都是通过可达性分析(Reachability Analysis)来判定对象是否存活的。其思想是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明该对象是不可用的。

在Java语言中,可作为GC Roots的对象包括以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  • 方法去中类静态属性引用的对象;
  • 方法去中常量引用的对象;
  • 本地方法栈中JNI(即一般说的native方法)引用的对象。

注意: 通过可达性算法分析后被判定为不可达的对象不一定会被回收,对象最终被正真回收需要经历两次标记阶段:1.可达性分析后被判断为不可达时进行第一次标记并进行筛选,筛选的标准是有无必要执行finalize()方法。如果对象没有重写finalize()方法,或已经执行过finalize()方法,则认为没有必要;2.对于认为有必要执行的对象,会被放入“F-Queue"队列中,由虚拟机自动创建的低优先级的Finalizer线程去执行它(只触发finalize()方法,不确保执行完)。如果在finalize()方法执行过程中,对象能重新与任何一个对象建立起关联即可成功“拯救”自己。

注意: finalize()方法只会被执行一次,同时应该尽量避免使用这种方式去“拯救”对象!

2、垃圾收集算法

当前商业虚拟机都采用“分代收集”(Generational Collection)算法,根据对象存活周期的不同将内存划分为几块。一般将Java对划分为新生代和老年代,并根据各个年代的特点选用最适当的收集算法。一般而言,新生代选用复制算法,老年代选用“标记-清理”或者“标记-整理”算法来进行回收。

2.1、标记清除算法

标记-清除算法(Mark-Sweep)算法是最基础的收集算法,分为“标记”和“清除”两个阶段:首先标记出所需要回收的对象,在标记完成后统一回收所有被标记的对象。

存在的主要问题:1.效率低,标记和清除两个过程的效率都不高;2.易产生大量的不连续内存碎片。内存空间碎片太多可能会导致程序后续在分配较大对象时无法找到连续的内存块而提前触发下一次垃圾收集动作。

2.2、复制算法

为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,其思想是:将内存空间分为两块:每次只使用其中一块,当这块内存使用完以后,就将还存活的对象复制到另外一块内存上,然后把已经使用过的内存空间一次清理掉。该算法不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

该算法应用于回收新生代。研究结果表明,大部分新生代的对象都是朝生暮死,所以并不需要按照1:1的比例进行内存分配,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认的比例是8:1。(当Survivor内存不够用,即存活比例高于10%时,需要依赖其他内存进行分配担保(Handle Promotion)。

2.3、标记整理算法

当对象存活比例较高时复制收集算法要进行较多的复制操作,效率较低。另外,存在内存浪费或需要分配担保或需要应对对象百分百存活的极端情况,所以老年代不能直接采用这种算法。

根据老年代的特点,有人提出了一种“标记-整理”(Mark-Compact)算法,其标记过程与标记-清理算法一致,不同点在于标记完以后不是直接对可回收的对象进行清理,而是让所有存活对象都向一端移动,最后直接清理掉端边界以外的内存。

3、垃圾收集器

JDK 1.7 Update 14之后的HotSpot虚拟机包含的垃圾收集器如图所示:

3.1、Serial收集器

Serial收集器是一个单线程的收集器,在进行垃圾收集时必须暂停其他所有的工作线程,直至收集结束。优点是相比于其他收集器的单线程简单而高效,对于单CPU环境而言,没有线程交互的开销,可以获得最高的单线程收集效率。是Client模式下默认的新生代收集器。Serial与Serial Old配合时的运行过程如图:

3.2、ParNew收集器
ParNew收集器是Serial的多线程版本,除了使用多线程进行垃圾收集外,其余行为与Serial收集器完全一样。

ParNew说机器是Server模式下的首选收集器,一个与性能无关但是很重要的原因是:除了Serial收集器之外只有parNew收集器能与CMS收集器配合工作。

ParNew收集器在单CPU的环境中不会比Serial收集器性能更好,甚至由于线程交互的开销,两个CPU的环境都不能保证百分百超越Serial收集器。但是随着CPU数量的增加,它对GC时的系统资源的有效利用是有帮助的。其默认的收集线程与CPU的数量相同,可以使用-XX:ParallelGCThreads参数限制垃圾收集的线程数。

3.3、Parallel Scavenge收集器

Parallel Scavenge收集器的关注点不同于其他收集器关注垃圾收集时用户线程的停顿时间,其目标是达到一个可控制的吞吐量(Throughput),吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),所以Parallel Scavenge收集器也经常被称为“吞吐量优先”收集器。

停顿时间越短就越适合与用户交互的程序,提升用户体验;而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量。第一个参数是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMills,该参数允许的值是一个大于0的毫秒数,收集器会尽量保证垃圾收集时间不超过该值。该值不是越小越好,GC停顿时间的缩短是以牺牲吞吐量和新生代空间来换取的;第二个参数是直接设置吞吐量大小的-XX:GCTimeRatio,该参数的值应当是一个大于0且小于100的整数,即垃圾收集时间占总时间的比率。(此处根据《深入理解Java虚拟机》P79的示例,该参数应该是垃圾收集时间:运行客户代码时间,该参数默认值99,则垃圾收集时间比例为:1/(1+99)=1%)

Parallel Scavenge区别于ParNew收集器的另外一个地方是它通过设置-XX:+UseAdaptiveSizePolicy参数可以开启自适应调节策略,虚拟机能根据当前系统运行情况收集性能监控信息,动态调节新生代大小、Eden与Survivor的比例等参数以提供最合适的停顿时间或者最大的吞吐量。用户只需要设置基本的内存数据,并通过MaxGCPauseMills(更关注停顿时间)或者GCTimeRatio(更关注吞吐量)参数设置好优化目标级即可。

3.4、Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,其主要意义也是用于Client模式。如果在Server模式下,其用途有二:

  • 在JDK 1.5及以前版本中与Parallel Scavenge收集器搭配使用;
  • 作为CMS收集器的后备方案,在并发收集发生Concurrent Mode Failure时使用。

3.5、Parallel Old收集器

parallel Old收集器是Parallel Scavenge收集器的老年代版本,自JDK 1.6开始提供。该收集器的出现化解了Parallel Scavenge的尴尬,**在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old的组合。

3.6、CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器。对于尤其重视服务响应速度,希望系统停顿时间最短的系统,CMS收集器非常符合其需求。

该收集器基于“标记-清除”算法,整个过程分为4个步骤:

  • 初始标记(initial mark)
  • 并发标记(concurrent mark)
  • 重新标记(remark)
  • 并发清除(concurrent sweep)

其中,初始标记和重新标记两个步骤仍然需要暂停所有用户线程。初始标记仅仅标记GC Roots能直接关联到的对象,速度很快。并发标记阶段就是进行GC Roots Tracing的过程。重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记发生变动的对象。该阶段停顿的时间一般比初始标记阶段稍长,但是短于并发标记的时间。

整个过程中耗时最长的并发标记和并发清除阶段,垃圾收集线程是与用户线程并行的,所以总体而言CMS收集器的内存回收过程是与用户线程一起并发执行的。执行过程如图:

CMS收集器的优势是:并发收集、低停顿,同时也有3个明显的缺点:

  • 对CPU资源非常敏感。其默认的垃圾回收线程数为(CPU数量+3)/4,当CPU数量不足4个时,CMS对用户程序的影响可能变得很大。
  • CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”而导致另一次Full GC的产生。并发清理阶段用户线程仍然在运行,该阶段产生的垃圾发生在标记阶段之后,当次清理过程无法清理,只能等下一次GC时进行清理,这部分垃圾即为“浮动垃圾”。同时也因为垃圾清理阶段,用户线程也在运行,需要预留内存空间给并发收集时的程序使用,如果预留的内存空间无法满足用户程序的需要,则会出现一次“Concurrent Mode Failure”,此时虚拟机将会启动后备方案,临时使用Serial Old收集器重新进行老年代的垃圾收集。可以通过参数-XX:CMSInitiatingOccupancyFraction对触发CMS收集器的内存比例进行设置,但是该值设置的太高容易导致大量的“Concurrent Mode Failure”,性能反而会降低。
  • 因为CMS采用标记清除算法,会产生大量的内存空间碎片。CMS为了解决内存碎片的问题,提供了-XX:+UseCMSCompactAtFullCollection开关参数(默认开启),用以在CMS收集器要进行FullGC时开启内存碎片合并的整理过程,但是该方案会带来时间停顿变长的问题。另外,还有一个参数-XX:CMSFullGCsBeforeCompaction,用以设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次Full GC时都进行碎片整理)。

3.7、G1收集器
G1收集器是一款面向服务端应用的收集器,背负着替换掉CMS收集器的使命,与其他收集器相比,G1收集器具有以下特点:

  • 并行与并发,能够充分利用多CPU、多核环境下的硬件优势;
  • 分代收集,G1收集器可以不需要其他收集器配合就能完成整个GC堆的管理,但是其设计仍然保留了分代概念;
  • 空间整合,G1从整体上看基于“标记-清理”算法,但是局部看(两个Region之间)是基于“复制”算法,能够保证运行期间不产生内存空间碎片;
  • 可预测的停顿。G1与CMS都关注降低停顿,相比于CMS收集器,G1能建立可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内垃圾收集的时间不超过N毫秒。G1能做到该点是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。

G1将整个Java堆划分为为多个大小相等的独立区域(Regina),通过跟踪每个Region里面垃圾堆积的价值大小,在后台维护一个有限列表,每次根据允许的收集时间,优先回收价值最大的Region。这种回收方式保证了G1在有限时间内能尽可能的提高收集效率。

G1的执行流程:

G1相关的其余部分有点复杂,总结不出来了。。。见深入理解Java虚拟机P85.。。

各垃圾收集器的线程特点和算法汇总:

4、内存分配与回收策略

  • 对象优先在Eden分配
  • 大对象直接进入老年代
  • 长期存活的对象将进入老年代
  • 动态对象年龄判定
  • 空间分配担保

5、概念区分

(1)并发与并行

  • 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
  • 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。

    该部分内容来源于参考链接:
    https://www.jianshu.com/p/cbf9588b2afb

(2)Minor GC与Full GC

  • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具有朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
  • 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC。Major GC的速度一般会比Minor GC慢10倍以上。

参考资料:
1.《深入理解Java虚拟机》
2.并发和并行的区别 https://www.jianshu.com/p/cbf9588b2afb

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值