vlambda博客
学习文章列表

Java 面试之 JVM 考察点和加分项

本文节选自《32个Java面试必考点》第03讲:深入浅出JVM
https://kaiwu.lagou.com/course/courseInfo.htm?courseId=1#/detail/pc?id=5 
未经允许,请勿转载,违者必究。

JVM 是 Java 程序运行基础,面试时一定会遇到 JVM 相关的题。本文会先对面试中 JVM 的考察点进行汇总介绍。然后对 JVM 内存模型、Java 的类加载机制、常用的 GC 算法这三个知识点进行详细讲解。最后汇总 JVM 考察点和加分项,以及这部分知识的面试真题。
01  JVM 知识点汇总
首先看看 JVM 的知识点汇总。
如上图所示,JVM 知识点有 6 个大方向,其中,内存模型、类加载机制、GC 垃圾回收是比较重点的内容。性能调优部分偏重实际应用,重点突出实践能力。编译器优化和执行模式部分偏重理论基础,主要掌握知识点。
各个部分需要了解的知识点如下。
  • 内存模型:程序计数器、方法区、堆、栈、本地方法栈的作用,保存哪些数据。
  • 类加载:双亲委派的加载机制,以及常用类加载器分别加载哪种类型的类。
  • GC:分代回收的思想和依据,以及不同垃圾回收算法实现的思路、适合的场景。
  • 性能调优:常用的 JVM 优化参数的作用,参数调优的依据,常用的 JVM 分析工具能分析哪类问题,以及使用方法。
  • 执行模式:解释、编译、混合模式的优缺点,Java7 提供的分层编译技术。需要知道 JIT 即时编译技术和 OSR(栈上替换),知道 C1、C2 编译器针对的场景,其中 C2 针对 Server 模式,优化更激进。在新技术方面可以了解 Java10 提供的由 Java 实现的 Graal 编译器。
  • 编译优化:前端编译器 javac 的编译过程、AST 抽象语法树、编译期优化和运行期优化。编译优化的常用技术包括公共子表达式的消除、方法内联、逃逸分析、栈上分配、同步消除等。明白了这些才能写出对编译器友好的代码。
JVM 的内容相对来说比较集中,但是对知识深度的掌握要求较高。
02  详解 JVM 内存模型
JVM 内存模型主要指运行时的数据区,包括 5 个部分,如下图所示。           
Java 面试之 JVM 考察点和加分项
  • 栈也叫方法栈,是线程私有的,线程在执行每个方法时都会同时创建一个栈帧,用来存储局部变量表、操作栈、动态链接、方法出口等信息。调用方法时执行入栈,方法返回时执行出栈。
  • 本地方法栈与栈类似,也是用来保存线程执行方法时的信息,不同的是,执行 Java 方法使用栈,而执行 native 方法使用本地方法栈。
  • 程序计数器保存着当前线程所执行的字节码位置,每个线程工作时都有一个独立的计数器。程序计数器为执行 Java 方法服务,执行 native 方法时,程序计数器为空。
       栈、本地方法栈、程序计数器这三个部分都是线程独占的。
  • 堆是 JVM 管理的内存中最大的一块,堆被所有线程共享,目的是为了存放对象实例,几乎所有的对象实例都在这里分配。当堆内存没有可用的空间时,会抛出 OOM 异常。根据对象存活的周期不同,JVM 把堆内存进行分代管理,由垃圾回收器来进行对象的回收管理。
  • 方法区也是各个线程共享的内存区域,又叫非堆区。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,JDK 1.7 中的永久代和 JDK 1.8 中的 Metaspace 都是方法区的一种实现。
