面试题——JVM之垃圾回收

文章目录


1、Java 怎么进行垃圾回收的?

垃圾回收需要考虑三件事情:

  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

1.1 哪些内存需要回收,或者说能够被回收呢?

  • 当前很多语言中,都是通过可达性分析算法来判定对象是否存活的。该算法的基本思路就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。

1.1.1 那么哪些对象可以作为GC Root呢?

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如
    NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。

https://blog.csdn.net/weixin_38007185/article/details/108093716

1.1.2 那么什么是引用呢?

  • Java 中有四种引用,由强到弱依次是:强引用、软引用、弱引用、虚引用
  • 强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

1.1.3 类的 static 变量是 GC Root 吗?

  • 如果是引用类型的变量,由 1.1.1 节中的第三条可知,它是 GC Root。

1.1.4 一个普通 HashMap 对象中 key 和 value 所引用的对象可以被回收吗?

  • 不可以。HashMap 中保存了对 key 和 value 的强引用关系,这就意味着只要 HashMap 对象不销毁,那么 key 和 value 所引用的对象就不会被垃圾回收。

1.1.5 想要 HashMap 里的 key 和 value 所引用的对象在 GC 时可以被及时回收,应该对 Map 做什么样的改造呢?

  • 可以使用 WeakHashMap。WeakHashMap的 key 只保留了对实际对象的弱引用,这就意味着如果 WeakHashMap 对象的 key 所引用的对象没有被其他强引用变量所引用时,则 key 所引用的对象就可以被垃圾回收,WeakHashMap 也可以自动删除这些 key 所对应的 key-value对。
  • 接下来我们从源码的角度分析一下上面的话:

首先看到 WeakHashMap 中多了一个 引用队列:

	/**
     * Reference queue for cleared WeakEntries
     */
    private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

WeakHashMap 使用内部类 Entry 来存储键值对,下面是 Entry 的构造方法

  • 它继承了 WeakReference 类,说明 Entry 同时也是一个弱引用对象
  • 在构造方法中调用了父类 WeakReference 的构造方法,即 Entry 弱引用了 key 对象,并且将自己和上面的成员属性——引用队列 queue 关联起来。
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
	V value;
	final int hash;
	Entry<K,V> next;
	
	/**
	 * Creates new entry.
	 */
	Entry(Object key, V value,
	      ReferenceQueue<Object> queue,
	      int hash, Entry<K,V> next) {
	    super(key, queue);
	    this.value = value;
	    this.hash  = hash;
	    this.next  = next;
	}
	//省略
}

// WeakReference
public class WeakReference<T> extends Reference<T> {
   // 调用Reference的构造方法初始化 key 和 引用队列
    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }

}
public abstract class Reference<T> {

    // 实际存储key的地方
    private T referent;      
    // 引用队列
    volatile ReferenceQueue<? super T> queue;

    Reference(T referent) {
        this(referent, null);
    }

    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
}
  • 那么如何回收 WeakHashMap 中 key 和 value 所引用的对象呢?
  • 由上面的分析可知,WeakHashMap 中的 Entry 是一个弱引用对象,它弱引用了 key,当 key 没有其他强引用 引用它时,表明它可以被垃圾回收了。
  • 并且 Entry 和引用队列 queue 关联了,当 Entry 弱引用的对象(也就是 key)被垃圾回收之后,Entry (它本身是一个弱引用)会被加入到关联的引用队列中,随后 Entry 也会被垃圾回收器回收 —— 即 WeakHashMap 中也删除了该键值对。
  • 至此,WeakHashMap 中 key 和 value 所引用的对象就都可以被垃圾回收了。

参考文章:https://blog.csdn.net/fd2025/article/details/118963533

1.2 那如何回收呢?

1.2.1 回收过程概述

  • Java 中的堆空间划分为了新生代和老年代,新生代又划分为了伊甸园区幸存区
  • 大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC
  • 对于存活下来的对象,其年龄+1,年龄增加至阈值时会晋升至老年代。老年代空间不足时会触发 Full GC

1.2.2 什么时候触发 Minor GC?什么时候触发 Full GC?

  • 当要创建对象,而 eden 区没有足够的空间进行分配时,虚拟机准备发起一次 Minor GC。而在发生 Minor GC 之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间或者历次晋升到老年代的对象的平均大小,如果大于,就会进行 Minor GC,如果小于,则转而触发 Full GC。

