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

JVM-类加载详情

一、为什么需要“类加载器”

        在 Java 语言中,类加载并非在编译期完成,而是延迟到程序运行时进行。这种 “运行时加载” 策略虽会带来轻微的性能开销,但为 Java 应用提供了极高的灵活性,主要体现在:

  • 支持面向接口编程的动态实现,可以在运行时才决定具体实现类;
  • 扩展类的加载来源:通过自定义类加载器,程序能从网络、本地文件或其他动态来源加载二进制字节流作为代码(这是 Android 插件化、动态更新 APK 的核心基础)。

二、类加载的过程

        Java 编译器(或其他语言编译器)会将源代码编译为存储字节码的.class文件。JVM 并不依赖源码语言,只需.class文件符合规范即可。类加载机制指的是:JVM 将.class文件中的字节流加载到内存,经过校验、转换解析、初始化后,最终形成可直接使用的 Java 类型。

        类加载的完整生命周期包括:加载(Loading)→ 链接 → 初始化(Initialization)→ 使用(Using)→ 卸载(Unloading)
其中,链接过程又细分为三步:验证(Verification)→ 准备(Preparation)→ 解析(Resolution)

注意:加载、验证、准备、初始化、卸载这 5 个阶段的启动顺序是固定的;而解析阶段则可能在初始化之后触发(为支持运行时动态绑定,如接口的实现类仅在调用时才确定)。实际执行中,各阶段常交叉进行(一个阶段可能调用或激活另一个阶段)。

1.加载:(重点)

        加载阶段是“类加载机制”中的一个阶段,这个阶段通常也被称作“装载”,主要完成:

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

        相对于类加载过程的其他阶段,加载阶段(准备地说,是加载阶段中获取类的二进制字节流的动作)是开发期可控性最强的阶段,因为加载阶段可以使用系统提供的类加载器(ClassLoader)来完成,也可以由用户自定义的类加载器完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式。

2.验证:(了解)

        验证是链接阶段的第一步,目的是确保.class文件的字节流符合 JVM 规范,避免危害虚拟机安全。验证过程分为 4 个阶段:

  • 文件格式验证:校验.class文件的格式合法性,例如:是否以魔数0xCAFEBABE开头、主 / 次版本号是否在 JVM 支持范围内等。
  • 元数据验证:对字节码描述的类信息进行语义分析,确保符合 Java 语言规范。例如:除java.lang.Object外,所有类必须有父类;子类不能继承被final修饰的类;抽象类的子类需实现所有抽象方法等。
  • 字节码验证:通过数据流和控制流分析,确保类的方法体运行时不会危害 JVM。例如:类型转换必须合法(子类对象可赋值给父类,反之则不行);跳转指令不能超出方法体范围等。
  • 符号引用验证:校验符号引用的有效性,例如:符号引用描述的类、字段、方法是否存在;访问权限(private/protected/public/ 默认)是否允许当前类访问等。

