JVM类加载机制

一、类加载机制是什么?

Java虚拟机把描述类的数据从Class文件加载到内存 ,并对数据进行校验转换解析初始化 ,最终形成可以被虚拟机直接使用的Java类型,这个一过程被称作虚拟机的类加载机制。

二、类加载的时机

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历七个阶段:

加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)

,其中验证、准备、解析三个部分统称为连接(Linking)。

类加载的时机,通常指的就是类生命周期中**"加载(Loading)阶段"开始的时机。**

加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按

照这种顺序按部就班地开始(不是按部就班地"进行"或按部就班地"完成",这些阶段通常都
是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段。),而解析阶段则不一定。

《Java虚拟机规范》则是严格规定了有且只有六种情况必须立即对类进行"初始化"(而加载、验证、准备自然需要在此之前开始)

  1. 创建对象实例时(new),或访问类的静态成员→ 静态加载

  2. 子类初始化时,先初始化父类(或接口)→ 静态加载

  3. 通过反射→ 动态加载

  4. JVM启动的主类(main方法所在的类)→ 静态加载

  5. 使用MethodHandle(方法句柄,JDK 7引入的java.lang.invoke.MethodHandle)来调用某些特定的静态操作,操作类型分为四种

    • REF_getStatic:获取静态字段(相当于getstatic字节码)
    • REF_putStatic:设置静态字段(相当于putstatic字节码)
    • REF_invokeStatic:调用静态方法(相当于invokestatic字节码)
    • REF_newInvokeSpecial:调用构造器(相当于new + invokespecial字节码)
  6. 一个接口定义了默认方法(用default修饰 JDK8加入),那么当它的某个实现类要初始化时,JVM会先初始化这个接口本身

三、类的加载过程

系统加载 Class 类型的文件主要五步:加载->连接->初始化 。连接过程又可分为三步:验证->准备->解析

加载

在加载阶段,Java虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

虚拟机规范上面这 3 点并不具体,因此是非常灵活的。比如:"通过全类名获取定义此类的二进制字节流" 并没有指明具体从哪里获取(ZIP、JAR、EAR、WAR、网络、动态代理技术运行时动态生成、其他文件生成比如JSP...)怎样获取。

加载这一步主要是通过类加载器 完成的。类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载由 双亲委派模型 决定(不过,我们也能打破双亲委派模型)。

每个Java类都有一个引用指向加载它的ClassLoader。

  • 数组类不是通过ClassLoader创建的,而是 JVM 在需要的时候自动创建的。数组类通过getClassLoader()方法获取ClassLoader的时候和该数组的元素类型的ClassLoader是一致的。
  • 一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass() 方法)。

加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。

连接

可分为三步:验证->准备->解析

验证

验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚

拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

验证阶段这一步在整个类加载过程中耗费的资源还是相对较多的,但很有必要,可以有效防止恶意代码的执行。任何时候,程序安全都是第一位。

不过,验证阶段也不是必须要执行的阶段。如果程序运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。但是需要注意的是 -Xverify:none-noverify 在 JDK 13 中被标记为 deprecated ,在未来版本的 JDK 中可能会被移除。

验证阶段主要由四个检验阶段组成:

  • 文件格式验证(Class 文件格式检查)
  • 元数据验证(字节码语义检查)
  • 字节码验证(程序语义检查)
  • 符号引用验证(类的正确性检查)

文件格式验证这一阶段是基于该类的二进制字节流进行的,主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java 类型信息的要求。除了这一阶段之外,其余三个验证阶段都是基于方法区的存储结构上进行的,不会再直接读取、操作字节流了。

方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域。当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据

符号引用验证 发生在类加载过程中的解析阶段,具体点说是 JVM 将符号引用转化为直接引用的时候(解析阶段会介绍符号引用和直接引用)。

符号引用验证的主要目的是确保解析阶段能正常执行,如果无法通过符号引用验证,JVM 会抛出一个java.lang.IncompatibleClassChangeError的子类异常。

准备

准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初
始值的阶段
,这些内存都将在方法区中分配。

对于该阶段有以下几点需要注意:

  • 这时候进行内存分配的仅包括类变量( 即静态变量,被static关键字修饰的变量**)** ,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
  • 初始值"通常情况"下是数据类型的零值。

基本数据类型的零值:

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。

直接引用与符号引用之间关联

