您的位置:首页 > 编程语言 > Java开发

Java虚拟机(二):GC垃圾收集器与内存分配策略

2017-05-19 16:18 176 查看

2.1概述

          垃圾收集(Garbage Collection)简称GC。1960年诞生于MIT的Lisp语言,GC的历史比Java久远。

          程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作;方法或线程结束时内存自动回收。Java堆和方法区内存的分配和回收都是动态的,故垃圾收集主要针对的就是这部分内存。

2.2 对象存活判断

    2.2.1 引用计数算法

           给对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1;计数器值为0时即为不可能再被使用,可被回收。
           缺点:无法解决对象之间相互循环引用问题,如objA.instance = objB;objB.instance = objA。

    2.2.2 可达性分析算法

           以“GC Roots”的对象作为起始点,向下检索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。即为不可达对象。如下图对象Object5、Object6、Object7互相有关联,但到GC皆不可达,所以均被判定是可回收的对象。



          在Java语言中,可作为GC Roots的对象包括下面几种:
         (1)虚拟机栈(栈帧中的本地变量表)中引用的对象。
         (2)方法区中静态属性引用的对象。
         (3)方法区中常量引用的对象。
         (4)本地方法栈中JNI(即一般说的Native方法)引用的对象。
         四种引用类型:
         (1)强引用:类似“Object obj = new Object()”,若强引用还存在,垃圾收集器永远不会回收被引用的对象。
         (2)软引用:SoftReference类实现,内存溢出前会对软引用关联的对象进行二次回收,若回收后还没有足够的内存,才会抛出内存溢出异常。
         (3)弱引用:WeakReference类来实现。垃圾收集器工作时,百分百回收掉只被弱引用关联的对象。
         (4)虚引用:PhantomReference类来实现。

2.3 生存还是死亡

         即使在可达性分析算法中可不达的对象,也并非是“非死不可”,首次不可达时会被第一次标记并进行一次筛选(对象没有覆盖finalize()方法或者已被虚拟机调用过)。在第二次标记前,若能重新与引用链上的任何一个对象建立关联即可,即变为可达对象,否则将会被回收。

2.4 垃圾收集算法

         介绍几种经典算法

    2.4.1 标记-清除(Mark-Sweep)算法

          该算法分为“标记”和“清除”两个阶段:标记即标记出所有需要回收的对象,清除即在标记完成后统一回收所有被标记的对象。



            该算法有两个不足:一是效率问题,标记和清除两个过程效率都不高;二是空间问题,标记清楚后产生大量不连续的内存碎片,后续分配较大对象时可能无法找到足够的连续内存,从而提前触发新的一次垃圾收集动作。

    2.4.2 复制(Copying)算法

           为了解决效率问题,它将可用内存按容量划分大小相等的两块,每次只使用其中的一块。当一块的内存用完了,就将还存活着的对象复制到另一块上面,再将之前已使用过的内存空间一次清理掉。



             算法不足:内存实际可用容量为原来的一半。

    2.4.3 标记-整理(Mark-Compact)算法

            为了解决复制算法不足,有效利用内存空间,它标记过程和“标记-清除”一样,但后面是让所有存活的对象都向一端移动,然后直接清理掉端端边界以外的内存。



    2.4.4 分代收集算法

           根据不同对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾收集都发现有大批对象死去,只有少量存活,选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。

2.5 垃圾收集器

         如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。如果两个收集器之间有连线,说明他们可以搭配使用。
         并行(Paraller):指多条垃圾收集线程并行工作,但此时用户线程还处于等待状态。
         并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定并行,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行与另一个CPU上。
         通俗解释:并行是指多个事件在同一时刻发生,并发是指多个事件在同一时间间隔发生。



