一文吃透JVM垃圾回收机制,轻松对线面试官

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

1. 如何判断对象已死

1.1 引用计数法

给对象添加一个引用计数器,每当有一个地方引用对象,计数器值+1,当引用失效,计数器值-1。当计数器为0时,表示对象已死,但会出现以下问题
Obj1=null,Obj2=null后,由于两个对象之间仍然相互引用,导致两个对象无法被清除
一文吃透JVM垃圾回收机制,轻松对线面试官_第1张图片

1.2 可达性分析

‘GC Roots’ 的对象作为起始点出发,通过引用链到达下一个对象

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

  • 可以作为GC Roots对象的东西

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

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

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

一文吃透JVM垃圾回收机制,轻松对线面试官_第2张图片

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. 垃圾回收器

一文吃透JVM垃圾回收机制,轻松对线面试官_第3张图片

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的老年代版本
  • 采用标记-整理算法
    一文吃透JVM垃圾回收机制,轻松对线面试官_第4张图片

5.2.3 CMS

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

    一文吃透JVM垃圾回收机制,轻松对线面试官_第5张图片

    • ①初始标记:(导致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值

      一文吃透JVM垃圾回收机制,轻松对线面试官_第6张图片

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

      一文吃透JVM垃圾回收机制,轻松对线面试官_第7张图片

  • 从整体上看采用的是标记-整理算法,从局部(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的全部空间

你可能感兴趣的:(后端java)