Java面试题-JVM

一、JVM结构

JVM 本质是一台 "虚拟的计算机",核心作用是运行 Java 字节码(.class 文件),屏蔽不同操作系统的底层差异,实现 "一次编写,到处运行"。

1、 类加载子系统

核心作用

负责将磁盘上的.class字节码文件加载到 JVM 的运行时数据区中,是 JVM 执行代码的 "前置准备环节"。

具体流程 & 作用

  • 加载:通过类的全限定名(如java.lang.String)找到.class文件,读取字节码并生成对应的Class对象;
  • 链接:验证字节码合法性(防止恶意代码)、为类变量分配内存并设置默认值、将符号引用替换为直接引用;
  • 初始化:执行类的静态代码块(static{})、为静态变量赋初始值,完成类的初始化。

关键特性:遵循 "双亲委派模型",保证类加载的安全性(避免核心类被篡改)。

2、运行时数据区

这是 JVM 最核心的内存区域,所有运行时的数据都存储在这里,分为线程私有线程共享两类。

结构 归属 核心作用
程序计数器 线程私有 记录当前线程执行的字节码指令地址(行号),保证线程切换后能恢复到正确执行位置;执行 native 方法时值为undefined
虚拟机栈 线程私有 存储方法调用的 "栈帧"(每个方法执行时创建一个栈帧),包含局部变量表、操作数栈、方法出口等;方法调用入栈,执行完出栈,是线程安全的。
本地方法栈 线程私有 与虚拟机栈功能类似,但专门为 native 方法(如调用 C/C++ 代码)服务;HotSpot VM 中常与虚拟机栈合并。
堆(Heap) 线程共享 JVM 中最大的内存区域,唯一作用是存储对象实例和数组;是垃圾回收(GC)的核心区域,分为新生代(Eden、S0、S1)和老年代。
方法区(Method Area) 线程共享 存储类的元数据(类名、字段、方法定义)、常量池、静态变量、JIT 编译后的代码等;JDK8 后改用 "元空间"(Metaspace),使用本地内存而非 JVM 堆内存。
运行时常量池 方法区子集 存放编译期生成的字面量(如字符串常量)和符号引用,运行时也可动态添加(如String.intern())。

3、执行引擎

核心作用

负责执行运行时数据区中的字节码指令,是 JVM 的 "执行核心"。

核心组件

  • 解释器:逐行解释执行字节码,启动速度快,但执行效率低;
  • 即时编译器(JIT):识别 "热点代码"(频繁执行的代码),将其编译为机器码并缓存,大幅提升执行效率;
  • 垃圾回收器(GC):属于执行引擎的子集,负责回收堆 / 方法区中不再使用的对象,释放内存(如 Serial GC、G1 GC 等)。

4、本地方法接口

核心作用

作为 Java 代码与本地代码(C/C++、汇编等)之间的 "桥梁",允许 JVM 调用本地方法,也支持本地方法反向调用 Java 方法。

应用场景

当需要调用操作系统底层 API(如文件操作、硬件交互)时,通过 JNI 实现。

5、本地方法库

核心作用

存放本地方法的具体实现代码(如 C/C++ 编写的.so/.dll库文件),供 JNI 调用,是本地方法的 "实际执行载体"。

二、JDK、JRE、JVM、JNI

1、JVM(Java 虚拟机)

  • 核心定义 :是一台 "虚拟的计算机",是 Java 程序运行的核心执行引擎,负责解释 / 编译执行 Java 字节码(.class 文件),屏蔽 Windows、Linux、Mac 等不同操作系统的底层差异,实现 "一次编写,到处运行"。
  • 关键特点:单独的 JVM 无法运行 Java 程序(需要依赖类库),它是纯执行层,不包含任何开发 / 运行所需的类库或工具。
  • 通俗比喻:相当于汽车的 "发动机",负责驱动程序运行,但发动机需要燃油(类库)才能工作。

2、JRE(Java 运行时环境)

  • 核心定义 :是运行 Java 程序的最小必备环境 ,包含两部分核心内容:
    • 基础:JVM(执行引擎);
    • 核心类库:Java 标准库的核心部分(如java.langjava.utiljava.io等),以及必要的配置文件。
  • 关键特点:只能运行已编译好的 Java 程序(.class 文件),无法开发 / 编译程序。
  • 通俗比喻:相当于 "发动机(JVM)+ 燃油(核心类库)",有了它就能跑 Java 程序,但不能造程序。

