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

来源:互联网 发布:mac配置adb环境变量 编辑:程序博客网 时间:2024/05/07 16:48

一、对象已死嘛

  1. 引用计数算法
    (1)引用计数算法定义:给对象中添加一个引用计数器,每当在一个地方引用它,计数器值加1,引用实效计数器值减1,当计数器值为0时,对象就不再被使用。
    (2)引用计数算法问题:主流的java虚拟机中没有选用引用计数算法来管理内存,主要是因为难以解决对象间相互循环引用问题。
/** * testGC()方法执行后,objA、objB会不会被GC? * @author shier * 这两个对象是可以被回收的,因为java虚拟机没有采用引用计数的算法 */public class ReferenceCountingGC {    public Object instance = null;    private static final int SIZE = 1024*1024;    /**     * 主要是占内存,可以用来查看GC日志时看清楚对象是否被回收     */    private byte[] bigSize = new byte[2 * SIZE];    public static void testGC(){        ReferenceCountingGC objA = new ReferenceCountingGC();        ReferenceCountingGC objB = new ReferenceCountingGC();        objA.instance = objB;        objB.instance = objA;        objA = null;        objB = null;        System.gc();    }}
  1. 可达性分析算法
    (1)可达性分析算法定义:以一些“GC Roots”对象作为起点,从这些节点向下搜索,搜索走过路径称为引用链,当一个对象没有任何引用链相连(就是GC Roots到这个对象不可达),则证明这个对象不可用。
    (2)可作为GC Roots的几种对象:虚拟机栈(本地变量表)中引用的对象;方法区中类静态属性引用的对象;方法区中常量引用的对象;本地方法栈中JNI(Native方法)引用的对象。

  2. 再谈引用
    (1)引用的分类:强引用(Strong Reference),使用new创建,只要引用还存在,垃圾回收器永远不会回收被强引用的对象;软引用(Soft Reference),在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行二次回收,如果回收后还没有足够内存,才会抛出内存溢出异常;弱引用(Weak Reference),被弱引用关联的对象只能生存到下一次GC之前,当GC工作时,无论内存是否足够,都会回收这些对象;虚引用(Phantom Reference),一个对象是否有虚引用存在,不会对其生存时间构成影响,并且也无法通过这个引用取得这个对象实例,对象关联虚引用的目的就是这个对象被GC时会收到一个系统通知。

  3. 生存还是死亡
    (1)对象死亡的两次标记过程:对象进行可达性分析过后发现没有与GC Roots相连的引用链,会被第一次标记并筛选(筛选条件看对象是否有必要执行finalize ()方法,当对象没有覆盖或者已经被虚拟机调用过finalize ()方法,那就没有必要执行),如果筛选结果是有必要执行finalize ()方法,首先会把这个对象放置在一个F-Queue队列中,并稍后有一个虚拟机自动建立的、低优先级的Finalizer线程去执行它(所谓执行是虚拟机去触发这个方法,但并不承诺会等待它运行结束,因为如果一个对象在finalize ()方法中执行缓慢,或者发生死循环,将导致F-Queue队列其他对象永远处于等待,甚至导致整个内存回收系统奔溃),finalize ()方法是对象逃脱死亡的唯一一次机会,稍后GC会对F-Queue队列中的对象进行第二次标记,然后对象被判定死亡。
    (2)finalize ()方法中的自我拯救:只要对象在执行finalize ()方法时与对象链上的任何对象建立关联,譬如把自己(this关键字)赋值给某个类变量或者对象成员变量,第二次标记时就会把它移除F-Queue队列。

