JVM-垃圾收集算法、垃圾收集器

 一、对象已死么?

  判断一个对象是否存活一般有两种方式:

  1、引用计数算法:每个对象都有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1。计数为0时可以回收。(无法解决循环依赖问题

  2、可达性分析算法(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的不可达对象。 

  Java语言中,可作为GC Roots的对象包括下面几种:
  (1) 虚拟机栈(栈帧中的本地变量表) 中引用的对象;
  (2) 方法区中类静态属性引用的对象;
  (3) 方法区中常量引用的对象;
  (4) 本地方法栈中JNI(即一般说的Native方法) 引用的对象;

  要真正宣告一个对象死亡,必须经历两次标记。如果对象在进行可达性分析后发现没有跟 GC Root 相关联,就标记第一次。

  永久代的垃圾收集主要回收两部分内容 :废弃常量和无用的类

  判断一个类是否为“无用的类” 要同时满足下面3个条件:  

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

  是否对类进行回收,HosSpot虚拟机提供了 -Xnoclassgc 参数进行控制,还可以使用 -verbose:class

  以及 -XX:TraceClassUnLoading 查看类加载和卸载的信息。

二、浅谈引用:

  HotSpot虚拟机是通过可达性分析算法来判断对象是否存活的。所以要谈一下Java的引用,Java的引用分为:强引用(Final Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)

  这4种引用在java.lang.ref包下:

  

  强引用(Final Reference)

   就是指在程序代码中普遍存在的,类似Object obj = new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

   强引用具备以下三个个特点:

   1、强引用可以直接访问目标对象;
   2、强引用锁指向的对象在任何时候都不会被系统回收。JVM宁愿抛出OOM异常也不回收强引用所指向的对象;
   3、强应用可能导致内存泄露;

  软引用(Soft Reference)

    是用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。

  对于软引用关联着的对象,如果内存充足,则垃圾回收器不会回收该对象,如果内存不够了,就会回收这些对象的内存。

   弱引用(Weak Reference)

    用来描述非必须的对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发送之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。一旦一个弱引用对

  象被垃圾回收器回收,便会加入到一个注册引用队列中。

   虚引用(Phantom Reference)

    虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个持有虚引用的对象,和没有引用几乎是一样的,随时都有可能被垃圾回收器回收。当试图通过虚引用的get()方法取得强引用时,总是会失败。并且,虚

  引用必须和引用队列一起使用,它的作用在于跟踪垃圾回收过程。

三、垃圾收集算法:

  1、标记-清除算法

    标记-清除算法分为两个阶段“标记”阶段和“清除”阶段.。首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点

 进行改进而得到的。

    它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法

  找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

                                             

  2、复制算法

   复制(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。新生代

 一般用复制算法。

   这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长

 生存期的对象则导致效率降低。

                                  

  3、标记-整理算法

   复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老

        年代一般不能直接选用这种算法。

   根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边

        界以外的内存。

                        

  4、分代收集算法

   GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。

   分代收集(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算

        法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

四、垃圾收集器:

                      

  1、Serial (Serial Old) 收集器:

    Serial 是串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩算法

         这个收集器的Minor GC 和 Full GC都是以Stop The World 的方式运行。

    在新生代的名称是“Default New Generation”,在GC日志文件中的名称是“DefNew”,是Client模式下新生代的默认垃圾收集器。

    Serial Old 是单线程收集器,采用标记-整理算法。这个收集器的主要意义是给Client模式下的虚拟机使用;

    使用场景:Serial垃圾收集器常用于在同一台机器运行超多的jvm(通常是jvm的数量比处理器的数量要多)。在这种环境下,最后使用一个cpu进行垃圾回收,尽量减少对其他jvm的影响(其他jvm需要使用剩下的cpu)。但垃圾回收的时间可能会很长。也适用于一些具有小量核心和小量内存的嵌入式硬件

    参数控制:

      -XX:+UseSerialGC 串行收集器

  2、ParNew收集器:

      ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩算法。

      在新生代的名称是"Parallel New Generation" 在GC日志文件中的名称是  "ParNew" ,是Serial的多线程版本,

                      是Server模式下的首选的新生代收集器,只有parNew收集器能够跟CMS 收集器配合使用。

    参数控制:

      -XX:+UseParNewGC ParNew收集器

      -XX:ParallelGCThreads 限制线程数量

  3、Parallel Scavenge (Parallel Old)收集器

         吞吐量为先!并行的收集器,默认并行垃圾回收器会使用和机器cpu数目一致的垃圾回收器线程进行回收:

    Parallel Scavenge(并行的多线程收集器,多线程执行年轻代的垃圾回收,单线程执行老年代的垃圾收集)收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供

    最合适的停顿时间或最大的吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间));也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-压缩。

    在GC日志文件中的名称是 "PSYoungGen"。

    自适应调节也是Parallel Scavenge收集器和ParNew收集器最的大区别。

    并行线程默认值:

      CPU核数 <= 8 : = CPU核数

      CPU核数 > 8 := (3  + CPU核数*5)/8

    也可以强制指定线程数: -XX:ParallelGCThreads=4

    可取消这一特性-XX:-UseAdaptiveSizePolicy

    Parallel Old Parallel Scavenge的老年代版本,使用多线程和标记-整理算法

    使用场景:并行垃圾回收器用于需要进行大量的工作以及可用接受较长的jvm暂停时间的情况。例如:想打印报告和执行大量的数据库查询等批处理过程。

    参数控制:

      -XX:+UseParallelGC 年轻代使用Parallel收集器 + 老年代使用Serial收集器

      -XX:ParallelGCThreads 控制线程数量

     精确控制吞吐量参数:

       -XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间,值是一个大于0的毫秒数

       -XX:GCTimeRatio 直接设置吞吐量大小。它的参数是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数

       -XX:UseAdaptiveSizePolicy 这个参数打开后,就不需要手工指定新生代大小(-Xmn)、Eden区和Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了。

  4、CMS收集器--低延迟为先并发的收集器

   CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器(基于标记-清除算法实现)。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

   CMS收集器的GC周期由6个阶段组成,其中 4 个阶段 (名字以 Concurrent 开始的) 与实际的应用程序是并发执行的,而其他 2 个阶段需要暂停应用程序线程。

   (1) 初始标记(CMS initial mark):标记从外部(即GC Root)直接可达的老年代对象

   (2) 并发标记(CMS concurrent mark):标记从上一步标记的老年代对象可达的存活对象,在并发标记期间应用可能正在运行并更新引用,所以再并发阶段结束时,未必所有存活的对象都能确保被标记,为了应对这种情况,应用需要再次停顿进行重新标记。

   (3) 并发预清理:改变当运行第二阶段时,由应用程序线程产生的对象引用,以更新第二阶段的结果。

   (4) 重新标记(CMS Remark):重新遍历所有在并发标记期间有变动的对象并进行最后的标记。

   (5) 并发清除(CMS concurrent sweep)清除整个Java堆,释放没有迁移的垃圾对象。

   (6) 并发重置:收集器做一些收尾的工作,以便下一次 GC 周期能有一个干净的状态。

   其中始标记、重新标记这两个步骤仍然需要 “Stop The World。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

   CMS收集器的缺点:

   CMS收集器与ParNewParallel收集器相比较有一个缺点,就是需要更大的Java堆空间。老年代是标记-清除算法,所以缺乏压缩和形成空间碎片化,这将导致垃圾收集器无法最大程度地利用所有可用的空闲空间。

   浮动垃圾concurrent sweep 阶段有新的垃圾产生,只能下一次GC的时候被收集;GC的同时应用也在运行,老年代需要预留一些空间,达到一定比例即触发GC(-XX:CMSInitiatingOccupancyFraction=68指定,68%是默认值),当预留空间不够,出现Concurrent Mode Failure。

   什么叫做浮动垃圾?假如你标记了一个对象,然后用户线程说,这个对象我不要了,你回收吧。这个时候怎么办?其实这个时候暂时没办法处理,只能留到下一次 GC 的时候再回收,这次GC 不好意思,GC 不了,这个就叫做浮动垃圾。

   内存碎片:free list 中没有合适内存空间来分配对象,只能触发Full GC

   由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。

   老年代收集器(新生代使用ParNew

    使用场景:CMS回收用于应用程序要求具有较低的jvm暂停时间,例如桌面ui程序,web服务器,数据库查询。

    优点: 并发收集、低停顿
    缺点: 产生大量空间碎片、并发阶段会降低吞吐量

    参数设置:

    -XX:+UseConcMarkSweepGC 使用CMS收集器
    -XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理,用于在CMS收集器顶不住要进行Full GC时开启内存碎片合并整理;整理过程是独占的,会引起停顿时间变长(默认开启的)

    -XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理
    -XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)

  5、G1收集器

      G1收集器是面向Server的收集器,与CMS相比有以下特点:

    1、空间整合:G1收集器采用标记-整理算法,不会产生内存空间碎片,分配大对象时不会因为找不到连续的空间而提前触发一次Full GC。

    2、可预测停顿:这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收

                集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

     上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽

           然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。

                              

    G1的新生代收集跟ParNew类似,当新生代占用达到一定比例的时候,开始出发收集。和CMS类似,G1收集器收集老年代对象会有短暂停顿。

      收集步骤:    

    1、标记阶段,首先初始标记(Initial-Mark),这个阶段是停顿的(Stop the World Event),并且会触发一次普通Mintor GC。对应GC log:GC pause (young) (inital-mark)

    2、Root Region Scanning,程序运行过程中会回收survivor区(存活到老年代),这一过程必须在young GC之前完成。
    3、Concurrent Marking,在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收(图中打X)。同

                 时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。

                           

    

    4、Remark, 再标记,会有短暂停顿(STW)。再标记阶段是用来收集并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。

    5、Copy/Clean up,多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。

                           

    6、复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。

    JVM默认的垃圾收集器组合:

      jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

      jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

      jdk1.9 默认垃圾收集器G1

五、常用的收集器组合:

                         

参考:

【1】微信,http://mp.weixin.qq.com/s?__biz=MzI4NDY5Mjc1Mg==&mid=2247483952&idx=1&sn=ea12792a9b7c67baddfaf425d8272d33&chksm=ebf6da4fdc815359869107a4acd15538b3596ba006b4005b216688b69372650dbd18c0184643&scene=21#wechat_redirect

【2】《深入理解Java虚拟机:JVM高级特性与最佳实践》(第二版)周志明

【3】博客,http://www.importnew.com/20468.html

【4】https://github.com/13428282016/elasticsearch-CN/wiki/jvm%E7%9A%84%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8%E6%9C%89%E5%93%AA%E4%BA%9B%EF%BC%8C%E5%90%84%E8%87%AA%E7%9A%84%E4%BD%BF%E7%94%A8%E6%83%85%E6%99%AF%E6%98%AF%E4%BB%80%E4%B9%88

【5】《Java性能优化权威指南》

【6】极客学院,http://wiki.jikexueyuan.com/project/jvm-parameter/cms.html

【7】ImportNew,http://www.importnew.com/1993.html

原文地址:https://www.cnblogs.com/happyflyingpig/p/8795332.html