3、JDK(Java 开发工具包)

  • 核心定义 :是开发、编译、调试 Java 程序的完整工具包 ,包含:
    • 基础:完整的 JRE(运行时环境);
    • 开发工具:javac(Java 编译器,将.java 文件编译为.class)、javadoc(文档生成工具)、jdb(调试器)、jar(打包工具)等。
  • 关键特点:程序员开发 Java 程序的全套装备,包含运行和开发的所有能力。
  • 通俗比喻:相当于 "运行环境(JRE)+ 工具箱(开发 / 编译 / 调试工具)",既能造程序,也能跑程序。

4、JNI(Java 本地接口)

  • 核心定义 :不是独立的 "软件包",而是一套编程接口 / 规范,属于 JVM 的核心组成部分。它的作用是搭建 Java 代码和本地代码(C/C++、汇编等)之间的通信桥梁,允许 Java 程序调用操作系统底层的本地方法,也支持本地代码反向调用 Java 方法。
  • 关键特点:是 JVM 的 "内置功能",而非独立层级,服务于 Java 与底层系统的交互。
  • 通俗比喻:相当于 "发动机(JVM)上的一个接口 / 通道",让 Java 程序能和操作系统的底层代码(如文件操作、硬件交互)对话。

5、四者完整关系

1. 核心层级(包含关系):JDK ⊃ JRE ⊃ JVM
bash 复制代码
JDK(Java开发工具包)
├── JRE(Java运行时环境)
│   ├── JVM(Java虚拟机,核心执行层)
│   └── 核心类库(java.lang、java.util等,运行程序的基础)
└── 开发工具集(javac、javadoc、jdb、jar等,开发/编译/调试用)
2、 JNI 与三者的关联关系(非包含,是 JVM 的内置能力)
  • JNI 是 JVM 的 "内置接口":JNI 属于 JVM 的核心结构,不是独立的层级,只要有 JVM 就有 JNI;
  • JNI 服务于 JRE/JDK:
    • JRE 中的核心类库(如java.io文件操作、java.net网络通信)底层会通过 JNI 调用操作系统的本地代码;
    • 开发人员用 JDK 编写程序时,若需调用本地代码(如优化性能、访问硬件),可基于 JNI 规范编写本地方法,最终由 JVM 执行。

三、java堆

Java堆是和Java应用程序关系最为密切的内存空间,几乎所有的对象都存放在堆中。并且Java堆是完全自动化管理的,通过垃圾回收机制,垃圾对象会被自动清理,而不需要显式地释放。

根据垃圾回收机制的不同,Java堆有可能拥有不同的结构。最为常见的一种构成是将整个Java堆分为新生代和老年代。新生代包含Eden+Survivor区,survivor区里面分为from和to区,内存回收时,如果用的是复制算法,从from复制到to(我们也教s0,s1),当经过一次或者多次GC之后,存活下来的对象会被移动到老年区其中,新生代有可能分为eden区、s0区、s1区,s0 和s1也被称为from和to区域,它们是两块大小相等、可以互换角色的内存空间。

在绝大多数情况下,对象首先分配在eden区,在一次新生代回收后,如果对象还存活,则会进入s0或者s1,之后,每经过一次新生代回收, 对象如果存活,它的年龄就会加1。当对象的年龄达到一定条件后,就会被认为是老年对象,从而进入老年代。

栈、堆、方法区的关系

声明的局部变量存放在栈中,而方法中的对象实例创建放在堆中,剩下的类与方法的实现在方法区。

四、出入java栈

Java栈是一块线程私有的内存空间。如果说,Java堆和程序数据密切相关,那么Java栈就是和线程执行密切相关的。线程执行的基本行为是函数调用,每次函数调用的数据都是通过Java栈传递的。

Java栈只支持出栈入栈两种操作。

栈帧

在Java栈中保存的主要内容为栈帧。每一次函数调用, 都会有一个对应的栈帧

被压入Java栈,每一个函数调用结束,都会有一个栈帧被弹出Java栈。

五、局部变量表

局部变量表是 JVM中每个栈帧的核心组成部分之一,专门用于存储方法执行过程中的局部数据,是理解 JVM 方法调用和内存分配的关键概念。

1、定位

局部变量表是栈帧的 "本地数据存储区",仅服务于当前方法的执行,是线程私有、完全隔离的内存区域。

2、存储内容:

方法的形参 :包括普通参数、this引用(非 static 方法),参数会优先占用局部变量表的前几个位置。

方法内定义的局部变量:包括基本数据类型(8 种)、引用类型(对象引用 / 数组引用)、返回值地址(极少场景)。

