第二部分-垃圾回收算法以及算法实现和垃圾收集器

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jtracydy/article/details/80056654

嘚不嘚:5月份之前把要写博客更新完的计划可能完不成了,利用五一假期多搞一点吧,计划完不成真是挺糟糕的,计划完不成就相当于堆积任务,堆积越多的事情,就越容易失去信心,越容易放弃。小伙伴你给自己制定计划了吗?付出实际行动了吗?如果问题希望各位大神多多不吝赐教。

概述
  1. 整体思路:确定要回收的区域->确定要回收的对象->确定要采用回收对象的方法->确定要使用的垃圾收集器。
  2. 动态分配:JVM在运行期间会动态的为对象分配内存,这部分内存在分配和收回期间都是动态的。为对象分配内存的大小只有在JVM运行期间才能确认,因此会出现一系列的GC问题。
  3. 确定要回收的对象:可达性分析算法、引用计数法。我的另一篇博客有介绍。确定要回收的对象 。利用可达性分析算法标记存活的引用的对象。
  4. 引用:四种引用类型,引用的类型主要和GC相关。
    1. 强引用:GC时,只要该引用存在,该引用指向的对象不会被收回。
    2. 软引用:GC时,首次不会被回收,第二次会被回收。
    3. 弱引用:GC时,直接被回收。
    4. 虚引用:无法通过虚引用来获取一个对象,仅仅是一个通知,在GC时,系统会收到一个系统通知。
  5. finalize方法简介:GC时,如果对象首次调用此方法,此对象不会可能不会被回收,完成自我的救赎,二次调用此方法的对象会被回收。
  6. 内存回收的主要区域:堆和方法区。堆是GC中最大的那一块,而方法区的主要是类型卸载和回收字面量。
    1. 字面量:String s = "abc"; 字符串常量池中abc在JVM运行过程中没有任何的地方使用该字面量,也就是说没有任何的字符串的引用地址指向字面量abc的。
    2. 类型卸载:条件比较苛刻,回收的效率较低。
      1. 该类的所有实例都已经被回收,堆中不存在任何该实例的对象。
      2. 该类对应的java.lang.class对象没有在任何地方被引用,也就是说无法通过反射获取类的任何信息。
      3. 加载该类的classLoader已经被回收。
垃圾收集算法以及实现

垃圾收集器有多种,每种采用不用的垃圾收集算法,每种垃圾收集算法各有自己的优势。

垃圾收集算法
  1. 标记清除(Mark-sweep):

    1. 过程:该算法分为两个阶段,标记阶段和清理阶段。标记阶段:标记出堆内存中要回收的对象。清理阶段:直接清理掉被标记的内存。
    2. 不足:
      1. 空间问题:内存中会产生内存碎片,在为对象进行内存配时,内存区域的可分配内存大与对象的所需内存,所以无法为对象分配内存,会触发一次GC。
      2. 效率问题:标记和清理阶段的效率都不高。
  2. 复制算法(copying):

    1. 过程:将内存区域分为两部分,为对象分配内存时,只使用其中的一块,在回收内存时,将存活的对象复制到另一块内存,同时清理掉为对象分配内存区域的部分。
    2. 不足:
      1. 空间利用率较低,对象复制时,如果另一块内存无法提供足够的空间时,需要其他的内存担保,也就是将对象分配到其他的内存空间。
      2. 如果为对象分配内存区域的存活率比较高时,复制算法的效率比较低。
    3. 优点:
      1. 优化了标记清除算法效率问题,该算法实现简单,运行高效。
      2. 不会产生空间碎片。
  3. 标记整理(Mark-compact):

    1. 过程:标记存活的对象,将存活的对象移动到内存的一侧,另一侧的空间直接被清理。
    2. 优点:不会产生内存碎片,空间利用率比较高,不需要用内存担保。
  4. 分代回收法:现在比较流行的收集算法,根据堆内存的区域划分,对不同的区域采用不同的垃圾回收算法。

