乐趣区

关于后端:一文吃透JVM垃圾回收机制轻松对线面试官

大家好,这里是 淇妙小屋 ,一个分享技术,分享生存的博主
以下是我的主页,各个主页同步更新优质博客,创作不易,还请大家点波关注
掘金主页
知乎主页
Segmentfault 主页
开源中国主页
后续会公布更多 MySQL,Redis,并发,JVM,分布式等面试热点常识,以及 Java 学习路线,面试重点,职业规划,面经等相干博客
转载请表明出处!

1. 如何判断对象已死

1.1 援用计数法

给对象增加一个援用计数器,每当有一个中央援用对象,计数器值 +1,当援用生效,计数器值 -1。当计数器为 0 时,示意对象已死,但会呈现以下问题
Obj1=null,Obj2=null 后,因为两个对象之间依然互相援用,导致两个对象无奈被革除

1.2 可达性剖析

‘GC Roots’的对象作为起始点登程,通过 援用链 达到下一个对象

当一个对象到 GC Roots 没有任何援用链相连的时候阐明对象不可达

  • 能够作为 GC Roots 对象的货色

    • 栈帧中的局部变量表援用的对象
    • 本地办法栈中 JNI 援用的对象
    • 动态属性援用的对象
    • 常量援用的对象
  • 可达性剖析 要求全过程都基于一个 能保障一致性的快照 ,在该快照中进行 对象图的遍历

    • 在可达性剖析时 Stop The World,很容易的满足
    • 但如果用户线程与可达性剖析并发,那么有两种解决方案

      • 增量更新——CMS 应用
      • 原始快照——G1 应用

2. 对象被回收的条件

  • 可达性剖析,该对象没有与 GC Roots 相连接的援用链,将进行如下操作
  • 如果对象还没有执行 finalize()办法,就会被放入 F -Queue 中
  • 当 GC 触发时,Finalizer 线程会 F -Queue 中的对象的 finalize()办法
  • 执行完 finalize()办法后,会再次判断对象是否可达,如果不可达,才会被回收

    (所以对象能够通过在 finalize()中将本人连贯上某个 GC Root 链的形式来援救本人)

3. 垃圾回收算法

  • 标记 - 分明算法

    将要被清革除对象进行标记,触发 GC 时,回收被标记的对象(也能够反过来标记存活的对象)

    • 毛病

      • 执行效率不稳固
      • 非移动式,不须要挪动对象,但会造成内存空间的碎片化
  • 标记 - 复制算法(JVM 新生代应用)

    将内存空间分为两块,每次只应用一块,当应用的内存块满了的时候,将该内存块中的存活对象复制到另一个内存块上,而后清空该内存块

    • JVM 新生代应用的复制算法
      空间利用率低下,因为绝大多数新生代熬不过第一轮 GC,所以没必要 1:1 划分内存空间
      JVM 新生代采纳的就是复制算法,不过新生代中,将内存空间划分为 Eden+Survivor1+Survivor2(8:1:1) 三块内存空间
      每次只会应用 Eden 和一块 Survivor,
      当 Eden 空间有余时,触发 GC,将 Eden 和应用的 Survivor 中的存活对象复制到另一块 Survivor 上 (如果存活的对象 Survivor 装不下,那么多进去的对象进入老年代),
      而后清空 Eden 和应用的 Survivor
    • 毛病

      • 对象存活率较高时,须要进行较多的复制,效率升高——不适用于老年代
      • 空间利用率低
  • 标记 - 整顿算法(JVM 老年代应用)

    将存活的对象挪动到内存的一端,而后将剩下的局部革除

    • 毛病

      • 标记 - 整顿算法是移动式的,须要挪动存活的对象,挪动存活对象时必须全程暂停用户利用线程(Stop The World)
  • 分代算法(JVM 采纳的)

    JVM 将内存分代,不同的代采纳不同的垃圾回收算法

    • 新生代——每次 GC 都有大量对象死去,采纳下面的复制算法
    • 老年代——对象存活率高,采纳下面的标记 - 整顿算法
    • 永恒代 (办法区就是永恒代,jdk1.8 破除了永恒代)
      永恒代要回收的——废除的常量和不再应用的类(Class 对象)

      • 判断废除常量
        个别是判断没有该常量的援用。
      • 判断不再应用的类,必须以下 3 个条件 都满足

        • 该类的所有实例都已被回收
        • 加载该类的 ClassLoader 已被回收
        • 该类的 Class 对象没有被援用

4. 援用的类型

  • 强援用

    相似于 Object obj = new Object(); 创立的
    强援用不置为 null 的话,其指向的对象不会被回收

  • 软援用

    SoftReference 类实现软援用,软援用指向的对象,在内存不足时会被回收

  • 弱援用

    WeakReference 类实现弱援用,弱援用指向的对象,只有触发 GC 就会被回收

  • 虚援用

    PhantomReference 类实现虚援用。
    无奈通过虚援用获取一个对象的实例,
    为一个对象设置虚援用关联的惟一目标就是能在这个对象被收集器回收时收到一个零碎告诉。

5. 垃圾回收器

5.1 新生代垃圾回收器

5.1.1 Serial

  • 单线程收集器,只会应用一个 GC 线程来进行实现垃圾收集工作
    并且在进行垃圾回收时必须 Stop The World
  • 应用标记 - 复制算法
  • 应用场景
    Client 模式下的虚拟机

5.1.2 ParNew

  • Serial 的多线程版本,应用多个 GC 线程来实现垃圾收集工作,在垃圾回收时,会 Stop The World
  • 应用标记 - 复制算法
  • 应用场景

    • 工作在 Server 模式
    • 只有 ParNew 能与 CMS 配合工作

5.1.3 Parallel Scavenge

  • 并行 的多线程垃圾处理器,会触发 Stop The World
  • 应用标记 - 复制算法
  • 与 ParNew 相似,不同在于 parallel Scavenge 能够采纳 GC 自适应策略
  • 该收集器的指标是达到一个 可管制的吞吐量,吞吐量 = 运行用户代码工夫 /(运行用户代码工夫 + 垃圾收集工夫)
  • Parallel Scavenge 收集器应用 2 个参数管制吞吐量

    • XX:MaxGCPauseMillis:管制最大的垃圾收集进展工夫
    • XX:GCRatio:间接设置吞吐量大小
  • Parallel Scavenge 还提供第三个参数—— -XX:UseAdaptiveSizePolicy,开启 GC 自适应调节策略
    开启这个参数后,不须要手工指定新生代大小,eden 和 survivor 的比例等细节,只须要设置好堆大小,最大垃圾收集工夫玉吞吐量大小,虚构机会依据零碎运行状况,动静调整这些参数

5.2 老年代垃圾回收期

5.2.1 Serial Old

  • 单线程垃圾收集器,会导致 Stop The World
  • 采纳标记 - 整顿算法

5.2.2 Parallel Old

  • Parallel Scavenge 的老年代版本
  • 采纳标记 - 整顿算法

5.2.3 CMS

  • Concurrent Mark Sweep
  • 应用 标记 - 革除 算法
  • 工作流程:

    • ①初始标记:(导致 Stop The World)标记下 GC Roots 间接关联到的对象,速度很快
    • ②并发标记:(不会导致 Stop The World,与用户线程并发)从 GC Roots 间接关联的对象登程,进行 可达性剖析(应用增量更新算法)开始遍历整个对象图,耗时长
    • ③从新标记:(导致 Stop The World)并发标记期间,用户程序持续运作,可能会导致局部对象的标记变动,从新标记就是为了修改这些对象的标记记录
    • ④并发革除:(不会导致 Stop The World)革除掉标记为曾经死亡的对象,因为不会挪动存活对象,所以用户线程不用暂停
  • 毛病

    • 对处理器资源非常敏感,会占用一部分处理器资源而导致应用程序变慢

      CMS 默认启动的回收线程数 =(处理器外围数量 +3)/4

    • Concurrent Mode Failure 问题 ——因为 CMS 进行 GC 时,大多数时候用户线程扔持续运行,就必须在老年代预留足够的内存空间给用户线程应用,所以 CMS 不是等老年代内存空间没了才开始 GC,而是当老年代内存空间应用了肯定比例后开始 GC,这种会呈现一种状况,当 CMS 开始 GC 时,预留的内存比拟少,但在 CMS 执行 GC 的过程中,用户线程继续执行,耗尽了预留的内存,就会呈现 并发失败(Concurrent Mode Failure),这时 JVM 会长期启动 Serial Old 收集器进行老年代的垃圾收集,会 Stop The World

      • 解决方案——通过两个参数来设置让老年代内存空间应用超过肯定比例就开始 GC
    • Promotion Failed——在进行 Minor GC 时,Survivor 空间有余,对象只能放入老年代,而此时老年代也放不下造成的,少数是因为老年代有足够的闲暇空间,然而因为碎片较多,新生代要转移到老年带的对象比拟大, 找不到一段间断区域寄存这个对象导致的
    • CMS 采纳 标记 - 革除算法,会产生大量空间碎片

      • 解决方案——设置 -XX:CMSFullGCsBeforeCompaction=n,上一次 CMS GC 后,要执行 n 次 Full GC 后对内存进行压缩

