gc垃圾收集笔记整理

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

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

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

  一开始大多数都是用引用计数算法,当增加一个对这个对象的引用时,引用计数器就+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呢? 下面来列举一下:

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

    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)

  

  

    

    

  

  

 

posted @ 2018-04-12 17:24  qven  阅读(124)  评论(0编辑  收藏  举报