HotSpot虚拟机的垃圾收集器

     2.5.1 Serial收集器

            Serial收集器是最基本、发展历史最悠久的收集器。是一个单线程的收集器,进行垃圾收集时,会Stop The World(暂停其它所有的工作线程)。
            新生代采取复制算法暂停所有用户线程。老年代采取标记-整理算法暂停所有用户线程。

    2.5.2 ParNew收集器

    ParNew收集器是Serial收集器的多线程版本。目前只有它能与CMS收集器配合工作。
            新生代采取复制算法暂停所有用户线程。老年代采取标记-整理算法暂停所有用户线程。

    2.5.3 Parallel Scavenge收集器

            Paraller Scavenge收集器的目标是达到一个可控制的吞吐量,多线程。吞吐量=运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间) ,虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
            Paraller Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
            可以通过参数开关控制GC自自适应调节策略,即虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整关于JVM新老年代的一些参数以提供最合适的停顿时间或者最大的吞吐量。

    2.5.4 Serial Old收集器

            Serial Old是Serial收集器的老年代版本,单线程,给Client模式下的虚拟机使用。

    2.5.5 Parallel Old收集器

            Parallel Old是Parallel Scavenge收集器的老年代版本,多线程。在注重吞吐量和CPU资源敏感的场合,可以考虑Parallel Scavenge + Parallel Old收集器组合。

    2.5.6 CMS收集器

            CMS收集器是一种以获取最短回收停顿时间为目标的收集器。适合互联网网站或者B/S系统的服务端,因为系统停顿时间最短,响应速度快。

            CMS收集器是基本“标记-清除”算法实现的,整个过程分为四个步骤:

          (1)初始标记

          (2)并发标记

          (3)重新标记

          (4)并发清除

            其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长,但远比并发标记的时间短,最后是并发清除。 

            优点:并发收集、低停顿。

            缺点:对CPU资源非常敏感,占用CPU资源,降低吞吐量;无法处理浮动垃圾;产生不连续的空间碎片。

    2.5.7 G1收集器

           G1是一款面向服务端应用的垃圾收集器,发展最前沿成果之一。
           与其它GC收集器相比,G1具有以下特点:
         (1)并发与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿的时间,部分其它收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
        (2)分代收集:G1可以不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同的方式新老对象,以获取更好的效果。
        (3)空间整合:采用“标记-整理”算法。
        (4)可预测的停顿:建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
          G1收集器可分为以下几个步骤:
        (1)初始标记
        (2)并发标记
        (3)最终标记
        (4)筛选回收 

2.6 内存分配与回收策略



        堆分为新生代和老年代,堆大小=Young Generation+Old Generation,默认新生代占1/3,老年代占2/3。其中新生代eden:from
survivor:to survivor为8:1:1。
        上述比例均可自定义设置。
        Young Generation:主要存放新生的对象。
        Old Generation:主要存放生命周期较长的对象。
        Permanent Generation:亦可称为永久代,存放Class和Meta的信息,运行时常量池等。
        Heap中分类:Eden Space称为伊甸园、from+to为Survivor Space(幸存者区)、tenured称为老年代。

        新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
        老年代GC(Major GC/ Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(Parallel Scavenge收集器有直接Major GC的策略)。Major GC的速度一般会比Minor GC慢10倍以上。
        Major GC 和Full GC很多时候都认为是等价的,但还有一种说法是:
        Major GC: 只清理老年代。
        Full GC:清理整个堆(包括老年代和新生代)。

  2.6.1 对象优先在Eden(伊甸园)分配

         大多数情况下,对象在新生代的Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。   

  2.6.2 大对象直接进入老年代

         大对象即指需要大量连续内存空间的Java对象,最典型的如很长的字符串或数组。

  2.6.3 长期存活的对象将进入老年代

         虚拟机给每个对象定义了一个对象年龄(Age)计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的阀值,可以通过参数-XX:MaxTenuringThreshold设置。

  2.6.4 动态对象年龄判定

         为了能更好地适应不同程度的内存情况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入到老年代,无须等到MaxTenuringThreshold中要求的年龄。

  2.6.5 空间分配担保

         在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否运行担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full
GC。
         新生代采用复制(Copying)算法,当出现大量对象在Minor GC后仍然存活的情况,就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: