JVM 面试题
1. 描述一下 JVM 内存模型,以及这些空间存放的内容
2. 堆内存划分的空间,如何回收这些内存对象,有哪些回收算法
3. 如何解决线上 GC 频繁的问题
- 查看监控,了解问题出现的时间点以及当前 FGC 的频率(可对比正常情况判断频率是否正常)
- 了解该时间点前是否有程序上线、基础组件升级等情况
- 了解 JVM 参数设置,包括:堆空间各区域大小设置、新生代和老生代分别采用的垃圾回收器,然后分析 JVM 参数设置是否合理
- 再对步骤 3 中列出的可能原因做排除法,其中元空间被打满、内存泄漏、代码显式调用 GC 方法比较容易排查
- 针对大对象或者长生命周期对象导致的 FGC,可通过
jmap -histo
命令并结合 dump 堆内存文件作进一步分析,需要先定位到可疑对象 - 通过可疑对象定位到具体代码再次分析,此时要结合 GC 原理和 JVM 参数设置,弄清楚可疑对象是否满足了进入老年代的条件才能下结论
4. 描述一下 class 初始化过程
- 一个类初始化就是执行 clinit() 方法,过程如下:
- 父类初始化
- static 变量初始化 / static 块(按照文本顺序执行)
- Java Language Specification 中描述的类初始化详细过程(类初始化是线程安全的):
- 每个类都有一个初始化锁 LC,进程获取 LC(如果没有获取到就一直等待)
- 如果 C 正在被其他线程初始化,释放 LC 并等待 C 初始化完成
- 如果 C 正在被本线程初始化,则递归初始化,释放 LC
- 如果 C 已经被初始化了,释放 LC
- 如果 C 处于 erroneous 状态,释放 LC 并抛出异常 NoClassDefFoundError
- 否则,将 C 标记为正在被本线程初始化,释放 LC,然后初始化那些 final 且为基础类型的类成员变量
- 初始化 C 的父类 SC 和各个接口 Sl_n(按照 implements 子句中的顺序来);
如果 SC 或 Sl_n 初始化过程中抛出异常,则获取 LC,将 C 标记为 erroneous,并通知所有线程,然后释放 LC,然后在抛出同样的异常 - 从 classloader 处获取 assertion 是否被打开
- 接下来,按照文本顺序执行类变量初始化和静态代码块,或接口的字段初始化,把它们当作是一个个独立的代码块
- 如果执行正常,获取 LC,标记 C 为已初始化,并通知所有线程,然后释放 LC
- 否则,如果抛出了异常 E,若 E 不是 Error,则以 E 为参数创建新的异常 ExceptionInInitializerError 作为 E;
如果因为 OutOfMemoryError 导致无法创建 ExceptionInInitializerError,则将 OutOfMemoryError 作为 E - 获取 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 方法,而没有进行 clearjava.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 为什么要增加元空间,有什么好处
- 原因
- 字符串存在永久代中,容易出现性能问题和内存溢出问题
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低
- 元空间的特点
- 每个加载器都有专门的存储空间
- 不会单独回收某个类
- 元空间里的对象的位置是固定的
- 如果发现某个加载器不再存活,会把相关的空间整个回收
9. G1 垃圾收集器的了解及特点
10. 垃圾回收算法
11. Happens-Before 规则
- 先行发生规则(Happens-Before) 是判断数据是否存在竞争、线程是否安全的主要依据
先行发生是 Java 内存模型中定义的两项操作之间的偏序关系,如果操作 A 先行发生于操作 B,那么操作 A 产生的影响能够被操作 B 观察到,用于解决可见性问题 - 口诀:如果两个操作之间具有 Happens-Before 关系,那么前一个操作的结果就会对后一个操作可见
- 常见的 Happens-Before 规则
- 程序顺序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁规则:一个 unlock 操作先行发生于后面对同一个锁的 lock 操作(指时间的先后)
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 线程启动规则:Thread 对象的 start() 方法先行发生于此线程的每一个动作
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值等手段检测到线程已经终止执行
- 线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 对象终结规则:一个对象的初始化完成先行发生于他的 finalize() 方法的开始
- 传递性:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出熬做 A 先行发生于操作 C
12. Java 类加载和初始化的过程
13. JVM 监控系统是通过 JMX 做的吗
- 一般都是,如果要记录比较详细的性能定位指标,都会导致进入 safepoint,从而降低线上应用性能
例如 jstack、jmap 打印堆栈、内存使用情况,都会让 JVM 进入 safepoint,才能获取线程稳定状态从而采集信息
同时 JMX 暴露向外的接口采集信息,例如使用 jvisualvm 时还会涉及 rpc 和网络消耗;JVM 忙时会无法采集到信息从而出现指标断点。这些都是基于 JMX 的外部监控很难解决的问题 - 推荐使用 JVM 内部采集 JFR,这样即使在 JVM 很忙时,也能采集到有用的信息