1.2.3 如何降低 Full GC 的频率?

  • Full GC 最根本的产生原因就是有对象不停的进入老年代,最后导致空间不足,引发 Full GC。解决思路就是直接破坏掉产生条件,尽量做到让对象在 Minor GC 阶段被回收。
    1. 尽量不要创建过大的对象及数组
    2. 合理配置 -XX:PretenureSizeThreshold 大小(超过设置的大小的对象直接进入老年代)。避免过多非必要对象进入老年代。
    3. 合理分配新生代老年代内存比例大小。

    参考:https://www.jianshu.com/p/e13ef4988ffa

1.2.4 那么什么对象会进入老年代?

1.2.5 Eden 和 Survival 区域的大小比例可以调整吗?参数是什么?调整后会有什么影响?

1.2.6 有哪些垃圾回收算法?

  • 针对不同区域的特点,发展出了标记-复制算法、标记-清除算法、标记-整理算法等垃圾收集算法。

    https://javaguide.cn/java/jvm/jvm-garbage-collection.html
    《深入理解Java虚拟机》

1.3 那什么时候回收呢?

1.3.1 jvm 中的安全点和安全区代表什么?写屏障你了解吗?

2、有哪些垃圾回收器?

  • 对于每一种垃圾回收器,我们需要知道它们的目标、特性、原理及使用场景。
    在这里插入图片描述
  1. Serial 收集器是历史最悠久的收集器,运行在新生代,是单线程工作,在进行垃圾收集时必须暂停其他工作线程,采用标记-复制算法一般会与运行在老年代的 Serial Old 收集器组合工作。(《深入理解Java虚拟机》)
    • 适用场景:堆内存较小时;适合个人电脑,也就是 cpu 少的环境。
      在这里插入图片描述
  2. ParNew 收集器:实质上是 Serial 收集器的多线程并行版本,其他方面和 Serial 并无太大区别,JDK 9 之后,ParNew 和 CMS 只能互相搭配使用。
  3. Parallel Scavenge 收集器:运行在新生代,使用标记-复制算法,是能够并行收集的多线程收集器。
    • 它和 ParNew 很像,但是关注点在于吞吐量
    • -XX: MaxGCPauseMiIlis —— 控制最大垃圾收集停顿时间
    • -XX: GCTimeRatio —— 垃圾收集时间占总时间的比例
    • -XX: +UseAdaptiveSizePolicy —— 自动调节新生代各区域大小等参数

下面介绍运行在老年代的垃圾回收器

  1. Serial Old:Serial 收集器的老年代版本,单线程,标记整理算法。

    • 它的一个重要的可能的用途是:作为 CMS 收集器发生失败时的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。(下面讲)
  2. Parallel Old 是 Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。使用在注重吞吐量的场景。

重点来喽!

  1. CMS垃圾回收器(下节具体说)
    • 目标:获取最短最短回收停顿时间。关注响应速度。
    • 场景:互联网网站、基于浏览器的 B/S 系统的服务端
  2. Garbage First 垃圾回收器(下节具体说)

https://javaguide.cn/java/jvm/jvm-garbage-collection.html

2.1 新生代有哪些垃圾回收器?

  • Serial、ParNew、Parallel Scavenge

2.1 能讲一下 CMS 垃圾回收的过程吗?

  • 四个步骤:
    1. 初始标记:Stop The Word,标记 GC Root 能直接关联到的对象,速度很快。
    2. 并发标记:无需 STW,从 GC Root 直接关联到的对象开始遍历对象图,耗时较长。
    3. 重新标记:STW,修正并发标记期间,因用户程序继续运行而导致标记产生变动的对象的标记记录。
    4. 并发清除:无需STW,清理删除标记阶段判断为已经死亡的对象。耗时较长。
      在这里插入图片描述

2.1.1 为什么 CMS 的停顿时间短?

  • CMS 垃圾回收的四个阶段中,只有初始标记和重新标记阶段需要 STW,并且这两个阶段的速度都很快。