5.3 Garbage First(G1)

5.3.1 G1 特点

  • 回收的范畴是整个 Java 堆
  • G1 的 Region

    • G1 基于 Region的堆内存布局,将堆划分为多个大小雷同的 Region(默认是 2048 个)
    • 每个 Region 都能够依据须要,表演 4 个角色——新生代的 EdenSurvivor老年代Humongous,G1 收集器对表演不同角色的 Region 采纳不同的策略去解决
    • 每个 Region 都能够划分成 2 个局部——已调配的和未调配的,它们之间的界线为 top

      将一个对象调配到 Region,只须要减少 top 值

    • Region 是单次回收的最小单元,每次收集到的内存空间都是 Region 大小的整数倍
    • 对于超过半个 Region 容量的大对象,会寄存在 N 个间断的 Humongous Region 中

  • 从整体上看采纳的是标记 - 整顿算法,从部分(2 个 Region)上看采纳的是标记 - 复制算法

    不会产生内存碎片

  • 在 Stop The World 根底上建设了 可预测 的进展工夫模型,用户能够指定冀望进展工夫,G1 会将进展工夫管制在用户设定的进展工夫内(单次 STW 默认最多 200ms)
  • 其余垃圾垃圾收集器触发 GC 时,指标是对负责的区域进行全量回收,然而 G1 进行垃圾回收时,只谋求在 限度的工夫内 回收尽可能多的垃圾(STW 工夫不会太长)

5.3.2 G1 解决思路

  • 让 G1 依据各个 region 回收所取得的空间大小以及回收所需工夫,保护一个优先级列表,每次依据用户设定容许的收集进展工夫,优先解决回收收益最大的 Region
  • 依据用户冀望的 GC 进展工夫来回收(能够通过参数设置,G1 会在规定的工夫内尽可能地回收垃圾)
  • Minor GC——如果 eden 和 survivor 占用的内存超过了整个堆的 60%,触发一次 Minor GC,只对 eden 和 survivor 进行回收
  • Full GC——如果老年代超过堆的 45%,进行一次 FullGC,对新生代和老年代进行回收

5.3.3 G1 垃圾回收过程

  • 初始标记

    标记下 GC Roots 能间接关联到的对象,G1 会应用 SATB 记录存活对象的快照

  • 并发标记

    与用户线程并发 ,从 GC Roots 开始进行 可达性剖析,找出存活对象

    在此期间,用户线程可能批改了本来的援用,所以须要查看存活的对象与其对照是否统一,如果不统一对象图扫描后,要重新处理 SATB 记录下的在并发时有援用变动的对象

  • 最终标记

    Stop The World,解决那些在并发标记阶段发生变化的对象

  • 筛选回收

    Stop The World,多条收集器线程并行实现

    更新 Region 的统计数据,对各个 Region 的回收价值和老本进行排序,依据用户所冀望的进展工夫来制订回收打算,能够自由选择任意多个 Region 形成回收集,而后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全副空间

退出移动版