JVM-java虚拟机垃圾收集

 

  前面有篇文章介绍了JVM的基本信息,而众所周知,Java带有垃圾收集机制,那本篇就介绍一下Java中的垃圾收集算法与垃圾收集器。

 

  朋友,再花20分钟了解下噻 (嘿嘿...)...

 

  在Java开发中,开发者无需关注动态分配与垃圾回收,更加专注于开发事项,那么本篇就介绍一下关于GC (Garbage Collection)方面的一些内容:

  此处提问一个问题:Java中程序计数器,本地方法栈等是线程私有的区域,随着线程的消失而消除,那么此处的垃圾收集是如何进行的呢?

 

  垃圾判定依据

    如何判定一个Java对象(堆中对象)已经成为垃圾对象,或对象已经死亡呢?有两种方式:

      1.引用计数算法:通过判断对象的引用数量,决定对象是否可以被回收。

        给对象中添加一个引用计数器,每有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1;任何时刻计数器值为0表示对象不能再被使用。

        这种算法的实现简单,效率高,但很难解决对象之间循环引用的问题。(Java中不采用此算法

        

      2.可达性分析算法:通过判断对象的引用链是否与根节点可达,决定对象是否可以被回收。

        根搜索算法是通过一些“GC Roots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径成为引用链(Reference Chain),当一个对象没有被GC Roots 的引用链连接的时候,说明这个对象是不可用的。

        

      扩展:如果某一对象与GCRoots没有引用,则一定会进行回收吗?可以看一下对象的自我救赎

 

  垃圾回收算法(理论)

    那判断完对象是垃圾对象,按照什么样的方式(算法)对垃圾对象/死亡对象进行回收呢?

    1.标记-清楚算法(Mark-Sweep)

      该算法分为标记、清除两个阶段,先标记处所有需要回收的对象,在标记完成后统一回收所有被标记的对象。是最基础的收集算法,后续的收集算法都是基于这种思路并对其不足进行改进而得到的。

      它存在两个不足:一是效率问题,因为标记和清楚的过程效率都不高,二是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的内存而不得不提前触发另一次垃圾收集动作。

      

    2.复制算法(Copy)

      复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法适用于对象存活率低的场景,比如新生代。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

      复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。

      

     3.标记-整理算法(Mark-Compact)

      标记—整理算法和标记—清除算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存。标记—整理算法提高了内存的利用率,并且它适合在收集对象存活时间较长的老年代。

      注:标记整理算法与标记清除算法最显著的区别是:标记清除算法不进行对象的移动,并且仅对不存活的对象进行处理;而标记整理算法会将所有的存活对象移动到一端,并对不存活对象进行处理,因此其不会产生内存碎片。

    分代收集算法

      当前商用的虚拟机都采用的是 “分代收集” 算法,其实质上是前三个算法理论在JVM中的一个区域分布,相当于前面三个的综合。

      不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率。一般把Java堆分为新生代和老年代。新生代中,每次垃圾收集都发现有大批对象死去,只有少量存活,就用复制算法;老年代中,对象存活率高、没有额外空间对它进行分配担保,就用“标记-清除”或“标记-整理”算法。

 

      在JVM内存区域章节中,介绍了JVM的划分,而堆空间中分为了新生代、老年代等空间,每个空间的特性决定其使用不同的GC算法。

 

  垃圾收集器(实现)

     确认了垃圾对象,且了解了按照什么样的逻辑进行垃圾对象收集,那具体的是由什么来进行收集操作的呢?

 

    在了解这个内容之前,先得有一个小的讨论,讨论几下几点:

    1.既然每个算法中有有标记相关的一个过程,那么在标记时,整个程序是并行的呢?还是直接停止程序,再进行呢?

      按照图上理论阶段的标记来讲,如果程序再执行中进行垃圾对象标记,则其中会发生大概率的遗漏标记的发生,还有一个是如果某一GCRoot是垃圾对象呢?因此保证该阶段的一致性,此阶段是停止程序的。有一个浅显的例子:你妈打扫房间时,你在扔垃圾,那这种情况下,垃圾会肯定会有遗漏的呐,估计孩子可能被打S吧...所以按照这个逻辑来讲,此时在标记执行的时间点或时间段,程序犹如冻结一般,这个被称为STW事件(Stop The World),停止世界,意思是GC标记时需要停顿所有的Java执行线程。

      STW原因:因为可达性分析算法必须是在一个确保一致性的内存快照中进行。如果在分析的过程中对象引用关系还在不断变化,分析结果的准确性就不能保证。

    2.接上个问题,如果标记阶段程序停顿,那么什么时候,什么时间点停顿呢?如何选择该时间呢?

      STW事件发生的是什么时候?这个就得从与GCRoots的引用链开始介绍了。

      首先得清除哪些对象是GCRoots,这个有两种方式:一是遍历方法区和栈区查找(保守式 GC,消耗太高)。二是通过 OopMap 数据结构来记录 GC Roots 的位置(准确式 GC,且OopMap可以作为安全点,记录该位置可停顿)。

      安全点的作用主要使程序再特定的位置可以停顿,进行GC标记。安全点意味着在这个点时,所有工作线程的状态是确定的,JVM 就可以安全地执行 GC

      那么停顿也有两种:一是抢先式中断,发生GC时,若程序没有停顿到安全点,则恢复并达到安全点时停顿,二是主动式中断,发生GC时,不直接操作线程中断,而是简单地设置一个标志,让各个线程执行时主动轮询这个标志,发现中断标志为真时就自己中断挂起。JVM 采取的就是主动式中断。轮询标志的地方和安全点是重合的。

      安全点(safe Point)主要是针对于正在执行的线程来决定的,那么对于正在阻塞或休眠的线程呢?这个如何处理呢?

      --- 如果线程处于中断状态,则未必会刚好停顿到安全点上,此时引入了一个安全区(Safe Region)的概念,这个表示在该区域内,GC的引用关系是不会发生变化的,任意时刻进行回收都是安全的。

      这儿借助一下参考博客:https://blog.csdn.net/atongmu2017/article/details/80826471

                   http://wuzhangyang.com/2019/01/18/understand-safe-point/

    3.同理,标记对象之后的清除阶段是并行进行的呢?还是停止程序,再进行清除操作呢?这个清除的过程程序会如何进行呢?有什么具体的操作演示呢?

      同理,按照现有的理论来讲,若对象已经被标记为垃圾对象了,那么此时直接进行清除处理即可,这个无关程序是否停顿,随意啦...

      而且清除是否是串行还是并行,这个是与垃圾收集器相关的。

        串行采用单线程处理,适用于单CPU或并发能力弱的系统,当回收器启动后会暂停工作线程,更由于是单线程,导致其STW的时间会很长

        并行采用多线程处理,适用于多CPU或并发能力强的系统,当回收器启动后也会暂停其他工作线程,但由于是多线程,会减少其STW时间

 

                 ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ 

    垃圾收集器  是随着JDK版本的升级而不断的优化,这与JDK不断发展是息息相关的,因此下面介绍的就是JDK中目前已存在的垃圾收集器。

    

    1.Serial 收集器

      最先的收集器,单线程,采用复制算法,用于新生代,进行垃圾回收时,会产生STW,主要适用于单核CPU的场景,可以避免多线程上下文切换带来的消耗。单线程:只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。

    2.ParNew 收集器

      Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样,也是采用复制算法,同样会产生STW。

    3.parallel Scavenge 收集器

      Parallel Scavenge 收集器类似于ParNew 收集器。Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器

      吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。提供了两个参数来控制吞吐量。

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

        -XX:GCTimeRatio 设置吞吐量大小,大于0且小于100的整数,即垃圾收集时间占总时间的比率

    4.Serial Old 收集器

       Serial收集器的老年代版本,它同样是一个单线程收集器,采用标记-整理算法。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。

    5.Parallel Old 收集器

       Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。

    6.CMS 收集器

       CMS(Concurrent Mark Sweep)垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验),是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用。CMS(Concurrent Mark Sweep)收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。

      CMS主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:对CPU资源敏感;无法处理浮动垃圾;它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

    7.G1 收集器

       G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器, 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。

      G1将Java堆内存划分为很多个大小相等的独立区域Region,对新生代老年代也没有物理上的划分了,都是多个Region的集合。因此G1是作用在新生代和老年代的。从整体上看是使用的标记-整理算法,也避免了空间碎片的问题,从Region局部来看也有复制算法

 

       G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。    

 

    8.ZGC 收集器 (目前只支持Linux)

      Java 11包含一个全新的垃圾收集器--ZGC,设计目标是:支持TB级内存容量,暂停时间低(<10ms),对整个程序吞吐量的影响小于15%。 将来还可以扩展实现机制,以支持不少令人兴奋的功能,例如多层堆(即热对象置于DRAM和冷对象置于NVMe闪存),或压缩堆。

        ZGC给Hotspot Garbage Collectors增加了两种新技术:着色指针和读屏障

        着色指针(colored pointers):一种将信息存储在指针(或使用Java术语引用)中的技术。因为在64位平台上(ZGC仅支持64位平台),指针可以处理更多的内存,可使用一些位来存储状态。而取消着色时,采用了多重映射的技巧(没明白);

        读屏障:每当应用程序线程从堆加载引用时运行的代码片段(即访问对象上的非原生字段non-primitive field),读屏障的工作是通过测试加载的引用来执行此任务,以查看是否设置了某些位。

      参考文档:https://blog.csdn.net/j3t9z7h/article/details/87128403

              https://www.cnblogs.com/huanchupkblog/p/10947919.html

           https://www.wangxinshuo.cn/2018/09/09/G1/ZGC/

 

    9.Shenendoah 收集器

      JDK12中的新版本垃圾收集器--Shenandoah收集器,一款concurrent及parallel的垃圾收集器;跟ZGC一样也是面向low-pause-time的垃圾收集器,不过ZGC是基于colored pointers来实现,而Shenandoah GC是基于brooks pointers来实现。

       参看文档:https://blog.csdn.net/qq_33330687/article/details/90314347

 

  JVM调优主要就是调整下面两个指标

    停顿时间:垃圾收集器做垃圾回收中断应用执行的时间。-XX:MaxGCPauseMillis

    吞吐量:垃圾收集的时间和总时间的占比:1/(1+n),吞吐量为1-1/(1+n)。-XX:GCTimeRatio=n

 

 

  (愿你的每一行代码,都有让世界进步的力量    ------   fn)

 

posted @ 2019-08-08 23:44  fn-f  阅读(166)  评论(0编辑  收藏  举报