2.1.2 CMS 有什么缺点?

  • 对处理器资源很敏感:如果本身处理器核心数量就较少,在并发阶段还要分出一部分资源去执行垃圾收集任务,就可能导致用户程序的执行速度忽然大幅下降。

  • 无法处理浮动垃圾:不能像其他垃圾回收器那样等到老年代几乎被填满了再进行收集。

    • 什么是浮动垃圾?
    • 由于并发标记和并发清除阶段用户线程仍在运行,那么就会伴随垃圾的产生。但是这部分垃圾是在标记阶段结束以后产生的,CMS 无法在本次垃圾收集中处理掉它们,只能等下一次。但是垃圾回收阶段用户线程还需要并发运行,就需要预留足够内存给用户线程使用。
  • 产生大量的内存碎片:CMS 采用标记-清除算法。内存碎片过多会造成老年代空间可能还有很多,但是找不到足够大的连续空间来分配当前对象,而不得不提前触发一次 Full GC。

2.1.3 什么情况下出现 Concurrent Mode Failure?出现后 jvm 是怎么解决的?

  • 这还是由于上面说的 CMS 无法处理浮动垃圾的缺点所造成的。
  • 如果 CMS 运行期间预留的内存无法满足用户线程的需要,就会出现一次 “并发失败”(Concurrent Mode Failure)
  • 出现这种情况之后,JVM 会执行以下操作:冻结用户线程,临时启用 Serial Old 垃圾回收器来重新进行老年代的垃圾回收,但这样停顿时间就会很长。

2.1.4 CMS 有什么参数,设置大了或小了,会有什么影响?

  1. -XX:ParallelGCThreads=n: 设置并行线程数。一般设置为 cpu 核心数的 1/4。

    • 设置大了,造成垃圾回收线程占用过多资源,使得用户程序的执行速度大幅降低(上面讲过的 CMS 缺点1)。
    • 设置小了,垃圾回收速度慢。
  2. -XX:CMSInitiatingOccupancyFraction=percent: 垃圾回收的时机。(针对上面提到的 CMS 缺点2)

    1. 设置大了,造成预留给用户的内存不足,出现并发失败 Concurrent Mode Failure。
    2. 设置小了,会进行频繁的垃圾回收。
  3. -XX:+CMSScavengeBeforeRemark:启用之后,在重新标记之前,做一次新生代的垃圾回收,减少由新生代对象引起的可达性分析,减轻重新标记阶段的压力。

2.1.5 Minor GC 会 stop the world 吗?

  • 会的。
  • 所有工作在新生代上的垃圾回收器,执行的都是复制算法,需要 STW。

https://www.zhihu.com/question/29114369?sort=created

2.1.6 CMS + ParNew 对象分配及垃圾回收流程?

2.2 能讲一下 G1 垃圾回收器吗?

2.2.1 先说一下 G1 的特点

  1. 开创了收集器面向局部收集的设计思路基于 Region 的内存布局形式
  2. G1 收集器出现之前的收集器,垃圾收集的目标范围要么是整个新生代(Minor GC),要么是整个老年代(Major GC),再要么就是整个Java 堆(Full GC)。而G1 可以面向堆内存任何部分来组成回收集进行回收 —— Mixed GC 模式。

2.2.2 了解 G1 的原理吗?

  • G1 把连续的Java堆划分为多个大小相等的独立区域(Region),每一个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间。
  • 当新生代内存占用超过我们设置的阈值之后,进行 Young Collection,过程也是采用标记-复制算法,在 Eden、Survival To、Survival From 间拷贝。
  • 老年代空间不足的时候执行 Mixed GC,进行混合收集。

2.2.3 那么 G1 中每个 Region 的大小,一般是多少?Region 的结构了解吗?

  • 每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为 2 的 N 次幂。

2.2.4 那么 G1 怎么建立可预测的停顿时间模型的?

  • G1 收集器之所以能建立可预测的停顿时间模型,是因为它将 Region 作为单次回收的最小单元,即每次收集到的内存空间都是 Region 大小的整数倍,这样可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。
  • 更具体的处理思路是让 G1 收集器去跟踪各个 Region 里面的垃圾堆积的价值大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的那些 Region,这也就是“Garbage First”名字的由来。
  • 这种使用 Region 划分内存空间,以及具有优先级的区域回收方式,保证了 G1 收集器在有限的时间内获取尽可能高的收集效率。