符号引用 (Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何

形式的字面量,只要使用时能无歧义地定位到目标即可。

符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到虚拟机内存当中的内容。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。

直接引用 (Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能

间接定位到目标的句柄。

直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。

举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。

综上,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。

初始化

初始化阶段是执行初始化方法<clint>()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。

说明:<clint>()方法是编译之后自动生成的。

对于<clinit>()方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为<clinit>() 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起多个线程阻塞,并且这种阻塞很难被发现。

必须对类进行初始化的六种条件上文(类的加载时机中)已经提到,这里不在过多赘述。

类卸载

卸载类即该类的 Class 对象被 GC。

卸载类需要满足 3 个要求:

  1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
  2. 该类没有在其他任何地方被引用
  3. 该类的类加载器的实例已被 GC

所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。

只要想通一点就好了,JDK 自带的 BootstrapClassLoader, ExtClassLoader, AppClassLoader 负责加载 JDK 提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。

四、类加载机制经典面试题

序号 面试题 答案
1 什么是 Java 的类加载机制? Java 类加载机制是指 JVM 将 .class 字节码文件加载到内存中,并转化为可执行类的过程。它遵循"懒加载"原则,包括加载(Loading)、链接(Linking,包括验证、准备、解析)和初始化(Initializing)三大阶段。核心目的是确保类的安全性和高效性,同时通过双亲委派模型避免重复加载和核心类篡改。
2 描述类加载的完整过程(三大阶段)。 1. 加载(Loading) :类加载器根据全限定名查找字节码,读取到方法区,生成 Class 对象。 2. 链接(Linking) :包括验证(检查字节码合法性)、准备(静态变量分配内存并设默认值)、解析(符号引用转为直接引用,可懒解析)。 3. 初始化(Initializing):执行 <clinit>() 方法,包括静态变量赋值和静态代码块。整个过程是线程安全的,只执行一次。
3 什么是双亲委派模型?它有什么优势? 双亲委派模型是指类加载器在加载类时,先委托父加载器加载,无法加载时才由自己尝试。层级:Bootstrap(核心类)→ Extension(扩展类)→ Application(应用类)→ 自定义。 优势:1. 安全性(防止用户覆盖核心类);2. 避免重复加载;3. 命名空间隔离(不同加载器加载的同名类视为不同)。
4 类加载的时机有哪些?(主动引用场景) 根据 JVM 规范,主动引用会触发类加载和初始化,包括:1. new 对象;2. 访问/赋值静态字段(非 final 常量);3. 调用静态方法;4. 反射(如 Class.forName());5. 初始化子类时先初始化父类;6. JVM 启动主类;7. MethodHandle 特定类型(如 REF_getStatic);8. 接口有 default 方法时,实现类初始化前先初始化接口。被动引用(如数组定义、子类引用父类静态字段)不会触发。
5 什么是 <clinit>() 方法?它在什么时候执行? <clinit>() 是编译器生成的类初始化方法,包含静态变量赋值和静态代码块。它在初始化阶段执行,且只执行一次(JVM 锁保证)。执行时机:主动引用类时(如 new、访问静态成员)。父类 <clinit>() 先于子类执行。
6 如何自定义类加载器?有什么应用场景? 继承 ClassLoader,重写 findClass() 方法:读取字节码,调用 defineClass() 生成 Class 对象。 示例代码: ```java:disable-run
7 双亲委派模型可以被打破吗?举例说明。 可以,重写 loadClass() 方法即可破坏。 示例:1. Tomcat 的 WebAppClassLoader 先自己加载 web 应用类,再委托父类(隔离应用);2. JDBC 驱动使用线程上下文类加载器(ThreadContextClassLoader)加载 SPI 实现类;3. OSGi 模块化框架的网络状加载;4. Java 9+ 模块系统(Jigsaw)。破坏目的是实现类隔离或动态加载。
8 类加载过程中,静态变量的赋值发生在哪个阶段? 在初始化阶段(<clinit>() 方法中)。准备阶段只设默认值(如 int=0),初始化阶段才执行显式赋值(如 static int a=100)。final static 常量在准备阶段直接赋值(编译期常量优化)。
9 解释 NoClassDefFoundError 和 ClassNotFoundException 的区别。 NoClassDefFoundError:类加载失败(如找不到字节码),运行时异常,通常因 classpath 问题或初始化失败。 ClassNotFoundException:显式加载类失败(如 Class.forName()),检查异常,通常因反射或动态加载类不存在。简单说,前者是 JVM 自动加载失败,后者是代码手动加载失败。
10 类可以被卸载吗?什么条件下? 可以,但很少见。条件:1. 类所有实例被 GC;2. 类的 ClassLoader 被 GC;3. 类无其他引用(如反射)。常用于热部署场景(如 OSGi),但 HotSpot JVM 不常用 Full GC 卸载类。
相关推荐
alonewolf_9910 小时前
JVM核心技术深度解析:从类加载到GC调优的全栈指南
jvm
peixiuhui1 天前
Iotgateway技术手册-2. 技术栈
jvm
a努力。1 天前
虾皮Java面试被问:JVM Native Memory Tracking追踪堆外内存泄漏
java·开发语言·jvm·后端·python·面试
这周也會开心1 天前
JVM-垃圾回收器
jvm·算法
找不到、了1 天前
JVM 跨代引用与 Card Table 机制
java·jvm
sunywz1 天前
【JVM】(2)java类加载机制
java·jvm·python
alonewolf_991 天前
深入浅出JVM:从Class文件到GC调优的全方位解析
jvm
学编程的小鬼1 天前
JVM 常见的问题
开发语言·jvm
进阶小白猿1 天前
Java技术八股学习Day13
java·jvm·学习