JVM总结(三):垃圾回收器

回收器

判断对象引用是否失效

对象生存判断算法

  • 引用计数法 
    给对象中添加一个引用计数器,每当一个地方引用到这个对象的时候,计数器值就加1,当引用失效时,计数器的值就减1,当计数器值变为0时,便说明该对象不可能再被使用了。 
    优点:实现简单,判定效率较高。 
    缺点:当出现对象之间的相互循环引用时,即两个类中都存在引用字段分别引用着对方的时候,在回收过程中这时该算法无效。
  • 可达性分析算法 
    为了克服引用计数法的弊端,现在比较主流的实现算法是可达性分析算法。该算法的基本思想是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。如果在一次的搜索中,一个对象到GC Roots没有任何的引用链相连,则说明此对象是不可用的。具体如图所示 

  GC Roots的判定: 
  1、虚拟机栈(栈帧中的本地变量表)中引用的对象 
  2、方法区中静态属性引用的对象 
  3、方法区中常量引用的对象 
  4、本地方法栈中JNI(即一般说的Native方法)引用的对象

引用判断过程

判断引用是否无效的过程分为三个阶段 
1、当JVM进行垃圾收集时,JVM使用可达性分析算法进行分析,如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,此时该对象将被第一次标记,并进行一次筛选,筛选的条件是此对象有没有必要执行finalize()方法,如果对象没有覆盖该方法,或者该方法已经被虚拟机调用过了,虚拟机将这两种情况都视为“没有必要执行”。 
2、如果该对象被判定为有必要执行finalize()方法,那么对象将会被放置到一个叫做F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的执行是指虚拟机会触发这个方法,但并不承若会等待它运行结束。因为一个对象可能在finalize()方法中执行缓慢,或者发生了死循环,这将导致该队列中的其他对象长期处于等待阶段,甚至导致整个内存系统的奔溃。 
3、F-Queue中的标记筛选。 
finalize()方法是对象逃脱死亡命运的最后一次机会,然后GC将对F-Queue中的对象进行第二次小规模的标记。如果对象在finalize()方法中成功拯救了自己,即与引用链上的任何一个对象建立关联,那么在第二次标记的时候,该算法将被移出F-Queue的集合,如果对象这个时候还没有逃脱,那基本上它就真的被回收了。

方法区回收

主要回收两部分内容:废弃常量和无用的类

回收废弃常量:和回收堆对象类似,如有一个字符串“abc”,没有任何String对象引用常量池中的“abc”,则可以被回收。

回收无用的类:需要满足三个条件

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
  • 加载该类的ClassLoader已经被回
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

垃圾收集算法简介

目前比较主流的垃圾收集算法有四种:标记-清除算法、复制算法、标记-整理算法、分代收集算法。具体分析对比如下:

分类标记-清除算法(Mark-Sweep)复制算法(Coping)标志-整理算法(Mark-Compact)分代收集算法(Generational Collection)
进行整理 是  
算法实现过程 该算法分为两个过程:标记和清除。先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的要回收的对象。 将内存按容量划分为大小相等的两块区域,每次使用其中的一块,当一块的内存用完了,执行GC算法时将还存活的对象整理复制到另外一块上,然后清理所有的内存块。 该算法分为两个过程:标记和整理。首先标记出所有需要回收的对象,然后让存活的对象都向内存的一端移动,然后直接清除掉端边界以外的内存。 根据对象存活周期的不同将内存划分为几块,一般是划分为新生代和老年代,然后根据各个年代的特点采用不同的最适当的收集算法。
优点 简单,易于实现 内存分配时算法不产生内存碎片 内存分配时算法不产生内存碎片,也比较易于实现 分代收集,效率较高
缺点 1、效率低 2、会产生大量不连续的内存碎片 空间消耗太大,内存被压缩为原来的一半 算法复杂度大,执行步骤较多 算法复杂度大,执行步骤较多

垃圾收集器

常见的JVM垃圾收集器有七种,具体如下图所示: 

如果两个收集器之间存在连线,就说明它们可以搭配使用。

新生代垃圾收集器

  • Serial 
    Serial收集器是单线程的一个收集器,但它的单线程的意义是它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集的时候,必须暂停其他所有的工作线程,直到它收集结束。 
    分代收集算法:新生代单线程采用复制算法,并暂停所有用户线程;老年代单线程采用标记-整理算法,并暂停所有用户线程。
  • ParNew 
    ParNew收集器是Serial收集器的多线程版。其基本操作和Serial算法基本一致。该收集器一般搭配CMS收集器进行工作。‘ 
    分代收集算法:新生代采用复制算法,并暂停所有用户线程;老年代采用标记-整理算法,并暂停所有用户线程。

  

  • Parallel Scavenge 
    Parallel Scavenge收集器是也与ParNew算法十分相似,但是与其他收集器的关注点大多是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器目的是达到一个可控制的吞吐量。吞吐量就是CPU用于运行用户代码的时间与CPU总消耗的时间的比值。即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),举个例子,虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。 
    GC自适应调节策略:JVM会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大吞吐量。Parallel Scavenge收集器可以搭配自适应调节策略。 
    分代收集算法:新生代采用复制算法,并暂停所有用户线程;老年代采用标记-整理算法,并暂停所有用户线程。

