JVM类加载机制

loadClass的类加载过程:

加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载
在这里插入图片描述
其中验证,准备,解析统称为链接
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。

加载过程需要完成一下三件事:

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

验证:

这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。

文件格式的验证:

第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
元数据验证:
第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要

字节码验证:

第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型校验完毕以后,这阶段就要对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。

符号引用验证:

最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。

准备

准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域,在JDK 7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆中,这时候“类变量在方法区”就完全是一种对逻辑概念的表述了

解析

解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。

初始化

类的初始化阶段是类加载过程的最后一个步骤,之前几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控制。直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。
进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。

双亲委派机制

加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。

上面的类加载过程主要是通过类加载器来实现的,Java里有如下几种类加载器
● 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等
● 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
● 应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
● 自定义加载器:负责加载用户自定义路径下的类包

自定义类加载器示例:

自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法,所以我们自定义类加载器主要是重写findClass方法。

 public class MyClassLoaderTest {
		static class MyClassLoader extends ClassLoader {
		private String classPath;
    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    private byte[] loadByte(String name) throws Exception {
        name = name.replaceAll("\\.", "/");
        FileInputStream fis = new FileInputStream(classPath + "/" + name
                + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadByte(name);
            //defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }

}

public static void main(String args[]) throws Exception {
    //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
    MyClassLoader classLoader = new MyClassLoader("D:/test");
    //D盘创建 test/com/tuling/jvm 几级目录,将User类的复制类User1.class丢入该目录
    Class clazz = classLoader.loadClass("com.tuling.jvm.User1");
    Object obj = clazz.newInstance();
    Method method = clazz.getDeclaredMethod("sout", null);
    method.invoke(obj, null);
    System.out.println(clazz.getClassLoader().getClass().getName());
}
}

运行结果:


com.tuling.jvm.MyClassLoaderTest$MyClassLoader
为什么要设计双亲委派机制?

● 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
● 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

### JVM 类加载机制工作原理 JVM类加载机制是一种动态加载的方式,通过类加载器(Class Loader)将 `.class` 文件加载到内存中,并对其进行验证、准备、解析和初始化。这种机制使得 Java 应用能够在运行时按需加载所需的类。 #### 1. **类加载器分类** JVM 默认提供了三种主要的类加载器[^2]: - **启动类加载器(Bootstrap ClassLoader)** 负责加载 JRE/lib 目录下的核心类库,例如 `rt.jar` 和 `charsets.jar` 等。它是用原生 C++ 实现的,无法在 Java 层面直接获取其引用[^4]。 - **扩展类加载器(Extension ClassLoader)** 负责加载 `<JAVA_HOME>/lib/ext` 或由系统变量 `-Djava.ext.dirs` 指定路径中的类库。它继承自 `java.lang.ClassLoader` 并依赖于启动类加载器。 - **应用类加载器(Application ClassLoader)** 又被称为系统类加载器,负责加载用户类路径(classpath)上的指定类库。这是开发人员最常接触的一种类加载器。 #### 2. **双亲委派模型** 双亲委派模型是 JVM 类加载的核心机制之一。它的基本思想是:当某个类加载器收到类加载请求时,不会立即尝试自己去加载这个类,而是先把这个请求委托给父类加载器处理;只有当父类加载器无法找到该类时,当前类加载器才会尝试自行加载[^3]。 具体过程如下: - 当一个类加载器接收到类加载请求时,首先会交给其父类加载器进行加载; - 如果父类加载器仍然找不到目标类,则继续向上级传递直到到达顶层的启动类加载器; - 若所有上级都无法加载此目标类,则交回最初发起请求的那个类加载器执行自己的加载逻辑(通常为调用 `findClass()` 方法)[^5]。 这种方式可以确保不同环境下的相同类具有唯一性,从而避免重复定义带来的冲突问题。 #### 3. **类加载的过程** 类加载分为五个阶段[^1]: - **加载(Loading)**: 将字节码读入内存并创建对应的 `java.lang.Class` 对象实例表示此类结构信息。 - **验证(Verification)**: 验证字节码是否符合 JVM 规范要求,防止非法代码被执行。 - **准备(Preparation)**: 分配静态字段所需存储空间并将它们设置初始值(通常是零值或其他默认值),但此时并不执行任何赋初值操作。 - **解析(Resolution)**: 把符号引用转换成为直接引用形式,即将字符串描述的目标转译成本地指针地址或者偏移量等实际数据位置。 - **初始化(Initialization)**: 执行类构造函数 `<clinit>()` 来赋予静态变量正确的原始值以及执行其他必要的初始化动作。 以下是简单的类加载流程图示例: ```plaintext +-------------------+ | Bootstrap CL | + | + v +-------------------+ | Extension CL | + | + v +-------------------+ | Application CL | +-------------------+ ``` #### 4. **如何打破双亲委派模型?** 虽然双亲委派模型能够很好地解决类的唯一性和隔离性问题,但在某些特殊场景下可能需要绕过这一规则。可以通过以下方式实现: - **重写 `loadClass()` 方法**:完全控制整个类加载过程,但这可能会破坏原有设计原则,因此一般不建议这样做。 - **仅重写 `findClass()` 方法**:允许子类加载器优先查找特定资源而不影响全局一致性。 --- ### 示例代码展示 下面是一个简单演示如何自定义类加载器的例子: ```java public class MyClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] b = loadClassData(name); return defineClass(name, b, 0, b.length); // 定义新的类对象 } catch (IOException e) { throw new ClassNotFoundException(name); } } private byte[] loadClassData(String className) throws IOException { String path = "/path/to/classes/" + className.replace('.', '/') + ".class"; try (InputStream is = getClass().getResourceAsStream(path)) { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); int data; while ((data = is.read()) != -1) { buffer.write(data); } return buffer.toByteArray(); } } } ``` 上述代码展示了如何通过覆盖 `findClass()` 方法来自定义类加载行为。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值