当前位置: 首页 > news >正文

JVM——八股文

1. JDK, JRE和JVM的关系

  • JDK = JRE + Java开发工具
  • JRE = JVM + Java核心类库

JDK供Java程序开发人员开发软件,JRE供客户使用,只需要JVM运行环境即可。

JVM运行的是class字节码,不仅能运行Java代码,还能运行其他语言,只要语言能编译成字节码文件即可,比如Kotlin。

2. JVM的主要组成部分

  • 类加载器
    • 加载Loading
    • 链接Linking
    • 初始化Initialization
  • 运行时数据区
  • 执行引擎
  • 本地接口

3. 你能解释一下JVM类加载器的作用吗

1. 加载 (Loading)

  • 目的:将类的 .class 文件的二进制数据读入内存,并在方法区中创建一个 java.lang.Class 对象来表示这个类。
  • 主要工作
    1. 通过类的全限定名获取其二进制字节流:这可以通过多种方式实现,比如从本地 .class 文件、JAR 包、网络、动态生成(如 Proxy 类)、数据库等。
    2. 将字节流代表的静态存储结构转化为方法区的运行时数据结构:JVM 解析字节码,并在方法区(Method Area)或元空间(Metaspace,Java 8+)中创建该类的数据结构。
    3. 在内存中创建一个 java.lang.Class 对象:这个对象是 java.lang.Class 的实例,它作为程序访问该类各种数据的入口。这个对象通常存储在堆(Heap)中。
  • 关键点
    • 这个阶段主要由类加载器(ClassLoader) 完成。
    • 类加载器遵循双亲委派模型(Parent Delegation Model),即先委托父类加载器尝试加载,只有当父类加载器无法完成时,子加载器才会尝试自己加载。

2. 链接 (Linking)

链接阶段确保加载的类是正确且符合 JVM 规范的,并为其分配内存。它分为三个子阶段:

(1) 验证 (Verification)

  • 目的:确保 .class 文件的字节流包含的信息符合当前 JVM 的要求,不会危害 JVM 的安全。
  • 主要检查
    • 文件格式验证:检查字节流是否符合 .class 文件格式规范(如魔数 0xCAFEBABE、版本号等)。
    • 元数据验证:检查类的元数据信息是否有矛盾(如是否继承了 final 类、是否实现了不存在的接口等)。
    • 字节码验证:这是最复杂和关键的一步。通过数据流和控制流分析,确定字节码指令不会做出危害 JVM 安全的操作(如类型转换错误、非法跳转、访问不存在的字段等)。
    • 符号引用验证:确保解析动作能正常执行(如检查符号引用中描述的类、字段、方法是否存在)。
  • 重要性:这是 JVM 防止恶意代码攻击的重要屏障。虽然验证很耗时,但可以保证运行时的安全性。

(2) 准备 (Preparation)

  • 目的:为类的静态变量static fields)分配内存,并设置这些变量的初始值
  • 关键点
    • 分配内存:在方法区(或元空间)为 static 变量分配内存。
    • 设置初始值:这里的“初始值”通常是零值(zero value),而不是你在代码中赋的值。
      • int 类型的 static 变量初始值为 0
      • boolean 类型的 static 变量初始值为 false
      • 引用类型(Object)的 static 变量初始值为 null
    • final static 常量:如果 static 变量同时被 final 修饰,并且是基本类型或 String 字面量,那么它的值(编译期常量)会在这个阶段直接赋值,而不是零值。例如:public static final int MAX = 100; 的值 100 会在准备阶段就设置好。

