你必须了解的java内存管理机制(四)-垃圾回收

本文在我的技术博客不一样步发布,详情可用力戳
亦可扫描屏幕右侧二维码关注我的公众号,公众号内有我的联系方式,等你来撩...html

相关连接(注:文章讲解JVM以Hotspot虚拟机为例,jdk版本为1.8)
一、 你必须了解的java内存管理机制-运行时数据区
二、 你必须了解的java内存管理机制-内存分配
三、 你必须了解的java内存管理机制-垃圾标记
四、 你必须了解的java内存管理机制-垃圾回收java

前言

  在前面三篇文章中,对JVM的内存布局、内存分配、垃圾标记作了较多的介绍,垃圾都已经标记出来了,那剩下的就是如何高效的去回收啦!这篇文章将重点介绍如何回收旧手机、电脑、彩电、冰箱~啊呸(⊙o⊙)…将重点介绍几种垃圾回收算法、HotSpot中经常使用的垃圾收集器的主要特色和应用场景。同时,这篇文章也是这个系列中的最后一篇文章啦!算法

正文

  上一篇文章中,咱们详细介绍了两种标记算法,而且对可达性分析算法作了较多的介绍。咱们也知道了HotSpot在具体实现中怎么利用OopMap+RememberedSet的技术作到“准确式GC”。无论使用什么优化的技术,目标都是准确高效的标记回收对象!那么,为了高效的回收垃圾,虚拟机又经历了哪些技术及算法的演变和优化呢?(注:G1收集器及回收算法本文不涉及,由于我以为后面能够单独写一篇文章来谈!)多线程

回收算法

  在这里,咱们会先介绍几种经常使用的回收算法,而后了解在JVM中式如何对这几种算法进行选择和优化的。并发

标记-清除

  "标记-清除"算法分为两个阶段,“标记”和“清除”。标记仍是那个标记,在上一篇文章中已经作了较多的介绍了,JVM在执行完标记动做后,还在"即将回收"集合的对象将被统一回收。执行过程以下图:框架

  

  优势:
    一、基于最基础的可达性分析算法,它是最基础的收集算法。
    二、后续的收集算法都是基于这种思路并对其不足进行改进而获得的。
  缺点:
    一、 执行效率不高。
    二、 由上图能看到这种回收算法会产生大量不连续内存碎片,若是这时候须要建立一个大对象,则没法进行分配。布局

复制算法

  “复制”算法将内存按容量划分为大小相等的两块,每次使用其中的一块。当一块的内存用完了,就将还存活的对象复制到另外一块上面,而后将已经使用过的存储空间一次性清理掉,这样每次都是针对整个半区的内存进行回收,不用考虑碎片问题。执行过程以下图:优化

  

  优势:
    一、每次针对半个区域进行回收,实现简单,运行高效。
    二、不会产生内存碎片问题。
  缺点:
    一、 内存会缩小为原来的通常,代价高。
    二、 当对象存活率较高时,须要进行较多复制操做,效率将会变低。线程

复制算法改良版

  “复制算法改良版”替代原来将内存一分为二的方案,将内存分为一块较大的内存(称为Eden空间)和两块较小的内存(称为Survivor空间),每次使用Eden空间和其中一块Survivor空间。当回收时,将Eden和其中一块Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才使用过的Survivor空间。执行过程以下图:3d

  

  优势:
    一、改善了普通复制算法的缺点,提升了空间利用率

标记-整理算法

  “标记-整理”算法的标记过程与“标记-清除”算法是同样同样的,但后续步骤不是直接对可回收对象进行清理,而是让全部的对象都向一端移动,而后直接清理掉端边界之外的内存。执行过程以下图:

  

  优势:
    一、改善了“标记-清除”算法会产生内存碎片的缺点。
    二、不会像“复制”算法那样效率随对象存活率升高而变低。
  缺点:
    一、 依然没有解决 “标记-清除”算法存在的缺点,那就是回收效率问题。还多了须要整理的过程,效率更低。

分代收集算法

  咱们都知道,在主流的虚拟机中都是采用分代收集算法来进行堆内存的回收,在第一篇文章中咱们也用了一张图展现了JVM堆内存的划分。以下:

  

  分代回收根据对象存活周期的不一样将内存划分为几块,这样就能够根据各个年代的特色采用最适当的收集算法。通常把Java堆分为新生代老年代

  新生代

  在Hotspot虚拟机中,新生代的收集器都是采用的改良版的复制算法进行垃圾回收。将新生代一分为三,一块Eden区和两块Survivor区。Eden区与两块Survivor区的比例为8:1:1。这样划分的依据是什么呢?基于弱代理论,IBM研究代表新生代中98%的对象都是"朝生夕死",大多数分配了内存的对象并不会存活太长时间,在处于年轻代时就会死掉。

  在原始的复制算法中,空间一分为二,空间利用率为50%,也就是说有新生代中50%的空间会被浪费,没法分配内存。Hotspot虚拟机使用改良的复制算法,而且设置合理的空间比例,新生代中可用的内存空间为整个新生代容量的90%,只有10%的空间会被浪费,大大的提升的新生代的空间利用率。若是存活对象占用的内存大于新生代容量的10%怎么办?这就须要依赖其余内存(老年代)进行分配担保了。新生代回收动图以下:

  

  老年代

  因为老年代的对象存活周期通常相对较长,不会像新生代对象那样“朝生夕死”,因此对象存活率高是老年代的特色,而且老年代也没有额外的空间能够分配担保,因此不适合采用复制算法进行回收。根据老年代的特色,通常会使用"标记-清理"或"标记-整理"算法来进行垃圾回收。

