JVM——垃圾回收

目录:

  •   如何判断垃圾是否回收?
    • 引用计数法
    • 可达性分析算法
    • 四种引用
    • 引用队列
  •   垃圾回收算法
    • 标记清除算法
    • 复制算法
    • 标记整理算法
  •   分代垃圾回收
    • 新生代
    • 老年代
    • Minor GC 和 Full GC的区别
    • 总结
  •   垃圾回收器
    • 原理
    • 串行回收器
    • 吞吐量优先
    • 获取最短停顿时间优先(CMS)
    • G1
  •   垃圾回收调优 
    • 方向
    • 新生代调优
    • 老年代调优
    • 案例

一、如何判断垃圾是否回收

  1.1 引用计数法

  在对象头处维护一个counter,每增加一次对该对象的引用计数器自加,如果对该对象的引用失联,则计数器自减当counter为0时,表明该对象已经被废弃,不处于存活状态。这种方式一方面无法区分软、虛、弱、强引用类别。另一方面,会造成死锁,假设两个对象相互引用始终无法释放counter,永远不能GC。

  

    a引用者b,b引用者a,永远无法gc

   1.2 可达性分析算法

    1.Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象

    2.扫描堆中的对象,GC Roots对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。(后面再详细介绍)

    3.GC停顿 

      可达性分析期间需要保证整个执行系统的一致性,对象的引用关系不能发生变化,从而导致GC进行时必须停顿所有Java执行线程(称为"Stop The World");

    

   1.3 四种引用

    1. 强引用  (Strong Reference): 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收

      Java中默认声明的就是强引用,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了

    2. 软引用(SoftReference): 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象(可以配合引用队列来释放软引用自身)

      在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象。

      在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用

      软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中

    3. 弱引用(WeakReference): 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 (可以配合引用队列来释放弱引用自身)

      弱引用与软引用的区别在于:弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

      jdk也提供了java.util.WeakHashMap这么一个key为弱引用的Map。

      弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

    4. 虚引用(PhantomReference): 仅有虚引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收虚引用对象,必须配合引用队列使用      

      “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动( ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法释放直接内存)。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

    以上引用不理解的可以进这里看代码详解https://blog.csdn.net/rodbate/article/details/72857447

    5. 终结器引用(FinalReference) :无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象 暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象 

  1.4 引用队列

    引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。

 

二、垃圾回收算法

  2.1 标记清除算法( Mark Sweep)

    首先通过可达性分析算法标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

  

    总结: 1.标记和清除都需要遍历对象,效率不是很高

        2.清除之后会导致很多内存碎片问题,使得下一次内存分配管理成本很高

 

  2.2 复制算法 (copy)

    将可用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块内存用完时,就将还存活的对象复制到另外一块上,然后把已使用过的内存空间一次性清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,简单高效。

    

    总结:1.内存的利用效率变低,必须有额外内存作为备用

       2.解决了内存碎片的问题。

       3.新生代中采用这种算法效率较高。

 

   2.3 标记整理算法(Mark Compact)

    根据老年代的特点,提出了一种“标记-整理”算法,标记过程与“标记-清除”算法一样,不同的是这种算法不直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

     

     总结:1.标记和整理的效率都不高

        2.解决内存碎片问题

        3.老年代采用这种算法

 

  问:那种算法适合JVM垃圾回收器?    

    1. 主要看对象存活时间
    2. JVM根据对象存活时间不同采用分代回收算法

 

三、分代垃圾回收(分代回收算法)

   Java虚拟机将堆内存划分为新生代、老年代,是垃圾回收的主要区域

  

  3.1 新生代(Young):

    新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。

    HotSpot将新生代划分为三块,一块较大的Eden空间和两块较小的Survivor空间默认比例为8:1:1。划分的目的是因为HotSpot采用复制算法来回收新生代,新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。

      GC开始时,对象只会存在于Eden区From Survivor区To Survivor区是空的(作为保留区域)。GC进行时Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15(最大寿命),新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中没有达到阀值的对象会被复制到To Survivor区接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。

  3.2 老年代:(Old) :

     在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。

  3.3 Minor GC 和 Full GC的区别:

    Minor GC:Minor GC指发生在新生代的GC,因为新生代的Java对象大多都是朝生夕死,所以Minor GC非常频繁,一般回收速度也比较快。当Eden空间不足以为对象分配内存时,会触发Minor GC。

              Full GC   :Full GC指发生在老年代的GC,出现了Full GC一般会伴随着至少一次的Minor GC(老年代的对象大部分是Minor GC过程中从新生代进入老年代),Full GC的速度一般会比Minor GC慢10倍以上。当老年代内存不足或者显式调用System.gc()方法时,会触发Full GC。所以我们应该尽量避免或者减少Full GC的发生。

  3.4 小总结

  1. 对象首先分配在伊甸园区域
  2. 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的 对象年龄加 1并且交换 from to
  3. minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  4. 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
  5. 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时 间更长
  6. 新生代采用复制算法
  7. 老年代采用标记整理算法+标记删除算法
  8. 大对象直接进入老年代

  3.5 相关VM参数

    

 