3.准备:(了解)

        准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的知识点,首先是这时候进行内存分配的仅包括类变量(static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量定义为:

        public static int value = 12;

        那么变量value在准备阶段过后的初始值为0而不是12,因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为12的动作将在初始化阶段才会被执行。

        上面所说的“通常情况”下初始值是零值,那相对于一些特殊的情况,如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,假设上面类变量value定义为:

        public static final int value = 123;

        编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value设置为123。

4.解析:(了解)

        解析阶段将常量池中的符号引用替换为直接引用

  • 符号引用:用字符串描述目标(如类名、方法名),与 JVM 内存布局无关,目标不一定已加载。
  • 直接引用:指向目标的指针、偏移量或句柄,与内存布局相关,目标必定已存在。

        JVM 未规定解析的具体时间,仅要求在执行 13 条特定字节码指令(如newgetfieldinvokevirtual等)前完成对应符号引用的解析。解析动作主要针对类 / 接口、字段、类方法、接口方法四类符号引用。

5.初始化:(了解)

        初始化是类加载的最后一步,目的是根据程序逻辑初始化类变量和其他资源,本质是执行类构造器<clinit>()方法的过程。

触发初始化的场景:
  1. 执行new(实例化对象)、getstatic(读取静态字段)、putstatic(修改静态字段)、invokestatic(调用静态方法)字节码指令时(final修饰且已入常量池的静态字段除外)。
  2. 通过反射(java.lang.reflect包)调用类时。
  3. 初始化子类时,若父类未初始化则先初始化父类。
  4. JVM 启动时,初始化包含main()方法的主类。
<clinit>()方法的特性:
  • 由编译器自动收集类中静态变量的赋值动作和静态代码块(static {}),按源码顺序合并生成。
  • 静态代码块只能访问定义在其之前的变量(可赋值给之后的变量,但不能访问)。
  • 与实例构造器<init>()不同,<clinit>()无需显式调用父类构造器,JVM 会保证子类<clinit>()执行前,父类的<clinit>()已完成(因此java.lang.Object<clinit>()是第一个执行的)。
  • 接口的<clinit>()无需先执行父接口的<clinit>(),仅当父接口的变量被使用时才初始化;接口的实现类初始化时,接口的<clinit>()不会执行。
  • JVM 保证<clinit>()在多线程环境中被正确同步:若多个线程同时初始化一个类,仅一个线程执行<clinit>(),其他线程阻塞等待(若<clinit>()耗时过长,可能导致线程阻塞)。
  • 若类中无静态变量赋值和静态代码块,编译器可不生成<clinit>()

三、类加载器

1.类与类加载器

        一个类在 JVM 中的唯一性由加载它的类加载器类本身共同决定。即:两个类若来源于同一.class文件,但由不同类加载器加载,则视为不同的类(equals()isInstance()等方法返回false)。

2.双亲委派模型

        从虚拟机的角度来说,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),该类加载器使用C++语言实现,属于虚拟机自身的一部分。另外一种就是所有其它的类加载器,这些类加载器是由Java语言实现,独立于JVM外部,并且全部继承自抽象类java.lang.ClassLoader。

  1. 启动类加载器(Bootstrap ClassLoader):负责加载JAVA_HOME\lib目录中并且能被虚拟机识别的类库到JVM内存中,如果名称不符合的类库即使放在lib目录中也不会被加载。该类加载器无法被Java程序直接引用。
  2. 扩展类加载器(Extension ClassLoader):该加载器主要是负责加载JAVA_HOME\lib\,该加载器可以被开发者直接使用。
  3. 应用程序类加载器(Application ClassLoader):该类加载器也称为系统类加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

双亲委派模型的工作过程为:

        如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。

3.打破双亲委派模型方法

        尽管双亲委派机制有着诸多好处,但有时我们也需要打破它,比如在某些场景下需要加载自定义的类库,或者需要实现热部署等功能。这时,我们就需要自定义类加载器,并在其中打破双亲委派机制。

        以Java代码为例,我们可以通过继承ClassLoader类并重写loadClass方法来打破双亲委派机制。在loadClass方法中,我们可以根据需要自行加载类文件,并通过defineClass方法将其转换为Class对象。

public class MyClassLoader extends ClassLoader {@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException {// 首先检查是否已经加载该类Class<?> clazz = findLoadedClass(name);if (clazz == null) {try {// 尝试委托给父类加载器加载clazz = getParent().loadClass(name);} catch (ClassNotFoundException e) {// 如果父类加载器无法加载,则自行加载该类byte[] classData = getClassData(name);if (classData == null) {throw new ClassNotFoundException();} else {clazz = defineClass(name, classData, 0, classData.length);}}}return clazz;}private byte[] getClassData(String name) {// 从本地磁盘或网络等获取类文件数据// ...}}

4.自定义类加载器

