转载

垃圾收集器与内存分配策略

ffw垃圾收集器与内存分配策略

判断对象是否存活的方法

  • 引用计数法

    每个对象上都有一个引用计数,对象每被引用一次,引用计数器就+1,对象引用被释放,引用计数器-1,直到对象的引用计数为0,对象就标识可以回收。但是这个算法有明显的缺陷,对于循环引用的情况下,循环引用的对象就不会被回收。

    实例:

    public class ReferenceCountingGC{
        public Object instance = null;
        private static final int _1MB = 1024 * 1024;
        private  byte[] bigSize = new byte[2 * _1MB];
        public static void testGC() {
            ReferenceCountingGC objA = new ReferenceCountingGC();
            ReferenceCountingGC objB = new ReferenceCountingGC();
            objA.instance = objB;
            objB.instance = objA;
            objA = null;
            objB = null;
            System.gc();
        }
    }
    //发生嵌套引用,但是在java中,objA和objB的内存可以被回收
    
  • 可达性分析算法

    基本思路是:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下探索,搜索所走过的路径称为“引用链”,当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。在Java语言中,可作为GC Roots的对象包括下面几种:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

  • 引用类型

    • 强引用

      强引用就是指在程序代码之中普遍存在的,类似“Object obj = new Object()’'这类的引用,只要强引用存在,垃圾收集器永远不会回收掉被引用的对象

    • 软引用

      软引用用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次还没有足够的内存,才会抛出内存溢出异常。

    • 弱引用

      弱引用也是用来描述非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象

    • 虚引用

      虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

  • 对象是否一定要清除

    假设在可达性分析算法中某个对象不可达,它也并非”非死不可”。如果这个对象覆盖了finalize()方法且这个方法没有被JVM调用过,则JVM会执行finalize()方法。这时你可以在这个方法中重新使某个引用指向该对象。当然,finalize()方法只能救它一次。

垃圾回收算法

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

    最基础的垃圾回收算法,分为两个阶段,标记和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间 。从图中我们就可以发现,该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。

  • 复制(Coping)算法

    为了解决标记清除(Mark-Sweep)算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉,如图。这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying算法的效率会大大降低。

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

    结合了以上两个算法,为了避免缺陷而提出。标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。如图:

  • 分代收集(Generational Collection)算法

    分代收集法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。

    目前大部分JVM的GC对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照1:1来划分新生代。一般将新生代划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space),每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块Survivor空间中。

    而老生代因为每次只回收少量对象,因而采用Mark-Compact算法。另外,不要忘记在Java基础:Java虚拟机(JVM)中提到过的处于方法区的永生代(Permanet Generation)。它用来存储class类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。

    • Young区分为Eden区和两个Survivor区域,其中所有新创建的对象都在Eden区,当Eden区满后会触发minor GC将仍然存活的对象复制到其中一个Survivor区域中,另外一个Survivor区域中的存活对象也复制到这个Survivor区域中,这样可以保证始终有一个Survivor区域时空的。

    • Old区域存放的是Young区的Survivor满后触发minor GC后仍然存活的对象,当Eden区域满后会将对象存放到Survivor区域中,如果Survivor区域仍然存放不下这些对象,GC收集器会将这些对象直接存放到Old区域。如果在Survivor区域中的对象足够老,也直接存放到Old区域中。如果Old区域也满了,将会触发Full GC,回收整个堆内存。

    • Perm区域存放的主要是类的Class对象,如果一个类被频繁地加载,也可能会导致Perm区满,Perm区的垃圾回收也是由Full GC触发的。