四、垃圾回收器(垃圾收集器)

   在介绍垃圾回收器之前,我们要懂得怎样评估一个垃圾回收器的好坏?

    1.吞吐量指在应用程序的生命周期内,应用程序所花费的时间和系统总运行时间的比值。系统总运行时间=应用程序耗时+GC 耗时。如果系统运行了 100分钟,GC 耗时 1分钟,那么系统的吞吐量就是 (100-1)/100=99%。从吞吐量这个层面可以大致去评估一个垃圾回收器的效率。

    2. 停顿时间指在进行垃圾回收的时候,中断系统应用线程的时间。当然我们是希望不中断系统的应用线程,让JVM垃圾回收和应用线程同时进行,但是这种情况往往会增大垃圾回收时间,同时也会导致系统的应用线程处理速度变慢,也会导致吞吐量会降低。总之,停顿时间越短,越是我们希望看到的

  垃圾回收器可以按照线程分为 串行和并行,按照以往划分:

    1.新生代串行: Serial

    2.新生代并行:   ParNew、Parallel Scavenge

    3.老年代串行:   Serial Old

    4.老年代并行:   CMS、Paralled Old

    5.G1

  在这里为了清晰理解我按照功能目的进行划分:

  4.1 串行 :-XX:+UseSerialGC = Serial + SerialOld

    1.SerialSerialOld这两款收集器,分别用于新生代和老年代的垃圾收集工作。

    2.Serial收集器是一款新生代的垃圾收集器,使用标记-复制垃圾收集算法Serial收集器只能使用一条线程进行垃圾收集工作,并且在进行垃圾收集的时候,所有的工作线程都需要停止工作,等待垃圾收集线程完成以后,其他线程才可以继续工作(STW)。虽然只能使用单核CPU,但是正是由于它不能利用多核,在一些场景下,减少了很多线程的上下文切换的开销,可以在进行垃圾收集过程中专心处理GC过程,而不会被打断,所以如果GC过程很短暂,那么这款收集器还是非常简单高效的。

    3.Serial Old收集器是Serial收集器的老年代版本,它也是一款使用标记-整理算法的单线程的垃圾收集器。

    特点:1. 它是使用单线程回收,也就是说它在进行垃圾回收的时候只会启动一个线程

       2. 在进行回收的时候会暂停系统应用线程(用户线程)。

    

  4.2 以吞吐量优先:-XX:+UseParallelGC ~ -XX:+UseParallelOldGC (1.8默认)

     1.Parallel ScavengeParalled Old这两款收集器,分别用于新生代和老年代的垃圾收集工作

     2.Parallel Scavenge收集器是是一款新生代的收集器,它使用标记-复制垃圾收集算法。Parallel Scavenge收集器关注的是如何控制系统运行的吞吐量。

       Parallel Scavenge收集器提供了两个参数用于控制吞吐量。-XX:MaxGCPauseMillis用于控制最大垃圾收集停顿时间,-XX:GCTimeRatio用于直接控制吞吐量的大小

       MaxGCPauseMillis参数的值允许是一个大于0的整数,表示毫秒数,默认值为200毫秒,收集器会尽可能的保证每次垃圾收集耗费的时间不超过这个设定值。但是如果这个这个值设定的过小,那么ParallelScavenge收集器为了保证每次垃圾收集的时间不超过这个限定值,会导致垃圾收集的次数增加和增加新生代的空间大小,垃圾收集的吞吐量也会随之下降。

      GCTimeRatio这个参数的值应该是一个0-100之间的整数,表示应用程序运行时间和垃圾收集时间的比值。一般把值设置为19,即系统运行时间 : GC收集时间 = 19 : 1,那么GC收集时间就占用了总时间的5%(1 /(19 + 1) = 5%),该参数的默认值为99,即最大允许1%(1 / (1 + 99) = 1%)的垃圾收集时间。

      Parallel Scavenge收集器还有一个参数:-XX:UseAdaptiveSizePolicy。这是一个开关参数,当开启这个参数以后,就不需要手动指定新生代的内存大小(-Xmn)、Eden区和Survivor区的比值(-XX:SurvivorRatio)以及晋升到老年代的对象的大小(-XX:PretenureSizeThreshold)等参数了,虚拟机会根据当前系统的运行情况动态调整合适的设置值来达到合适的停顿时间和合适的吞吐量,这种方式称为GC自适应调节策略。

     3.Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用标记-整理算法。Parallel Old垃圾收集器和Parallel Scavenge收集器一样,也是一款关注吞吐量的垃圾收集器,和Parallel Scavenge收集器一起配合,可以实现对Java堆内存的吞吐量优先的垃圾收集策略。

    

   4.3 以获取最短停顿时间优先  :  -XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld

  

    CMS垃圾回收器比较注重回收的停顿时间,以最短回收停顿时间为目的的收集器。CMS 是 Concurrent Mark Sweep 的缩写,意为并发标记清除,从名称上可以得知,它使用的是标记-清除算法,同时它又是一个使用多线程并发回收的垃圾收集器。一般比较注重服务端性能的时候使用它,它的回收回收过程可以分成四个步骤:   

    1.初始标记(CMS initial mark)阶段

    2.并发标记(CMS concurrent mark)阶段

    3.重新标记(CMS remark)阶段

    4.并发清除(CMS concurrent sweep)阶段

    从图中可以看出,在这4个阶段中,初始标记和重新标记这两个阶段都是只有GC线程在运行,用户线程会被停止,所以这两个阶段会发生STW(Stop The World)。初始标记阶的工作是标记GC Roots可以直接关联到的对象,速度很快。并发标记阶段,会从GC Roots 出发,标记处所有可达的对象,这个过程可能会花费相对比较长的时间,但是由于在这个阶段,GC线程和用户线程是可以一起运行的,所以即使标记过程比较耗时,也不会影响到系统的运行。重新标记阶段,是对并发标记期间因用户程序运行而导致标记变动的那部分记录进行修正,重新标记阶段耗时一般比初始标记稍长,但是远小于并发标记阶段。最终,会进行并发清理阶段,和并发标记阶段类似,并发清理阶段不会停止系统的运行,所以即使相对耗时,也不会对系统运行产生大的影响。

    缺点:

    1. 虽然在并发清理过程不会导致线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,导致总的吞吐量会降低。

    2. CMS收集器无法处理浮动垃圾。由于CMS并发清理阶段用户线程还在运行,伴随程序运行自然就会产生新的垃圾,这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉他们,只好留在下一次GC时再清理掉。由于垃圾收集阶段用户线程还需要运行,那也就需要预留出足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满的时候才进行收集,需要预留一部分空间提供并发收集时的程序运行使用。在JDK1.5中,默认设置的是68%的空间后会被激活,这是一个偏保守的方案。可以自己调节这个比例。如果在CMS运行期间预留的内存无法满足程序需要,这个时候就会出现”Comcurrent Mode Failure”失败,这是细腻集就会启动后背预案,临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

    3. CMS使用的是“标记-清除”算法实现的收集器。这种算法是不带内存整理的,必然会产生很多的内存碎片。如果要分配一个大的对象,但是这时无法满足内存分配,就必须提前触发一次FULLGC,那一般情况下CMS收集器如果需要提前进行FULL GC的时候会开启内存碎片的合并整理,整理的过程是无法并发执行的,这个时候停顿的时间就会变长。

    
  4.4 G1 (Garbage First)

    G1(Garbage First)垃圾收集器是当今垃圾回收技术最前沿的成果之一。在JDK7就已加入JVM的收集器大家庭中,成为HotSpot重点发展的垃圾回收技术。同优秀的CMS垃圾回收器一样,也同样适合大尺寸堆内存的垃圾收集,官方也推荐使用G1来代替选择CMS。G1最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。

    4.4.1G1收集器

    1.G1的设计原则是"首先收集尽可能多的垃圾(Garbage First)"。因此,G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部采用了启发式算法,在老年代找出具有高收集收益的分区进行收集

    2.G1采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此G1天然就是一种压缩方案(局部压缩);

    3.G1虽然也是分代收集器,但整个内存分区不存在物理上的新生代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换;

    4.G1的收集都是STW的,但新生代和老年代的收集界限比较模糊,采用了mixed gc的方式。即每次收集既可能只收集新生代分区(新生代收集),也可能在收集新生代的同时,包含部分老年代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。

    5.同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms 

    6.整体上是 标记+整理 算法,两个区域之间是 复制 算法

    4.4.2 分区:

  


    4.4.3 垃圾回收阶段

    

 

    4.4.4  Young Collection 

      1.当eden数据满了,则触发G1 YGC ,会STW

      2.并行的执行:YGC 将 eden region 中存活的对象拷贝到survivor,或者直接晋升到Old Region中;将Survivor Regin中存活的对象复制到新的Survivor或者晋升old region。

      

      

 

 

 

      

 

 

 

     4.4.5 Young Collection + CM

      1.在 Young GC 时会进行 GC Root 的初始标记

      2.老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定 

    

 

 

       

 

 

     4.4.6  Mixed Collection 

       会对 E、S、O 进行全面垃圾回收

      最终标记(Remark)会 STW

      拷贝存活(Evacuation)会 STW

      

 

       (以上之所以只有俩老年代拷贝存货,是因为根据最大暂停时间有选择的进行高收益回收,见4.4.1第一条)

    4.4.7 FULL GC  概念辨析

      SerialGC

        新生代内存不足发生的垃圾收集 - minor gc

        老年代内存不足发生的垃圾收集 - full gc

      ParallelGC

        新生代内存不足发生的垃圾收集 - minor gc

        老年代内存不足发生的垃圾收集 - full gc

      CMS

        新生代内存不足发生的垃圾收集 - minor gc

        老年代内存不足(分情况)

      G1 新生代内存不足发生的垃圾收集 - minor gc

        老年代内存不足 

          收集速度>垃圾产生速度

          收集速度<垃圾产生速度   full gc

    4.4.8 Young Collection 跨代引用

      新生代回收的跨代引用(老年代引用新生代)问题

    

 

 

       1.卡表与 Remembered Set 减少搜索范围

       2.在引用变更时通过 post-write barrier(写屏障,异步) + dirty card queue

       3.concurrent refinement threads 更新 Remembered Set

 

     

 

    4.4.9  remark阶段

        pre-write barrier + satb_mark_queue

 

       

 

 

     由于并发标记是并发的,对象引用可能发生进一步变化。因此,应用程序线程会再一次被暂停(stw)以更新这些变化,并且在进行实际的清理之前确保一个正确的对象引用视图。这一阶段十分重要,因为必须避免收集到仍被引用的对象。

 