(3) 解析 (Resolution)

  • 目的:将常量池内的符号引用(Symbolic References)替换为直接引用(Direct References)。
  • 概念解释
    • 符号引用:以一组符号来描述所引用的目标。它可以是任何形式的字面量,只要能无歧义地定位到目标即可。例如,常量池中用 类名.方法名.描述符 来表示一个方法。
    • 直接引用:可以直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。直接引用是与内存布局相关的。
  • 解析的内容
    • 类或接口解析:将符号引用解析为具体的类或接口的 Class 对象。
    • 字段解析:将符号引用解析为字段在类中的内存偏移量。
    • 方法解析:将符号引用解析为方法在方法表中的索引或直接指针。
    • 接口方法解析:类似方法解析。
  • 时机:解析动作不一定在链接阶段一次性完成,它可能在初始化之后才进行(称为“延迟解析”或“惰性解析”)。只有当真正需要使用某个符号引用时,才会触发解析。

3. 初始化 (Initialization)

  • 目的:执行类的初始化代码,为类的静态变量赋予程序中指定的值,并执行 static 代码块。
  • 主要工作
    • 执行 <clinit>() 方法。<clinit> 是由编译器自动收集类中所有 static 变量的赋值语句和 static 代码块中的语句合并产生的类构造器方法。
    • 按照代码中出现的顺序执行这些初始化语句。
  • 关键点
    • 这是类加载过程的最后一步
    • <clinit>() 方法是线程安全的:JVM 会保证一个类的 <clinit>() 方法在多线程环境下只被执行一次。其他线程会阻塞,直到第一个线程完成初始化。
    • 触发时机:这是主动使用一个类的时刻。以下操作会触发类的初始化:
      1. 创建类的实例(new 关键字)。
      2. 访问类的静态变量(public static 除外,final static 编译期常量也不会触发)。
      3. 调用类的静态方法。
      4. 使用反射(Class.forName())。
      5. 初始化一个类的子类(会先触发父类的初始化)。
      6. 虚拟机启动时,包含 main() 方法的主类。
      7. MethodHandle 和 VarHandle 的某些操作。
    • 被动引用不会触发:访问 final static 编译期常量、通过子类引用父类的 static 变量(只会触发父类初始化,不会触发子类)、数组定义(new MyClass[10] 不会触发 MyClass 的初始化)等属于被动引用,不会触发初始化。

4. 使用 (Using)

  • 目的:类初始化完成后,就可以被程序正常使用了。
  • 工作:程序通过 new 创建对象、调用静态方法、访问实例方法等。

5. 卸载 (Unloading) 

  • 目的:当类不再被任何地方引用,满足垃圾回收条件时,JVM 可以卸载该类,回收其占用的内存(主要是方法区/元空间和 Class 对象本身)。
  • 条件:非常严格。需要该类的 ClassLoader 被回收、该类的所有实例都已被回收、该类的 Class 对象没有被任何地方引用。

总结流程图

加载 (Loading)↓
链接 (Linking)├── 验证 (Verification)├── 准备 (Preparation)  <-- static 变量赋零值 (或 final static 常量值)└── 解析 (Resolution)   <-- 符号引用 -> 直接引用↓
初始化 (Initialization)    <-- 执行 <clinit>(), static 变量赋程序值, 执行 static 块↓
使用 (Using)↓
卸载 (Unloading) (可选)

4. 你知道JVM的类加载器有哪些?双亲委派机制是什么?

一、JVM 的类加载器 (Class Loaders)

JVM 在启动时会创建一系列的类加载器,它们形成了一个层次结构。主要的类加载器有三种(从顶层到底层):

1. 启动类加载器 (Bootstrap ClassLoader)

  • 角色:最顶层的类加载器,是 JVM 自身的一部分,通常由 C/C++ 实现。
  • 负责加载
    • JAVA_HOME/lib 目录下的核心类库(如 rt.jartools.jarresources.jar 等)。
    • 或者被 -Xbootclasspath 参数指定的路径中的类库。
  • 特点
    • 用 C/C++ 编写,不是 Java 类,因此在 Java 代码中无法直接引用它(getClassLoader() 返回 null)。
    • 负责加载最基础、最核心的 Java 类(如 java.lang.*java.util.*java.io.* 等)。

