gc垃圾收集笔记整理

  上一次整理了一下深刻理解jvm虚拟机内存,本章来整理一下gc垃圾收集。html

  虚拟机内存分为线程隔离内存部分和线程共享内存部分,其中线程隔离部分包括程序计数器,虚拟机栈和本地方法栈;线程共享部分分为堆和方法区。线程隔离内存中的数据随线程而生,随线程而灭,不须要gc来管理,但堆和方法区内存的分配和回收是动态的,这就须要用到gc来及回收机制来管理。java

  垃圾回收须要知道三个部分,如何判断对象死亡? 何时进行回收? 如何回收? 接下来就针对这三个问题进行分析。算法

  一开始大多数都是用引用计数算法,当增长一个对这个对象的引用时,引用计数器就+1;不然引用计数器-1,知道引用计数器的值为0的时候才会断定该对象能够被回收,每个对象都有一个对应的引用计数器。可是引用计数算法中对象之间有可能会出现互相引用的状况,如对象A和对象B:A a = new A();  B b = new B();  a.instance = b; b.instance = a; 这样就造成了互相引用而致使计数器永远都不可能为0,从而不会对该对象进行回收。所以这种断定对象死亡的算法逐渐被抛弃。数组

  后面提出了一种可达性分析算法,它是以GC roots 对象做为起始对象,经过起始对象为节点寻找对象的引用,想下进行搜索,走过的节点路径称之为引用链。若是对象没有直接或间接的关联到GC Roots(即对象引用链不可达GC roots时),则称此对象可被回收了。下图展现GCRoots引用链示意图:多线程

                

                (图片来源于:http://www.importnew.com/23035.html并发

   那哪些对象能够做为GCRoots呢? 下面来列举一下:jvm

    1.虚拟机栈(栈帧的本地变量表)中引用的对象this

    2.方法区中类静态属性引用的对象线程

    3.方法区中常量引用的对象指针

    4.本地方法栈中JNI(通常说的是Native方法)中引用的对象

  对象在通过可达性分析算法确认不可达以后,并非立刻就会进行回收的。首先会对不可达对象进行标记,而后会进一步进行筛选来确认对象是否有必要执行finalize()方法,若是对象以前执行过一次finalize或对象没有覆盖finalize方法,虚拟机都会认为finalize方法不必执行。 当虚拟机认为有必要执行finalize方法的时候,会将该对象放入F-Queue队列中。虚拟机会自动建立一个低优先级的Finalizer线程执行这个队列。finalize方法是对象自我挽救的最后一根救命稻草,若是对象想要自我挽救,能够再覆盖的finalize方法中将对象从新根据引用链关联上GC Roots便可(如将对象赋值给this类或类成员变量)。这样进行二次标记的时候若是发现对象是可达的,就会将其踢出F-Queue队列;若是二次标记仍是不可达的话,那就真的要被回收了。

  以上讲的都是对堆中的对象进行回收状况,那么方法区呢?  方法区通常称之它为永久代,它回收的数据是废弃常量以及无用的类。String存储数据通常都存储到常量池中,若是常量池中数据没有对应的引用的话就会将其进行置为废弃常量从而进行回收。而无用的类回收条件就比回收废弃常量苛刻多了,回收无用类的条件有三个:

  1.类没有任何对应的实例对象,即全部对应的实例对象都已被回收

  2.类对应的classLoader被卸载

  3.该类对应的java.lang.class实例没有任何地方被引用,没法在任何地方经过反射访问该类方法

  

  聊完了哪些对象须要被回收后,接下来讲说回收都有哪些算法。

  1.标记-清理算法(mark-sweep)

    标记-清理算法是先将对象进行标记,标记就是不须要回收的对象,标记完后回收清理全部没有被标记的对象。但这种算法的缺点也比较明显,首先标记和清理过程效率较低;其次这种算法会产生大量的内存碎片,这样当为大对象分配内存时,对致使没有连续的存储空间而提早触发一次垃圾回收。

                      

                      (图片来源于:http://www.importnew.com/23035.html

  2.复制算法

    复制算法是将堆内存按照容量平分为2份,其中一份用于存储对象,另外一份空置。当触发垃圾回收时,会先回收对象,而后将剩余存活的对象复制到另外一块空置对象中。虽然这种方法简单高效,可是内存利用率减半,并且成活对象过多时,复制的对象会较多,因此这种算法通常用于新生代。下图是复制算法模型图:

                      

                    (图片来源于:http://www.importnew.com/16173.html

   3.标记-整理算法(mark-compact)

     标记整理算法和标记清除算法相似,只不过多了一步整理。它是先将对象用可达性分析算法进行标记,而后统一将未被标记的对象进行清除,对于产生的内存碎片虚拟机会经过改变对象与对象之间的指针进行整理,以免为大对象分配内存是没有足够大的连续内存空间。

                          

                          (图片来源于:http://www.importnew.com/23035.html

 

  讲完了一些基本的内存回收算法,那如何利用这些算法来回收垃圾呢? 通常对内存会按照对象的存活时间来进行分代,通常分为新生代,老年代,永久代(它通常是方法区,有些虚拟机会将方法区也看做是堆的一部分), 接下来介绍一些新生代和老年代垃圾收集器:

   1.serial收集器

    它是一种新生代收集器,采用上面所述的复制算法。它是一种单线程收集器,只有有一条线程去执行GC回收,并且gc线程执行时,其余用户线程所有中止(俗称stop the world),执行完以后才会释放用户线程。可是这有一个缺点,就是用户线程在用户不知情的状况下被迫停止,这很是的不友好若是GC线程致使的停顿时间较长则会严重影响用户体验。全部就有了parNew收集器。

              

              (图片来源于:-气宗】深刻理解Java虚拟机:JVM高级特性与最佳实践(最新第二版).pdf)

   2.parNew收集器

    它也是一种新生代收集器,采用的也是复制算法,但它是一种多线程收集器,也就是说当执行GC回收的时候也能够同时执行用户线程,它是第一款实现并发的收集器。但若是是单核CPU的状况下,它的效果是不如serial收集器的。但用户线程并发执行的时候也会产生一些浮动垃圾。

              

              (图片来源于:-气宗】深刻理解Java虚拟机:JVM高级特性与最佳实践(最新第二版).pdf)

   3.parallel scavenge收集器

    它也是新生代收集器,采用的依旧是复制算法,是一种多线程收集器支持并发。但它主要的不一样是他关注的是吞吐量,而前面两种关注的是GC回收停顿时间,吞吐量的定义是  用户线程执行时间/(用户线程执行时间+GC回收执行时间),GC回收停顿时间越短并不表明吞吐量越高,这二者仍是有区别的,好比原本要回收500M内存,经过设置回收300M,虽然一次停顿时间缩短了,但这样回收频率天然也会增长。可是吞吐量仍是仍是没有改变。停顿时间越短表明响应用户速度越快;而高吞吐量则是高效的利用CPU时间

                

   4.serial old收集器

    serial收集器是一种老年代收集器,采用的标记-整理算法,采用的是单线程收集。它和serial收集器相似,只不过它是针对老年代。

             

                (图片来源于:-气宗】深刻理解Java虚拟机:JVM高级特性与最佳实践(最新第二版).pdf)

   5.parallel old收集器

    parallel scavenge收集器也是一种老年代收集器,采用的是标记-整理算法,采用多线程并发收集。它和parallel scavenge收集器对应,都是吞吐量优先。

              

                (图片来源于:-气宗】深刻理解Java虚拟机:JVM高级特性与最佳实践(最新第二版).pdf)

   6.CMS(conccurent mark sweep)收集器

    顾名思义,它是并发标记清理收集器,是一种老年代收集器,采用的是标记-整理算法,它是以获取最短停顿时间为目标的收集器。他会经历以下几个阶段:

      初始标记:此时会对可达性分析中与GC Roots直接关联的对象进行标记

      并发标记:此时会对GC Roots Tracing跟踪引用链下全部的对象进行标记,此线程会与用户线程并发执行,这个阶段停顿时间会比较长

      从新标记:此时会对并发标记过程当中用户线程对象引用链产生的改变进行从新标记

      并发清理:此时会对未被标记的对象进行清除,这个线程是与用户线程并发执行的

    CMS虽然有并发收集,低停顿的优势,但缺点也比较明显。首先CMS对 CPU资源比较敏感。因为它在并发阶段占用必定CPU资源,因此会致使应用程序变慢,总吞吐量会下降。其次,在并发清理阶段会产生一些浮动垃圾,这是因为用户线程在这个阶段也会产生一些须要回收的对象,但本次收集不能回收,只能等待下次垃圾回收。最后,CMS采用的是标记-清除算法,这种算法在前面就说明了它的标记和清除效率是比较低的,并且会产生大量的内存碎片,从而致使分配大对象(通常是很长的字符串或长度较大的数组)内存时,没有足够的连续内存空间。

              

                (图片来源于:-气宗】深刻理解Java虚拟机:JVM高级特性与最佳实践(最新第二版).pdf)

 

  7.g1收集器

    g1收集器它是一种分区收集器,是将堆内存分为若干个大小相等region,但仍是保留了新生代和老年代,只不过不一样代会有多个region区。G1相对于其余的收集器具备以下优势:

    并发与并行:G1能充分利用多CPU,多核环境下的硬件优点,可经过冰法的方式让程序继续执行。

    分代收集:分代概念在G1中依然得以保留。它可以采用不一样的方式去处理新建立的对象和已经存活了一段时间,熬过屡次GC的旧对象以获取更好的收集效果。

    空间整合:采用了标记-整理算法,前面已经说了这种算法相对于标记-清楚算法的优点。

    可预测的停顿:G1除了追求低停顿以外还能创建可预测的停顿时间模型

  Region之间的对象引用以及其余收集器中的新生代和老年代之间的对象引用,虚拟机都是使用Remembered Set,虚拟机发现Reference类型的数据进行写操做时,会产生一个Write Barrier暂时中断写操做,检查Reference对象是否处于不一样的Region中,若是是,经过CardTable把相关引用信息记录到引用对象所述的Region的Remembered Set之中。当进行内存时,Rememmbered便可保证不对全堆扫描也不会有遗漏。

  引入Remembered Set以后,g1收集器会通过以下几个步骤:

    初始标记:初始标记阶段会对直接与GC Roots对象相关联的对象进行标记。

    并发标记:并发标记阶段会会标记根据可达性分析GC Roots节点引用链向下搜索对象进行标记,此过程可与用户线程并发执行

    终极标记:终极标记阶段会将并发标记的时候用户线程执行过程当中对象引用变化部分进行从新标记,且生成Remembered Set Logs,并将其加入到Remembered Set中,加入过程当中可能会产生停顿,但能够并发执行

    筛选回收:筛选回收阶段会将Region区域按回收价值和成本进行优先排序,并按照用户指定的回收时间进行制定回收计划

            

                (图片来源于:-气宗】深刻理解Java虚拟机:JVM高级特性与最佳实践(最新第二版).pdf)

   

 

  说完垃圾收集器以后,再来讲说垃圾回收策略。

  当虚拟机为对象分配内存空间时,会先将对象分配到新生代Eden区域,若是Eden内存不够的时候,会进行一次minor GC,将须要回收的对象复制到survivor区中。新生代区域分为一个Eden和2个survivor区,其中Eden与survivor区域大小比值为8:1。若是survivor区中的已满,则会触发一次full GC,将survivor区中的对象放到老年代中。虚拟机为每个对象定义了一个年龄计数器,Eden对象出生并通过第一次minor GC的时候,就会将Eden区域存活的对象复制到survivor中,并将对象的年龄计数器设为1,之后对象每通过一次minor GC ,survivor区中的对象年龄计数器就会+1,当对象熬到必定程度的时候(通常称为阈值,默认15),就会将对象放到老年代中。但也不是必定要等年龄到阈值才能放到老年代,当survivor区中年龄相同的对象占空间的一半以上,则年龄大于或等于此对象的对象就能够进入老年代了。每次进行mimor GC以前都会断定一下Eden区域对象的大小总和是否比老年代剩余空间大小要小,若是是,则可确保不会有问题;若是不是,可能回榆中比较极端的状况,即当Eden区域中有大量对象存活的状况下,survivor区不能放下复制过来的对象,这样就直接晋升为老年代,但若是老年代空间大小不够就只能触发一次full gc了。因此在minor GC以前,须要有空间分配担保,即老年代的可用空间是否足够容纳Eden区全部存货的对象或历次晋升至老年代的平均大小。

 

  本文仅仅是我的对深刻理解虚拟机的笔记整理,若有不当之处欢迎点评,转载请注明:http://www.cnblogs.com/qven/p/8797349.html

  (参考文献:-气宗】深刻理解Java虚拟机:JVM高级特性与最佳实践(最新第二版).pdf)

相关文章
相关标签/搜索