收集器

  上面咱们介绍了在JVM中经常使用的垃圾回收算法及每一种算法的优缺点。接下里会介绍在HotSpot虚拟机中经常使用的几种垃圾收集器,垃圾收集器是垃圾回收算法的具体实现,不一样的商家、不一样版本的JVM所提供的垃圾收集器可能会存在差别。这几种收集器分别是Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1。在了解垃圾收集器以前,咱们先来区分几个概念:

  并发收集器VS并行收集器
  并行:指多条收集线程同时进行收集工做,但此时用户线程处于等待状态。如ParNew、Parallel Scavenge、Parallel Old。
  并发:指用户线程与垃圾收集线程同时执行(并不必定是并行,可能会交替执行)。如CMS、G1。

  YoungGC VS OldGC VS MinorGC VS MajorGC VS FullGC
  Minor GC、YoungGC:Minor GC又称为新生代GC,因此等价于Young GC,在新生代的Eden区分配满的时候触发。在Young GC后新生代中有部分存活对象会晋升到老年代,有多是年龄达到阈值(默认为15岁,在JVM里面15岁就步入老年生活了,O(∩_∩)O哈哈~)了,也多是Survivor区域满了,若是是Survivor区域被填满,会将全部新生代中存活的对象移动到老年代中!

  Major GC、Old GC、Full GC:Old GC从字面能理解是老年代的GC,可是对Major GC和Full GC存在多种说法,有的认为Major GC等价于Old GC只是针对老年代的GC,有的认为Major GC和Full GC是等价的。可是我我的认为Major是指老年代GC,而Full GC针对新生代、老年代、永久代整个的回收。因为老年代的GC都会伴随一次新生代的GC,因此习惯性的把Major GC和Full GC划上了等号。前面Young GC时候说到“在Young GC后新生代中有部分存活对象会晋升到老年代”,万一老年代的空间不够存放新生代晋升的对象怎么办呢?因此当准备要触发一次Young GC时,若是发现统计数据以前Young GC的平均晋升大小比目前老年代剩余的空间大,则不会单独触发Young GC,而是转为触发Full GC,也就是整堆的收集!

串行收集器

  串行垃圾收集器是最基本、发展历史最悠久的收集器。主要包含Serial和Serrial Old两种收集器,分别用来收集新生代和老年代。串行收集器因为是单线程收集,在进行垃圾收集时,必须暂停(Stop The World)全部的工做线程,直到GC线程工做完成。运行示意图以下:

  

  Serial 收集器:主要针对新生代回收,采用复制算法,单线程收集。
  Serial Old收集器:主要针对老年代回收,采用“标记-整理”算法,单线程收集。

  串行收集器在单CPU的环境下,没有线程切换的开销,能够得到最高的单线程收集效率,可是因为如今广泛都是多CPU(或者多核)环境,因此除了在桌面应用中仍然将串行收集器做为默认的收集器,其余场景已经不多(不多不表明没有,后面CMS会讲到)使用。

  在上面咱们谈到一个词,须要暂停(Stop The World)全部的工做线程,这个概念在后面也会屡次提到,为何须要暂停呢?一是为了方便GC动做,否则在GC过程当中又会额外产生新的垃圾,或者分配新的对象。二是由于GC过程当中对象的地址会发生变化,若是不暂停线程,可能会致使引用出现问题。

并行收集器

  并行收集器是串行收集器的多线程版本,除了多线程外,其他的行为、特色和串行收集器同样。主要包含ParNew收集器、Parallel Scavenge收集器、Parallel Old收集器。运行示意图以下:

  

  ParNew收集器:主要针对新生代回收,采用复制算法,多线程收集。通常老年代若是使用CMS收集器,则默认会使用ParNew做为新生代收集器
  Parallel Scavenge收集器:该收集器与ParNew收集器相似,也是新生代收集器,采用复制算法,多线程收集。其余收集器关注点是尽量地缩短垃圾收集时用户线程停顿的时间,可是Parallel Scavenge收集器的目标则是达到一个可控的吞吐量(吞吐量=CPU运行用户代码时间/(CPU运行用户代码时间+CPU垃圾收集时间)),因此该收集器也成为吞吐量收集器。因为该收集器没有使用传统的GC收集器代码框架,是另外独立实现的,因此没法和CMS收集器配合工做。
  Parallel Old收集器:主要针对老年代回收,采用“标记-整理”算法,多线程收集。该收集器是Parallel Scavenge收集器的老年代版本。在JDK1.6以后用来替代老年的Serial Old收集器。在注重吞吐量以及CPU资源敏感的场景,通常会选择Parallel Scavenge+Parallel Old的组合进行垃圾收集。

