跳到主要内容

JVM 垃圾回收

JVM 如何确定垃圾

  1. 引用计数法
    在 Java 中,引用和对象是有关联的,如果要操作对象则必须使用引用进行。
    实现:给每个对象添加一个引用计数器,每当有一个地方引用它时,引用计数值就 +1,当引用失效时,引用计数值就 -1,任何时刻引用计数值为 0 的对象就可以被回收,即一个对象如果没有任何与之关联的引用,则说明对象不太可能再被用到,那么这个对象就是可回收对象。在这种方法中,一个对象被垃圾收集会导致其他对象的垃圾收集行动。
    • 优点:简单高效。
    • 缺点:当两个对象互相引用(循环引用)时就无法回收,导致内存泄漏,因此 JVM 没有选择使用引用计数法来管理内存。
  2. 可达性算法
    基本思路:通过一系列 GC Roots 对象作为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则称该对象是不可达的。但是不可达对象不等价于可回收对象,不可达对象变为可回收对象需要至少经过两次标记过程,两次标记后仍然是可回收对象,才会被回收。
    可达性算法
    • 可以被作为 GC Roots 的对象:
      • Java 虚拟机栈中引用的对象:Java 虚拟机在调用方法的时候会创建相应的栈帧,栈帧中包含了这个方法内部使用的所有对象的引用,方法执行结束后,这些临时对象引用也就不存在了,这些对象就会被垃圾收集器回收。
      • 方法区中的类静态属性引用的方法:一般指被 static 修饰的对象,加载类的时候就加载到内存中
      • 方法区中的常量引用的对象:即被 final 修饰的对象
      • 本地方法栈中的 JNI(native 方法)引用的对象
      • 处于激活状态的线程
      • 正在被用于同步的各种锁对象
      • JVM 自身持有的对象:比如系统类加载器
  • 宣告对象死亡需要经过的两个过程
    1. 可达性分析后没有发现引用链
    2. 查看对象是否有 final 方法,如果有则重写且在方法内完成自救(比如重新建立引用)

对象的引用

  • 无论是引用计数法还是可达性算法,判断对象是否存活都与引用相关,JDK 1.2 后引用分为四种类型
    • 强引用(Strong Reference)
      在代码中普遍存在的引用,类似于 Object object = new Object() 这类的引用,只要强引用存在,垃圾回收器则永远不会回收它
    • 软引用(Soft Reference)
      软引用用于描述一些还有用但是非必须的对象,这些对象通常不会被回收。
      在虚拟机内存即将溢出之前,垃圾回收器会回收这部分软引用的内存,如果还是内存不够,则抛出内存溢出异常 (存在软引用的对象只在虚拟机内存即将溢出时被回收)
    • 弱引用(Weak Reference)
      弱引用也是用来描述非必须对象的,它的强度比软引用更弱。它的生命周期只能存活到下一次垃圾回收之前。当下一次垃圾回收发生时,无论内存是否足够,都会回收弱引用的内存
    • 虚引用(Phantom Reference)
      最弱的一类引用,一个对象无论是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过一个虚引用来获取一个对象实例(即无论是否存在此引用,都不会影响一个对象的回收)
      为一个对象设置虚引用,该对象在被垃圾收集器回收之后能收到一个系统通知

Java 堆内存分代

  • Java 堆是垃圾收集器管理的主要内存,主流的虚拟机实现中,垃圾收集器大多数采用分代式垃圾回收算法(Generational Garbage Collection),所以会将垃圾收集器所管理的对内存划分为不同的代

  • Java 堆从 GC 的角度可以细分为:

    • 新生代(Young Generation):Eden 区、From Survivor 区、To Survivor 区
    • 老年代(Old Generation)
    • 永久代(Permanent Generation)

    Java 分代

  • 在 Java8 以后,由于方法区的内存不再分配在 Java 堆上,而是存储在本地内存元空间(MeatSpace)中,因此永久代就不存在了,因此无论是串行垃圾收集器(Serial Collector)还是并行垃圾收集器(Parallel Collector),目前的分代情况只有两种:新生代和老年代