2.2.4 垃圾回收过程

  1. 初始标记:和 CMS 类似,标记 GC Root 能直接关联到的对象。同时要修改一下每个 Region 中能分配新对象的位置,这是为了让下一阶段用户线程并发运行时,能正确地在 Region 的可用部分分配新对象。
  2. 并发标记:从 GC Root 开始对堆中对象进行可达性分析,找出要回收的对象。
  3. 最终标记:处理并发标记时有引用变动的对象。
  4. 筛选回收:对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间,决定回收哪些 Region。**执行复制算法,**将要被回收的 Region 中的对象复制到空 Region 中,再清理掉旧 Region 的全部空间。(与 CMS 不同的是,这个阶段 G1 需要暂停用户线程,因为涉及到存活对象的移动
  • 上面的 初始标记 阶段,需要停顿线程,但耗时很短,而且是借用进行 Minor GC 的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
  • 至于 Minor GC 的过程,和之前讲的没什么区别,就是在扮演 Eden 和 Survival 区域的 Region 之间执行标记-整理算法。

2.2.3 G1 和 CMS 分别适用什么场景?

  1. CMS 关注响应时间,追求低延迟;而 G1 不仅面向低延迟,还可在不同场景中取得关注吞吐量和关注延迟之间的最佳平衡。
  2. 在 Java 堆内存较大时,G1 才能发挥其优势(这个Java堆容量一般在 6~8G)。

3、谈谈对 OOM 的认识?哪些区域会出现 OOM ?

  • 除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生 OutOfMemoryError 异常的可能。
  • 堆内存: Java堆用于储存对象实例,我们只要不断地创建对象,并且保证 GC Roots 到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。
  • 栈内存: 栈内存一般出现 StackOverflowError,出现在线程请求的栈深度大于虚拟机所允许的最大深度。
    • 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出
      OutOfMemoryError异常。(而 HotSpot 虚拟机的选择是不支持扩展。)
    • 如果在多线程的情况下,通过不断创建线程的方式,在 HotSpot 虚拟机上也是可以产生内存溢出 OOM 异常的。(不过这样产生的内存溢出异常和栈内存是否足够就没有任何关系了)
  • 方法区: 经常会遇到的是动态生成大量的类。例如:当前的很多主流框架,如Spring、Hibernate对类进行增强时,都会使用到 CGLib 这类字节码技术,当增强的类越多,就需要越大的方法区以保证动态生成的新类型可以载入内存。

3.1 那么内存溢出和内存泄漏有什么区别呢?

  • 内存泄漏:程序中已经不再使用的内存,由于某些原因,却无法释放,造成内存的浪费。
  • 常见的情况有:各种连接,如数据库连接、网络连接和IO连接等,使用完没有即使关闭。

详见:https://blog.csdn.net/weter_drop/article/details/89387564

3.2 那么出现 OOM 之后你会怎么排查问题?使用过哪些工具?

参考:https://zhuanlan.zhihu.com/p/165981061
https://zhuanlan.zhihu.com/p/95150243
https://blog.csdn.net/weixin_42140580/article/details/112480127
https://gitee.com/souyunku/NewDevBooks/blob/master/docs/Jvm/Jvm%E9%9D%A2%E8%AF%95%E9%A2%98%E9%99%84%E7%AD%94%E6%A1%88%EF%BC%882021%E5%B9%B4Jvm%E9%9D%A2%E8%AF%95%E9%A2%98%E5%8F%8A%E7%AD%94%E6%A1%88%E5%A4%A7%E6%B1%87%E6%80%BB%EF%BC%89.md#3%E4%BD%A0%E9%83%BD%E6%9C%89%E5%93%AA%E4%BA%9B%E6%89%8B%E6%AE%B5%E7%94%A8%E6%9D%A5%E6%8E%92%E6%9F%A5%E5%86%85%E5%AD%98%E6%BA%A2%E5%87%BA

4、GC性能指标了解吗?JVM 的调优过程?有哪些调优经验?

参考文章

https://zhuanlan.zhihu.com/p/399138005
https://www.zhihu.com/column/c_1366591040686903296

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值