2. 扩展类加载器 (Extension ClassLoader)

  • 角色Bootstrap ClassLoader 的子加载器,由 Java 实现。
  • 负责加载
    • JAVA_HOME/lib/ext 目录下的类库。
    • 或者被 java.ext.dirs 系统变量所指定的路径中的所有类库。
  • 特点
    • 允许开发者将具有通用功能的 JAR 包放在这个目录下,自动被加载,无需在 -classpath 中指定。
    • 在 Java 9 的模块化系统(JPMS)之后,其重要性有所下降。

3. 应用程序类加载器 (Application ClassLoader) / 系统类加载器 (System ClassLoader)

  • 角色Extension ClassLoader 的子加载器,也是 Java 实现。
  • 负责加载
    • 用户类路径(ClassPath)上所指定的类库。
    • 即我们通常通过 -classpath 或 -cp 参数指定的 .jar 文件或 .class 文件目录。
  • 特点
    • 这是默认的类加载器,我们编写的 Java 类和第三方依赖库(如 Maven/Gradle 依赖)通常由它加载。
    • 可以通过 ClassLoader.getSystemClassLoader() 获取它的实例。

(可选) 自定义类加载器 (Custom ClassLoader)

  • 角色:开发者可以继承 java.lang.ClassLoader 类来创建自己的类加载器。
  • 目的
    • 从非标准来源加载类(如网络、数据库、加密的 JAR 包)。
    • 实现类的隔离(如 Tomcat 的 Web 应用隔离、OSGi 模块化)。
    • 实现热部署(Hot Swap)。
  • 常用场景:Web 服务器(Tomcat, Jetty)、应用服务器(WebLogic, WebSphere)、插件化框架、热更新系统。

二、双亲委派机制 (Parent Delegation Model)

双亲委派机制是 JVM 类加载器加载类时遵循的一种工作模式。它的核心思想是:当一个类加载器收到类加载请求时,它不会自己先去加载,而是把这个请求委派给它的父类加载器去完成,每一层的类加载器都是如此,因此所有的加载请求最终都会传送到顶层的启动类加载器。只有当父类加载器无法完成这个加载请求(即在它的搜索路径下找不到所需的类)时,子加载器才会尝试自己去加载。

工作流程

  1. 发起请求:假设应用程序类加载器(AppClassLoader)收到一个加载 java.lang.String 的请求。
  2. 向上委派:AppClassLoader 不会直接加载,而是将请求委派给它的父加载器——扩展类加载器(ExtClassLoader)。
  3. 继续委派:ExtClassLoader 收到请求后,也不会直接加载,而是继续委派给它的父加载器——启动类加载器(Bootstrap ClassLoader)。
  4. 顶层尝试加载:Bootstrap ClassLoader 尝试在 rt.jar 等核心库中查找 java.lang.String,找到了,于是加载成功,返回 Class 对象。
  5. 逐层返回:加载结果从 Bootstrap ClassLoader 逐层返回给 ExtClassLoader,再返回给 AppClassLoader,最终返回给发起请求的代码。

如果父加载器找不到呢?

  • 假设请求加载一个用户自定义的类 com.example.MyClass
  • 请求最终传到 Bootstrap ClassLoader,它在核心库中找不到。
  • Bootstrap ClassLoader 返回失败。
  • 请求返回到 ExtClassLoader,它在 lib/ext 目录下也找不到。
  • 请求返回到 AppClassLoader,它在 ClassPath 下找到了 com.example.MyClass.class,于是由它自己加载。

为什么需要双亲委派机制?

  1. 避免类的重复加载

    • 保证一个类在 JVM 中只有一个唯一的 Class 对象。
    • 例如,无论哪个类加载器发起加载 java.lang.Object 的请求,最终都会由 Bootstrap ClassLoader 加载,确保所有地方使用的都是同一个 Object 类。
  2. 保证核心类库的安全性

    • 这是最关键的一点。它防止了恶意代码通过自定义类加载器来替换核心 Java 类。
    • 例如,你不能自己写一个 java.lang.String 类放在 ClassPath 下,期望它被加载。因为当请求到达时,Bootstrap ClassLoader 会先加载它自己的、可信的 String 类,你的恶意类永远没有机会被加载。
    • 这确保了 Java 核心 API 的稳定性和安全性

