【JVM】面试题总结(1)

举报
一颗小谷粒 发表于 2025/03/31 16:33:39 2025/03/31
【摘要】 在 Java 开发领域,JVM(Java Virtual Machine)作为 Java 程序运行的核心,是面试过程中不可或缺的重要考点。无论是初级开发者,还是经验丰富的资深工程师,对 JVM 知识的深入理解,不仅能帮助我们在面试中脱颖而出,更能在实际工作中,优化程序性能,解决各种复杂的线上问题。本文将对常见的 JVM 面试题进行系统梳理,帮助大家加深对 JVM 的理解。一、JVM 内存区域...


在 Java 开发领域,JVM(Java Virtual Machine)作为 Java 程序运行的核心,是面试过程中不可或缺的重要考点。无论是初级开发者,还是经验丰富的资深工程师,对 JVM 知识的深入理解,不仅能帮助我们在面试中脱颖而出,更能在实际工作中,优化程序性能,解决各种复杂的线上问题。本文将对常见的 JVM 面试题进行系统梳理,帮助大家加深对 JVM 的理解。

一、JVM 内存区域

1. 运行时数据区域划分

JVM 运行时数据区域主要分为程序计数器Java 虚拟机栈本地方法栈方法区。面试中常被问到它们各自的作用。
  • 程序计数器:可以看作是当前线程所执行字节码的行号指示器,每个线程都有自己独立的程序计数器。字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
  • Java 虚拟机栈:线程私有的,它描述的是 Java 方法执行的内存模型:每个方法被执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。当方法调用结束时,栈帧就会被移除。
  • 本地方法栈:与 Java 虚拟机栈类似,只不过它服务于本地方法。
  • :被所有线程共享的一块内存区域,几乎所有的对象实例以及数组都在这里分配内存,是垃圾收集器管理的主要区域,也被称为 “GC 堆”。
  • 方法区:被所有线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。在 JDK 8 之后,方法区的实现由永久代变为元空间,元空间使用本地内存。

2. 栈和堆的区别

面试中,面试官可能会让你比较栈和堆的差异。
  • 分配方式:栈采用自动分配和回收的方式,方法执行时栈帧被创建,方法结束时栈帧被销毁;而堆的对象实例需要通过new关键字进行分配,对象的回收由垃圾收集器负责。
  • 数据结构:栈是一种后进先出(LIFO)的数据结构;堆是一种树形结构,更准确地说是一种优先队列,用于高效地进行对象的存储和管理。
  • 内存大小:栈的内存相对较小,每个线程的栈大小一般在几百 K 到几 M 之间;堆的内存可以根据需要进行调整,通常比栈大得多。

二、垃圾回收机制

1. 垃圾对象的判定算法

  • 引用计数法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加 1;当引用失效时,计数器值就减 1,当计数器为 0 时,该对象就可以被回收。这种算法实现简单,判定效率高,但无法解决循环引用的问题。
  • 可达性分析算法:通过一系列的 “GC Roots” 对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,就证明此对象是不可用的,可以被回收。在 Java 中,可作为 GC Roots 的对象包括虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象以及本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。

2. 常见的垃圾收集算法

  • 标记 - 清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。这种算法的缺点是效率较低,而且会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
  • 复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法实现简单,运行高效,但代价是将内存缩小为原来的一半。
  • 标记 - 整理算法:标记过程与 “标记 - 清除” 算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。该算法解决了 “标记 - 清除” 算法碎片化的问题。
  • 分代收集算法:根据对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都有大量对象死去,只有少量存活,选用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须使用 “标记 - 清理” 或 “标记 - 整理” 算法来进行回收。