堆内存区域划分:

堆内存主要分为两个区域,年轻代和老年代,永久代就是方法区,在JDK8以后被MetaSpace取代了。堆内存各个区域的划分可以用JVM参数配置。

  1. 新生代:
    1. 区域划分:Eden、SurvivorTo、SurvivorFrom。
      1. Eden:为对象进行内存分配时,通常直接在Eden区域中为对象分配内存空间,如果Eden区域不足,会触发Monir GC,如果GC后还是无法为对象分配内存,可以直接将对象分配到老年代。
      2. SurvivorTo:每次为对象分配内存时,都会保留SurvivorFrom空间,在进行GC时,将存活的对象直接复制到SurvivorFrom空间,在GC结束后,交换SurvivorFrom和SurvivorTo的空间,SurvivorFrom是GC后存活对象复制的空间。
  2. 老年代:相对比较稳定,采用标记清理算法,对象的存活时间相对比较久,也有的对象直接在老年代进行内存分配。Major GC时,通常会伴随着Minor GC,通常Major GC的时间比较长,Major GC过后老年代会产生内存碎片。
垃圾收集算法实现
  1. 枚举根节点:

    1. 作用:可达性分析法则是从引用出发标记所有存活的对象,引用主要存在位置:虚拟机栈、方法区中静态属性引用的对象、方法区中常量引用的对象和本地方法栈中引用的对象。乍一看引用存在的区域这么多,需要标记的存活的对象区域范围好大,标记阶段势必花费很多时间。枚举根节点的作用就是确认引用的范围。
    2. 实现:利用数据结构OopMap确认那些地方存储着对象的引用,在类加载完成时,,引用的作用范围就是已知的,超过这个范围此引用就不在生效。
    3. 一致性:程序执行的过程中,引用之间的关系是不断发生变化的,在标记存活对象的阶段需要在某个时刻达到一致性,即该阶段对象和引用的关系不发现变化,所以在这个一致性的点需要停止所有线程,才能准确的标识GC Roots的对象。
  2. 安全点:

    1. 概述:JVM 没有为每条指令都生成OopMap结构,只有在安全点才记录了当前引用的信息,也只有在安全点才能够让各个线程停止进行GC,安全点不能过少,如果过程每次程序GC等待的时间太长。
    2. 问题:如何让JVM中运行的多个线程同时达到安全点就行GC,两种方式
      1. 抢断式中断:在GC时,JVM主动停止所有的线程,如果该线程不在安全点,那么让该线程跑到安全点,然后进行GC。
      2. 主动式中断:设置标识,让线程不断轮询这个标识,发现中断时就轮询挂起,安全点和轮询标识重合。
  3. 安全区域:

    1. 作用:处于Blocked和sleep中的线程在GC时应该处于安全区域才能保证GC时,该线程的状态不发生变化。线程在安全区域时,不必理会处与安全区域中的线程,当安全区域的线程要离开时,需要检查GC是否完成或者系统是否已经完成了根节点枚举。
  4. 目的:GC时,运行线程需要在安全点停止,同时在安全点处,确认枚举根节点,处于Blocked和sleep中的线程无法达到安全点,所以处于Blocked和sleep中的线程应该在安全区域才能够进行GC。

