深入理解java虚拟机学习笔记-第3章

时间:2023-01-02 13:33:34

深入理解java虚拟机学习笔记-第3章

1 第3章-垃圾收集器与内存分配策略

1.1 对象已死吗

1.1.1 引用计数算法

  • 测试java不是使用引用计数算法进行垃圾回收的代码
/**
 * VM Args: -XX:PrintGCDetails
 * @author devinkin 
 */
public class ReferenceCountingGC {
    public Object instance = null;

    private static final int _1MB = 1024 * 1024;

    /**
     * 这个成员是属性的唯一意义就是占点内存,以便能在GC日志中清楚看到是否被回收过
     */
    private byte[] bigSize = new byte[2 * _1MB];

    public static void main(String[] args) {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;

        // 假设在这行发生GC,objA和objB是否能被回收
        System.gc();
    }
}

1.1.2 可达性分析算法

  • 可达性分析算法是用来判定对象是否存活的.
  • 可达性分析算法的基本思想就是通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(GC Roots到这个对象不可达),则证明此对象是不可用的.
  • Java语言中,可作为 GC Roots 的对象包括下面几种:
    • 虚拟机栈(栈帧中的本地变量表)中引用的对象.
    • 方法区中类静态属性引用的对象.
    • 方法区中常量引用的对象.
    • 本地方法中JNI(即一般说的Native方法)引用的对象.

1.1.3 再谈引用

  • JDK1.2后,Java将引用分为: 强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference),虚引用(Phantom Reference)4种.这四种引用强度依次逐渐减弱.
    • 强引用是指在程序代码之中普遍存在的,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象.
    • 软引用是用来描述一些还有用但并非必须的对象.对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收.如果这次回收还没有足够的内存,才会抛出内存溢出异常.
    • 弱引用也是用来描述非必须对象的,它的强度比软引用更弱一点.被弱引用关联的对象只能生存到下一次垃圾收集发生之前.当垃圾收集器工作时,无论当前内存对象是否足够,都会回收掉只被弱引用关联的对象.
    • 虚引用是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例.为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知.

1.1.4 生存还是死亡

  • 对象真正死亡,只要要经历两次标记过程
    • 如果对象在进行可达性分析后发现没有与"GC Roots"相连接的引用链,那么它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否由必要执行finalize()方法.
    • 当对象没有覆盖finalize()方法 , 或者finalize()方法已经被虚拟机调用过 ,虚拟机将这两种情况都视为"没有必要执行".
    • 如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做 F-Queue 的队列之中等待回收.
    • 稍后 GC 将对 F-Queue 中的对象进行第二次小规模标记,如果对象要自救,要在第二次标记时将被移除出"即将回收"的集合.
  • 一次对象自我拯救的演示
    • 任何一个对象的finalize()方法都只会被系统自动调用一次.
    • 如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了.
/**
 * 此代码演示了两点
 * 1. 对象可以在被GC时自我拯救.
 * 2. 这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
 * @author devinkin
 */
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("no, I am dead :(");
        }

        // 下面这段代码与上面完全相同,但是这次自救却失败了
        SAVE_HOOK = null;
        System.gc();
        // 因为finalize方法优先级很低,所以暂停0.5秒以等待它.
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, I am dead :(");
        }
    }
}

1.1.5 回收方法区

  • 永久代的垃圾收集主要回收两部分内容: 废弃常量和无用的类.
  • 回收废弃的常量与回收Java堆中的对象非常类似.
  • 类需要同时满足下面3个条件才能算是"无用的类"
    • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例.
    • 加载该类的ClassLoader已经被回收.
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法.
  • 是否对类进行回收,HotSpot虚拟机提供了 -Xnoclassgc 参数进行控制.
  • 使用 -verbose:class 以及 -XX:+TraceClassLoading, -XX:+TraceClassUnLoading 查看类加载和卸载信息.

1.2 垃圾收集算法

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

  • 算法分为"标记"和"清除"两个阶段
    • 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象.
  • 标记-清除算法的不足
    • 标记和清除两个过程的效率都不高.
    • 标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前出发另一次垃圾收集动作.

1.2.2 复制算法

  • 复制收集算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块.
  • 当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把自己使用过的内存空间一次清理掉.
  • 每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复制情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效.
  • 不足: 将内存缩小为原来的一半,代价太高.

1.2.3 标记-整理算法

  • 标记-整理算法,标记过程依旧一样,整理过程是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存.

1.2.4 分代收集算法

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

1.3 HotSpot的算法实现

  • 在HotSpot的实现中,使用一组称为OopMap的数据结构来记录哪些地方存放着对象引用.