局部变量表不存储对象实例本身(对象实例在堆中),仅存储引用类型的 "指针" 或基本数据类型的直接值。

3、结构与组成:变量槽

局部变量表的最小存储单元是变量槽。

1、Slot 的大小:JVM 规范中定义 1 个 Slot 占用 4 个字节(32 位),可存储:

基本数据类型:byte、short、char、int、float、boolean(都会占用 1 个 Slot,boolean 实际按 int 存储)。

引用类型:所有对象 / 数组引用。

2、占用规则:

long、double 类型占用2 个连续的 槽位。

其他类型均占用 1 个 槽位

3、参数存储顺序

方法形参按声明顺序依次占用槽,非 static 方法的第 0 个 槽位固定存储this引用。

4、特性

1、生命周期

局部变量表随栈帧创建而初始化,随栈帧销毁而释放。

作用域仅限当前方法,方法执行完毕后,局部变量表中的数据会立即失效,无法被其他方法访问。

2、不可动态扩展

局部变量表的大小在编译期就已确定,写入到方法的字节码中,运行时无法修改。

方法中局部变量的数量 / 类型一旦确定,运行时不会因为逻辑变化(如循环创建变量)而改变局部变量表的大小。

3、与垃圾回收无关

局部变量表存储的是方法内临时数据,生命周期与栈帧绑定,栈帧销毁后数据直接释放,无需 GC 参与。

只有堆中的对象实例才会被 GC 回收,局部变量表中的引用仅作为指向堆对象的 "指针"。

5、影响局部变量表大小的因素

1、方法参数的数量和类型

参数越多、包含 long/double 类型越多,占用槽位越多。

2、方法内局部变量的数量和类型

定义的局部变量越多(尤其是 long/double),Slot 占用越多。

3、方法类型

static 方法无this引用,比同参数的非 static 方法少占用 1 个槽位。

6、局部变量的回收

6.1局部变量本身的"回收"(非GC)

局部变量本身存储在虚拟机栈的局部变量表,其回收和垃圾回收无关,是 JVM 方法调用机制的自动行为。

回收本质:

不是 "垃圾回收",而是栈帧销毁时的内存释放。

回收时机:

方法正常执行完毕或异常终止,栈帧从虚拟机栈中出栈。

局部变量表随栈帧一起被销毁,其中存储的内容占用的槽位空间被 JVM 回收,用于后续方法调用的栈帧分配。

核心特点:

即时性:方法结束后立即释放,无需等待任何 GC 线程。

不可控性:程序员无法通过代码手动触发(如调用 System.gc () 也无效),完全由 JVM 的方法调用栈机制管理。

线程隔离:局部变量是线程私有,回收仅影响当前线程的栈空间,无线程安全问题。

6.2 局部变量引用的堆对象的回收(GC)

局部变量不会存储对象实例本身(对象实例在堆中),仅存储对象的引用指针。堆对象的回收才是 GC 的核心,局部变量的状态直接影响堆对象的可达性。

核心规则

堆对象是否被回收,取决于是否存在可达的引用链------ 只要局部变量还持有堆对象的引用,该对象就会被标记为 "存活",不会被 GC 回收;反之则可能被回收。

注意点:

GC 的 "延迟性":即使满足上述场景,堆对象只是被标记为 "可回收",实际回收需等待 GC 线程执行,而非立即释放。

System.gc () 仅为 "建议":调用该方法只是提醒 JVM 执行 GC,JVM 可忽略,无法保证堆对象立即回收。

六、操作数栈

操作数栈是 JVM中每个栈的核心组成部分,是方法执行过程中完成算术运算、方法调用的 "临时计算区",也是理解 JVM 字节码执行逻辑的关键。

1、本质

操作数栈是一个后进先出 的栈结构(无索引,只能操作栈顶元素),专门用于存储方法执行过程中需要计算的临时数据、指令的操作数和运算结果。

2、核心作用

1、支撑字节码指令的算术运算。

2、支撑方法调用 / 返回。

3、作为局部变量表和计算逻辑之间的 "数据中转站"。

3、所属关系

每个栈帧对应一个独立的操作数栈,随栈帧创建而初始化,随栈帧销毁而释放,线程私有,完全隔离。

4、核心结构与存储规则

1、存储单元

以 "数据项" 为基本单位,无局部变量表的 "Slot" 概念,但有明确的类型和深度规则:

01、可存储数据类型:基本数据类型(8 种)、引用类型(对象 / 数组引用)、方法返回地址。