垃圾收集器
  1. 概述:这部分主要介绍一些学习这部分过程中不太理解的一定定义和概念。

    1. 并发与并行:
      1. 并发:单核CPU,利用时间块轮询执行线程。几乎同时打开谷歌浏览器和IE浏览器两个进程,它们是CPU轮询分配时间块执行的。
      2. 并行:多核CPU,一个CPU用来执行谷歌浏览器,一个CPU执行IE浏览器。
        多核和单核的处理器可能在单个任务上执行看不出什么区别,当运行多个线程时,多核CPU整体上的运行时间会比单核少一些,因为多核CPU既存在并发,又存在并行。
    2. JVM类型:
      1. Client模式:轻量级的C1编译器,适合启动较快和需要内存较小的内存空间应用。例如:GUI程序。
      2. Server模式:重量级的C2编译器,启动较慢,运行时效率较高。适合在服务器端运行程序。
    3. 工作模式:
      1. -Xint:解释模式,在JVM执行过程中,只会调用解释器执行指令。
      2. -Xcomp:编译模式,将所有的字节码编译成本地代码,绕开了解释执行器,但是没有开启JIT编译器。
      3. -Xmixed: 混合模式执行,同时使用解释模式和编译模式,在适当的时候对代码进行JIT编译执行,以提供效率,同时还支持解释器执行,对于只执行一次的指令,解释器执行会比JIT编译器执行的效率高。
    4. GC的类别
      1. Minor GC:也成为Young GC,是指发生在新生代的垃圾收集动作。
      2. MajorGC/Full GC:老年代的垃圾收集动作,有能会伴随着Minor GC。
  2. GC收集器的种类

    1. 垃圾收集器的关系图,有连线的可以相互配合使用。
      垃圾收集器关系图
    2. Serial:
      1. 采用的算法:复制算法
      2. 过程:只会有一条线程和一个CPU对垃圾进行回收,避免了切换线程时的开销,简单而高效,但是大大的浪费了系统的硬件资源。
      3. 并行、并发、单多线程:串行、单线程。
      4. 关注点:尽量减少用户线程的停顿时间。
      5. 用途:Client模式。
        这里写图片描述
    3. ParNew:
      1. 采用的算法:复制算法
      2. 过程:在GC时,暂停用户线程,并行的执行GC线程进行GC操作。如果只有单个CPU处理GC的效率会比Serial收集器的效率低,因为该CPU不光要进行GC还要进行线程的切换,虽然CPU数量的增加,会提高GC时系统资源的有效利用。
      3. 并行、并发、单多线程:并发,多线程。
      4. 关注点:尽量减少用户线程的停顿时间。
      5. 用途:Server模式。
        这里写图片描述
    4. Parallel Scavenge:
      1. Parallel Scavenge收集器和ParNew收集器基本上都是一样的,但是Parallel Scavenge收集器关注的吞吐量,而ParNew关注的是用户线程的暂停时间。Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。
        这里写图片描述
    5. Serial old:
      1. 老年代收集器,采用标记整理算法,主要用于跟其他的收起器搭配使用, 用于Client模式。
    6. Parallel Scavenge old:
      1. 使用多线程和标记整理算法。是server模式下,关注点为吞吐量的GC收集器和Parallel Scavenge搭配使用。
    7. cms:
      1. 采用的算法:标记清除算法。
      2. 并行、并发、单多线程:并发、多线程。
      3. 关注点:尽量减少用户线程的停顿时间。
      4. 用途:Server模式。
      5. 过程:
        1. 标记阶段:标记GC Roots直接能关联到的对象。
        2. 并发阶段:和用户线程一起执行,标记间接和GC Roots对象关联的引用。
        3. 重新标记:标记和用户线程一起执行的过程中,产生变动的引用对象。
        4. 并发清理:和用户线程一起执行,清理被标记的对象。整个GC的过程中耗时最长的及时并发标记和并发清理阶段,在标记阶段和初始标记阶段仍然会停止线程的执行。
      6. 缺点:
        1. GC线程占用用户线程,影响了用户线程的执行效率,增加了GC的时间。
        2. 存在无法回收的垃圾,因为在并发清理阶段,用户线程仍然会执行,所以执行的过程中会产生垃圾,是无法回收的。
        3. 因为采用标记清除算法,所以GC之后,会产生空间碎片,对象进行内存分配时,仍然无法为其分配内存,此时会触发Serial old收集器对整个空间进行GC,但是用户的停顿时间更长了。

    这里写图片描述

    1. G1:

      1. 优点:

        1. 并行与并发:缩短用户线程的停顿时间,利用并发继续执行线程。
        2. 分代回收:分代收集:分代的概念仍然存在,但是G1可以独立管理整个堆内存,采用不同的方式处理新创建对象、存活一段时间的对象和永久代中的对象
        3. 采用标记-整理或者复制算法,不会产品内存碎片。
        4. 可以预测GC的停顿时间。
      2. 处理方式

        1. 区域划分:不在物理上划分老年代和新生代了,将整个堆内存分成多个大小相等的独立区域(region),新生代老年代都是一部分region(不需要连续)集合。
        2. 停顿时间模型:G1收集器每次GC不需要对整个堆进行GC,G1会跟踪各个region垃圾的价值,在后台维护一个优先列表,每次根据回收的时间回收价值最大的垃圾。
        3. 区域划分产生的对象引用问题:各个region都存在相互引用的问题,GC的过程中,判定对象是否存活需要对各个区域进行判断。
        4. Remembered Set:G1中region之间的对象应用以及其他收集器中的新生代和老年代之间的对象引用,JVM都使用Remembered Set避免对整个堆的扫描。G1中每个区域都有自己的Remembered Set,当出现Reference类型的数据进行写时,会产生中断写的操作,检查引用的对象是否在其他的region之中,如果存在,则在被引用对象的region的Remembered Set记录。
      3. 过程:

        1. 初始标记:标记GC Roots直接能关联到的对象,同时修改TAMS的值,让下一阶段程序运行时,能够在正确可用的region创建对象,初始标记阶段需要停顿线程,耗时很短。我理解这个TAMS是一个标识,堆中存在多个region,在并发标记阶段,TAMS标识会让用户尽量在没有标识的region区域进行初始化对象。
        2. 并发标记:利用可达性分析法则,遍历Remembered Set标识对象,并发执行,耗时相对较长。
        3. 最终标记:这个阶段通过线程的Remembered Set Logs记录用户线程并发过程中导致标记变化的那部分记录。最终标记阶段需要把Remembered Set Logs 合并到Remembered Set中,这阶段停顿用户线程,但是可并行执行标记线程。
        4. 筛选回收:对各个Region区域的回收价值和成本进行排序,根据用户所期望的时间执行GC计划。