/**  * 此代码演示了两点:  * 1.对象可以在GC时候被自救  * 2.这种自救机会就一次,因为一个对象的finalize()方法最多被系统执行一次  * @author shier  *  */public class FinalizeEscapeGC {    public static FinalizeEscapeGC SAVE_HOOK = null;    public void isAlive(){        System.out.println("yes,i am still alive : ) ");    }    @Override    protected void finalize() throws Throwable {        super.finalize();        System.out.println("finalize method executed!");        FinalizeEscapeGC.SAVE_HOOK = this;    }    public static void main(String[] args) throws InterruptedException {        SAVE_HOOK = new FinalizeEscapeGC();        //对象第一次成功拯救自己        SAVE_HOOK = null;        System.gc();        //因为finalize方法优先级很低,所以暂停0.5秒等待它        Thread.sleep(500);        if(SAVE_HOOK != null){            SAVE_HOOK.isAlive();        }else{            System.out.println("yes,i am dead : ) ");        }        //下面代码与上面代码相同,这次却自救失败        SAVE_HOOK = null;        System.gc();        Thread.sleep(500);        if(SAVE_HOOK != null){            SAVE_HOOK.isAlive();        }else{            System.out.println("yes,i am dead : ) ");        }    }}
  1. 回收方法区
    (1)永代区垃圾回收主要两部分:废弃常量(系统中不再使用的常量)和无用的类
    (2)无用的类的定义:该类所有的实例都已经被回收(java堆中不存在);加载类的ClassLoader已经被回收;该类对应的java.lang.Class对象没有在任何地方被引用(无法在任何地方通过反射访问这个对象的任何方法)
    (3)Hotspot虚拟机提供-Xnoclassgc参数进行控制是否对无用的类进行回收,并且还可使用-verbose:class 以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载和卸载信息

二、垃圾收集算法

  1. 标记-清除算法
    (1)标记-清除算法定义:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它是最基础的算法。
    (2)标记-清除算法问题:效率问题,标记和清除的效率都不高;标记清除后产生大量不连续碎片,在下次分配大内存对象无法找到连续的内存空间,从而提前触发垃圾回收动作。

  2. 复制算法
    (1)复制算法定义:可用内存划分成大小相同两块,每次使用一块,当这块用完,将还存活的对象复制到另一块,然后清楚已使用的内存空间。
    (2)复制算法问题:将内存缩小为原来的一半,这个代价太高了。
    (3)现在商业虚拟机都采用这种算法来回收新生代,将内存分为一块较大的Eden空间和两块Survivor空间,然后每次使用其中Eden空间和一块From Survivor空间;回收时,将Eden空间和From Survivor空间存活的对象一次性复制到To Survivor空间,当To Survivor空间内存不够时,需要依赖老年代进行分配担保。Hotspot中Eden空间、From Survivor、To Survivor默认比例是8:1:1

  3. 标记-整理算法
    (1)标记-整理算法定义:首先标记出所有需要回收的对象,在标记完成后让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存,老年代中使用。

  4. 分代收集算法
    (1)在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法;而老年代中因为对象存活率高,没有额外的空间对它进行分配担保,那就必须使用“标记-清除”、“标记-整理”算法进行回收。

    三、Hotspot的算法实现

    1. 枚举根节点
      (1)枚举根节点必须要停顿的,当系统停顿下来后,并不需要一个不漏的检查完所有执行上下文和全局的引用位置,在Hotspot的实现中,Hotspot把对象上什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用,然后使用一组称为OopMap的数据结构保存,这样GC在扫描时就可以直接得知这些信息了。

    2. 安全点
      (1)安全点选定的标准:程序执行时并非所有的地方都能停顿下来GC,只有在安全点才能暂停。而安全点选定的标准是按照是否具有让程序长时间执行的特征为标准选定的
      (2)如何在程序GC时让所有线程都跑到最近安全点上停顿下来:抢先式中断,不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上;主动式中断,当GC需要中断时,不直接对线程进行操作,仅仅简单设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时,就自己中断挂起,轮询标志的地方和安全点是重合的。

    3. 安全区域
      (1)线程处于Sleep、Blocked状态,这时候线程无法响应JVM的中断请求,走到安全的地方去中断挂起,JVM也不会等待线程重新分配CPU时间,对于这种情况就需要使用安全区域来解决。
      (2)安全区域定义:指在一段代码片段中,引用关系不会发生变化,在这个区域的任何地方开始GC都是安全的,安全区域是安全点的扩展。
      (3)安全区域执行过程:在线程执行到安全区域的中的代码时,首先表示自己进入安全区域,当在这段时间里JVM发起GC时,就不用管标识自己为安全区域的线程。在线程离开安全区域时,它要检查系统是否已经完成了根节点枚举,如果完成了,线程就继续执行,否则必须等待直到收到可以安全离开安全区域的信号为止。

四、垃圾收集器

  1. Serial收集器
    (1)Serial收集器特性:Serial收集器是一个采用复制算法的单线程收集器,它进行垃圾收集时,必须暂停其他的所有工作线程,直到它收集结束。它是虚拟机运行在Cilent模式下的默认新生代收集器。它相对于其他收集器来说简单而高效
    (2)Serial收集器应用场景:主要应用于用户的桌面应用系统

  2. ParNew收集器
    (1)ParNew收集器特性:ParNew收集器是Serial收集器的多线程版本,其余特性和控制和Serial收集器一样。是许多运行在Server模式下的虚拟机中首选的新生代收集器。可以与CMS收集器配合工作。使用-XX:+UseConcMarkSweepGC选项后默认的新生代收集器,也可以使用-XX:+UseParNewGC选项强制指定它,可以使用-XX:+ParallelGCThreads来限制垃圾收集的线程数。

  3. Parallel Scavenge收集器
    (1)Parallel Scavenge收集器特性:Parallel Scavenge收集器是一个使用复制算法的新生代的并行的多线程的收集器。Parallel Scavenge收集器是达到一个可控制的吞吐量(吞吐量 = 运行用户代码时间/( 运行用户代码时间+垃圾收集时间)),并且Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是最大垃圾停顿收集时间-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。还有一个-XX:+UseAdaptiveSizePolicy开关参数,这个参数打开后会动态调节一些细节参数,这种行为被称为GC自适应调节策略。这也是Parallel Scavenge收集器区别ParNew收集器的一个地方。

  4. Serial Old收集器
    (1)Serial Old收集器特性:Serial收集器的老年版本,单线程收集器,使用“标记-整理”算法,给Client模式下的虚拟机使用。
    (2)Serial Old收集器用途:jdk1.5以及以前版本中与Parallel Scavenge收集器搭配使用;作为CMS收集器的后备预案。

  5. Parallel Old收集器
    (1)Parallel Old收集器特性:Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源敏感的场合,可以优先考虑Parallel Scavenge收集器加上Parallel Old收集器组合。

  6. CMS收集器
    (1)CMS收集器特性:CMS收集器是一种以获取最短回收停顿时间为目标,并且基于“标记-清除”算法的收集器。目前主要用于互联网站和B/S系统的服务端的java应用上。
    (2)CMS收集器运作过程:初始标记;并发标记;重新标记;并发清除
    (3)CMS收集器的三个缺点:CMS收集器对CPU资源非常敏感;CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败导致另一次Full GC的产生;基于“标记-清除”算法,当空间碎片过多的时候,将会给大对象分配带来大麻烦,往往出现老年代还有很多剩余空间,却没有足够大的连续空间分配当前对象,不得不提前触发一次Full GC,提供一个-XX:+UseCompactAtFullCollection开关参数设置内存碎片的合并整理过程。

  7. G1收集器
    (1)G1收集器特性:G1收集器是一款面向服务端应用的垃圾收集器。具备以下的特点:并行与并发;分代收集;空间整合,G1从整体上看是基于“标记-整理”算法的收集器,从局部上看是基于“复制”算法实现的;可预算停顿。保留新生代和老年代的概念,但是新生代和老年代不再是物理隔离的,它们都是一部分Region(不需要连续)的集合。
    (2)G1建立可预测停顿时间模型:G1跟踪各个Region里面的垃圾堆积的价值大小(回收空间大小和所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。这种方式可以提高G1的收集效率。
    (3)G1收集器中Region对象引用如何避免全堆扫描:G1的每个Region都有一个与之对应的Remembered Set,虚拟机发现程序对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查引用的对象是否处于不同Region之中,如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中,当内存进行回收时,在GC根节点枚举范围中Remembered Set即可保证不对全堆进行扫描也不会有遗漏。
    (4)G1运作步骤:初始标记,仅仅标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,需要停顿,但耗时短;并发标记,从GC Roots开始对堆中对象进行可达性分析,找出存活对象,耗时长,可以用户程序并发执行;最终标记,修正并发编程阶段用户线程产生的标记变动的标记记录,将这段时间的对象变化记录在Remembered Set Logs中,并最终合并到Remembered Set中,需要停顿线程,可并行执行;筛选标记,对各个Region回收价值和成本进行排序,根据用户期望的GC停顿时间制定回收计划。

五、内存分配与回收策略

  1. 对象优先在Eden分配
    (1)大多数情况,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机发起一次Minor GC
    (2)Minor GC、Full GC区别:Minor GC发生在新生代的GC,GC频繁,回收速度快;Full GC发生在老年代GC,速度一般比Minor GC慢10倍。
public class Test {    private static final int SIZE = 1024*1024;    /**     * VM Args: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails     *  -XX:SurvivorRatio = 8     */    public static void testAllocation(){        byte[] allocation1,allocation2,allocation3,allocation4;        allocation1 = new byte[2*SIZE];        allocation2 = new byte[2*SIZE];        allocation3 = new byte[2*SIZE];        allocation4 = new byte[4*SIZE];//出现一次Minor GC    }}
  1. 大对象直接进入老年代
    (1)Serial和ParNew两款收集器,通过设置-XX:PretenureSizeThreshold参数令大于这个设置值的对象直接在老年代分配。
    /**     * VM Args: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails     *  -XX:SurvivorRatio = 8     *  -XX:PretenureSizeThreshold = 3145728     */    public static void testPretenureSizeThreshold(){        byte[] allocation;        allocation = new byte[4*SIZE];//直接分配在老年代中    }
  1. 长期存活的对象将进入老年代
    (1)-XX:MaxTenuringThreshold参数设置老年代的年龄阀值。当新生代中存活对象的年龄达到这个值后进入老年代。
/**     * VM Args: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails     *  -XX:SurvivorRatio = 8 -XX:MaxTenuringThreshold = 1     *  -XX:MaxTenuringDistribution     */    public static void testTenuringThreshold(){        byte[] allocation1,allocation2,allocation3;        allocation1 = new byte[SIZE/4];        //什么时候进入老年代取决于-XX:MaxTenuringThreshold设置        allocation2 = new byte[4 * SIZE];        allocation3 = new byte[4 * SIZE];        allocation3 = null;        allocation3 = new byte[4 * SIZE];    }
  1. 动态对象年龄判断
    (1)如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄等于或者大于该年龄的对象就可以直接进入老年代,无需等到-XX:MaxTenuringThreshold参数设置的年龄。
/**     * VM Args: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails     *  -XX:SurvivorRatio = 8 -XX:MaxTenuringThreshold = 15     *  -XX:MaxTenuringDistribution     */    public static void testTenuringThreshold2(){        byte[] allocation1,allocation2,allocation3,allocation4;        allocation1 = new byte[SIZE/4];        //allocation1+allocation2大于survivor空间一半        allocation2 = new byte[SIZE/4];        allocation3 = new byte[4 * SIZE];        allocation4 = new byte[4 * SIZE];        allocation4 = null;        allocation4 = new byte[4 * SIZE];    }
  1. 空间分配担保
    (1)jdk6 update 24之后只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC
0 0
原创粉丝点击