深入浅出Java类加载机制(双亲委派模型)与自定义类加载器实践


前言

在Java世界中,类加载机制是其运行时系统的核心组成部分之一。它负责将编译后的.class文件转换为JVM可识别和执行的类或接口,并确保类型安全与隔离性。了解类加载机制有助于我们更好地优化性能、设计插件化系统以及解决运行时类找不到等异常问题。


一、类加载器概述

1、定义与分类

类加载器(ClassLoader)的概念:类加载器(ClassLoader)在Java运行时环境中是一个非常重要的组件,它负责查找和装载类型(类或接口)的二进制数据到Java虚拟机(JVM)中。具体来说,类加载器根据给定的全限定名(即包含包名的类名)将.class文件(或其他格式的类定义资源)转换成JVM能够识别的数据结构——Class对象,然后由JVM执行这些类的代码。

JVM预置的系统类加载器及其层次结构:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader)、用户自定义类加载器

2、类加载过程

(1)加载阶段:
类加载器根据全限定名(包括包名和类名)查找对应的.class文件。
将找到的字节码数据流读入到内存,并创建一个代表该类的Class对象。

(2)验证阶段:
检查载入的字节码是否符合Java虚拟机规范,确保不会对JVM的安全性和稳定性造成威胁。
验证内容包括类文件格式、元数据、字节码验证、符号引用验证等。

(3)准备阶段:
为类变量(静态变量)分配内存空间,并初始化为默认值(0或null)。
此阶段不执行任何实际的Java代码。

(4)解析阶段:
将常量池中的符号引用替换为直接引用(如方法区中的指针),这一步主要处理类或接口、字段、方法的访问信息。

什么是符号引用?
Java代码在编译期间,是不知道最终引用的类型,具体指向内存中哪个位置的,这时候会用一个符号引用,来表示具体引用的目标是"谁"。Java虚拟机规范中明确定义了符号引用的形式,符合这个规范的前提下,符号引用可以是任意值,只要能通过这个值能定位到目标。

什么是直接引用?
直接引用就是可以直接或间接指向目标内存位置的指针或句柄。

(5)初始化阶段:
这是类加载的最后一个步骤,初始化的过程,就是执行类构造器 ()方法的过程。
当初始化完成之后,类中static修饰的变量会赋予程序员实际定义的“值”,同时类中如果存在static代码块,也会执行这个静态代码块里面的代码。

(6)加载过程总结:
当一个符合Java虚拟机规范的类文件(以字节流形式存在)经过完整的加载流程,即加载、验证、准备、解析和初始化阶段后,其包含的Class字节流信息将在方法区中被转换并持久化为特定格式的数据结构。这个数据结构详细记录了该类的所有静态和非静态结构信息,包括但不限于字段、方法和常量池等元数据。

与此同时,在Java堆内存中会生成一个代表该类的实例对象,即java.lang.Class类型的一个对象。此对象作为运行时每个已加载类的唯一标识与入口点,不仅封装了方法区内存储的全部类结构信息,还提供了访问这些信息以及执行类加载器相关功能的方法接口。通过操作这个Class对象,开发者能够在程序运行期间动态地获取并操控类的各种属性及行为,实现诸如反射等高级特性。

3、更通俗的语言来解释一下Java中的类加载器

想象一下你是一个图书馆的管理员,你的工作是找到书并把它放到书架上供读者阅读。在Java世界里,类加载器就像是这个图书馆管理员,但它管理的是编译好的Java类文件(也就是.class文件)而不是书籍。

当你运行一个Java程序时,它是由许多不同的类组成,这些类需要被加载到JVM(Java虚拟机)中才能真正执行。类加载器就是负责这项工作的角色。

类加载过程就像这样:

找书(加载类):当程序需要使用某个类时,类加载器会去特定的地方(比如磁盘上的目录、jar包或者网络地址等)查找对应的.class文件。

检查和整理书籍(验证与准备):一旦找到了类文件,类加载器就会确保这本书(类)是按照Java语言规范编写且安全的,并为类中的静态变量分配内存空间。

建立索引(解析):将类内部对其他类的引用转换成可以直接使用的“指向”,好比把书中提到的其他章节链接到实际的位置。

打开书籍并标记已读(初始化):如果类还没有被初始化过,类加载器还会触发类的初始化过程,即执行类中的静态初始化块和初始化静态变量。