02、类型转换规则:byte/short/char/boolean 类型入栈前会先转换为 int 类型。

2、栈深度限制

01、操作数栈的最大深度编译期就已确定,写入到方法的字节码中,运行时不可动态扩展。

02、若运行时操作数栈的实际深度超出编译期设定的最大值,会抛出StackOverflowError。

3、64 位数据处理

01、long、double 类型占用2 个连续的栈深度位置。

02、其他类型(int、float、引用等)仅占用 1 个栈深度位置。

5、基础操作指令分类

指令类型 示例指令 作用
入栈指令 bipush(常量入栈)、iload_1(局部变量表数据入栈) 将常量、局部变量表中的数据压入操作数栈
运算指令 iadd(int 相加)、lmul(long 相乘)、if_icmpgt(int 比较) 弹出栈顶数据进行运算,结果压回栈(或直接用于判断)
出栈指令 istore_2(栈顶数据存入局部变量表) 将栈顶数据弹出,写入局部变量表指定位置
方法调用 / 返回 invokevirtual(调用实例方法)、ireturn(返回 int 值) 方法调用时传递参数(压栈),返回时将结果压栈
字节码指令 操作数栈状态(栈底→栈顶) 指令说明
iload_1 [x] 将局部变量表 Slot1 的参数 x 压入栈
bipush 5 [x, 5] 将常量 5 压入栈
iadd [x+5] 弹出 x 和 5,相加后将结果压栈
istore_2 [] 弹出 x+5,存入局部变量表 Slot2(即变量 y)
iload_2 [y] 将局部变量表 Slot2 的 y 压入栈
bipush 2 [y, 2] 将常量 2 压入栈
imul [y*2] 弹出 y 和 2,相乘后将结果压栈
ireturn [] 弹出栈顶的 y*2,作为方法返回值

操作数栈和局部变量表是栈帧中最核心的两个组件,二者频繁交互完成方法执行,关系如下:

  1. 数据流向 1(局部变量表 → 操作数栈) :通过xload_n指令(如iload_1aload_2),将局部变量表中指定位置的数据压入操作数栈,为计算做准备;
  2. 数据流向 2(操作数栈 → 局部变量表) :通过xstore_n指令(如istore_2astore_3),将操作数栈顶的计算结果弹出,存入局部变量表指定位置(即赋值给局部变量);
  3. 核心定位对比
    • 局部变量表:是 "数据存储区",存储方法参数、局部变量,可通过索引随机访问;
    • 操作数栈:是 "数据计算区",仅支持栈顶操作,专门完成运算和指令交互。

6、特性

1、线程私有:每个线程的每个方法调用都有独立的操作数栈,无并发安全问题。

2、无 GC 参与:操作数栈存储的是临时计算数据,随栈帧销毁而释放,无需垃圾回收。

3、类型严格匹配 :JVM 会严格校验操作数栈中数据的类型与指令要求是否匹配(如iadd只能处理 int 类型),不匹配则抛出VerifyErrorClassCastException。

7、异常场景

1、StackOverflowError:操作数栈深度超出编译期设定的最大值(多因方法递归过深、局部变量 / 运算过多导致栈帧整体过大)。

2、NullPointerException :若操作数栈中存储的引用为 null,却执行调用实例方法 / 访问字段的指令(如invokevirtual)。

3、VerifyError :字节码校验阶段发现操作数栈类型不匹配(如用ladd处理 int 数据)。

相关推荐
while(1){yan}2 小时前
Spring,SpringBoot,SpringMVC
java·spring boot·spring
a程序小傲2 小时前
字节跳动Java面试被问:Fork/Join框架的使用场景
开发语言·python
秋饼2 小时前
【spring-framework 本地下载部署,以及环境搭建】
java·后端·spring
刘宇涵492 小时前
根节点Java
java
zwjapple2 小时前
React + Java 技术面试完整指南
java·开发语言·jvm·react
XMYX-02 小时前
从 Pod 资源到 JVM 参数:我再生产环境中踩过的 Kubernetes 资源配置那些坑——2025 年度技术总结
jvm·容器·kubernetes
秋邱2 小时前
Java匿名内部类的使用场景:从语法本质到实战优化全解析
android·java·开发语言·数据库·python
不会c嘎嘎2 小时前
QT中的常用控件(一)
开发语言·qt
悟乙己2 小时前
anthropics Skills pptx深度解读:从官方规范到实战案例(二)
java·llm·pptx·skills·anthropics