跳到主要内容

JVM 面试题

1. 描述一下 JVM 内存模型,以及这些空间存放的内容

JVM 内存模型

2. 堆内存划分的空间,如何回收这些内存对象,有哪些回收算法

3. 如何解决线上 GC 频繁的问题

  1. 查看监控,了解问题出现的时间点以及当前 FGC 的频率(可对比正常情况判断频率是否正常)
  2. 了解该时间点前是否有程序上线、基础组件升级等情况
  3. 了解 JVM 参数设置,包括:堆空间各区域大小设置、新生代和老生代分别采用的垃圾回收器,然后分析 JVM 参数设置是否合理
  4. 再对步骤 3 中列出的可能原因做排除法,其中元空间被打满内存泄漏代码显式调用 GC 方法比较容易排查
  5. 针对大对象或者长生命周期对象导致的 FGC,可通过 jmap -histo 命令并结合 dump 堆内存文件作进一步分析,需要先定位到可疑对象
  6. 通过可疑对象定位到具体代码再次分析,此时要结合 GC 原理和 JVM 参数设置,弄清楚可疑对象是否满足了进入老年代的条件才能下结论

4. 描述一下 class 初始化过程

  • 一个类初始化就是执行 clinit() 方法,过程如下:
    • 父类初始化
    • static 变量初始化 / static 块(按照文本顺序执行)
  • Java Language Specification 中描述的类初始化详细过程(类初始化是线程安全的):
    1. 每个类都有一个初始化锁 LC,进程获取 LC(如果没有获取到就一直等待)
    2. 如果 C 正在被其他线程初始化,释放 LC 并等待 C 初始化完成
    3. 如果 C 正在被本线程初始化,则递归初始化,释放 LC
    4. 如果 C 已经被初始化了,释放 LC
    5. 如果 C 处于 erroneous 状态,释放 LC 并抛出异常 NoClassDefFoundError
    6. 否则,将 C 标记为正在被本线程初始化,释放 LC,然后初始化那些 final 且为基础类型的类成员变量
    7. 初始化 C 的父类 SC 和各个接口 Sl_n(按照 implements 子句中的顺序来);
      如果 SC 或 Sl_n 初始化过程中抛出异常,则获取 LC,将 C 标记为 erroneous,并通知所有线程,然后释放 LC,然后在抛出同样的异常
    8. 从 classloader 处获取 assertion 是否被打开
    9. 接下来,按照文本顺序执行类变量初始化和静态代码块,或接口的字段初始化,把它们当作是一个个独立的代码块
    10. 如果执行正常,获取 LC,标记 C 为已初始化,并通知所有线程,然后释放 LC
    11. 否则,如果抛出了异常 E,若 E 不是 Error,则以 E 为参数创建新的异常 ExceptionInInitializerError 作为 E;
      如果因为 OutOfMemoryError 导致无法创建 ExceptionInInitializerError,则将 OutOfMemoryError 作为 E
    12. 获取 LC,将 C 标记为 erroneous,通知所有等待的线程,释放 LC,并抛出异常 E

5. 简述一下内存溢出的原因,如何排查线上问题

  • java.lang.OutOfMemoryError:...java heap space...: 堆栈溢出,代码问题的可能性极大
  • java.lang.OutOfMemoryError:GC over head limit exceeded:系统处于高频的 GC 状态,而且回收的效果依然不佳时,就会开始报这个错误,这种情况一般是产生了很多不可以被释放的对象,有可能是引用使用不当导致,或者申请大对象导致,但是 java heap space 的内存溢出有可能不会提前报这个错(内存直接不够导致,而非高频 GC)
  • java.lang.OutOfMemoryError:PermGen space:JDK1.7 之前才会出现的问题,原因是系统的代码非常多或引用的第三方包非常多、或者代码中使用了大量的常量、或通过 intern 注入常量、或通过动态代码加载等方式,导致了常量池的膨胀
  • java.lang.OutOfMemoryError:Direct buffer memory:直接内存不足,因为 JVM 垃圾回收不会回收掉直接内存这部分的内存,因此可能的原因是直接或间接使用了 ByteBuffer 中的 allocateDirect 方法,而没有进行 clear
  • java.lang.StackOverflowError:Xss 设置的太小了
  • java.lang.OutOfMemoryError:unable to create new native thread:堆外内存不足,无法为线程分配内存区域
  • java.lang.OutOfMemoryError:request {} byte for {} out of swap:地址空间不足

6. JVM 有哪些垃圾回收器,实际中如何选择

7. 简述一下 Java 类加载模型

  • 双亲委派模型
    在某个类加载器加载 Class 文件时,它首先委托父类去加载这个类,依次加载到启动类加载器(Bootstrap),如果启动类加载器加载不了(搜索范围中找不到此类),子加载器才会尝试加载这个类
    双亲委派模型
  • 好处
    • 每一个类只会被加载一次,避免了重复加载
    • 每一个类都会被尽可能的加载(从启动类加载器往下,每个加载器都可能会根据优先次序尝试加载它)
    • 有效避免了某些恶意类的加载(比如自定义了 java.lang.Object 类,一般而言在双亲委派模型下会加载系统的 Object 类而不是自定义的 Object 类)

8. JVM 为什么要增加元空间,有什么好处

  • 原因
    1. 字符串存在永久代中,容易出现性能问题和内存溢出问题
    2. 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出
    3. 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低
  • 元空间的特点
    1. 每个加载器都有专门的存储空间
    2. 不会单独回收某个类
    3. 元空间里的对象的位置是固定的
    4. 如果发现某个加载器不再存活,会把相关的空间整个回收

9. G1 垃圾收集器的了解及特点

10. 垃圾回收算法

11. Happens-Before 规则

  • 先行发生规则(Happens-Before) 是判断数据是否存在竞争、线程是否安全的主要依据
    先行发生是 Java 内存模型中定义的两项操作之间的偏序关系,如果操作 A 先行发生于操作 B,那么操作 A 产生的影响能够被操作 B 观察到,用于解决可见性问题
  • 口诀:如果两个操作之间具有 Happens-Before 关系,那么前一个操作的结果就会对后一个操作可见
  • 常见的 Happens-Before 规则
    1. 程序顺序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
    2. 锁规则:一个 unlock 操作先行发生于后面对同一个锁的 lock 操作(指时间的先后)
    3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
    4. 线程启动规则:Thread 对象的 start() 方法先行发生于此线程的每一个动作
    5. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值等手段检测到线程已经终止执行
    6. 线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生
    7. 对象终结规则:一个对象的初始化完成先行发生于他的 finalize() 方法的开始
    8. 传递性:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出熬做 A 先行发生于操作 C

12. Java 类加载和初始化的过程

13. JVM 监控系统是通过 JMX 做的吗

  • 一般都是,如果要记录比较详细的性能定位指标,都会导致进入 safepoint,从而降低线上应用性能
    例如 jstack、jmap 打印堆栈、内存使用情况,都会让 JVM 进入 safepoint,才能获取线程稳定状态从而采集信息
    同时 JMX 暴露向外的接口采集信息,例如使用 jvisualvm 时还会涉及 rpc 和网络消耗;JVM 忙时会无法采集到信息从而出现指标断点。这些都是基于 JMX 的外部监控很难解决的问题
  • 推荐使用 JVM 内部采集 JFR,这样即使在 JVM 很忙时,也能采集到有用的信息