JVM 垃圾回收算法

  • 垃圾回收算法

    1. 标记-清除算法(Mark-Sweep)
      最基础的垃圾回收算法,分两个阶段:标记和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间
      这个算法最大的问题是内存碎片化严重,后续可能发生大对象无法找到可利用空间的问题,并且标记和清除两个过程的效率都比较低
      标记清除算法

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

    3. 标记-压缩算法(Mark-Compact)
      结合了标记清除和复制算法,为了避免两者的缺陷而提出。标记阶段和 Mark-Sweep 算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象
      标记整理算法

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

      • 新生代和复制算法
        目前大部分 JVM 的 GC 对新生代都采用 Copying 算法,因为新生代中每次垃圾回收(Young GC)都要回收大部分对象,即要复制的操作比较少,但通常不是按照 1:1 来划分新生代。一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space、To Space),每次使用 Eden 空间和其中一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间
        经过多次 Young GC 之后,存活次数达到阈值的对象就而会被复制到老年代
        新生代
      • 老年代和标记-压缩算法
        老年代因为每次只回收少量对象,因此采用 Mark-Compact 算法
        当老年代空间用完,无法再容纳新对象时,就会触发全量回收(Full GC),性能影响较大,因此尽量避免 Full GC
  • 注意

    1. JVM 中处于方法区的永生代(Permanent Generation)用于存储 Class 类、常量、方法描述等,对永生代的回收主要包括废弃常量和无用的类
    2. 对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(即目前存放对象的那一块 Survivor),少数情况下会直接分配到老年代
    3. 当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后 Edge Space 和 From Space 中的存活对象会被复制到 To Space 中,然后对 Eden Space 和 From Space 进行清理
    4. 如果 To Space 不足以存储某个对象,那么这个对象将会被存储到老年代
    5. 在进行 GC 后新生代使用的便是 Eden Space 和 To Space 了,如此反复循环

垃圾收集器

概述

  • Serial 收集器
  • Parnew 收集器
  • Parallel Scavenge 收集器
  • Serial Old 收集器
  • Parallel Old 收集器
  • CMS 收集器
  • G1 收集器
  • 下图展示了 7 种作用于不同分代的收集器
    收集器关系图
    新生代收集器(均为复制算法):Serial、ParNew、Parallel Scavenge
    老生代收集器:CMS(标记-清理)、Serial Old(标记-整理)、Parallel Old(标记-整理)
    整堆收集器:G1(一个 Region 中是标记-清除算法、2 个 Region 之间是标记-整理算法)
  • 名词解释
    1. 并行(Parallel):多个垃圾收集线程并行工作,此时用户线程处于等待状态
    2. 并发(Concurrent):用户线程和垃圾收集线程同时执行
    3. 吞吐量:CPU 用于运行用户代码的时间/CPU 总消耗时间,吞吐量 = 运行用户代码的时间 / (运行用户代码时间 + 垃圾收集时间)

