美文网首页
3.垃圾收集器与内存分配策略

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

作者: xMustang | 来源:发表于2020-02-22 23:16 被阅读0次

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

    1. 概述

    垃圾回收器关注的是堆内存、方法区。

    程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,所以这3个区域不需要过多考虑回收问题。

    2. 对象已死吗

    2.1 引用

    从Java 1.2开始,JVM开发团队发现,单一的强引用类型,无法很好的管理对象在JVM里面的生命周期,垃圾回收策略过于简单,无法适用绝大多数场景。为了更好的管理对象的内存,更好的进行垃圾回收,JVM团队扩展了引用类型,从最早的强引用类型增加到强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference):

    • 强引用。

      StrongRerence这个类并不存在,而是在JVM底层实现。默认的对象都是强引用类型,继承自Reference、SoftReference、WeakReference、PhantomReference的引用类型非强引用。

      Object obj = new Object();
      

      强引用类型,如果JVM垃圾回收器GC Roots可达性分析结果为可达,表示引用类型仍然被引用着,这类对象始终不会被垃圾回收器回收,即使JVM发生OOM也不会回收。而如果GC Roots的可达性分析结果为不可达,那么在GC时会被回收。

    • 软引用。

      在JVM内存充足的情况下,软引用并不会被垃圾回收器回收,只有在JVM内存不足的情况下,才会被垃圾回收器回收。主要用于实现类似缓存功能,在内存足够时直接通过软引用取值,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。

      软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java 虚拟机就会把这个软引用(而不是这个软引用关联的对象)加入到与之关联的引用队列中。

      String s = new String("Frank");    // 创建强引用与String对象关联,现在该String对象为强可达状态
      SoftReference<String> softRef = new SoftReference<String>(s);     // 再创建一个软引用关联该对象
      s = null;        // 消除强引用,现在只剩下软引用与其关联,该String对象为软可达状态
      s = softRef.get();  // 重新关联上强引用
      
      public SoftReference(T referent) {
      }
      
      public SoftReference(T referent, ReferenceQueue<? super T> q) {
      }
      
    • 弱引用。

      不论当前内存是否充足,都只能存活到下一次垃圾收集之前。

      在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象(如果弱引用对象较大,直接进到了老年代,那么就可以苟且偷生到Full GC触发前,所以弱引用对象也可能存在较长的一段时间)。

      弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

      WeakReference<String> weakReference = new WeakReference<String>(new String("Hello"));
      System.gc();
      if(weakReference.get() == null) {
          System.out.println("weakReference已经被GC回收");
      }
      
      public WeakReference(T referent) {
      }
      
      public WeakReference(T referent, ReferenceQueue<? super T> q) {
      }
      
    • 虚引用。

      虚引用与前面的几种都不一样,这种引用类型不会影响对象的生命周期,所持有的引用就跟没持有一样,随时都能被GC回收。

      在使用虚引用时,必须和引用队列关联使用(看其他引用类型的构造方法,可以与引用队列关联,也可以不与引用队列关联)。

      在对象的垃圾回收过程中,如果GC发现一个对象还存在虚引用,则会把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象内存被回收之前采取必要的行动防止被回收。

      虚引用主要用来跟踪对象被垃圾回收器回收的活动。业界暂无使用场景,可能被JVM团队内部用来跟踪JVM的垃圾回收活动。

      PhantomReference<String> phantomReference = new PhantomReference<String>(new String("Hello"), new ReferenceQueue<String>());
      

    2.2 判断对象是否已死算法

    1. 引用计数算法

      给对象添加一个引用计数器,每当一个地方引用它时,计数器值加1;引用失效时,计数器值减1。任何时刻计数器为0的对象就是不可能再被使用的。

      这种算法很难解决对象循环引用问题。

      public class ReferenceCountingGC {
          public Object instance = null;
          public static void main(String[] args) {
              ReferenceCountingGC objA = new ReferenceCountingGC();
              ReferenceCountingGC objB = new ReferenceCountingGC();
              // 循环引用
              objA.instance = objB;
              objB.instance = objA;
      
              objA = null;
              objB = null;
          }
      }
      该例中,ObjA、ObjB都不能再被访问,但是它们的引用计数值都不为0,于是引用计数算法无法通知 GC 回收器回收他们。
      
    2. 可达性分析算法

      主流虚拟机包括HotSpot使用这种算法。

      通过一系列的称为GC Roots的对象作为起始点,从这些起始点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,证明此对象是不可用的。

      可达性分析算法

      可作为GC Roots的对象有:

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

    2.3 生存还是死亡

    宣告一个对象死亡,经历两次标记过程:

    1. 经过可达性分析发现对象不可达后,进行第一次标记,加入“即将回收”的集合。
    2. 对第1步标记的对象,进行finalize()是否有必要执行判断。没有必要执行finalize方法的对象包括:没有覆盖finalize方法、已经执行过finalize方法的对象(一个对象的finalize方法最多只会被系统自动调用一次)。如果判定为需要执行finalize方法,会将对象放入F-Queue队列中,由虚拟机自动建立的、低优先级的Finalizer线程去触发F-Queue中对象的finalize的执行,并不承诺会等待finalize方法执行结束。稍后GC会对F-Queue中的对象进行第二次小规模的标记,如果对象成功救活自己(重新与引用链上的任何一个对象建立关联,譬如把自己(this关键字)赋值给某个类变量或对象的成员变量),会被移除“即将回收”的集合(这个”即将回收“的集合是第1点中说的”即将回收“的集合,而不是F-Queue队列)。仍在“即将回收”的集合中的对象基本上就真的被回收了。

    对象执行finalize方法逃出被回收的方式,且只能自救一次。因为对于任何给定对象,Java 虚拟机最多只调用一次 finalize 方法。

    public class FinalizeEscapeGC {
        public static FinalizeEscapeGC SAVE_HOOK = null;
    
        @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.5s等待它执行
            Thread.sleep(500);
            if (SAVE_HOOK != null) {
                System.out.println("yes, i am still alive");
            } else {
                System.out.println("no, i am dead");
            }
    
            // 对象第二次自救失败
            SAVE_HOOK = null;
            System.gc();
            Thread.sleep(500);
            if (SAVE_HOOK != null) {
                System.out.println("yes, i am still alive");
            } else {
                System.out.println("no, i am dead");
            }
        }
    }
    

    运行结果:

    生存还是死亡

    不推荐使用finalize方法来拯救对象,因为它运行代价高昂、不确定性大、无法保证每个对象的调用顺序。关闭资源工作使用try-finally,不使用finalize(),可以忘掉这个方法的存在。

    3. 回收方法区

    方法区的垃圾收集主要包括两部分:

    • 废弃常量
    • 无用的类
    1. 废弃常量

      没有任何String对象引用常量池中的常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个常量会被系统清理出常量池。

      常量池中的其他类(接口)、方法、字段的符号引用与此类似。

    2. 无用的类

      判断条件:

      • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
      • 加载该类的ClassLoader已经被回收。
      • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

      类需要同时满足上面3个条件才是无用的类。

    虚拟机参数:

    HotSpot提供了-Xnoclassgc控制是否对类回收。

    查看类的加载、卸载信息:

    • HotSpot VM Product版:-verbose:class、-XX:+TraceClassLoading
    • HotSpot VM FastDebug版:-XX:+TraceClassUnLoading

    在大量使用反射、动态代理、CGLib等ByteCode框架,动态生成JSP、OSGi这类频繁自定义ClassLoader的场景,需要虚拟机具备类卸载的功能。

    4. 垃圾收集算法

    垃圾收集算法

    4.1 标记—清除算法

    Mark-Sweep,分为标记,清除两个阶段:

    • 首先标记出所有需要回收的对象
    • 在标记完成后统一回收所有被标记的对象。

    主要不足:

    • 效率问题:标记和清除两个过程的效率都不高
    • 空间问题:标记清除后产生大量不连续的内存碎片

    后续的收集算法都基于标记—清除算法的思路,并对其不足进行改进。

    4.2 标记—复制算法

    Mark-Copying

    解决标记—清除算法的效率问题。

    将内存按容量划分成大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

    4.3 标记—整理算法

    Mark-Compact

    解决标记—清除算法的空间问题。

    不是直接对可回收对象进行清理,而是让所有存活着的对象都向一端移动,然后直接清理掉端边界外的内存。这种算法针对老年代设计。

    4.4 分代收集算法

    HotSpot VM把Java堆分成新生代、老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法:在新生代采用标记—复制算法,在老年代采用标记—整理算法:

    1. 新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
    2. 老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
    • 新生代:Young Generation。

      8:1:1的方式,分成一块 Eden、两块 Survivor。每次只使用1块 Eden、1块 Survivor,当回收时,复制存活的对象到另一块 Survivor。如果另一块 Survivor 存不下,通过分配担保进入老年代。

    • 老年代:Tenured Generation(Old Generation)

    • 永久代:Permanent Generation。是JDK1.6中对方法区的实现。

    5. HotSpot垃圾回收

    5.1 发起内存回收

    1. 枚举根节点

      可达性分析必须在一个能确保一致性的快照中进行。在整个分析期间,整个系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,否则无法保证分析结果准确性。GC进行时必须要停顿所有Java执行线程(Stop the World),即使在号称不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。

      可达性分析中的枚举根节点,在执行系统停顿下来后,不需要一个不漏的检查完所有执行上下文和全局的引用位置,在HotSpot VM中使用一组称为OopMap的数据结构知道哪些地方存在对象的引用。

      OopMap

      OopMap 记录了栈上本地变量到堆上对象的引用关系。作用是:垃圾收集时,收集线程会对栈上的内存进行扫描,看看哪些位置存储了 Reference 类型。如果发现某个位置确实存的是 Reference 类型,就意味着它所引用的对象这一次不能被回收。

      栈上的本地变量表里面只有一部分数据是 Reference 类型的,但垃圾收集时还是不得不对整个栈全部扫描一遍。

      一个线程意味着一个栈,一个栈由多个栈帧组成,一个栈帧对应着一个方法,一个方法里面可能有多个安全点。 gc 发生时,程序首先运行到最近的一个安全点停下来,然后更新自己的 OopMap ,记下栈上哪些位置代表着引用。枚举根节点时,递归遍历每个栈帧的 OopMap ,通过栈中记录的被引用对象的内存地址,即可找到这些对象( GC Roots )。

      因此,垃圾收集时,OopMap 可以避免全栈扫描,加快枚举根节点的速度。

    2. 安全点

      SafePoint

      HotSpot VM没有为每条指令都生成OopMap,只有在特定的位置记录这些信息,这些位置称为安全点(Safepoint),即程序执行时,并非在所有地方都能停下来GC,只有在到达安全点时才能暂停。线程进入到安全点,也就是线程从此不再执行任何字节码指令,只有当出了安全点时才让他们继续执行原来的指令。

      为了让GC发生时,所有线程都跑到最近的安全点上停顿,HotSpot采用主动式中断。当GC需要中断线程时,简单设置一个标志,各个线程执行时会去轮询这个标志,发现中断标志为真时,就自己中断挂起,轮询标志的地方和安全点是重合的。

    3. 安全区域

      SafeRegion

      安全点机制保证程序运行时,在不太长的时间内就会遇到可进入GC的安全点,但是像处于Sleep、Blocked等状态的线程没有被分配CPU时间,无法响应JVM的中断请求,"走"到安全点中断挂起。此时需要安全区域解决这个问题。

      安全区域是指一段代码片段,引用关系不会发生变化,在这个区域的任何地方开始GC都是安全的。

      当线程要离开SafeRegion时首先检查系统是否完成了根节点枚举(或是整个GC过程),没有完成,就等待直到收到可以离开安全区域的信号为止。

    5.2 垃圾收集器

    下图是JDK 1.7 Update 14之后的HotSpot VM的垃圾收集器。如果两个收集器之间有连线,代表它们可以搭配使用。

    HotSpot VM垃圾收集器

    在垃圾收集器中:

    1. 并行:多条垃圾收集线程并行工作,用户线程处于等待状态。
    2. 并发:用户线程和垃圾收集线程同时执行(但不一定是并行的,可能会交替执行)。

    5.2.1 Serial

    新生代收集器,采用复制算法。

    是一个单线程的收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作,在进行垃圾收集时,会暂停其他所有的工作线程(Stop The World),直到它收集结束。

    是虚拟机运行在Client模式下的默认新生代收集器。

    它简单高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本上不会再大了),停顿时间完全可以控制在几十毫秒最多一百多毫秒内,只要不是频繁发生,这点停顿是可以接受的。所以,Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。

    Seria搭配Serial Old收集器运行示意图

    5.2.2 ParNew

    新生代收集器,采用复制算法。

    是Serial收集器的多线程版本。

    ParNew收集器其他行为(包括可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等)都与Serial收集器完全一样。

    ParNew搭配Serial Old收集器运行示意图

    ParNew收集器是许多运行在 Server 模式下的虚拟机的首要选择。

    除了Serial收集器外,目前只有ParNew收集器能与CMS收集器配合工作。

    5.2.3 Parallel Scavenge

    新生代收集器,使用复制算法。

    CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,适合用于和用户交互的程序。

    Parallel Scavenge收集器关注点是吞吐量。目标是达到一个可控制的吞吐量,高效率地利用CPU时间,适合用于后台运算而不需要太多交互的任务。

    吞吐量:CPU 运行用户代码的时间与 CPU 总消耗时间的比值。吞吐量 = 运行用户代码的时间/(运行用户代码的时间 + 垃圾收集时间)

    Parallel Scavenge参数:

    • -XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间。
    • -XX:GCTimeRatio:设置吞吐量大小。
    • -XX:+UseAdaptiveSizePolicy:这个参数打开后,虚拟机会根据系统运行情况收集性能监控信息,动态调整提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略。

    5.2.4 Serial Old

    老年代收集器,采用标记-整理算法。

    是Serial收集器的老年代版本,也是一个单线程收集器。

    5.2.5 Parallel Old

    老年代收集器,采用标记-整理算法。

    Parallel Old收集器是Parallel Scavenge收集器的老年代版本。

    5.2.6 CMS

    老年代收集器,名字中的Mark Sweep表示使用标记-清除算法。

    CMS收集器是获取最短回收停顿时间为目标的收集器。

    是HotSpot虚拟机中第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

    CMS收集器运行示意图

    整个过程分为四个步骤:

    • 初始标记:暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快。
    • 并发标记:同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断地更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
    • 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。
    • 并发清除:开启用户线程,同时 GC 线程开始对标记的区域做清扫。

    它有下面三个明显的缺点:

    1. 对 CPU 资源敏感
    2. 无法处理浮动垃圾
    3. 它使用的回收算法——“标记-清除”算法会导致收集结束时会有大量空间碎片产生

    5.2.7 G1

    是一款面向服务端应用的垃圾收集器,未来可替换掉CMS收集器。

    主要针对配备多颗处理器及大容量内存的机器,以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。

    它具备以下特点:

    1. 并行与并发

      G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。

    2. 分代收集

      虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。

    3. 空间整合

      与 CMS 的“标记--清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。

    4. 可预测的停顿

      这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。

    G1将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代已经不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

    Remembered Set

    一般来说, GC 过程是这样的:首先枚举根节点。根节点有可能在新生代中,也有可能在老年代中。这里由于我们只想收集新生代(换句话说,不想收集老年代),所以没有必要对位于老年代的 GC Roots 做全面的可达性分析。但问题是,确实可能存在位于老年代的某个 GC Root,它引用了新生代的某个对象,这个对象不能清除。

    事实上,对于位于不同年代对象之间的引用关系,虚拟机会在程序运行过程中给记录下来。对应上面所举的例子,"老年代对象引用新生代对象"这种关系,会在引用关系发生时,在新生代边上专门开辟一块空间记录下来,这就是 RememberedSet 。所以"新生代的 GC Roots" + "RememberedSet 存储的内容",才是新生代收集时真正的 GC Roots。然后就以此为据,在新生代上做可达性分析,进行垃圾回收。

    G1 收集器为了达到可以以 Region 为单位进行垃圾回收的目的,也使用了 RememberedSet 这种技术,在各个 Region 上记录自家的对象被外面对象引用的情况。

    G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region。

    G1收集器运行示意图

    5.3 垃圾收集器参数

    垃圾收集器参数

    5.4 GC日志

    VM参数:

    -verbose:gc -XX:+PrintGCDetails

    3324K—>152K(3712K):GC前该内存区域已使用容量 —> GC后该内存区域已使用容量(该内存区域总容量)。

    5.5 内存分配与回收策略

    • Minor GC:从新生代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。Minor GC 非常频繁,回收速度一般也比较快。
    • Major GC:老年代GC。发生一次Major GC,经常会伴随至少一次的Minor GC。Major GC速度一般会比Minor GC慢10倍以上。
    • Full GC:全局范围的GC,清理整个堆空间——包括新生代和老年代。
    内存分配与回收策略

    下面是使用Serial、Serial Old收集器下的内存分配和回收的策略:(可写代码验证,对于其他垃圾收集器的内存分配策略需要写代码另行验证。)

    1. 对象优先分配在新生代Eden区。

      如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。当Eden区没有足够的空间分配时,发起一次Minor GC。

      /**
       * 测试对象优先分配在Eden区
       * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
       */
      public class Allocation {
          private static final int _1MB = 1024 * 1024;
      
          public static void testAllocation() {
              byte[] allocation1, allocation2, allocation3, allocation4;
              allocation1 = new byte[2 * _1MB];
              allocation2 = new byte[2 * _1MB];
              allocation3 = new byte[2 * _1MB];
              allocation4 = new byte[4 * _1MB];
          }
      
          public static void main(String[] args) {
              testAllocation();
          }
      }
      
    2. 大对象直接进入老年代。

      大对象就是需要大量连续内存空间的Java对象。最典型的大对象就是那种很长的字符串以及数组。

      通过设置 -XX:PretenureSizeThreadhold,让大于该值的对象直接在老年代分配。

      大对象直接进入老年代是为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。

      /**
       * 大对象直接进入老年代
       * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
       * -XX:PretenureSizeThreadhold=3145728
       */
      public class PretenureSizeThreadhold {
          private static final int _1MB = 1024 * 1024;
      
          public static void testPretenureSizeThreadhold() {
              byte[] allocation;
              allocation = new byte[4 * _1MB];
          }
      
          public static void main(String[] args) {
              testPretenureSizeThreadhold();
          }
      }
      
    3. 长期存活的对象将进入老年代。

      对象在Survivor中每熬过一次Minor GC,年龄增加1岁。

      通过-XX:MaxTenuringThreshold设置进入老年代对象的年龄。默认为 15 岁。

      /**
       * 长期存活的对象进入老年代
       * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
       * -XX:MaxTenuringThreshold=1
       * -XX:+PrintTenuringDistribution
       */
      public class TenuringThreshold {
          private static final int _1MB = 1024 * 1024;
      
          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];
      
              allocation3 = null;
              allocation3 = new byte[4 * _1MB];
          }
      
          public static void main(String[] args) {
              testTenuringThreshold();
          }
      }
      
    4. 动态对象年龄判定。

      为了更好的适应不同程序的内存情况,VM设定,如果在Survivor中相同年龄的所有对象大小的总和大于Survivor的一半,年龄大于或等于该年龄的对象直接进入老年代,无需等到MaxTenuringThreshold要求的年龄。

      uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
          //survivor_capacity是survivor空间的大小
          size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);
          size_t total = 0;
          uint age = 1;
          while (age < table_size) {
              total += sizes[age]; //sizes数组是每个年龄段对象大小
              if (total > desired_survivor_size) break;
              age++;
          }
          uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
              ...
      }
      
      /**
       * 动态对象年龄判定
       * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
       * -XX:MaxTenuringThreshold=15
       * -XX:+PrintTenuringDistribution
       */
      public class TenuringThreshold2 {
          private static final int _1MB = 1024 * 1024;
      
          @SuppressWarnings("unused")
          public static void testTenuringThreshold() {
              byte[] allocation1, allocation2, allocation3, allocation4;
              allocation1 = new byte[_1MB / 4];
              // allocation1+allocation2大于Survivor空间一半
              allocation2 = new byte[_1MB / 4];
              allocation3 = new byte[4 * _1MB];
              allocation4 = new byte[4 * _1MB];
      
              allocation4 = null;
              allocation4 = new byte[4 * _1MB];
          }
      
          public static void main(String[] args) {
              testTenuringThreshold();
          }
      }
      
    5. 空间分配担保。

      当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。(也就是每次只使用1块 Eden、1块 Survivor,回收时,复制存活的对象到另一块 Survivor。如果另一块 Survivor 存不下,通过分配担保进入老年代。)

      在发生Minor GC之前,虚拟机会先检查老年代最大可用连续空间是否大于新生代所有对象总空间。如果大于,进行Minor GC。如果小于,虚拟机会查看HandlePromotionFailure设置是否允许担保失败。如果允许,会继续检查老年代最大可用的连续空间是否大于历次晋升老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,如果失败,则进行Full GC。如果小于或者HandlePromotionFailure设置不允许,则进行一次Full GC。

      /**
       * 空间分配担保,在JDK 6 Update 24之前版本测试
       * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
       * -XX:-HandlePromotionFailure
       */
      public class HandlePromotion {
          private static final int _1MB = 1024 * 1024;
      
          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 = null;
              allocation4 = new byte[2 * _1MB];
              allocation5 = new byte[2 * _1MB];
              allocation6 = new byte[2 * _1MB];
              allocation4 = null;
              allocation5 = null;
              allocation6 = null;
              allocation7 = new byte[2 * _1MB];
          }
      }
      

      在JDK 6 Update 24之后,HandlePromotionFailure不会影响虚拟机分配担保策略,也就是在老年代最大可用连续空间大于新生代对象总大小,或者大于历次晋升老年代对象的平均大小,就会Minor GC,否则Full GC。

    总结:

    对象进入老年代的方式:

    1. 大对象直接进入老年代。
    2. 长期存活的对象进入老年代。
    3. Eden、Survivor回收时,如果复制对象到另一块Survivor时,这另一块Survivor放不下存活的对象,有些存活对象需要分配担保进入老年代。

    6. 虚拟机参数总结

    6.1 堆内存

    1. -Xms、-Xmx

      -Xms(-XX:InitialHeapSize)、-Xmx(-XX:MaxHeapSize):指定JVM初始占用的堆内存和最大堆内存。

    2. -XX:NewSize、-Xmn(-XX:MaxNewSize)

      指定JVM启动时分配的新生代内存、新生代最大内存。

    3. -XX:SurvivorRatio

      设置新生代中Eden区与Survivor区的大小比值。HotSpot VM中,如果新生代内存是10M,SurvivorRatio=8,那么Eden区占8M,2个Survivor区各占1M。

    4. -XX:NewRatio

      指定老年代/新生代的堆内存比例。

      -XX:NewRatio=4表示新生代与老年代所占比值为1:4,新生代占整个堆内存的1/5。

      在设置了-XX:MaxNewSize的情况下,-XX:NewRatio的值会被忽略,老年代内存 = 堆内存 - 新生代内存,老年代最大内存 = 最大堆内存 - 新生代最大内存。

    5. -XX:OldSize

      设置JVM启动时分配的老年代内存大小。

    6.2 永久代

    -XX:PermSize、-XX:MaxPermSize

    指定JVM中的永久代(方法区)的大小

    6.3 元空间

    -XX:MetaspaceSize=N    // 设置 Metaspace 的初始(和最小大小)。如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。默认值为 unlimited,这意味着它只受系统内存的限制。
    -XX:MaxMetaspaceSize=N // 设置 Metaspace 的最大大小。
    

    6.4 JVM内存参数使用格式

    -Xms=200M  -Xmx200M -XX:NewSize=100M -Xmn100M -XX:SurvivorRatio=8
    -XX:OldSize=60M
    -XX:PermSize=50M -XX:MaxPermSize=50M
    

    6.5 其他

    -verbose:gc
    打印GC信息
    
    -XX:+PrintGCDetails
    打印GC详细信息
    
    -XX:+PrintGCTimeStamps
    -XX:+PrintGCApplicationStoppedTime
    打印GC停顿时间
    
    -Xloggc:gc.log
    
    -XX:+HeapDumpOnOutOfMemoryError
    内存溢出时,生成堆转储快照
    

    相关文章

      网友评论

          本文标题:3.垃圾收集器与内存分配策略

          本文链接:https://www.haomeiwen.com/subject/bctdqhtx.html