【JVM】GC算法浅析

来源:互联网 发布:免费的office for mac 编辑:程序博客网 时间:2024/05/22 15:44

 如图,这三幅图是前几篇JVM博客中关于内存的介绍:

       1.Runtime Data Area

      

        2.立体化的Runtime Data Area

        

       3.JVM体系结构

       

       结合着上述三幅图,开始JVM中Garbage Collection的学习。


一、问题

     0)Garbage Collection(之后简称GC)是什么?

       垃圾回收机制(英文简写GC):当需要分配的内存空间不再使用的时候,JVM将调用垃圾回收机制来回收内存空间。即JVM中内存动态分配与回收中的一部分,“自动化”这个词有助于我们更好地理解GC。


     1)为什么学习GC?

      虽然内存动态分配和回收技术已经非常成熟,但是当出现类似于“StackOverFlow”或者“OutOfMemery”这样的问题的时候,就需要学习GC的原理,通过爆出的StackOverFlow或者OOM错误,去发现代码中存在的问题。同时,如果GC机制成为了更高并发时候影响系统性能的因素的话,就更有必要学GC,作为高级开发或者架构师,学习GC原理非常重要。


     2)垃圾回收究竟是回收内存中的那一块?为什么是这一块?

       如上两幅图所示,“PC Register”、“JVM Stack”、“Native Method Stack”都是非线程共享的,在Runtime Data Area区域,这3块内存区域随着线程而生,随线程而灭,因此这3个区域的内存分配和回收都具备确定性,GC机制不会对这3个曲云的内存进行回收。

       但是对于Java堆和方法区,在《深入理解Java虚拟机》这本书中讲到,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也不一样,只有在程序处于运行期间才能知道会创建哪些对象,因此这部分内存的分配、回收是动态的,GC收集器关注的是这部分内存。


     3)怎么判断回收哪些文件?

      从上述的总结,已经了解到GC会从哪块内存去回收垃圾文件,那么究竟满足什么条件的文件就属于垃圾文件,要被回收呢?这里有两种方法来判断文件是否会被回收:

      1.引用计数算法

      1.1 原理:

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

      1.2 优点

      实现简单、效率高。

      1.3 缺点

      很难解决对象之间相互循环引用的问题,代码如下:

[java] view plain copy
  1. public class ReferenceCountingGC {  
  2.     public Object instance = null;  
  3.     private static final int _1MB = 1024 * 1024;  
  4.       
  5.     private byte[] bigSize = new byte[2 * _1MB];  
  6.       
  7.     public static void testGC() {  
  8.         //实例化ReferenceCountingGC这个类的两个实例  
  9.         ReferenceCountingGC objA = new ReferenceCountingGC();  
  10.         ReferenceCountingGC objB = new ReferenceCountingGC();  
  11.           
  12.         //两个实例之间互相引用  
  13.         objA.instance = objB;  
  14.         objB.instance = objA;  
  15.           
  16.         objA = null;  
  17.         objB = null;  
  18.           
  19.         System.gc();  
  20.     }  
  21. }  
      如上代码,如果使用引用计数算法,发现根本不能把这块内存清空,因为他们互相引用着对方。但是在JVM中,发现竟然被回收了,说明HotSpot用的判断文件是否会被回收的算法,不是“引用计数算法”!

      2.可达性分析算法

      2.1 原理

      通过一系列的成为“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。可作为GC Roots的对象包括下面几种:1.虚拟机栈中引用的对象;2.方法区中静态属性引用的对象;3.方法区中常量引用的对象;4.本地方法栈中JNI引用的对象。

      2.2 图例

       

        如上图,深色的两个点点,就是会被GC掉的对象。


       回顾两种算法,判断是否对象会被回收,本质都是通过“引用”来判断,这里关于引用有一些介绍:

       1.强引用:类似 Object obj = new Object(); 只要引用仍存在,永远不会被GC。

       2.软引用:描述一些还有用但并非必须的对象,在系统发生OOM之前,才会把他们列入回收范围。   

       3.弱引用:描述非必须对象,只能生存到下一次垃圾收集发生之前。

       4.虚引用:唯一目的是能在这个对象被GC时候收到一个系统通知。


       通过4种类型的引用,我发现, 当内存还足够时,对于一些无关的对象,能够保存在内存之中;如果内存空间在进行GC后还是非常紧张,则可以抛弃这些对象。

       

