秃头系列-JVM-垃圾回收机制(面试篇)

407 阅读6分钟

「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战

前言

  • 关于作者:励志不秃头的一个CURD的Java农民工
  • 关于文章:以下内容单纯为作者觉得面试八股文中比较经常遇到的总结,同时会穿插一些作者面试遇到的问题作为记录,打*号的都是作者主观认为比较重要的。

本编文章主要记录作者整理的一些关于JVM的面试题,希望对大家有所帮助,也JVM系列的收官之作。

JVM在什么情况下触发垃圾回收*

  1. 年轻代Eden区域放不下,触发Minor GC
  2. 在Minor GC前,老年代担保机制触发,老年代不足触发Full GC
  3. Minor GC后,通过动态年龄判断、Suvivor区域空间不足、年龄晋升,对象进入老年代,老年代空间不足,触发Full GC
  4. 老年代空间使用达到参数-XX:CMSInitiatingOccupancyFraction设置的阈值,触发Full GC

在抛出OutOfMemoryError之前,一定会进行GC吗?*

  • 通常情况下在抛出OutOfMemoryError之前,垃圾收集器会被触发,尽其所能去清理出空间。

  • 但是,也不是在任何情况下垃圾收集器都会被触发的。
    比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OutOfMemoryError。

有了解过卡表吗?

卡表,主要是用来避免全局扫描老年代对象。在年轻代GC时,不需要扫描到老年代

  • 堆内存 中的每一小块区域形成卡页,卡表便是卡页的集合,当JVM判断一个卡页中有存在对象的跨代引用时(老年代对象持有年轻代对象的引用),将这个页标记为脏页。
  • 在知道卡表之后,每次Minor GC 的时候只需要去卡表找到脏页,找到后加入至GC Root,而不用去遍历整个老年代的对象了。

为什么超大内存(32G以上)推荐使用G1,小内存不可以使用吗?*

G1适合有超大堆(16G、32G) 或者 业务上不能有太高延时的场景

ParNew + CMS面临超大堆时,每一次回收的STW时间都会比较长
在小内存中也是可以使用的,只是效果没有大内存这么明显

JVM的年轻代垃圾回收算法,对象什么时候转移到老年代**

  • 新生代使用复制算法
  • 老年代使用标记-整理或者标记-清除算法
  1. 大对象生成直接存放在老年代
  2. 转移到Suvivor区域时空间不足,通过老年代担保机制进入老年代
  3. 年龄晋升或动态年龄判断 进入老年代

平时如何检查JVM运行的情况

  1. 公司的监控平台
  2. 可以通过GC的日志
  3. 通过jstat命令分析
  4. 通过导出jmap内存快照分析

线上环境发生OOM之后,如何排查和处理线上系统情况*

首先第一时间恢复,避免造成更大的影响

  1. 首先看看日志文件的异常信息,确定下是metaspace、栈、堆哪里内存溢出
  2. 一般来说开启了导出发生OOM的现场信息,导出到本地使用MAT工具排查
  3. 一个都是代码开发导致的,这时候就要修复优化产生OOM的代码

请说说内存泄露

只有对象不会再被程序用到了,但是GC又不能回收他们的情况,就叫内存泄漏

泄露情况:

  1. 静态集合类

    静态集合类,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与JVM程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。

    public class Test {
    
      static List list = new ArrayList();
    
      public void oomTests() {
        //局部变量
        Object obj = new Object(); 
        list.add(obj);
      }
    }
    
  2. 单例模式

    单例模式,和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。

  3. 内部类持有外部类

    内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。 这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。

  4. 各种连接,如:MySql连接、Es的连接

    在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。 否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。

  5. 变量不合理的使用

    一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生

  6. 改变哈希值

    当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。 否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄漏。

    这也是 String 被推荐作为HashMap的Key的原因,因为String 天然就被设置成不可变

  7. 缓存泄露

    内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘。比如:项目启动的时候,做缓存的初始化,如数据库中一张表加载到内存中去,这时候在项目运行的过程中,这个表的数据就一直存在内存中 对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值。

  8. 监听器和回调


后记

这是作者第一次完整更新完一个系列,也是秃头系列-JVM篇的最后一篇,希望大家多多阅读,多多分享。我是新生代农民工L_Denny,我们下个系列再见。