这里写图片描述

内存分配与回收策略
  1. 概述:自动化内存主要是指对象的自动分配和自动回收。对于对象的分配是指在堆上为对象分配内存,但是也有可能在栈上为对象分配内存。
    1. 栈上分配:JVM提供的一种优化技术,所谓的栈上分配就是指,栈内存充足的情况下,在栈上为对象分配内存。个人理解:如果对象的作用域只是在方法体内,而且对象需要分配的内存小于栈能够提供的内存,就可以直接在栈上直接为对象分配内存,当方法结束时,栈帧出栈,该对象的内存被回收,无需GC就可以直接进行回收的对象。如果对象的作用域超过方法体,也就是说在其他的方法中用到了这个对象,则对象是不能够在栈上分配内存的。
  2. 对象优先在Eden分配:在为对象进行分配时,优先在堆上为对象进行内存分配,如果堆上内存不足,进行Minor GC,如果Minor GC后,内存还是不足,将对象直接分配带老年代。
  3. 大对象直接进入老年代:可以在JVM上配置对象的参数,如果对象所需分配的内存大与3M,则直接可以在老年代为此对象分配内存。
  4. 长期存活的对象进入老年代:每个GC存活下来的对象,age都会加1,增加到一定的程度会直接进入老年代。
  5. 空间分配担保:如果在GC过后年轻代仍然无法为对象分配内存,则需要将内存分配年老代,而这种情况是有危险的,如果年老店内存不足也会出现OOM的。

参考博客:
http://www.importnew.com/20715.html
https://www.cnblogs.com/ygj0930/p/6522828.html
https://blog.csdn.net/u010570551/article/details/55684969

猜你喜欢

转载自blog.csdn.net/jtracydy/article/details/80056654
今日推荐