常用垃圾收集器

  1. Serial 收集器
    最基本、发展历史最悠久,在 JDK 1.3 以前是新生代垃圾收集的唯一选择。
    特点单线程、简单高效(与其他收集器的单线程相比),采用单线程串行工作,对于单个 CPU 的环境来说,由于没有线程交互的开销,因此可以获得最高的单线程垃圾收集效率。进行垃圾收集时必须暂停 JVM 中的其他线程,直到垃圾收集结束(Stop the World)
    应用场景:适用于 Client 模式下的虚拟机
    Serial/Serial Old 收集器运行示意图
    Serial/Serial Old 收集器运行示意图

  2. ParNew 收集器
    Serial 收集器的多线程版本,除了使用多线程以外,其它行为均和 Serial 收集器一模一样(参数控制、收集算法、Stop The World、对象分配规则、回收策略等),由于是多线程版本,因此在单 CPU 环境下效率不如传统的单线程 Serial 收集器。
    特点:多线程(默认开启的收集线程数与 CPU 数量相同,在 CPU 很多的环境中可以使用 -XX:ParallelGCThreads 参数限制线程数)
    应用场景:可以和 CMS 收集器配合一起工作,因此是虚拟机A中新生代常用的垃圾收集器
    运行示意图
    ParNew 运行示意图

  3. Parallel Scavenge 收集器
    一个用于新生代、采用复制算法的收集器。跟吞吐量关系密切,因此也被称为吞吐量优先收集器
    特点:采用复制算法,并行的多线程收集器
    与 CMS 收集器尽可能缩小垃圾收集时停顿时间(Stop The World)不同,Parallel Scavenge 收集器的主要关注点在于达到一个可控制的吞吐量(Throughtput)
    GC 自适应调节策略:可以设置 -XX:UseAdptiveSizePolicy 参数。开启时不需要手动指定新生代大小(-Xmn)、Eden 和 Survivor 区的比例(-XX"SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况手机性能监控信息,动态设置参数提供最优的停顿时间和最高的吞吐量
    低停顿时间的关注点在于以良好的响应速度和低延迟提高用户体验,适用于和用户有较多交互的场景,高吞吐量的关注点在于可以高效率利用 CPU 时间以尽快完成运算任务,适用于用户交互较少、后台计算任务较多的场景。

  4. Serial Old 收集器
    Serial 收集器在老年代上的版本
    特点:单线程收集器,采用标记-整理算法
    应用场景:主要用于 Client 模式下的虚拟机中,但也可以在 Server 模式下使用(Server 模式下两大用途:JDK1.5 之前与 Parallel Scavenge 搭配使用;作为 CMS 收集器的后备方案,在并发手机 Concurrent Mode Failure 时使用)
    工作过程图:同 Serial 收集器

  5. Parallel Old 收集器
    Parallel Scavenge 收集器的老年代版本
    特点:多线程,采用标记-压缩算法
    应用场景:注重高吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge + Parallel Old 收集器

  6. CMS 收集器
    全名 Concurrent Mark Sweep,一款比较优秀的基于标记-清除算法的并发收集器。目标在于尽可能缩小垃圾收集时停顿时间(Stop The World)
    特点:基于标记-清除算法,并发收集、低停顿
    应用场景:适用于注重服务响应速度,希望系统停顿时间最低,给用户带来更好体验的场景,比如 web 程序、B/S 服务
    收集过程

    • 初始标记:标记 GC Roots 能直接到达的对象。速度快但是仍存在 Stop The World 问题
    • 并发标记:进行 GC Roots Tracing 的过程,找出存活对象,且用户线程可并发执行
    • 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍存在 Stop The World 问题,时间比初始标记长一些,但远小于并发标记的时间
    • 并发清除:对标记的对象进行清除回收

    整个阶段并发标记和并发清除是耗时最长的两个阶段,但由于 CMS 收集器是并发执行的,可以跟用户线程一起工作

    优点:GC 收集间隔时间短,多线程并发
    缺点

    • 对 CPU 资源非常敏感
    • 由于采用标记清除算法,会产生内存碎片,导致大对象无法分配空间,不得不提前触发一个 Full GC
    • 无法处理浮动垃圾(CMS 并发清除阶段由于用户线程还可以继续执行,所以可能会产生新的垃圾),可能出现 Concurrent Model Failure 失败而导致另一次 Full GC 的产生

    工作过程图
    工作过程图

  7. G1 收集器(Garbage-First)
    收集器技术发展最前沿的成果之一,使命是替换 CMS 收集器。

G1 收集器

  • G1 取消了分代的概念,使用分区的概念,把内存切分成一个个小块,同时各个小块分为新生代(Eden、Survivor 区)、老年代、大对象(Humongous 区)等等
    G1 规定大于 Region 一半的对象为大对象,同时不参与分代
  • Region 并不是固定为新生代或者老年代,通常情况为自由状态,只有在需要的时候才会划分为指定的分代并且存放特定对象
  • 特点
    • 并行与并发:G1 能充分利用多 CPU 下的优势来缩短 Stop The World 的时间,同时在其他部分收集器需要停止 Java 线程来执行 GC 动作时,G1 收集器仍然可以通过并发来让 Java 线程同步执行
    • 分代收集:G1 能够独立管理整个 Java 堆,并且采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次 GC 的旧对象,以获取更好的收集效果
    • 空间整合:从整体来看,G1 使用标记-压缩算法实现;从局部两个 Region 看,G1 采用复制算法实现,内存空间利用效率高,不会像 CMS 一样产生内存碎片
    • 可预测停顿:除了追求低停顿之外,G1 还能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为 M 毫秒的时间段内,消耗在垃圾收集上的时间不得超过 N 毫秒
  • 运行示意图
    运行示意图

是否会产生垃圾碎片

  • 新生代使用复制算法,把存活对象拷贝到一整块 region 存放,同时存活对象大于一定 region 占比不会进行复制