3. 常见垃圾收集器

  • Serial 收集器:最基本、发展历史最悠久的收集器,是一个单线程收集器,在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。适用于客户端模式下的小型应用。
  • ParNew 收集器:Serial 收集器的多线程版本,除了使用多条线程进行垃圾收集外,其余行为包括算法、Stop The World、对象分配规则、回收策略等都与 Serial 收集器一样。它是很多运行在 Server 模式下的虚拟机中首选的新生代收集器。
  • Parallel Scavenge 收集器:新生代收集器,也是使用复制算法的多线程收集器,与 ParNew 收集器的主要区别在于它更关注吞吐量。吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值。
  • Serial Old 收集器:Serial 收集器的老年代版本,单线程收集器,使用 “标记 - 整理” 算法,主要供客户端模式下的虚拟机使用。
  • Parallel Old 收集器:Parallel Scavenge 收集器的老年代版本,使用多线程和 “标记 - 整理” 算法,注重高吞吐量,适用于后台运算而不需要太多交互的场景。
  • CMS 收集器:一种以获取最短回收停顿时间为目标的收集器,基于 “标记 - 清除” 算法实现,运作过程分为四个步骤:初始标记、并发标记、重新标记、并发清除。初始标记和重新标记阶段仍然需要 “Stop The World”,但这两个阶段耗时远小于并发标记和并发清除阶段。
  • G1 收集器:面向服务端应用的垃圾收集器,能充分利用多 CPU、多核环境。它将堆划分为多个大小相等的独立区域(Region),新生代和老年代不再是物理隔离,而是一部分 Region 的集合。G1 收集器可以预测停顿时间,通过维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。

三、类加载机制

1. 类加载的过程

类加载过程主要包括加载验证准备解析初始化五个阶段。
  • 加载:通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  • 验证:确保被加载类的正确性,包括文件格式验证、元数据验证、字节码验证和符号引用验证。
  • 准备:正式为类变量(静态变量)分配内存并设置初始值,这些变量所使用的内存都将在方法区中进行分配。这里的初始值通常是数据类型的零值,比如int类型初始值为 0,boolean类型初始值为false。
  • 解析:将常量池内的符号引用替换为直接引用的过程。符号引用以一组符号来描述所引用的目标,直接引用是可以直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。
  • 初始化:为类变量赋予正确的初始值,执行类构造器<clinit>()方法。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。

2. 类加载器

Java 中有三种类型的类加载器:启动类加载器扩展类加载器应用程序类加载器
  • 启动类加载器:由 C++ 实现,是虚拟机自身的一部分,负责加载<JAVA_HOME>\lib目录中或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库到虚拟机内存中。
  • 扩展类加载器:由 Java 语言实现,独立于虚拟机外部,继承自URLClassLoader,负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中的所有类库。
  • 应用程序类加载器:由 Java 语言实现,同样继承自URLClassLoader,负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

3. 双亲委派模型

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。当一个类加载器收到了类加载的请求时,它首先不会自己去尝试加载这个类,而是把请求委托给父类加载器去完成,依次向上,直到顶层的启动类加载器。只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。双亲委派模型保证了 Java 类的安全性,避免类的重复加载。

四、JVM 性能调优

1. 性能监控工具

  • JConsole:Java 自带的图形化监控工具,可以监控本地或者远程的 Java 虚拟机进程。通过它可以查看 JVM 内存、线程、类的加载情况,以及 MBean(管理 Bean)的信息。
  • VisualVM:功能更强大的性能分析工具,不仅可以监控 JVM 的运行状态,还可以进行采样分析,获取线程的堆栈信息,分析内存泄漏等问题。它支持安装插件,扩展更多功能。
  • JProfiler:商业性能分析工具,提供了丰富的功能,包括 CPU、内存、线程分析,以及代码执行时间分析等。在企业级开发中,被广泛用于定位性能瓶颈。

2. 调优思路

  • 合理设置堆大小:通过-Xms和-Xmx参数设置堆的初始大小和最大大小,避免频繁的垃圾回收和堆内存溢出。一般来说,堆大小设置为服务器可用内存的 60% 左右比较合适。
  • 优化垃圾收集器:根据应用的特点选择合适的垃圾收集器。对于响应时间要求较高的应用,如 Web 应用,可选择 CMS 或 G1 收集器;对于注重吞吐量的应用,如大数据计算任务,可选择 Parallel Scavenge 和 Parallel Old 收集器。
  • 分析线程状态:通过jstack命令获取线程的堆栈信息,分析线程是否存在死锁、阻塞等问题,优化线程的并发性能。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。