如何打破双亲委派?

虽然双亲委派是默认和推荐的模式,但在某些特殊场景下需要打破它:

  1. 基础类型回调用户代码

    • 典型例子JNDI (Java Naming and Directory Interface)。
    • JNDI 的核心类由 Bootstrap ClassLoader 加载,但它需要回调由应用程序提供的服务实现(SPI - Service Provider Interface)。
    • Bootstrap ClassLoader 无法加载应用类路径下的类。
    • 解决方案:通过线程上下文类加载器Thread.currentThread().getContextClassLoader())。这个加载器通常被设置为 AppClassLoader。JNDI 核心代码可以通过它来加载用户实现的 SPI 类,从而“逆向”委托。
  2. 实现热部署/模块化

    • 典型例子:Tomcat, OSGi。
    • 需要隔离不同 Web 应用或模块的类,避免相互影响和核心库冲突。
    • 解决方案:自定义类加载器,并重写 loadClass() 方法,改变委派逻辑。例如,Tomcat 的 Web 应用类加载器会优先尝试自己加载 Web 应用的类(/WEB-INF/classes/WEB-INF/lib),只有当自己找不到时,才委派给父加载器(打破了“先委派”的原则)。这实现了应用间的类隔离。

总结

  • 类加载器:Bootstrap -> Extension -> Application -> Custom,形成层次结构。
  • 双亲委派:加载请求优先向上委派给父加载器,父加载器无法完成时,子加载器才尝试自己加载。
  • 优点:保证类的唯一性、核心类库安全。
  • 打破场景:SPI(如 JNDI)、热部署/模块化(如 Tomcat, OSGi),通常通过线程上下文类加载器或重写 loadClass() 实现。
http://www.dtcms.com/a/355400.html

相关文章:

  • curl、python-requests、postman和jmeter的对应关系
  • DJI无人机云哨DroneID技术解析:天空中的数字身份证
  • 2025年KBS SCI1区TOP,矩阵差分进化算法+移动网络视觉覆盖无人机轨迹优化,深度解析+性能实测
  • Maven核心用法
  • ubuntu挂载外接硬盘
  • IDEA 中创建 Springboot 项目没有 Java8 选项的解决办法
  • 介绍智慧城管十大核心功能之一:风险预警系统
  • 关于npm安装electron和better-sqlite3失败问题
  • Copilot、Cursor、Trae、ChatGPT 的“四件套”场景选择表
  • 第六届智能计算与人机交互国际研讨会(ICHCI 2025)
  • MySQL 与 ClickHouse 深度对比:架构、性能与场景选择指南
  • 【数字IC后端】引导时钟树CTS的生成方向之anchor driver
  • 详细介绍Linux 内存管理struct page数据结构中的_count和_mapcount有什么区别?
  • Pyomo、PuLP 和 OR-Tools 解决约束优化问题效率对比
  • C# SIMD编程实践:工业数据处理性能优化案例
  • 基于SpringBoot的校园资料分享系统【2026最新】
  • 数据结构-哈夫曼树和B树
  • 安宝特方案丨安宝特工业AR全链路解决方案
  • Centos 8 磁盘扩展xfs文件系统 (LVM)
  • 利用 Java 爬虫获取 AQI 详情数据(代码示例)实战指南
  • 如何使用Windows自带的PnPUtil命令来禁用/停用和启用硬件设备
  • VPC私有域名解析DNS
  • 使用 Action 自动部署 VuePress 到 GitHub Pages
  • GRE隧道IPv6过渡技术
  • 数制与编码
  • 并发编程——04 深入理解CASAtomic原子操作类详解
  • Qt 中日志级别
  • JS中的String总结
  • Linux 环境源码安装 Docker
  • 影石insta360 DevOps工程师一面记录