面试回答这个问题时,要答出两个要点:一个是各部分的功能,另一个是哪些线程共享,哪些独占。
03  详解 JMM 内存可见性
JMM 是 Java 内存模型,与刚才讲到的 JVM 内存模型是两回事,JMM 的主要目标是定义程序中变量的访问规则,如下图所示,所有的共享变量都存储在主内存中共享。每个线程有自己的工作内存,工作内存中保存的是主内存中变量的副本,线程对变量的读写等操作必须在自己的工作内存中进行,而不能直接读写主内存中的变量。
Java 面试之 JVM 考察点和加分项       
在多线程进行数据交互时,例如线程 A 给一个共享变量赋值后,由线程 B 来读取这个值,A 修改完变量是修改在自己的工作区内存中,B 是不可见的,只有从 A 的工作区写回主内存,B 再从主内存读取自己的工作区才能进行进一步的操作。由于指令重排序的存在,这个写—读的顺序有可能被打乱。因此 JMM 需要提供原子性、可见性、有序性的保证。
03  详解 JMM 保证
如下图所示,来看 JMM 如何保证原子性、可见性,有序性。
Java 面试之 JVM 考察点和加分项 原子性
JMM 保证对除 long 和 double 外的基础数据类型的读写操作是原子性的。另外关键字 synchronized 也可以提供原子性保证。synchronized 的原子性是通过 Java 的两个高级的字节码指令 monitorenter 和 monitorexit 来保证的。
可见性
JMM 可见性的保证,一个是通过 synchronized,另外一个就是 volatile。volatile 强制变量的赋值会同步刷新回主内存,强制变量的读取会从主内存重新加载,保证不同的线程总是能够看到该变量的最新值。
有序性
对有序性的保证,主要通过 volatile 和一系列 happens-before 原则。volatile 的另一个作用就是阻止指令重排序,这样就可以保证变量读写的有序性。
happens-before 原则包括一系列规则,如:
  • 程序顺序原则,即一个线程内必须保证语义串行性;
  • 锁规则,即对同一个锁的解锁一定发生在再次加锁之前;
  • happens-before 原则的传递性、线程启动、中断、终止规则等。
04  详解类加载机制
类的加载指将编译好的 Class 类文件中的字节码读入内存中,将其放在方法区内并创建对应的 Class 对象。类的加载分为加载、链接、初始化,其中链接又包括验证、准备、解析三步。如下图所示。
Java 面试之 JVM 考察点和加分项
  1. 加载是文件到内存的过程。通过类的完全限定名查找此类字节码文件,并利用字节码文件创建一个 Class 对象。
  2. 验证是对类文件内容验证。目的在于确保 Class 文件符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种:文件格式验证,元数据验证,字节码验证,符号引用验证。
  3. 准备阶段是进行内存分配。为类变量也就是类中由 static 修饰的变量分配内存,并且设置初始值。这里要注意,初始值是 0 或者 null,而不是代码中设置的具体值,代码中设置的值是在初始化阶段完成的。另外这里也不包含用 final 修饰的静态变量,因为 final 在编译的时候就会分配。
  4. 解析主要是解析字段、接口、方法。主要是将常量池中的符号引用替换为直接引用的过程。直接引用就是直接指向目标的指针、相对偏移量等。
  5. 初始化,主要完成静态块执行与静态变量的赋值。这是类加载最后阶段,若被加载类的父类没有初始化,则先对父类进行初始化。
只有对类主动使用时,才会进行初始化,初始化的触发条件包括在创建类的实例时、访问类的静态方法或者静态变量时、Class.forName() 反射类时、或者某个子类被初始化时。
如上图所示,浅绿的两个部分表示类的生命周期,就是从类的加载到类实例的创建与使用,再到类对象不再被使用时可以被 GC 卸载回收。这里要注意一点,由 Java 虚拟机自带的三种类加载器加载的类在虚拟机的整个生命周期中是不会被卸载的,只有用户自定义的类加载器所加载的类才可以被卸载。
05  详解类加载器
Java 面试之 JVM 考察点和加分项
如上图所示,Java 自带的三种类加载器分别是:BootStrap 启动类加载器、扩展类加载器和应用加载器(也叫系统加载器)。图右边的桔黄色文字表示各类加载器对应的加载目录。启动类加载器加载 java home 中 lib 目录下的类,扩展加载器负责加载 ext 目录下的类,应用加载器加载 classpath 指定目录下的类。除此之外,可以自定义类加载器。  
Java 的类加载使用双亲委派模式,即一个类加载器在加载类时,先把这个请求委托给自己的父类加载器去执行,如果父类加载器还存在父类加载器,就继续向上委托,直到顶层的启动类加载器,如上图中蓝色向上的箭头。如果父类加载器能够完成类加载,就成功返回,如果父类加载器无法完成加载,那么子加载器才会尝试自己去加载。如图中的桔黄色向下的箭头。
这种双亲委派模式的好处,可以避免类的重复加载,另外也避免了 Java 的核心 API 被篡改。
06  详解分代回收
Java 的堆内存被分代管理,为什么要分代管理呢?分代管理主要是为了方便垃圾回收,这样做基于2个事实,第一,大部分对象很快就不再使用;第二,还有一部分不会立即无用,但也不会持续很长时间。
虚拟机划分为年轻代、老年代、和永久代,如下图所示。
Java 面试之 JVM 考察点和加分项       
  • 年轻代主要用来存放新创建的对象,年轻代分为 Eden 区和两个 Survivor 区。大部分对象在 Eden 区中生成。当 Eden 区满时,还存活的对象会在两个 Survivor 区交替保存,达到一定次数的对象会晋升到老年代。
  • 老年代用来存放从年轻代晋升而来的,存活时间较长的对象。
  • 永久代,主要保存类信息等内容,这里的永久代是指对象划分方式,不是专指 1.7 的 PermGen,或者 1.8 之后的 Metaspace。