二、双亲委派模型

在了解了类加载过程后,我们需要学习实现类加载机制时遵循的一种核心设计原则,那就是双亲委派模型

双亲委派模型是Java虚拟机(JVM)中类加载器的一种工作模式,它确保了类加载过程的有序性和安全性。在Java应用程序运行时,类加载器负责查找和加载类文件到JVM中,以便程序能够使用这些类。

结合上面的图书馆里例子,我们可以这样理解

假设我们的图书馆有多个管理员(类加载器),他们之间有层级关系。当一个底层管理员接到找书的任务时,它不会自己直接去找,而是先交给它的上级管理员。只有当上级管理员表示这本书不在自己的管理范围内时,底层管理员才会亲自去查找。

在Java中,类加载器也是采用类似的方式。当一个类加载器接收到加载类的请求时,它首先委托给父加载器加载,只有当父加载器无法完成任务时,子加载器才尝试自己加载。这样做保证了所有程序都以统一的视角看到相同的类库版本,避免了类的重复加载以及核心Java API的篡改。

按照双亲委派模型的工作机制:

1、当一个类加载器收到加载某个类型(类或接口)的请求时,并不会立即自己去执行加载操作,而是首先将该请求委派给它的父类加载器进行处理。

2、若父类加载器不是null(即存在),那么这个请求会沿着类加载器的层级向上委托,直到到达最顶层的启动类加载器(Bootstrap ClassLoader)。启动类加载器负责加载核心Java库中的类,如java.lang.*等。

3、如果父类加载器无法找到指定的类,也就是说,在其自身的命名空间内找不到对应的类定义,那么才会由原来的请求者——即子类加载器来尝试加载这个类。

4、这种层级式的委托加载机制,保证了类加载的基本规则:同一个类在JVM中只会被加载一次,并且保证了系统核心类库不会被用户自定义版本所替代,增强了系统的安全性和稳定性。

通过这样的设计,如果用户编写了一个与Java核心API类同名的类,由于类加载器的层次结构和双亲委派原则,JVM总会优先使用最高级别的类加载器加载标准库中的类,从而避免了恶意替换和破坏系统类的行为发生。

三、自定义类加载器

默认情况下,Java提供了多个内置的类加载器,如启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader),它们遵循双亲委派模型确保了类加载的有序性和安全性。

然而,在某些特定场景下,例如模块化系统、热部署、加密资源加载或隔离不同版本的库等,我们需要更灵活的类加载策略,这就引入了自定义类加载器的概念。自定义类加载器允许开发者根据自身应用需求定制类加载过程,通过继承标准的ClassLoader类或URLClassLoader类,并重写其中的关键方法来实现特定的类加载逻辑。

以下是一个基于java.lang.ClassLoader简单实现自定义类加载器的例子:

import java.io.InputStream;
import java.net.URL;

public class CustomClassLoader extends ClassLoader {
    
    private String customPath; // 自定义类路径

    public CustomClassLoader(String customPath, ClassLoader parent) {
        super(parent); // 继承父加载器
        this.customPath = customPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
        String path = className.replace('.', '/') + ".class";
        URL resource = getClass().getResource(customPath + '/' + path);
        try (InputStream is = resource.openStream()) {
            if (is != null) {
                // 读取类字节码数据并返回
                byte[] data = new byte[is.available()];
                is.read(data);
                return data;
            }
        } catch (Exception e) {
            throw new RuntimeException("Failed to load class: " + className, e);
        }
        return null;
    }

    // 示例用法:
    public static void main(String[] args) throws Exception {
        CustomClassLoader myClassLoader = new CustomClassLoader("/path/to/custom/classes", Thread.currentThread().getContextClassLoader());
        Class<?> loadedClass = myClassLoader.loadClass("com.example.MyCustomClass");
        Object instance = loadedClass.newInstance();
        // ...
    }
}

在这个例子中,我们创建了一个名为CustomClassLoader的自定义类加载器,它从指定的自定义路径加载类。findClass方法被重写以处理类查找逻辑,当需要加载一个类时,它首先尝试从自定义路径获取对应的类文件,然后将其转换为字节码数组,并调用defineClass方法将其转化为Class对象。


总结

小弟学习jvm也是皮毛,希望各位看完能有收获!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值