JVM 进阶面试主要包括两类:
一是和工程实践结合的面试题,考察面试者是临时抱佛脚背了一些知识点还是真的有将 JVM 知识应用到平常的项目中:
二是 JVM 相关领域的新知识点。
问题 1:你们生产环境线上服务器的 JVM 启动参数配置是什么样的?
-Xms4096m -Xmx4096m -Xmn2048m -Xss512k
-XX:MetaspaceSize=384m
-XX:MaxMetaspaceSize=384m
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:CMSFullGCsBeforeCompaction=5
-XX:+UseCMSCompactAtFullCollection
-XX:+DisableExplicitGC -verbose:gc
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:/home/admin/logs/gc.log
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/admin/logs
JVM 参数 | 说明 |
---|---|
Xms | 初始堆大小 |
Xmx | 最大堆大小 |
Xmn | 年轻代大小 |
Xss | 每个线程的堆栈大小 |
MetaspaceSize | 首次触发 Full GC 的阈值,该值越大触发 Metaspace GC 的时机就越晚 |
MaxMetaspaceSize | 设置 metaspace 区域的最大值 |
+UseConcMarkSweepGC | 设置老年代的垃圾回收器为 CMS |
+UseParNewGC | 设置年轻代的垃圾回收器为并行收集 |
CMSFullGCsBeforeCompaction=5 | 设置进行 5 次 full gc(CMS)后进行内存压缩。由于并发收集器不对内存空间进行压缩 / 整理,所以运行一段时间以后会产生 “碎片”,使得运行效率降低。此值设置运行多少次 full gc 以后对内存空间进行压缩 / 整理 |
+UseCMSCompactAtFullCollection | 在 full gc 的时候对内存空间进行压缩,和 CMSFullGCsBeforeCompaction 配合使用 |
+DisableExplicitGC | System.gc () 调用无效 |
-verbose:gc | 显示每次 gc 事件的信息 |
+PrintGCDetails | 开启详细 gc 日志模式 |
+PrintGCTimeStamps | 将自 JVM 启动至今的时间戳添加到 gc 日志 |
-Xloggc:/home/admin/logs/gc.log | 将 gc 日导输出到指定的 /home/admin/logs/gc.log |
+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/admin/logs | 当堆内存空间溢出时输出堆的内存快照到指定的 /home/admin/logs |
点评:
这是一个典型的结合实践的问题,这类问题的答案并不唯一,相关参数也并不唯一(上面的参数只是笔者从服务器上随意摘取的例子),重点是要言之有物,即需要面试者真正关注过,在熟悉的基础上自由发挥。话又说回来,线上的参数配置有两类:一类是内存相关配置(如 Xms/Xms/Xmn),一类是 GC 相关配置(如 UseConcMarkSweepGC 等),完全的启动参数可以通过命令 java -XX:+PrintFlagsFinal -version 来查看。
追问:是否进行过 GC 优化?如何优化?
GC 优化的一般步骤:
1. 评估现状及设定目标。评估是否需要调优及调优的目标优先级。比如说降低 Full GC 的的执行时间,降低 Young GC 的执行时间等等;
2. 调优。根据 gc 日志等找到优化空间,比如说 Full GC 执行时间太长可能是因为老年代太大了,看能否调整为并行 GC 或者增加并行 GC 的线程数或者减少老年代大小等;
3. 评估效果。根据 gc 日志、jstat 等命令、Mat/Visual VM 等工具来监控调优效果;
4. 细微调整。 根据评估效果来进一步调整相关参数。
这个追题可以即是上个问题的延伸,本质上二者也是一类问题,即和实践结合的问题。这类问题的答案一般是:框架 + 实例。比如说这里的四个步骤就是 “框架”,体现的是优化的系统化方案,即优化都跳不出这个圈子;但这个问题要答得出彩,最好附上优化实例,答案里并没有这部分,是因为这部分是需要候选人自己真正去动手实践过的,这样在回答时自己才会更有体感,也不怕进一步的追问。如果候选人没有相关的调优经历,可以去尝调整相关参数,比如说以降低 Young GC 时间次数为目的。
问题 2:介绍一下新的垃圾回收机制呢?
除了常规的 Serial/Parallel/CMS 等垃圾回收器外,目前比较新的垃圾回收器还有:
1、G1(Garbage First)GC,JDK7 开始引入,JDK9 以后的默认配置。
(1) 场景: 响应速度优先,面向服务端应用;
(2) 目标:尽量缩短处理超大堆(大于 4GB)时产生的停顿、解决 CMS 的内存碎片问题,替换 CMS;
(3) 特点:
A、分区收集,将整个 Java 堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分 Region(不需要连续)的集合;
B、可预测的停顿。根据各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。在回收时采用部分内存回收(在 YGC 时会回收所有新生代分区,在混合回收时会回收所有的新生代分区和部分老生代分区);
2、ZGC,JDK11 开始引入,目前尚处于实验阶段。
(1) 场景:解决 G1 GC 大内存支持不友好、内存利用率不高等问题;
(2) 目标:支持 TB 级内存、停顿时间控制在 10ms 之内、降低对整体应用性能的影响(对吞吐量的影响低于 15%)
(3) 特点:
A、不分代的垃圾回收器,即垃圾回收时对全量内存进行标记,以 page 为单位进行对象的分配和回收,但是回收时仅针对部分内存回收,优先回收垃圾比较多的 page。
B、当前只支持 Linux 的 64 位系统;
3、Shenandoah GC,JDK12 开始引入,也是一款实验性质的垃圾回收器。
(1) 目标: 最小化垃圾回收对用户代码造成的停顿(降至毫秒级)、支持 TB 级内存。
(2) 特点: 不分代的垃圾回收器。
点评:
这类题目偏向于问对 JVM 比较熟悉的候选人;主要考察候选人知识的广度,拓展性很大,答题要点不在于细节求多求全,而在于对知识点的准确了解,即知道这个点,且能准确说出若干个特性。
三、总结
JVM 的面试题的具有一定的区分度和灵活度,因此进阶题目的形式比较灵活多变,对于这类题目,整体上有两个方向:一是多和工程实践结合,比如说在测试环境的机器上调一下参数、观察一下日志、dump 一下内存进行分析,都是很好的学习手段,特别是带着问题去实践时,往往会很难忘;二是保持对新知识点的好奇。
问:常用的调优命令有哪些
问:常用的调优工具有哪些?
常用调优工具分为两类:**jdk自带监控工具:**jconsole、jvisualvm;第三方:MAT(Memory Analyzer Tool)、GChisto等。
问:什么是即时编译器?
Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频 繁,就会把这些代码认定为“热点代码”(Hot Spot Code)。
为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机 器码,并进行各种层次的优化,完成这个任务的编译器成为即时编译器(Just In Time Compiler,JIT编译器)。
问:热点代码有哪些?
被多次调用的方法;
被多次执行的循环体;
问:如何判断一段代码是不是热点代码?
方法调用计数器统计方法:统计的是一个相对的执行频率,即一段时间内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器的热度衰减,这个时间就被称为半衰周期。
问:什么是解释器和编译器?
许多主流的商用虚拟机,都同时包含解释器和编译器。
当程序需要快速启动和执行时,解释器首先发挥作用,省去编译的时间,立即执行。 当程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,可以提高执行效率。 如果内存资源限制较大(部分嵌入式系统),可以使用解释执行节约内存,反之可以使用编译执行来提升效率。同时编译器的代码还能退回成解释器的代码。
问:Java的方法调用,有什么特殊之处?
Class文件的编译过程不包含传统编译的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址。这使得Java有强大的动态扩展能力,但使Java方法的调用过程变得相对复杂,需要在类加载期间甚至到运行时才能确定目标方法的直接引用。
问:Java虚拟机调用字节码指令有哪些?
invokestatic:调用静态方法
invokespecial:调用实例构造器方法、私有方法和父类方法
invokevirtual:调用所有的虚方法
invokeinterface:调用接口方法
问:虚拟机是如何执行方法里面的字节码指令的?
当主流的虚拟机中都包含了即时编译器后,Class文件中的代码到底会被解释执行还是编译执行,只有虚拟机自己才能准确判断。
Javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。因为这一动作是在Java虚拟机之外进行的,而解释器在虚拟机的内部,所以Java程序的编译是半独立的实现。
问:Java 是如何实现跨平台的?
Java 源码编译后会生成一种.class 文件,称为字节码文件。Java 虚拟机(JVM)就是负责将字节码文件翻译成特定平台下的机器码然后运行,也就是说,只要在不同平台上安装对应的JVM,就可以运行字节码文件,运行我们编写的Java 程序。
而这个过程,我们编写的Java 程序没有做任何改变,仅仅是通过JVM 这一 “中间层” ,就能在不同平台上运行,真正实现了 “一次编译,到处运行” 的目的。
注意: 跨平台的是Java 程序,而不是JVM。JVM是用C/C++ 开发的,是编译后的机器码,不能跨平台,不同平台下需要安装不同版本的JVM
问:什么是内存屏障?
内存屏障,又称内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限制。
问:内存屏障为何重要?
对主存的一次访问一般花费硬件的数百次时钟周期。处理器通过缓存(caching)能够从数量级上降低内存延迟的成本这些缓存为了性能重新排列待定内存操 作的顺序。也就是说,程序的读写操作不一定会按照它要求处理器的顺序执行。当数据是不可变的,同时/或者数据限制在线程范围内,这些优化是无害的。如果把 这些优化与对称多处理(symmetric multi-processing)和共享可变状态(shared mutable state)结合,那么就是一场噩梦。当基于共享可变状态的内存操作被重新排序时,程序可能行为不定。一个线程写入的数据可能被其他线程可见,原因是数据 写入的顺序不一致。适当的放置内存屏障通过强制处理器顺序执行待定的内存操作来避免这个问题。
问:对象的内存布局是怎样的?
对象的内存布局包括三个部分:对象头,实例数据和对齐填充。
(1)对象头:
第一部分是与对象在运行时状态相关的信息,长度通过与操作系统的位数保持一致。包括对象的哈希值、GC分代年龄、锁状态以及偏向线程的ID等。由于对象头信息是与对象所定义的信息无关的数据,所以使用了非固定的数据结构,以便存储更多的信息,实现空间复用。因此对象在不同的状态下对象头的存储信息有所差别。
扩展阅读_对象头分布.jpeg
另一部分是类型指针,即指向该对象所属类元数据的指针,虚拟机通常通过这个指针来确定该对象所属的类型(但并不是唯一方式)。
另外,如果对象是一个数组,在对象头中还应该有一块记录数组长度的数据,因为JVM可以通过对象的元数据确定对象的大小,但不能通过元数据确定数组的长度。
(2)实例数据:
实例数据存储的是真正的有效数据,即各个字段的值。无论是子类中定义的,还是从父类继承下来的都需要记录。这部分数据的存储顺序受到虚拟机的分配策略以及字段在类中的定义顺序的影响。
(3)对齐补充:
这部分数据不是必然存在的,因为对象的大小总是8字节的整数倍,该数据仅用于补齐实例数据部分不足整数倍的部分,充当占位符的作用。
问:能保证GC 执行吗?
不能,虽然你可以调用System.gc() 或者Runtime.gc(),但是没有办法保证GC 的执行。