Java面试宝典——JVM(一)

77 阅读15分钟

可关注微信公众号“假装正经的程序员”查看更多面试题,回复“多线程”可获取详细答案

本篇为多线程基础内容,部分问题相对比较“刁钻”,却又是中高级开发工程师必须掌握的知识点,因此本篇内容需要全部熟知

问题

  1. 听说过哪些虚拟机
  2. 什么是类加载器
  3. 类是如何被加载的
  4. 什么是双亲委派机制
  5. Java内存模型是什么
  6. 说说Java内存布局
  7. 元空间和方法区是什么关系
  8. 为什么要使用元空间替代永久代
  9. 如何判断对象是否存活
  10. GC Roots有哪些
  11. 常见的垃圾回收算法有哪些
  12. 常见的垃圾回收器有哪些

回答

  1. 听说过哪些虚拟机
HotSpot JVM:这是Oracle JDK中默认的JVM实现,也是最常用的JVM。它具有优秀的性能和即时编译(JIT)功能。
OpenJ9:由IBM开发的JVM实现,专注于内存效率和快速启动。它在某些方面优于HotSpot,尤其适用于云环境。
GraalVM:由Oracle开发,支持多种语言(包括Java、JavaScript、Python等)。它具有即时编译、AOT编译和嵌入式模式。
Zing JVM:由Azul Systems开发,专注于大规模、高吞吐量的应用程序。它具有低延迟和高度可预测的性能。
JRockit:曾由BEA Systems开发,后被Oracle收购。它专注于服务器端应用程序,但已不再活跃维护。
  1. 什么是类加载器
类加载器(Class Loader)是 Java 虚拟机(JVM)的重要组成部分,负责将字节码文件加载到内存中并转换为可执行的类。
类加载总共分为以下四种:
1.启动类加载器(Bootstrap Class Loader):它是 JVM 的内部组件,负责加载 Java 核心类库(如java.lang)和其他被系统类加载器所需要的类。启动类加载器是由 JVM 实现提供的,通常使用本地代码来实现。
2.扩展类加载器(Extension Class Loader):它是 sun.misc.Launcher$ExtClassLoader 类的实例,负责加载 Java 的扩展类库(如 java.util、java.net)等。扩展类加载器通常从 java.ext.dirs 系统属性所指定的目录或 JDK 的扩展目录中加载类。
3.系统类加载器(System Class Loader):也称为应用类加载器(Application Class Loader),它是sun.misc.Launcher$AppClassLoader 类的实例,负责加载应用程序的类。系统类加载器通常从 CLASSPATH 环境变量所指定的目录或 JVM 的类路径中加载类。
4.用户自定义类加载器(User-defined Class Loader):这是开发人员根据需要自己实现的类加载器。用户自定义类加载器可以根据特定的加载策略和需求来加载类,例如从特定的网络位置、数据库或其他非传统来源加载类。
  1. 类是如何被加载的
1.加载:查找并加载类的二进制数据。
2.连接:将 Java 类的二进制数据合并到 JVM 运行状态之中。
    a.验证:验证加载的类是否符合 Java 虚拟机规范。
    b.准备:为类的静态变量分配内存,并设置默认初始值。
    c.解析:将类中的符号引用转换为直接引用。
3.初始化:执行类的初始化代码,包括静态变量赋值和静态代码块的执行
  1. 什么是双亲委派机制
双亲委派模型是 Java 类加载器的一种工作机制。
它是指当一个类加载器需要加载一个类时,它首先不会自己去尝试加载这个类,
而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,
因此所有的加载请求最 终都应该传送到最顶层的启动类加载器中,只有当父
加载器反馈自己无 法完成这个加载请求(它的搜索范围中没有找到所需的类)
时,子加载器才会尝试自己去完成加载。

双亲委派.jpg 5. Java内存模型是什么

Java Memory Model,简称 JMM,是用来定义 Java 线程和内存之间的操作规范的,目的是解决多线程正确执行的问题。
Java 内存模型规范的定义确保了多线程程序的可见性、有序性和原子性,从而保证了线程之间正确的交互和数据一致性。
Java 内存模型主要包括以下内容:
1.主内存(Main Memory):所有线程共享的内存区域,包含了对象的字段、方法和运行时常量池等数据。
2.工作内存(Working Memory):每个线程拥有自己的工作内存,用于存储主内存中的数据的副本。线程只能直接操作工作内存中的数据。
3.内存间交互操作:线程通过读取和写入操作与主内存进行交互。读操作将数据从主内存复制到工作内存,写操作将修改后的数据刷新到主内存。
4.原子性(Atomicity):JMM 保证基本数据类型(如 intlong)的读写操作具有原子性,即不会被其他线程干扰,保证操作的完整性。
5.可见性(Visibility):JMM 确保一个线程对共享变量的修改对其他线程可见。这意味着一个线程在工作内存中修改了数据后,必须将最新的数据刷新到主内存,以便其他线程可以读取到更新后的数据。
6.有序性(Ordering):JMM 保证程序的执行顺序按照一定的规则进行,不会出现随机的重排序现象。这包括了编译器重排序、处理器重排序和内存重排序等。
Java 内存模型通过以上规则和语义,提供了一种统一的内存访问方式,使得多线程程序的行为可预测、可理解,并帮助开发者编写正确和高效的多线程代码。
开发者可以利用 JMM 提供的同步机制(如关键字 volatilesynchronized、Lock 等)来实现线程之间的同步和通信,以确保线程安全和数据一致性。