老年代垃圾收集器

  • Serial Old 
    Serial Old是Serial算法的老年代版本,同样是一个单线程收集器。该收集器主要是给Client模式下的虚拟机使用的。 
    分代收集算法:新生代单线程采用复制算法,并暂停所有用户线程;老年代单线程采用标记-整理算法,并暂停所有用户线程。

   

  • Parallnel Old 
    Parallnel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

  

  • CMS 
    CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。该收集器是基于”标记-清除“算法实现的。 
    CMS收集器的收集过程分为以下4个步骤: 
    1、初始标记(Stop the World,标记GC Roots能直接关联到的对象) 
    2、并发标记(进行GC Roots Tracing的过程) 
    3、重新标记(Stop the World,休整并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录) 
    4、并发清除(并发清除无用的对象) 
    缺点: 
    a、CMS收集器对CPU资源非常敏感,并发阶段占用的线程资源较多。 
    b、CMS收集器无法处理浮动垃圾。因为CMS并发清理阶段用户线程还在运行着,所以也会有相应的垃圾产生,这部分垃圾CMS无法在此次的收集中处理掉它们。 
    c、CMS收集器由于是基于“标记-清除”算法,故会产生较多的内存空间碎片。

  

 

新生代和老年代垃圾收集器

  • G1 
    G1(Garbage-First)收集器所具备的特点: 
    1、并行和并发:使用多个CPU来缩短Stop-The-World的时间,部分垃圾收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。 
    2、分代收集 
    3、空间整合:标记-整理算法。 
    4、可预测的停顿。追求低停顿,并建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,达到了实时Java的垃圾收集器。 
    G1收集器分代策略: 
      G1收集器将整个Java堆划分为多个大小相等的独立区域(Region)。G1收集器之所以可以有计划地避免在整个Java堆中进行全区域的垃圾收据,是因为G1收集器跟踪各个Region里面的垃圾堆积的价值大小(回收获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。即Grabage-First。 
         在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是通过 
    Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set。在新建对象时,JVM会将相关的引用信息记录到被引用对象所属的Region的Remembered Set中。当进行回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对堆进行扫描也不会有遗漏。 
    G1收集器的手机阶段也分以下几个步骤: 
    1、初始标记(只是标记一下GC Roots能直接关联到的对象,并修改可以得Region中创建新对象,这阶段需要停顿线程,但耗时很短) 
    2、并发标记(从GC Roots开始对堆中对象进行可达性分析,找出存活对象) 
    3、最终标记(修正在并发标记期间因月洪湖程序继续运行而导致标记产生变动的那一部分标记记录) 
    4、筛选回收(首先对各个Regin的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收计划,回收一部分Region)

  

 

最后,我们总结一下JVM中的垃圾收集器:

分类所属分代使用线程使用算法
Serial 新生代 单线程 复制(新)、标记-整理(老)
ParNew 新生代 多线程 复制(新)、标记-整理(老)
Parallel Scavenge 新生代 多线程 吞吐量优先算法
Serial Old 老生代 单线程 复制(新)、标记-整理(老)
Parallel Old 老生代 多线程 复制(新)、标记-整理(老)
CMS 老生代 多线程 标记-清除算法(初始标记、并发标记、重新标记、并发清除)
G1 新生代&&老生代 多线程 标记-整理算法(初始标记、并发标记、最终标记、筛选回收)

垃圾收集器参数总结

 

常用的收集器组合

  新生代GC策略 年老代GC策略 说明
组合1 Serial Serial Old
Serial和Serial Old都是单线程进行GC,特点就是GC时暂停所有应用线程。
组合2 Serial CMS+Serial Old CMS(Concurrent Mark Sweep)是并发GC,实现GC线程和应用线程并发工作,不需要暂停所有应用线程。另外,当CMS进行GC失败时,会自动使用Serial Old策略进行GC。
组合3
ParNew
CMS
使用-XX:+UseParNewGC选项来开启。ParNew是Serial的并行版本,可以指定GC线程数,默认GC线程数为CPU的数量。可以使用-XX:ParallelGCThreads选项指定GC的线程数。
如果指定了选项-XX:+UseConcMarkSweepGC选项,则新生代默认使用ParNew GC策略。
组合4
ParNew
Serial Old 使用-XX:+UseParNewGC选项来开启。新生代使用ParNew GC策略,年老代默认使用Serial Old GC策略。
组合5
Parallel Scavenge
Serial Old
Parallel Scavenge策略主要是关注一个可控的吞吐量:应用程序运行时间 / (应用程序运行时间 + GC时间),可见这会使得CPU的利用率尽可能的高,适用于后台持久运行的应用程序,而不适用于交互较多的应用程序。
组合6
Parallel Scavenge
Parallel Old
Parallel Old是Serial Old的并行版本

 

组合7
G1GC
G1GC
-XX:+UnlockExperimentalVMOptions -XX:+UseG1GC        #开启
-XX:MaxGCPauseMillis =50                  #暂停时间目标
-XX:GCPauseIntervalMillis =200          #暂停间隔目标
-XX:+G1YoungGenSize=512m            #年轻代大小
-XX:SurvivorRatio=6                            #幸存区比例
posted @ 2018-05-04 11:08  Mr.years  阅读(114)  评论(0编辑  收藏  举报