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

垃圾收集器

  • 垃圾收集器的历史要远早于Java,不过我们很多人都是通过Java才了解垃圾收集器的。学习垃圾收集器可以更好进行内存泄漏,内存溢出的排查,以及高并发场景下垃圾收集成为系统性能瓶颈。

  • 垃圾收集器关注的问题很简单:

    哪些垃圾需要回收?--who
    什么时候回收?-- when
    怎么回收?--how
    
  • 程序计数器,虚拟机栈,本地方法栈是线程私有的,也就是和线程的生命周期一致,栈帧随着方法的执行进行进栈和出栈操作,内存分配和回收具有确定性,不需要考虑垃圾回收问题。方法区和Java堆则需要在运行时才确定需要的内存大小,从而需要我们动态的分配内存和垃圾收集。

那些垃圾需要回收?-who

已经“死掉”的对象需要回收,对象死掉的概念是指没有引用他的对象了。

  1. 引用计数法:给对象中添加一个引用计数器,当有一个地方引用他时,计数器+1,当引用失效时,计数器-1,当计数器为0时,对象死掉。java中并没有采用这种机制。

    • 优点:实现简单,判定效率高
    • 缺点:循环引用

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      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 testGC() {
      ReferenceCountingGC objA = new ReferenceCountingGC();
      ReferenceCountingGC objB = new ReferenceCountingGC();
      objA.instance = objB;
      objB.instance = objA;
      objA = null;
      objB = null;
      // 假设在这行发生GC,objA和objB是否能被回收?
      System.gc();
      }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      0.137: [GC (System.gc()) [PSYoungGen: 9344K->808K(76288K)] 9344K->816K(251392K), 0.0029826 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
      0.140: [Full GC (System.gc()) [PSYoungGen: 808K->0K(76288K)] [ParOldGen: 8K->662K(175104K)] 816K->662K(251392K), [Metaspace: 3464K->3464K(1056768K)], 0.0046314 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]
      Heap
      PSYoungGen total 76288K, used 655K [0x000000076b380000, 0x0000000770880000, 0x00000007c0000000)
      eden space 65536K, 1% used [0x000000076b380000,0x000000076b423ee8,0x000000076f380000)
      from space 10752K, 0% used [0x000000076f380000,0x000000076f380000,0x000000076fe00000)
      to space 10752K, 0% used [0x000000076fe00000,0x000000076fe00000,0x0000000770880000)
      ParOldGen total 175104K, used 662K [0x00000006c1a00000, 0x00000006cc500000, 0x000000076b380000)
      object space 175104K, 0% used [0x00000006c1a00000,0x00000006c1aa5b78,0x00000006cc500000)
      Metaspace used 3470K, capacity 4496K, committed 4864K, reserved 1056768K
      class space used 381K, capacity 388K, committed 512K, reserved 1048576K
  2. 可达性分析算法:起始点为GC Root Set中的GC Roots对象,由GC Root向下的路径成为引用链,当一个对象和GC Roots没有引用链连接时,此对象死掉。

    Alt text

    Java中可以作为GC Roots的对象包括:

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

Java1.2之前,reference类型的数据中存储的数值代表的是一个内存的起始地址,则称为一个引用。对象只存在被引用和不被引用两种状态。

  • 强引用:程序代码中普遍存在的
  • 软引用:描述一些还有用但不是必须的对象,系统内存溢出前会对其进行二次垃圾回收
  • 弱引用:描述非必须的对象,垃圾回收时直接回收
  • 虚引用:不影响其生存,回收时可以收到一个系统通知
  1. 对象死亡的过程

    1. 可达性分析后,对没有和GC Roots相连接的引用链的对象会被标记一次,同时对其进行一次筛选,筛选条件为此对象是否有必要执行finalize()方法,当对象没有重写finalize()方法或者finalize()方法被虚拟机调用过,则不执行finalize()方法。
    2. 有必要执行finalize()方法的对象会被放到F-Queue对象中,等待虚拟机的Finalizer线程去执行finalize()方法,GC会对F-Queue对象进行小范围标记,如果对象在finalize()方法中和引用链连接上,则会被移出队列,否则被真正回收。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      /**
      * 对象可以在被GC时自我拯救
      * 这种自救机会只有一次,因为一个对象的finalize()方法最多只会被系统调用一遍
      */
      public class FinalizeEscapeGC {
      public static FinalizeEscapeGC SAVE_HOOK = null;
      public void isAlive() {
      System.out.println(" I am 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.5s等待
      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.5s等待
      Thread.sleep(500);
      if (SAVE_HOOK != null)
      SAVE_HOOK.isAlive();
      else
      System.out.println("no, I am dead!");
      }
      }
      1
      2
      3
      finalize method executed!
      I am alive !
      no, I am dead!
  2. 回收方法区

虽然虚拟机规范中没有要求在方法区进行垃圾回收,但是方法区存在垃圾回收,回收效率很低。回收的内容为废弃常亮和无用的类。

  • 回收废弃常亮:看和GC Roots之间是否存在引用链
  • 回收无用类:
    1. 该类的所有实例都已经被回收
    2. 加载该类的ClassLoader已经被回收
    3. 该类对应的Java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾收集算法

  1. 标记-清除算法(Mark-Sweep):首先标记所有需要回收的对象,标记完成后统一回收被标记对象。

    • 存在两个不足:

      1. 效率问题:标记和清除两个过程的效率都不高
      2. 空间问题:标记清除之后会产生大量不连续的内存碎片,会导致存放大对象时没有足够的空间而提前出发垃圾清楚

      Alt text

  1. 复制算法(Copint):将内存分成一样的两块,每次只使用一块,当一块用完了就进行垃圾回收,并将还存活的对象复制到第二块硬盘上并整理顺序。每次回收针对的是一半内存空间,同时也没有了空间碎片的问题

    Alt text

    • 存在的问题是内存空间变为一半。实际上,新生的对象98%是朝生夕死的,所以不需要1:1来划分内存空间。而是将新生代内存化为一块Eden空间和两块Survivor空间,比例为8:1:1。
    • 每次使用Eden空间和其中一块Survivor空间,回收时将Eden和Survivor空间中还存活的对象复制到另一块Survivor中,然后清除Eden和Survivor空间。
    • 如果空间不够的话,需要老年代进行分配担保
  2. 标记-整理算法(Mark-Compact):标记过程和之前相同,整理过程将存活对象移动到一边,根据整理边界回收剩余空间内的对象,适用于老年代的垃圾收集

    Alt text

  3. 分代收集算法:将Java堆对象根据存货周期划分为新生代和老年代,新生代采用复制算法,老年代给新生代做分配担保,由于老年代的对象存活率高,所以老年代采用标记-整理或者标记-清除算法。

HotSpot的算法实现

  1. 枚举根节点:可作为根节点的主要在全局性的引用(常亮或者静态属性)与执行上下文(栈帧中的本地变量表),可达性分析时需要停止一切的Java程序执行,Stop the world,在Oop中保存着对象引用的地址。
  2. 安全点:程序执行过程中并不是所有位置都可以停下来GC,让系统中的所有线程到安全点的方式有两种:
    • 抢先式中断:所有线程中断,如果有线程不在安全点上,则恢复线程,让其跑到安全点上再中断
    • 主动式中断:设置一个标志,各个线程执行时去轮询这个标志,发现中断标志位真时就自己中断挂起,轮询标志的地方和安全点是重合的。
  3. 安全区域:
    • 解决线程处于休眠状态无法响应JVM的中断请求的情况。
    • 安全区域指在一段代码中,引用关系不会发生变化
    • 线程执行Safe Region中的代码时,会标识自己已经进入Safe Region,可以随时进行GC,如果线程离开安全区域时,会先检查枚举根节点或者GC是否完成,未完成则需要等待。

垃圾收集器

Java虚拟机规范中对垃圾收集器如何实现并没有规定,因此诞生了多种多样的垃圾收集器,适合各自的应用场景。HotSpot虚拟机中的垃圾收集器包括对新生代回收的Serial,ParNew,Parallel Scavenge,对老年代回收的CMS,Serial Old(MSC), Parallel Old及G1。

  1. Serial收集器

    • 最基本,历史最悠久的单线程收集器
    • GC过程中必须停止整个程序的运行,Stop the World
    • 优点:简单而高效,适用Client模式下工作的虚拟机

    Alt text

  2. ParNew收集器

    • Serial收集器的多线程版本
    • 运行在Server模式的虚拟机中首选的新生代收集器—重要原因是,其是除了Serial外唯一能和CMS收集器配合工作。
      Alt text
  3. Parallel Scavenge收集器

    • 新生代收集器,复制算法,并行的多线程收集器
    • 收集器的关注点不是尽可能缩短垃圾收集时用户线程的停顿时间,而是尽可能达到一个可控制的吞吐量(吞吐量=运行代码时间/(运行代码时间+GC时间))
    • 吞吐量优先收集器,
    • GC自适应调节策略
      Alt text
  4. Serial Old收集器

    • 单线程
    • 老年代
    • 标记-整理(Mark-Compact)算法
    • 适用Client模式的虚拟机,如果要用在Server模式下,有两个用途,一个是与Parallel Scavenge搭配适用,一个是作为CMS收集器的后备预案,在并发收集失败时使用。
      Alt text
  1. Parallel Old收集器

    • Parallel Scavenge老年代版本
    • 多线程+标记-整理算法
    • 在注重吞吐量和CPU资源敏感的场合,可以优先考虑和Parallel Scavenge搭配使用。
      Alt text
  2. CMS收集器:Concurrent Mark Sweep

    • 以获取最短回收停顿时间为目标的收集器
    • 标记-清除算法
    • 运作过程:
      1. 初始标记(CMS initial mark) – stop the world, 标记GC Roots能直接关联到的对象,速度很快
      2. 并发标记(CMS concurrent mark) – stop the world,进行GC Roots Tracing,
      3. 重新标记(CMS remark),修正标记期间因为用户线程运行而改变的对象的标记记录,时间长于初始标记但短于并发标记
      4. 并发清除(CMS concurrent sweep),清除标记的对象
        Alt text
    • 缺点:
      • CMS对CPU资源敏感,默认回收线程数是:(CPU数量+3)/4
      • CMS收集器无法处理浮动垃圾(Floating Garbage,并发清除阶段产生的垃圾,当前GC无法清除,需要下次GC才可以清除),会导致Concurrent Mode Failure而引发另一次Full GC。老年代需要保留足够的空间给用户线程使用,当CMS运行期间预留的内存空间不够时会发生Concurrent Mode Failure,这时候需要临时调用Serial Old来进行Full GC,停顿时间很长。
      • 产生内存碎片问题,解决方法,CMS进行Full GC时,先进行碎片整理,或者进行若干次不整理的GC时,进行一次碎片整理的GC。
  3. G1收集器:Garbage First

    • 并行与并发
    • 分代收集
    • 空间整合:marking-compact
    • 可预测的停顿
    • G1将整个堆划分为很多Region,新生代和老年代都是由很多Region组成,G1后台维护一个列表,列表中记录了各个Region里面的垃圾价值大小(回收获得的空间和回收需要时间),GC时,优先回收价值最大的Region,保证G1在有限的时间内获得最高的回收效率。RememberSet避免全堆扫描。
    • 运作过程:
      1. initial marking
      2. concurrent marking
      3. final marking
      4. live data counting and evacuation
        Alt text

内存分配与回收策略

  1. 对象优先在Eden区生成
  2. 大对象直接进入老年代,如字符串和数组
  3. 长期存活的对象将进入老年代
  4. 动态对象年龄判定,如果Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半,则大于或等于该年龄的对象将直接进入老年代。
  5. 空间分配担保:Minor GC之前检查老年代最大可用连续空间是否大于新生代所有对象总和或者大于新生代晋升老年代的历次平均大小,进行Minor GC,否则进行Full GC
Donate comment here