根据年轻代与老年代的特点,JVM 提供了不同的垃圾回收算法。垃圾回收算法按类型可以分为引用计数法、复制法和标记清除法。
  • 引用计数法是通过对象被引用的次数来确定对象是否被使用,缺点是无法解决循环引用的问题。

  • 复制算法需要 from 和 to 两块相同大小的内存空间,对象分配时只在 from 块中进行,回收时把存活对象复制到 to 块中,并清空 from 块,然后交换两块的分工,即把 from 块作为 to 块,把 to 块作为 from 块。缺点是内存使用率较低。

  • 标记清除算法分为标记对象和清除不在使用的对象两个阶段,标记清除算法的缺点是会产生内存碎片。

JVM 中提供的年轻代回收算法 Serial、ParNew、Parallel Scavenge 都是复制算法,而 CMS、G1、ZGC 都属于标记清除算法。
Java 面试之 JVM 考察点和加分项
考察点和加分项
考察点
总结 JVM 相关的面试考察点如下:
  1. 深入了解 JVM 的内存模型和 Java 的内存模型;
  2. 要了解类的加载过程,了解双亲委派机制;
  3. 要理解内存的可见性与 Java 内存模型对原子性、可见性、有序性的保证机制;
  4. 要了解常用的 GC 算法的特点、执行过程,和适用场景,例如 G1 适合对最大延迟有要求的场合,ZGC 适用于 64 位系统的大内存服务中;
  5. 要了解常用的 JVM 参数,明白对不同参数的调整会有怎样的影响,适用什么样的场景,例如垃圾回收的并发数、偏向锁设置等。
加分项
如果想要给面试官留下更好的印象,注意这些加分项。
  1. 如果在编译器优化方面有深入的了解的话,会让面试官觉得你对技术的深度比较有追求。例如知道在编程时如何合理利用栈上分配降低 GC 压力、如何编写适合内联优化等代码等。
  2. 如果你能有线上实际问题的排查经验或思路那就更好了,面试官都喜欢动手能力强的同学。例如解决过线上经常 FullGC 问题,排查过内存泄露问题等。
  3. 如果能有针对特定场景的 JVM 优化实践或者优化思路,也会有意想不到的效果。例如针对高并发低延迟的场景,如何调整 GC 参数尽量降低 GC 停顿时间,针对队列处理机如何尽可能提高吞吐率等;
  4. 如果对最新的 JVM 技术趋势有所了解,也会给面试官留下比较深刻的印象。例如了解 ZGC 高效的实现原理,了解 Graalvm 的特点等。
真题汇总
总结 JVM 相关的面试真题,第一部分真题如下,课后可以重点练习。
Java 面试之 JVM 考察点和加分项
解题思路如下所示:
  • 第 1 题 Java 内存模型前面讲过,面试时回答这个问题时记得和面试官确认是希望回答 JVM 的内存模型,还是 Java 对内存访问的模型,不要答跑偏。
  • 第 2 题要复习一下什么场景下会触发 FullGC,例如年轻代晋升时老年代空间不足,例如永久代空间不足等。
  • 第 3~6 题前面已经有过讲解,因此不再重复。
第二部分真题如下所示:
解题思路如下所示:
  • 第 7 题 volatile 要重点回答强制主内存读写同步以及防止指令重排序两点。
  • 第 8、9 题前面已经讲过。
  • 第 10 题重点介绍出强、弱、软、虚四种引用,以及在 GC 中的处理方式。
  • 第 11 题可以了解一下 Java 自带的几种工具的功能,例如 JMC 中的飞行记录器,堆分析工具 MAT,线程分析工具 jstack 和获取堆信息的 jmap 等。


— END —

【更多往期内容】