五、GC调优

  5.1 调优领域

    内存 锁竞争 cpu 占用 io

   5.2  新生代调优

    新生代的特点:

       1.所有的 new 操作的内存分配非常廉价

         TLAB thread-local allocation buffer

      2.死亡对象的回收代价是零

      3.大部分对象用过即死

      4.Minor GC 的时间远远低于 Full GC

    新生代越大越好吗?

    -Xmn Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery). GC is performed in this region more often than in other regions. If the size for the young generation is too small, then a lot of minor garbage collections are performed. If the size is too large, then only full garbage collections are performed, which can take a long time to complete. Oracle recommends that you keep the size for the young generation greater than 25% and less than 50% of the overall heap size. 

    新生代能容纳所有【并发量 * (请求-响应)】的数据

     幸存区大到能保留【当前活跃对象+需要晋升对象】

    晋升阈值配置得当,让长时间存活对象尽快晋升

      -XX:MaxTenuringThreshold=threshold

      -XX:+PrintTenuringDistribution

  5.3 老年代调优

    以 CMS 为例:

      1.CMS 的老年代内存越大越好

      2.先尝试不做调优,如果没有 Full GC 那么已经...,否则先尝试调优新生代

      3.观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3     

      4.-XX:CMSInitiatingOccupancyFraction=percent

  5.4 案例:

      案例1 Full GC 和 Minor GC频繁

      案例2 请求高峰期发生 Full GC,单次暂停时间特别长 (CMS)

      案例3 老年代充裕情况下,发生 Full GC (CMS jdk1.7)

posted @ 2019-11-21 03:06  萧然成长记  阅读(266)  评论(0编辑  收藏  举报