CMS收集器

  前面介绍的几种收集器都相对比较简单,也很好理解,因此也没作过多的介绍。接下来介绍的收集器相对前面几种收集器就要复杂一些,而且使用较广,因此介绍会较详细!并发标记清理(Concurrent Mark Sweep)收集器也称为并发低停顿收集器或低延迟收集器。CMS收集器采用的是“标记-清理”算法,因此不会进行压缩操做。咱们先来了解一下CMS收集器的运做过程:

  

  CMS收集器运做过程

  一、初始标记(CMS initial mark)
  仅标记GC Roots能直接关联的对象,这个阶段为速度较快,可是仍然须要“Stop The World”,可是停顿时间较短!

  二、并发标记(CMS Concurrent mark)
  进行GC Roots Tracing的过程,也就是查找GC Roots能直接关联的对象所引用的内存。在这个阶段,GC线程与用户线程是同时运行的,因此并不能保证能标记出全部存活的对象。

  三、从新标记(CMS remark)
  因为并发标记阶段,用户线程在并发运行,因此可能在并发标记阶段产生新的对象,因此在从新标记阶段也会须要“Stop The World”来标记新产生的对象,且停顿时间比初始标记时间稍长,但远比并发标记短。

  四、并发清除(CMS Concurrent sweep)
  在并发清除阶段用户线程与清理线程也是同时工做,清理线程回收全部的垃圾对象!

  CMS收集器缺点

  上面了解了CMS收集器的运做过程,不知道在了解过程当中你有没有发现一些问题,好比CMS收集器采用的是“标记-清除”算法,那会不会产生不少的内存碎片?好比在并发清理阶段,用户线程还在运行,会不会在清理的过程当中又产生了垃圾?总结CMS收集器的几个明显的缺点以下:

  一、 对CPU资源很是敏感
  并发收集虽然不会暂停用户线程,可是由于会占用一部分CPU资源,仍是会致使应用程序变慢,总吞吐量降低。CMS的默认收集线程的数量=(CPU数量+3)/4。因此,当CPU数量大于4个时,会有超过25%的资源用于垃圾收集。当CPU数量小于或等于4个时,默认一个收集线程。

  二、 产生大量内存碎片
  CMS收集器采用“标记-清除”算法,在清除后不会进行压缩操做,这样会致使产生大量不连续的内存碎片,在分配大对象时,没法找到足够的连续内存,从而须要提早触发一次FullGC的动做。针对该问题,提供了两个参数来设置是否开启碎片整理。
  1)、“-XX:+UseCMSCompactAtFullCollection”参数
  从名字能看出来,在收集的时候是否开启压缩。这个参数默认是开启的,可是是否开启压缩还须要结合下面的参数!
  2)、“-XX:+CMSFullGCsBeforeCompaction”参数
  该参数设置执行多少次不压缩的Full GC后,来一次压缩整理。这个参数默认为0,也就是说每次都执行Full GC,不会进行压缩整理。
  若是开启了压缩,则在清理阶段须要“Stop the world”,不能进行并发!

  三、 产生浮动垃圾
  上面说到过在并发清理阶段,用户线程还在运行,这时候可能就会又有新的垃圾产生,而没法在这次GC过程当中被回收,这成为浮动垃圾。

  四、 “Concurrent Mode Failure”失败
  不知道你们在开发过程当中有没有遇到过“Concurrent Mode Failure”失败的信息,无论你有没有遇到过,反正我是遇到过!这个异常是什么缘由致使的呢。在并发标记和并发清除阶段,用户线程与GC线程并发工做,这会致使在清理的时候又会有用户的线程在拼命的建立对象,自己垃圾回收时候确定是可用内存不够了,可万一这时候用户线程建立了大量的对象怎么办呢?因此通常CMS收集器的垃圾回收的动做不会在彻底没法分配内存的时候进行,能够经过“-XX:CMSInitiatingOccupancyFraction”参数来设置CMS预留的内存空间!若是预留的空间没法知足程序的须要,就会出现 “Concurrent Mode Failure”失败。这时候JVM会启用后备方案,也就是前面介绍过的Serial Old收集器,这样会致使另外一次的Full GC的产生,这样的代价是很大的,因此CMSInitiatingOccupancyFraction这个参数设置须要根据程序合理设置

  CMS收集器应用场景

  上面介绍了CMS收集器的缺点,那它固然也有它的优势啦,好比并发收集、低停顿等等……因此CMS收集器适合与用户交互较多的场景,注重服务的响应速度,能给用户带来较好的体验!因此咱们在作WEB开发的时候,常常会使用CMS收集器做为老年代的收集器!

相关文章
相关标签/搜索