1.3.1 安全点

  • 如果OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,需要大量的额外空间,GC成本会变得很高.
  • HotSpot在特定的位置记录了OopMap信息,这些位置称为安全点(Safepoint).即程序执行时并非在所有地方都能停顿下来开始GC,只有达到安全点时才能暂停.
  • 安全点的选择基本上是以程序"是否具有让程序长时间执行的特征"为标准进行选定的.
  • "长时间执行"的最明显特征就是指令序列服用,例如方法调用,循环跳转,异常跳转等,所以具有这些功能的指令才会产生Safepoint.
  • 对于Safepoint,在GC发生时让所有线程(不包括JNI调用的线程)都在到最近的安全点上停顿的解决方案有2个:
    • 抢先式中断(Preemptive Suspension)
    • 主动式中断(Voluntary Suspension)
  • 抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现由线程中断的地方不在安全点上,就恢复线程,让"跑"到安全点上.
  • 主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起.轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方.

1.3.2 安全区域

  • 安全区域是指在一端代码片段中,引用关系不会发生变化.在这个区域中的任意地方开始GC都是安全的.(Safe Region)

1.4 垃圾收集器

  • 收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现.

1.4.1 Serial收集器

  • Serial收集器是单线程的收集器
  • Serial收集器单线程的意义不仅说明它只会使用一个CPU或一条收集线程去完成收集工作,更重要的是它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束.

1.4.2 ParNew收集器

  • ParNew收集器是Serial收集器的多线程版.
  • 只有ParNew收集器能与CMS收集器配合工作.
  • 可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数.
  • 收集器并行(Parallcl): 指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态.
  • 收集器并发(Concurrent): 指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上.

1.4.3 Parallel Scavenge收集器

  • Parrallel Scavenge收集器是一个新生代的收集器,它也是使用复制算法的收集器,又是并行的多线程收集器.
  • Parrallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput).所谓吞吐量就是CPU用于运行用户代码的时间和CPU总消耗时间的比值.
    • 吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间).
  • Parrallel Scavenge收集器提供了两个参数用于精确控制吞吐量
    • 控制最大垃圾收集停顿时间的: --XX:MaxGCPauseMillis, 收集器将尽可能地保证内存回收花费的时间不超过设定值(毫秒).
    • 直接设置吞吐量大小的: -XX:GCTimeRatio, 值是大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数,默认值是99,也就是允许最大1%(即1/(1+99))的垃圾收集时间.
    • 参数: -XX:+UseAdaptiveSizePolicy 打开,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整一些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics).

1.4.4 Serial Old收集器

  • Serial Old是Serial收集器老年代版本,使用"标记-整理"算法.

1.4.5 Parallel Old收集器

  • Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和"标记-整理"算法.

1.4.6 CMS收集器

  • CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器.
  • 适用于重视服务的响应速度,希望系统停顿时间最短的场合.
  • CMS收集器是基于"标记-清除"算法,整个过程分为4个步骤:
    • 初始标记(CMS initial mark)
    • 并发标记(CMS concurrent mark)
    • 重新标记(CMS remark)
    • 并发清除(CMS concurrent sweep)
  • 初始标记,重新标记这两个步骤仍然需要"Stop The World".
  • 初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快.
  • 并发标记阶段就是进行GC Roots Tracing的过程.
  • 重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍微长一些,但远比并发标记的时间短.
  • 并发标记和并发清除过程收集器线程和用户线程一起工作,所以总体来说,CMS收集器的内存回收过程与用户线程一起并发执行的.
  • CMS收集器的优点
    • 并发收集
    • 低停顿.
  • CMS收集器的缺点
    • CMS收集器对CPU资源非常敏感
    • CMS收集器无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败而倒置另一次Full GC的产生.
    • 使用"标记-清除"算法,会导致大量的空间碎片产生.

1.4.7 G1收集器

  • G1(Garbage-First)收集器是一款面向服务端应用的垃圾收集器.
  • G1收集器的特点:
    • 并行与并发: G1能充分利用多CPU,多核的硬件优势来缩短Stop-The-World停顿时间.G1收集器仍可以通过并发的方式让Java程序继续执行.
    • 分代收集: 能采用不同的的方式处理新创建的对象和已经存活了一段时间的旧对象.
    • 空间整合: G1从整体来看是基于"标记-整理"算法实现的收集器,从局部(两个Region之间)来看是基于"复制"算法实现的.G1运作期间都不会产生内存空间碎片.
    • 可预测的停顿: 能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒.
  • G1收集器的运作大致可分为一下几个步骤
    • 初始标记(Initial Marking)
    • 并发标记(Concurrent Marking)
    • 最终标记(Final Marking)
    • 筛选回收(Live Data Counting and Evacuation)