6.说说Java内存布局

通常所说的 JVM 内存布局,一般指的是 JVM 运行时数据区(Runtime Data Area),也就是当字节码被类加载器加载之后的执行区域划分。
《Java虚拟机规范》中将 JVM 运行时数据区域划分为以下 5 部分:
1.程序计数器(Program Counter Register):用于记录当前线程执行的字节码指令地址,是线程私有的,线程切换不会影响程序计数器的值。
2.Java 虚拟机栈(Java Virtual Machine Stacks):用于存储方法执行时的局部变量表、操作数栈、动态链接、方法出口等信息,也是线程私有的。每个方法在执行时都会创建一个栈帧,栈帧包含了方法的局部变量表、操作数栈等信息。
3.本地方法栈(Native Method Stack):与 Java 虚拟机栈类似,用于存储本地方法的信息。
4.Java 堆(Java Heap):用于存储对象实例和数组,是 JVM 中最大的一块内存区域,它是所有线程共享的。堆通常被划分为年轻代和老年代,以支持垃圾回收机制。
- 年轻代(Young Generation):用于存放新创建的对象。年轻代又分为 Eden 区和两个 Survivor 区(通常是一个 From 区和一个 To 区),对象首先被分配在 Eden 区,经过垃圾回收后存活的对象会被移到 Survivor 区,经过多次回收后仍然存活的对象会晋升到老年代。
- 老年代(Old Generation):用于存放存活时间较长的对象。老年代主要存放长时间存活的对象或从年轻代晋升过来的对象。
5.方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区也是所有线程共享的。

7.元空间和方法区是什么关系

方法区是《Java虚拟机规范》中定义的内存区域,用于存储类的结构信息(如类的字节码、常量池、字段和方法信息等),
而 Java 默认虚拟机 HotSpot 中,在 JDK 1.8 之前的版本中,是通过永久代来实现方法区的,
但 JDK 1.8 之后,永久代被元空间(Metaspace)取代。
所以,方法区和永久代的区别在于,方法区是规范,而元空间(和永久代)是具体实现。

8.为什么要使用元空间替代永久代

1.提高稳定性,降低 OOM:当使用永久代实现方法区时,永久代的最大容量受制于 PermSize 和 MaxPermSize 参数设置的大小,而这两个参数的大小又很难确定,因为在程序运行时需要加载多少类是很难估算的,如果这两个参数设置的过小就会频繁的触发 FullGC 和导致 OOM(Out of Memory,内存溢出)。但是,当使用元空间替代了永久代之后,出现 OOM 的几率就被大大降低了,因为元空间使用的是本地内存,这样元空间的大小就只和本地内存的大小有关了,从而大大降低了 OOM 的问题。
2.降低运维成本:元空间使用的是本地内存,这样就无需运维人员再去专门设置和调整元空间的大小了。

9.如何判断对象是否存活

1.引用计数法
引用计数器算法的实现思路是,给对象增加一个引用计数器,每当有一个地方引用它时,计数器就 +1;当引用失效时,计数器就 -1;任何时刻计数器为 0 的对象就是不能再被使用的,即对象已"死"。
优点:实现简单,判定效率也比较高。
缺点:是引用计数法无法解决对象的循环引用问题。
2.可达性分析法
可达性分析算法是通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,
搜索走过的路径称之为"引用链",当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 
到这个对象不可达)时,证明此对象是不可用的。
如下图,对象 Object5-Object7 之间虽然彼此还有关联,但是它们到 GC Roots 是不可达的,因此它们会被判定为可回收对象。
目前主流的 Java 虚拟机使用的都是可达性分析算法来判断对象是否存活的。

可达性分析.png 10.GC Roots有哪些

1.Java 虚拟机栈(栈帧中的本地变量表)中引用的对象;
2.方法区中类静态属性引用的对象;
3.方法区中常量引用的对象;
4.本地方法栈中 JNI(Native方法)引用的对象。

11.常见的垃圾回收算法有哪些

