JVM 对象引用标记 与 内存回收算法

一、为何要进行垃圾回收?

        每当在咱们写代码的时候,无论是new一个对象,仍是引用,仍是填充数据到数组,都是要占用空间,那么若是不及时回收就会对系统的运行产生影响。java和c 一个很大的区别就在于,java的垃圾回收主要是jvm去作,而c语言是本身去控制。虽然JAVA能够手动的调用方法 system.gc 去手动控制垃圾回收,但听说达不到立马回收的效果。c 语言则是要本身去申请一块内存空间malloc ,使用完成还须要手动去释放掉,若是没有及时释放,或者申请出现内存过大等,会形成内存溢出等异常,不过功底深厚的大牛都会作的比较牛逼,很好的去控制。java

二、如何判断对象生死?

        在内存回收的过程当中,首先要肯定一点,该对象是否应该被回收,哪些还"存活",哪些已"死亡"。算法

  • 引用计数法

        当一个对象在代码中被引用,那么就会在这个对象的引用计数值 + 1,当引用失效的时候,计数值则相应 - 1。可是若是两个对象存在相互引用的状况,以下:数组

A a = new A(); 
B b = new B();
a.instance = b;
b.instance = a;

虚拟机就没法去判断这两个对象是否须要被回收,由于彼此的引用值都不是 0。就是说引用计数法很难解决对象之间的相互引用问题, 也没法通知GC回收器去及时回收它。由于存在这种缺点,因此如今的虚拟机基本上并非经过引用计数法来判断对象是否存活。那是经过什么方法? 请看官往下看。安全

  • 可达性分析算法

        如今主流的商用语言的视线中都是经过可达性分析来判断对象是否存活,好比JAVA,C#等。这种方法基本思想 ——以 "GC Roots "(1.虚拟机栈中reference对象,2.方法区静态属性引用对象,3.方法区常量引用对象,4.本地方法栈 所谓的native方法 引用的对象)的对象做为起点向下搜索,搜索走过的路径被称为"引用链",当一个对象没有任何引用链相连,那么这个对象就是不可用的。以下图所示:框架

        注:并非不可达的对象就必须 "死",他们仍是处于"缓刑", 真正要宣告一个对象死亡,须要通过两次标记的过程:通过可达性分析后对象没有和GC Roots 链接的引用链,那么须要被标记一次而后还须要通过筛选(筛选条件:判断该对象是否有必要执行finalize()方法),若是对象已经调用了或者没有覆盖finalize方法(finalize() 方法只会被执行一次!),那么 虚拟机断定该对象是 "没有必要执行该方法"。jvm

        若是该对象有必要执行finalize方法,那么对象会被放置在一个叫作F-Queue 的队列之中,以后会由虚拟机自动创建,由低优先级的Finalize 方法去执行它。(执行时只去触发对象的finalize()方法,可是并不等待他运行结束,防止有的对象finalize()进行缓慢,或者死循环,会致使队列持续等待,进而内存回收系统崩溃。)稍后GC 会对F-Queue 队列中的对象进行第二次标记,当finalize 方法执行后成功将对象链接到引用链上任何一个对象,那么这个对象就被拯救成功了,否则则go die!spa

  • 什么是引用?

        Java中定义:若是reference类型的数据中存储的数值表明的是另外一块内存的起始地址,就称是这块内存的一个引用。线程

        强引用:相似于A a = new A(),只要引用在,就永远不会回收被引用的对象。代理

        软引用:描述有用但并不是必须的独享。在系统将要发生内存溢出的时候,会将这部分对象回收。    code

        弱引用:非必需对象引用,能生存到下次发生GC以前。

        虚引用:一点用都没。只有当对象被垃圾回收器回收的时候会收到一个系统通知。

  • 方法区回收

        在我看来方法区的内存,回收起来并无新生代那么明显。方法区大多存有类的描述信息,静态变量,常量,方法等信息,这些大可能是系统经常使用的,不多去回收,回收效率微乎其微。

        然而永久带的方法回收主要分红两部分,一种是常量的回收,另外一种是类的回收。

  • 常量的回收    至关于这个常量已经被废弃掉了。例如:方法区的常量池中有一个字符串常量 "java", 当系统中没有一个String对象指向这个常量的值得时候,那么这个常量在发生GC的时候将会被回收。
  • 类的回收       类的回收相对于常量的回收会麻烦多,须要知足下面三个条件才会被回收: 
    • 类中全部的对象都被回收,就是堆中不存在该类的任何的实例;
    • 加载该类的classloader被回收;
    • 该类对应的java.lang.Class对象没有在任何地方引用,或是经过反射机制访问不到该类;

        注: 在大量使用反射,动态代理,cGLib等ByteCode框架、动态生成Jsp等频繁定义classLoader的场景都须要虚拟机具有类的卸载功能,防止永久带不会溢出。