二、GC基础知识

      1)GC算法

      常用的垃圾回收算法有4种,下面简介:

      1.“标记-清除”算法

       流程:1.标记要回收的对象 2.执行回收

       不足:1.标记和清除过程的效率都不是很高 2.清除之后会产生大量不连续内存碎片,以后分配大对象时,无法找到足够的连续内存而不得不提前出发另一次GC动作。

       


      2."复制"算法(商业虚拟机所采纳)

      目标:解决了“标记-清除”算法效率不高的问题。

      流程:将原本的内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完了,就将还活着的对象复制到另外一块上,然后把刚才使用过的内存空间清理掉。

      实际应用:对于堆区,内存被划分为酱紫:

     

       堆内存主要被划分为两大块,一块是新生代,一块是老年代,上图所示的Permanent,是永久代,是No-Heap中的,即方法区。在新生代当中(因为内存分配基本上都是在新生代当中),又被分为了Eden和S0、S1三块区域,每次使用Eden和(S0、S1其中一块区域),当回收时候,将Eden和其中一块Survivor中还存活的对象一次性地复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是“8:1:1”,每次新生代中可用的内存空间为整个新生代容量的90%,只有10%是用于做GC时候,存放剩余的活的对象的。

       观察,这时候没有用到老年代!!!

       

      (图片来源:http://blog.csdn.net/sinat_36246371/article/details/53002209)


      3.“标记-整理”算法

       使用范围:老年代

       流程:1.过程与“标记-清除”一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,“标记-整理”算法示意图如下:

       


      4.分代收集

       因为在堆区被分为“新生代”、“老年代”,这样就根据各个年代的特点采用最合适的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活的复制成本就可以完成收集。而老年代中因为对象存活率极高、没有额外空间对它分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。


      2)HotSpot上的GC实现

      1.枚举根节点

      2.安全点

      3.安全区域

      这部分内容比较深,这里先了解了GC算法之后,对于在HotSpot上的算法实现,知道这些概念, 学习更加深入之后,再进行单独总结。


      3)垃圾收集器

      GC收集算法如果算作是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现了。

      常用的GC收集器有6种:

       1.Serial

       2.ParNew

       3.Parallel Scavenge

       4.Serial Old

       5.Parallel Old

       6.CMS

       7.G1

       同时,参考ImportNew上的这篇文章,可以看到,GC收集器还可以分为4大种类:http://www.importnew.com/13827.html ,结合垃圾回收算法,我们知道在堆区,新生代一般适用“复制算法”,老年代适用“标记-整理算法”,那么是不是对于垃圾收集器,也有适用在新生代和老年代的区别呢?如图:

       

       一目了然了。


三、GC处理过程

     1)方法区

      方法区即HotSpot中的永久代,主要收集两部分:1.废弃常量和无用的类。
      1.废弃常量:比如常量池中的字面量,如果一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String常量叫做"abc",也没有其他地方引用了这个字面量,如 果这时候GC执行,而且必要的话,就会将这个“abc”常量清理出常量池。
      2.无用的类:需要满足如下条件
    (1)该类所有实例已经被回收,就是java堆中不存在该类的任何实例。
    (2)加载该类的ClassLoader已经被回收。
    (3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。


     2)堆区

      前提:结合上述内容,已经了解到在堆区分为“新生代”和“老年代”,而且新生代采用“复制算法”,Eden和S0、S1的内存分配比例为“8:1:1”。

      1.大部分对象是在Eden区分配内存。

      2.当Eden没有足够空间进行分配时,或者当Eden区域内存将要满的时候,执行Minor GC,把存活下的对象转移到其中一个Survivor区。

      3.Minor GC会检查存活下来的对象,并把他们转移到另一个survivor区。这样在一段时间内,总会有一个空的survivor区。

      4.经过多次GC周期,仍然存活下来的对象被转移到老年代内存空间。(通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的)


      概念:

      1.Minor GC -- 发生在新生代,由于java对象大多朝生夕灭,所以MinorGC非常频繁。

      2.Full GC/Major GC -- 发生在老年代,速度会相对Minor GC较慢。


      流程:

      1.如图,当Eden内存、(S0、S1其中一个)内存填满,触发minor gc。

      

       如图,金黄色的为没有被引用,一旦Eden满了,就要被回收;S0和S1中,一定有一个是空的,这是规则,被引用的,会放到S0或者S1中的任意一个当中。下次再触发minor GC的时候,会把S0的内容,拷贝到S1当中,同时对象被移动的次数从1变为2.(互相拷贝的时候,数字增1,就是幸存次数),次数到一定值,就步入老年代。

       变化过程:

     

     

     第二图,表示当对象被标记次数大了之后,被移动到老年代。当老年代满了之后,触发Full GC/Major GC.此时发生STW()

      

       一个通过图片展现的流程就是这样的。


四、Addition(补充)

      1.Stop the World事件
      程序所有线程停止,等待垃圾回收。
      年轻代 对象都是临时对象,执行Minor GC块,所以不会受到影响。

     

    (百度百科,对Stop the World中的内容截取)


      2.空间分配担保

      Minor GC之前,检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果成立,Minor GC可以确保安全。如果不成立,虚拟机查看HandlePromotionFailure设置值是否允许担保失败。如果允许,继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC有风险;如果小于,或者HandlePromotionFailurer设置不允许冒险,那这时也要改为进行一次Full GC.


      That's all.