1.4.8 理解GC日志

  • 最前面的数字,代表了GC发生的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数.
  • GC日志开头的 [GC[Full GC 说明这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC.
    • 如果有 FUll ,说明这次GC是发生了~Stop-The-World~的.
    • 如果调用 System.gc() 方法所触发的收集,那么这里显示 [Full GC(System)
  • [DefNew , [Tenured , [Perm 表示GC发生的区域.
    • Serial收集器,新生代名称为 [DefNew
    • ParNew收集器,新生代名称为 [ParNew
    • Parallel New Generation收集器,新生代名称为 PSYoungGen, 老年代为 PSOldGen, 永久代为 PSPermGen.
  • 方括号内部的 3324K->152K(3712K) a含义是"GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)".
  • 方括号之外的 3324K->152K(11904K) 表示"GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)".
  • 括号之外往后, 0.0025925 表示该内存GC所占用的时间,单位是秒.
  • 有的收集器会给出更具体的时间数据,如 [Times: user=0.01, sys=0.00, real=0.02 secs.
    • user: 用户态消耗的CPU时间
    • sys: 内核态消耗的CPU时间
    • real: 操作从开始到结束所经过的墙钟时间(Wall Clock Time)
    • CPU时间和墙钟时间的区别
      • 墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O,等待线程阻塞.
      • 系统由多个CPU或者多核,多线程操作会叠加这些CPU时间.user+sys>real很正常.

1.4.9 垃圾收集器参数总结

参数 描述
UseSerialGC 虚拟机运行在Client模式下的默认值,打开此开关后,使用Serial+Serial Old收集器组合进行内存回收.
UseParNewGC 打开次开关后,使用ParNew+Serial Old的收集器组合进行内存回收.
UseConcMarkSweepGC 打开此开关后,使用ParNew+CMS+Serial Old的收集器组合进行内存回收.Serial Old收集器将作为CMS收集器出现Concurrent Mode Failure失败后的后备收集器使用.
UseParallelGC 虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器组合进行内存回收.
SurvivoRatio 新生代中Eden区域与Survivor区域的容量比值,默认是8,代表Eden:Suvivor=8:1.
PretenureSizeThreshold 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配.
MaxTenuringThreshold 晋升到老年代的对象年龄,每个对象在坚持过一次Minor GC之后,年龄就增加1,当超过这个参数值时就进入老年代.
UseAdaptiveSizePolicy 动态调整Java堆中各个区域的大小以及进入老年代的年龄
HandlePromotionFailure 是否允许分配担保失败,即进入老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况.
ParallelGCThreads 设置并行GC时进行内存回收的线程数
GCTimeRatio GC时间占总时间的比率,默认是99,即允许1%的GC时间,仅在使用Parallel Scavenge收集器时生效.
MaxGCPauseMillis 设置GC的最大停顿时间,仅在使用Parallel Scavenge收集器时生效.
CMSInitiatingOccupancyFraction 设置CMS收集器在老年代空间被使用多少后出发垃圾收集,默认值为为68%,仅在使用CMS收集器时生效.
UseCMSCompactAtFullCollection 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片管理,仅在CMS收集器时生效.
CMSFullGCBeforeCompaction 设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片管理,仅在使用CMS收集器时生效.

1.5 内存分配与回收策略

  • 对象的内存分配,往大方向讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配.

1.5.1 对象优先在Eden分配

  • 大多情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC.
  • 虚拟机提供了 -XX:+PrintGCDetails 这个收集器日志参数,告诉虚拟机在垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况.
  • Minor GC 和 FULL GC 有什么不一样?
    • 新生代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倍以上.
  • 新生代Minor GC
/**
 * 内存分配与回收策略
 * @author devinkin
 */
public class AllocateGC {
    private static final int _1MB = 1024 * 1024;

    /**
     * VM 参数: -XX:+UseSerialGC -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
     */
    public static void testAllocation() {
        byte[] allocation1, allocation2, allocation3, allocation4;
        // 6M,eden区对象没有被垃圾回收成功,Minor GC时候,就把这6M转移到空的Survivor区域
        // SUrvivor由from to两个
        // 但Survivor只有1M,GC期间虚拟机又发现已有的3个2MB大小对象全部无法放入Survivor空间
        // 所以只好通过分配担保机制提前转移到老年区(tenured generation)
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        // 出现一次Minor GC,年轻代到达了10M
        allocation4 = new byte[4 * _1MB];
    }

    public static void main(String[] args) {
        testAllocation();
    }
}

1.5.2 大对象直接进入老年代

  • 大对象就是大量连续内存空间的Java对象,例如很长的字符串以及数组.
  • 经常出现大对象容易导致内存还有不少空间时就提前出发垃圾收集以获取足够的连续空间来"安置"它们.
  • 虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配.
    • 目的: 避免在Eden区及两个Survivor区之间发生大量的内存复制.(新生代采用复制算法收集内存).
  • 大对象直接进入老年代
/**
 * 大对象直接进入老年代
 */
public class AllocateGC2 {
    private static final int _1MB = 1024 * 1024;

    /**
     * VM参数: -XX:+UseSerialGC -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728
     */
    public static void testPretenureSizeThreshold() {
        byte[] allocation;
        // 直接分配在老年代
        allocation = new byte[4 * _1MB];
    }
    public static void main(String[] args) {
        testPretenureSizeThreshold();
    }
}

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

  • 虚拟机给每个对象定义了一个对象年龄(Age)计数器.如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1.
  • 对象在Survivor区中每"熬过"一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度(默认是15岁),就会被晋升到老年代中.
  • 对象晋升老年代的年龄阀值,可以通过参数 -XX:MaxTenuringThreshold 设置.
  • 长期存活的对象将进入老年代
/**
 * 长期存活的对象进入老年代
 */
public class AllocateGC3 {
    private static final int _1MB = 1024 * 1024;
    /**
     * VM参数: -XX:+UseSerialGC -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
     */
    @SuppressWarnings("unused")
    public static void testTenuringThreshold() {
        byte[] allocation1, allocation2, allocation3;
        allocation1 = new byte[_1MB / 4];
        // 什么时候进入老年代取决于XX:MaxTenuringThreshold设置
        allocation2 = new byte[4 * _1MB];
        allocation3 = new byte[4 * _1MB];
        // Minor GC,年龄为allocation1,2,3都被移动到Survivor空间,年龄为1
        //由于年龄为1,所以allocation1,2,3直接晋升为老年代,移动到tenured gen空间中
        allocation3 = null;
        allocation3 = new byte[4 * _1MB];
        // 这是第二次minor GC
    }
    public static void main(String[] args) {
        testTenuringThreshold();
    }
}

1.5.4 动态对象年龄判断

  • 虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升为老年代.
  • 如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄.
  • 动态对象年龄判断
/**
 * 动态对象年龄判定
 */
public class AllocateGC4 {
    private static final int _1MB = 1024 * 1024;
    /**
     * VM 参数:
     * -XX:+UseSerialGC
     * -verbose:gc
     * -Xms20M
     * -Xmx20M
     * -Xmn10M
     * -XX:+PrintGCDetails
     * -XX:SurvivorRatio=8
     * -XX:MaxTenuringThreshold=15
     * -XX:+PrintTenuringDistribution
     */
    public static void testTenuringThreeshold2() {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[_1MB / 4];
        // allocation1 + allocation2大于survivor空间一半
        allocation2 = new byte[_1MB / 4];
        allocation3 = new byte[4 * _1MB];
        // 第一次Minor GC
        allocation4 = new byte[4 * _1MB];
        allocation4 = null;
        // 第二次Minor GC
        allocation4 = new byte[4 * _1MB];
    }

    public static void main(String[] args) {
        testTenuringThreeshold2();
    }
}

1.5.5 空间分配担保

  • 在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间.
    • 如果这个条件成立,那么Minor GC可以确保是安全的.
    • 如果这个条件不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败.
      • 如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小.如果大于,将尝试进行一次Minor GC.如果小于,或者HandlePromotionFailure设置不允许担保失败,那这时也要改为进行一次Full GC.
  • 当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代.
  • 前提是老年代本身还有容纳这些对象的剩余空间,所以只好取每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间.
  • 空间分配担保
/**
 * 空间分配担保
 *
 * @author devinkin
 */
public class AllocateGC5 {
    private static final int _1MB = 1024 * 1024;

    /**
     * VM参数:
     * -XX:UseSerialGC
     * -Xms20M
     * -Xmx20M
     * -Xmn10M
     * -XX:+PrintGCDetails
     * -XX:SurvivorRatio=8
     * -XX:+HandlePromotionFailure
     */
    public static void testHandlePromotion() {
        byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        // 回收allocation1进入了老年代,因为Survivor内存为1M,不足以存放2M的内容
        allocation1 = null;
        // 第一次Minor GC,回收了allocation1
        allocation4 = new byte[2 * _1MB];
        // 第二次Minor GC,allocation5直接进入老年代
        allocation5 = new byte[2 * _1MB];
        // 第三次Minor GC,allocation6直接进入老年代
        allocation6 = new byte[2 * _1MB];

        allocation4 = null;
        allocation5 = null;
        allocation6 = null;
        // 在老年代分配allocation7,可能会导致Full GC的进行
        allocation7 = new byte[2 * _1MB];
    }

    public static void main(String[] args) {
        testHandlePromotion();
    }
}

Date: 2018-11-13 11:26

Author: devinkin

Created: 2018-11-13 二 23:05

Validate