三、垃圾回收算法

  • 标记-清除算法 

        标记-清除 算法是最基础的算法,为何呢?由于后面的要讲的算法不少是从这个基本的算法改变其不足演变而来。

        标记-清除(Mark-Sweep) 算法正如其名字所说由两个部分来完成。首先,要对须要回收的对象进行标记,如何标记上面已经提过。而后,要对这些被标记的对象进行收集。

     标记:标记的过程其实就是,遍历全部的GC Roots,而后将全部GC Roots可达的对象标记为存活的对象。 
   清除:清除的过程将遍历堆中全部的对象,将没有标记的对象所有清除掉。 

        以下图所示:

               

        感受清理完成以后,内存零零散散,故该算法有如下两个缺点:

        缺点一:被标记的对象在内存中分布很零散,回收以后可用内存很零碎。若是当一个进程须要申请一块连续的较大内存时,没法找到足够的连续内存,不得不提早触发一次垃圾回收的动做。

        缺点二:标记和清除的过程效率不高,并且在进行GC的时候,须要中止应用程序,这会致使用户体验很是差,尤为对于交互式的应用程序来讲简直是没法接受。

  • 复制算法

        复制算法是对标记-清除算法在回收后出现不少内存碎片的一种改进,并且效率也有所提高。

        复制算法(copying)将可用的内存容量划分红大小相等两块,每次只使用其中一块,当一块内存用完了,将还存活的对象复制到另外一块内存中,而后将以前使用过的内存空间所有清理掉。当有效内存空间耗尽时,JVM将暂停程序运行,开启复制算法GC线程 ,它会将活动区间内的存活对象,所有复制到空闲区间,且严格按照内存地址依次排列,与此同时,GC线程将更新存活对象的内存引用地址指向新的内存地址。 清空以前的内存块,减小了大量不连续的内存碎片的产生。

实现简单且运行高效。以下图所示:   

                  

        细心的读者会发现,这种算法有个很大的缺点——将可以使用的内存缩小成原来的一半,代价太大了!因此如今的虚拟机厂商都采用复制算法来回收新生代。研究代表 新生代对象98%都是 “朝生夕死” 因此不须要按照1:1划份内存空间,而是将内存分为一块较大的Eden 空间和两块较小的Survivor 空间。每次只是用Eden 和 其中一块 Survivor区域,另外一块Survivor 则用来看成保留区。那么这样一来,每次进行回收的时候只须要将Eden 和 Survivor生还的对象复制到另外一块 Survivor空间,而后清理。HotShot虚拟机 新生代划分比例默认 Eden:Survivor = 8:1。然而这样分配内存,有个问题。看成保留区的Survivor的内存大小不够承载 使用中的Eden和一块Survivor区域的存活对象怎么办?此时须要依赖其余内存(老年代)进行分配担保

        分配担保(Handle Promotion)——若是另外一块Survivor空间没有足够的空间存放上一次新生代收集下来的存活对象时,这些对象直接经过分配担保机制进入老年代。

                

        发生Minor GC(只回收Eden Survivor区)前,虚拟机检查老年代最大可用连续空间是否大于新生代全部对象总空间。若是大于,那么Minor GC确保是安全的。若是不大于,则须要查看虚拟机HandlePromotionFailure参数设置,是否容许担保失败。若容许(true),会继续检查老年代最大连续可用空间是是否大于历次晋升到老年代的对象平均大小。若是大于,会尝试一次 Minor GC,尽管是有风险。(由于仅仅是历次晋升到老年代对象平均大小与老生代最大连续空间比较,若是内存小没法容纳,此时进行Minor GC 会清理本来存活的对象因此是冒险的,进而须要进行Full GC)若是小于或者Handle Promotion Failure不容许冒险,那么要进行一次Full GC。

  • 标记-整理算法

        复制算法是须要将对象从从内存一个区域复制到另外一个区域,当发现对象存活率很高的状况下,效率很低。并且在老生代的回收中,大多不采用复制算法,没有额外的空间进行分配担保。

        标记-整理算法(Mark-Compact),过程和Mark-Sweep 方法过程同样,也须要对对象进行标记,不事后续步骤不是直接对可回收对象进行清理,算法分红两个部分。  

    标记:遍历GC Roots,而后将存活的对象标记。 
    整理:移动全部存活的对象,且按照内存地址次序依次排列,而后将末端内存地址之后的内存所有回收。

           

        标记-整理算法不只能够弥补标记-清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价。不过标记-整理算法,效率是惟一缺点。它须要对存活对象进行标记,而后要整理存活对象内存地址,相对于复制算法效率较低。

  • 分代收集算法

       分代收集算法,当前商用的虚拟机的垃圾收集大都采用这种算法。也不算是算法,只是把java 堆分红 新生代,老生代。分代以后,不一样区域可使用不一样的收集算法。好比:

        新生代 每次垃圾回收都会有大批对象死去,只有少许存活,那就采用复制-算法

        老生代 对象存活率高,也没额外的空间对它进行分配担保使用标记-整理/清除法会更好来回收。不一样场景使用不一样的算法更加有利于总体效率的提高。

        JVM在进行GC时,并不是每次都对上面三个内存区域一块儿回收的,大部分时候回收的都是指新生代。所以GC按照回收的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(major GC or Full GC),它们所针对的区域以下。 
  普通GC(minor GC):只针对新生代区域的GC。 
  全局GC(major GC or Full GC):针对年老代的GC,偶尔伴随对新生代的GC以及对永久代的GC。    因为年老代与永久代相对来讲GC效果很差,并且两者的内存使用增加速度也慢,所以通常状况下,须要通过好几回普通GC,才会触发一次全局GC。

相关文章
相关标签/搜索