垃圾收集器

  • Serial收集器(新生代、老年代)

    Serial收集器是最古老,最稳定以及效率高的收集器,只是用一个线程去回收,可能会产生较长的停顿,Serial收集器包括两种收集器,一是新生代收集器,二是老年代收集器,又称为(Serial Old)。新生代使用复制算法,老年代使用标记整理算法。垃圾收集的过程中会Stop The World(服务暂停)。详细过程如下如图。

  • ParNew收集器(新生代)

    ParNew收集器本质是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为和Serial收集器完全一致。在Server模式下,ParNew收集器是一个非常重要的新生代收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

  • Parallel Scavenge收集器(新生代、老年代)

    Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector)。有一些特点与ParNew收集器相似:新生代收集器;采用复制算法;多线程收器集;CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput),即减少垃圾收集时间,让用户代码获得更长的运行时间;

    吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间)

    Parallel Scavenge收集器的老年版本是Parallel Old,使用标记整理算法

  • CMS收集器(老年代)

    该收集器是一种以获取最短回收停顿时间为目标的收集器,使用“标记-清除”算法实现,整个过程分为4个步骤

    • 初始标记(CMS initial mark):仅仅是标记一下GC Roots能直接关联到的对象,速度很快。
    • 并发标记(CMS concurrent mark):进行GC Roots的Tracing的过程.
    • 重新标记(CMS remark):为了修正在并发标记期间因用户程序继续运行而导致的标记产生变动的那一部分对象的标记记录。
    • 并发清除(CMS concurrent): 清除垃圾的过程。

    初始标记阶段和重新标记阶段需要暂停所有的用户线程。

    在CMS垃圾收集器工作时,需要借助年轻代来判断当前老年代中的对象是否是存活着的。如下图所示,无法在老年代中直接使用GC ROOT TRACING来判断老年代的对象的存活状态。为了找出并标记老年代存活的对象,需要扫描年轻代中的对象。由于年轻代中的对象较多,一般会采取先进行一次Minor GC使得年轻代的对象大幅度减少,也即会进行一次并发预清理阶段

    老年代的机制与一个叫CARD TABLE的东西密不可分。CMS将老年代的空间分成大小为512bytes的块,card table中的每个元素对应着一个块。并发标记阶段会把引用发生变化的老年对象所在的Card标识为Dirty,后续CMS remark阶段就只需要扫描这些Dirty Card的对象,从而避免扫描整个老年代。

    举个例子:

    • 并发标记时对象的状态:

    • 但随后current obj的引用发生了变化:

      current obj所在的块被标记为dirty card.随后到了CMS remark阶段,通过currrent obj变得可达的对象也被重新标记了,变成下面这样

    CMS收集器有以下3个明显的缺点:

    • CMS收集器对CPU资源非常敏感:即在并发阶段,它虽然不会导致用户线程的停顿但是会因为占用了一部分线程(或者说CPU资源)而导致用户应用程序变慢,总吞吐量降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。

    • CMS收集器无法处理浮动垃圾(Floating Garbage),由于CMS在并发清理阶段用户线程还在运行着,伴随着程序的运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留到下一次GC时再清理掉,这一部分垃圾就称为“浮动垃圾”。

      因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留有足够的内存空间给用户线程使用。若CMS运行期间预留的内存无法满足用户程序的需要,就会出现一次“Concurrent Mode Failure”失败。这时虚拟机会临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间会更长。

    • 由于CMS采用的是标记清除算法,因此意味着收集结束时会有大量空间碎片产生,碎片产生,需要进行内存碎片整理,而碎片整理过程无法并发,因此会增加用户线程的停顿时间。

  • G1收集器

    将G1与CMS进行比较,G1是更好的解决方案。第一个区别是G1是压缩型收集器。G1的压缩功能,足以完全避免使用细粒度的空闲内存进行分配。这大大简化了收集器,并且消除了大部分的潜在碎片问题。此外,G1比CMS收集器提供可预测的垃圾回收暂停时间

    • Remembered Set

      G1被分为多个Region。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序对Reference类型数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之间,如果是便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当内存回收时,在GC根节点的枚举范围加入Remembered Set即可保证不对全局堆扫描也不会有遗漏。

    • G1工作原理概述

      G1收集器采取不同的方法。堆被分成一组大小相等的区域,每个是连续范围的虚拟内存。某些Regions被分配给和常规收集器一样的角色(eden区,survivor区,老年代),但他们没有固定的大小。这提供了更大的内存使用灵活性。

      G1 Heap Structure

      G1收集器与CMS收集器相比有两个显著的改进:一是G1收集器是基于标记整理算法实现的收集器,也就是说它不会产生空间碎片。二是它可以非常精确地控制停顿,既能让使用者明确指定一个长度为M毫秒的时间片段里,消耗在垃圾收集上的时间不得超过N毫秒。G1将整个Java堆(包括新生代、老年代)划分为多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(这就是GarbageFirst名称的来由)。区域划分及有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集效率。

      • 初始标记(Initial Marking):仅仅标记GC Roots能直接关联到的对象。
      • 并发标记(Concurrent Marking):从GC Roots开始对堆中对象进行可达性分析,找出存活的对象。
      • 最终标记(Final Marking):为了修正在并发标记期间因用户线程继续运行而导致标记产生变动的把一部分记录,虚拟机将这段时间对象变化记录在线程Remember Set Logs里面,最终标记阶段需要把Remember Set Logs的数据合并到Remember Set中,这阶段可以需要暂停用户线程,也可以进行并发。
      • 筛选回收(Live Data Counting and Evacuation):该阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
安全点
G1收集器
正文到此结束
本文目录