1.标记-清除算法:标记-清除(Mark-Sweep)算法属于早期的垃圾回收算法,它是由标记阶段和清除阶段构成的。标记阶段会给所有的存活对象做上标记,而清除阶段会把没有被标记的死亡对象进行回收。而标记的判断方法就是前面讲的引用计数算法和可达性分析算法
2.复制算法:复制算法是将内存分为大小相同的两块区域,每次只使用其中的一块区域,这样在进行垃圾回收时就可以直接将存活的东西复制到新的内存上,然后再把另一块内存全部清理掉。这样就不会产生内存碎片的问题了
3.标记-整理算法:标记-整理(Mark-Compat)算法是由两个阶段组成的:标记阶段和整理阶段。其中标记阶段和标记-清除算法的标记阶段一样,不同的是后面的一个阶段,标记-整理算法的后一个阶段不是直接对内存进行清除,而是把所有存活的对象移动到内存的一端,然后把另一端的所有死亡对象全部清除

标记-清除:

标记清除.png 复制:

复制算法.png 标记-整理:

标记整理.png 12.常见的垃圾回收器有哪些

JVM中常用的垃圾回收器有以下几种:
1.Serial收集器:这是最基本的、发展历史最悠久的收集器,它是一个单线程的收集器,采用复制算法,适用于新生代,它会暂停所有用户线程进行垃圾回收,也称为“Stop The World”。
 优点:简单高效,由于采用的是单线程的方法,因此与其他类型的收集器相比,对单个cpu来说没有了上下文之间的的切换,效率比较高。
 缺点:会在用户不知道的情况下停止所有工作线程,导致系统停顿时间较长,不适合对响应时间敏感的场景。
 
2.ParNew收集器:这是Serial收集器的多线程版本,除了使用多线程之外,其余行为和Serial收集器基本一样,它也采用复制算法,适用于新生代,它是目前唯一能和老年代的CMS收集器配合工作的新生代收集器。
 优点:多线程版本的Serial,可以更加有效的利用系统资源,减少系统停顿时间,适用于多核CPU的环境。
 缺点:同Serial,会在用户不知道的情况下停止所有工作线程,而且由于使用了多线程,会消耗更多的CPU资源。
 
3.Parallel Scavenge收集器:这是一个以达到可控制的吞吐量为目标的收集器,它也是多线程的,采用复制算法,适用于新生代,它可以设置垃圾收集时间占总时间的比率,以及最大垃圾收集停顿时间,从而提高系统的运行效率。
 优点:追求高吞吐量,高效利用CPU,是吞吐量优先,且能进行精确控制,适用于后台运算为主的场景。
 缺点:应该说是特点,追求高吞吐量必然要牺牲一些其他方面的优势,不能做到既,又。ParNew收集器关注点在于尽可能的缩短垃圾收集时用户线程的停顿时间,原本10s收集一次, 每次停顿100ms, 设置完参数之后可能变成5s收集一次, 每次停顿70ms. 停顿时间变短, 但收集次数变多。
 
4.Serial Old收集器:这是Serial收集器的老年代版本,它是一个单线程的收集器,采用标记-整理算法,适用于老年代,它也会暂停所有用户线程进行垃圾回收,它一般作为CMS收集器的后备方案,当CMS收集器失败时,会启动Serial Old收集器进行垃圾回收。
 优点:简单高效,由于采用的是单线程的方法,因此与其他类型的收集器相比,对单个cpu来说没有了上下文之间的的切换,效率比较高。
 缺点:会在用户不知道的情况下停止所有工作线程,导致系统停顿时间较长,不适合对响应时间敏感的场景。
 
5.Parallel Old收集器:这是Parallel Scavenge收集器的老年代版本,它是多线程的,采用标记-整理算法,适用于老年代,它和Parallel Scavenge收集器一起,可以达到一个可控制的吞吐量,适用于后台运算为主的场景。
 优点:多线程版本的Serial Old,可以更加有效的利用系统资源,减少系统停顿时间,适用于多核CPU的环境,追求高吞吐量。
 缺点:同Serial Old,会在用户不知道的情况下停止所有工作线程,而且由于使用了多线程,会消耗更多的CPU资源。
 
6.CMS收集器:这是一种以获取最短回收停顿时间为目标的收集器,它是多线程的,采用标记-清除算法,适用于老年代,它的特点是采用并发收集,尽量减少用户线程的停顿,但是会产生内存碎片,以及对CPU资源非常敏感,适用于用户交互较多的场景。
 优点:停顿时间短,吞吐量大,并发收集,适用于对响应时间敏感的场景,如WEB、B/S系统。
 缺点:对CPU资源非常敏感,无法收集浮动垃圾,容易产生大量内存碎片,可能导致频繁的Full GC。
 
7.G1收集器:这是一种面向服务端应用的收集器,它是多线程的,采用标记-整理算法,适用于整个堆内存,它的特点是把堆内存划分为多个大小相等的区域,然后根据垃圾最多的区域优先进行回收,从而实现高效的垃圾回收,同时也能控制停顿时间,避免内存碎片,适用于大内存的场景²。
 优点:并行与并发,分代收集,空间整合,可预测的停顿,适用于大内存,多核CPU的场景。
 缺点:还不够成熟,可能存在一些bug,需要调整更多的参数,对内存占用较高。