        自定义类加载器需继承java.lang.ClassLoader推荐重写findClass()方法(而非loadClass()),原因是:JDK 1.2 后双亲委派模型已通过loadClass()实现,重写findClass()可避免破坏委派逻辑。

/**
* 一、ClassLoader加载类的顺序
* 1.调用 findLoadedClass(String) 来检查是否已经加载类。
* 2.在父类加载器上调用 loadClass 方法。如果父类加载器为 null,则使用虚拟机的内置类加载器。
* 3.调用 findClass(String) 方法查找类。
* 二、实现自己的类加载器
* 1.获取类的class文件的字节数组
* 2.将字节数组转换为Class类的实例
* @author lei 2011-9-1
*/
public class ClassLoaderTest {public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {//新建一个类加载器MyClassLoader cl = new MyClassLoader("myClassLoader");//加载类,得到Class对象Class<?> clazz = cl.loadClass("classloader.Animal");//得到类的实例Animal animal=(Animal) clazz.newInstance();animal.say();}
}class Animal{public void say(){System.out.println("hello world!");}
}class MyClassLoader extends ClassLoader {//类加载器的名称private String name;//类存放的路径private String path = "E:\\workspace\\Algorithm\\src";MyClassLoader(String name) {this.name = name;}MyClassLoader(ClassLoader parent, String name) {super(parent);this.name = name;}/*** 重写findClass方法*/@Overridepublic Class<?> findClass(String name) {byte[] data = loadClassData(name);return this.defineClass(name, data, 0, data.length);}public byte[] loadClassData(String name) {try {name = name.replace(".", "//");FileInputStream is = new FileInputStream(new File(path + name + ".class"));ByteArrayOutputStream baos = new ByteArrayOutputStream();int b = 0;while ((b = is.read()) != -1) {baos.write(b);}return baos.toByteArray();} catch (Exception e) {e.printStackTrace();}return null;}
}

最后:一道面试题

能不能自己写个类叫java.lang.System?

答案:通常不可以,但可以采取特殊方式达到这个需求。

解释:类加载的双亲委派机制确保父加载器优先加载。java.lang.System由启动类加载器(Bootstrap ClassLoader)加载,若用户自定义同名类,由于启动类加载器已优先加载系统自带的System,自定义类无法被加载。

特殊方式:
自定义类加载器,并重写loadClass()跳过双亲委派,直接从自定义路径加载java.lang.System(需确保该类不在启动类加载器的搜索路径中)。但不推荐这么做,可能导致 JVM 运行异常(系统类依赖System的特定实现)。

http://www.dtcms.com/a/338168.html

相关文章:

  • Mysql——分库分表后id冲突解决方案(即分布式ID的生成方案)
  • 静态网站与动态网站的区别
  • MySQL分库分表实战指南
  • 电子电气架构 --- 软件开发数字化转型
  • Linux小白加油站,第三周周考
  • 永磁同步电机控制算法--转速环电流环超螺旋滑模控制器STASMC
  • 04 类型别名type + 检测数据类型(typeof+instanceof) + 空安全+剩余和展开(运算符 ...)简单类型和复杂类型 + 模块化
  • Maven依赖管理工具详细介绍
  • PowerShell定时检查日期执行Python脚本
  • 决策树的学习
  • 【EI会议征稿】2025第四届健康大数据与智能医疗国际会议(ICHIH 2025)
  • 基于STM32的电动车智能报警系统设计与实现
  • <数据集>遥感飞机识别数据集<目标检测>
  • rsync scp无法使用,踩坑破解之道!
  • 代理模式深度解析:从静态代理到 Spring AOP 实现
  • WAIC点燃人形机器人热潮,诠视SeerSense® DS80:多感融合的空间感知中枢,重新定义机器三维认知
  • 8月更新!Windows 10 22H2 64位 五合一版【原版+优化版、版本号:19045.6159】
  • 红日靶场01<超水版>
  • IDEA的创建与使用(2017版本)
  • 如何用企业微信AI 破解金融服务难题?
  • [Code Analysis] docs | Web应用前端
  • 深入解析:如何设计灵活且可维护的自定义消息机制
  • Spring AI + MCP Client 配置与使用详解
  • 专业高效的汽车部件FMEA解决方案--全星FMEA软件系统在汽车部件行业的应用优势
  • 百胜软件亮相CCDS2025-中国美妆数智科技峰会,解码美妆品牌数智化转型新路径
  • 【C语言16天强化训练】从基础入门到进阶:Day 2
  • 氯化铈:绿色科技的推动力
  • Tomcat Context的核心机制
  • LLM - windows下的Dify离线部署:从镜像打包到无网环境部署(亲测)
  • 【Goland】:Map