类加载器与类的加载过程,以及双亲委派机制和打破双亲委派机制

开局先扔一张图

这是JVM的一个整体结构,先有个印象,而第一部分就是类加载的一个过程

类的加载过程

类的加载过程由以下几个部分组成:

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

验证:

  • 目的在于确保Class文件的字节流中包含的信息符合当前虚拟机要求,保证被加载类的正确性
  • 主要是: 文件格式验证, 元数据验证, 字节码验证, 符号引用验证

准备:

  • 为类变量分配内存空间并且设置该类变量的默认初始值, 比如0,null
  • 这里不包括final修饰的static, 因为final在编译的时候已经进行了分配, 准备阶段会显示初始化
  • 这里不会为实例变量分配初始化, 类变量会分配在方法区中, 而实例变量是会随着对象一起分配到Java堆中

解析:

  • 将常量池内的符号引用转化为直接引用的过程

  • 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行*(如果A调用的B是一个具体的实现类那么就称为静态解析,因为解析的目标类型很明确,那么可以符号引用转化为直接引用,而假如上层Java代码使用了多态,这里B是一个抽象类或者是接口,那么B可能有两个具体的实现类C和D,此时B的具体实现并不明确,当然也就不知道使用那个具体类的直接引用来进行替换,既然不知道那就等一等吧,直到在运行过程中发生了调用,此时虚拟机调用栈中将会得到具体的类信息,这时候再进行解析,就能用明确的直接引用,来替换符号引用,这也就解释了为什么解析阶段有时候会发生在初始化阶段之后,这就是动态解析,用它来实现后期绑定)

  • 符号引用就是一组符号来描述所引用的目标,符号引用的字面量心事定义再《JAVA 虚拟机规范》的Class文件格式中,直接引用就是直接指向目标的指针,相对偏移量或一个间接定位到目标句柄

  • 解析动作主要针对类或接口,字段,类方法,接口方法,方法类型,对应常量池中的CONSTRANT_Class_info,CONSTANT_Fieldref_info, CONSTANT_Methodref_info

初始化:

  • 初始化阶段就是执行类的构造方法() 的过程
  • 此方法不需要定义,是javac编译器自动手机类中的所有类变量的复制动作和静态代码块中的语句合并而来
  • 构造方法中指令按语句在源文件中出现的顺序执行
  • ()不同于类的构造器(虚拟机视角下的())
  • 若该类具有父类,JVM会保证子类的执行之前,父类的已经执行完毕
  • 虚拟机必须保证一个类的方法在多线程下被同步加锁

类加载器

类加载器主要分为3中:

  • Bootstrap Class Loader 启动类加载器, 主要是用来加载java核心库,没有继承ClassLoader
  • Extension Class Loader 扩展类加载器,主要是从扩展目录中加载类库,派生于ClassLoader, 父类为启动类加载器
  • System Class Loader 系统类加载器,主要是加载环境变量下的类库,为系统默认加载器, 弗雷为扩展类加载器,派生于classLoader

当然也可以通过继承ClassLoader的方式去实现自定义类加载器,这个是根据业务需求决定的。比如说:

1:需要将自己的代码进行加密以防止反编译, 可以先将编译后的代码用某种算法加密,加密后就不再用默认ClassLoader去加载类了, 这时候需要自定义ClassLoader在加载类的时候先解密,然后加载

2: 如果字节码是放在数据库等非标准的来源的,需要自定义ClassLoader,从指定来源加载类

自定义类加载器,那么就涉及到三个重要函数:loadClassfindClassdefineClass

loadClass:调用父类加载器的loadClass,加载失败则调用自己的findClass方法

findClass:根据名称读取文件存入字节数组

defineClass:把一个字节数组转为Class对象

双亲委派机制

既然说到类类加载器,那么就不能说到类加载的一个重要的机制双亲委派机制,他的工作原理如下

  1. 当前类加载器从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。
  2. 如果没有找到,就去委托父类加载器去加载。父类加载器也会采用同样的策略,依次递归,一直到启动类加载器。因为如果父加载器为空了,就代表使用启动类加载器作为父加载器去加载。
  3. 如果启动类加载器加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用拓展类加载器来尝试加载,继续失败则会使用AppClassLoader来加载,继续失败则会抛出一个异常ClassNotFoundException,然后再调用当前加载器的findClass()方法进行加载。

这样做有什么优点呢?

  1. 主要是为了安全, 避免自己编写的类动态提花Java的核心类
  2. 避免了类的重复加载,因为JVM除了根据类的全限定类名作为类的区分, 还根据类加载器区分, 同一个类如果使用了不同的类加载器,那么在虚拟机中就属于不同的类

既然说到了双亲委派机制,那么就不得不说以下面试的热点问题“如何打破双亲委派机制

loadClass
public class MyLoad extends ClassLoader{
        private String path;

    public MyLoad(String path) {
        this.path = path;
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        String path = this.path + name.replace(".", File.separator).concat(".class");
        File file = new File(path);
        if (!file.exists()) {
            return super.loadClass(name);
        }
        try {
            InputStream is = new FileInputStream(file);
            byte[] b = new byte[is.available()];
            is.read(b);
            return defineClass(name, b, 0, b.length);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return super.loadClass(name);
    }

    public static void main(String[] args) {
        try{
            String path = "C:\\Users\\jianc\\Documents\\git\\JavaStudy\\jvm\\JvmStudy\\out\\production\\JvmStudy\\";
            MyLoad myLoad = new MyLoad(path);
            Class<?> clazz= myLoad.loadClass("com.dsh.jvm.classloader.MyLoadTest");
            myLoad.clearAssertionStatus();
            Field[] files=clazz.getDeclaredFields();
            System.out.println(files[0].getName());
            System.out.println(clazz);

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

执行结果
name
class com.dsh.jvm.classloader.MyLoadTest

通过重写loadClass的方式打破双亲委派机制

SPI(Service Provider Interface)

Java 在核心类库中定义了许多接口,并且还给出了针对这些接口的调用逻辑,然而并未给出实现。开发者要做的就是定制一个实现类,在 META-INF/services 中注册实现类信息,以供核心类库使用。
SPI的方式有很多,比如很典型的Dubbo,java.sql.Driver 下面以java.sql.Driver 为例子,java.sql.DriverManager 通过扫包的方式拿到指定的实现类,完成 DriverManager的初始化。

java.sql.DriverManager#loadInitialDrivers

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

Iterator<Driver> driversIterator = loadedDrivers.iterator();

 

/* 加载这些驱动程序,以便它们可以被实例化。有可能是驱动类可能不存在 即可能有一个带有服务类的打包驱动程序作为 java.sql.Driver 的实现,但实际的类
 * 可能会丢失。在这种情况下,java.util.ServiceConfigurationError将在运行时被试图定位的 VM 抛出并加载服务。添加一个 try catch 块来捕获那
 * 些运行时错误如果驱动程序在类路径中不可用,但它是打包为服务并且该服务在类路径中。
 */

try{

    while(driversIterator.hasNext()) {

        driversIterator.next();

    }

} catch(Throwable t) {

// Do nothing

}

return null;

java.util.ServiceLoader#load(java.lang.Class)

public static  ServiceLoader load(Class service) {

    ClassLoader cl = Thread.currentThread().getContextClassLoader();

    return ServiceLoader.load(service, cl);

}

如果创建线程时还未设置上下文类加载器,那么当前线程将会从父线程中继承一个上下文类加载器,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器

通过从线程上下文(ThreadContext)获取 classloader ,借助这个classloader 可以拿到实现类的 Class。源码上讲,这里是通过 Class.forName 配合 classloader拿到的)

线程上下文 classloader并非具体的某个loader,一般情况下是 application classloader, 但也可以通过 java.lang.Thread#setContextClassLoader这个方法指定 classloader。

下面我们来手动实现一下:

先生成一个接口和一个实现类

public interface UserService {

    void test();
}
public class UserServiceImpl implements UserService{
    @Override
    public void test() {
        System.out.println("UserServiceImpl");
    }
}

然后再在项目resources目录下新建一个META-INF/services文件夹,然后再新建一个以UserService接口的全限定名命名的文件,文件内容为:

# 文件名称 com.zhexinit.designpattern.spi.UserService

com.zhexinit.designpattern.spi.UserServiceImpl

编写测试文件

public class UserServiceTest {
    public static void main(String[] args) {
        ServiceLoader<UserService> serviceLoader = ServiceLoader.load(UserService.class);
        serviceLoader.forEach(UserService::test);
    }
}

执行结果

UserServiceImpl
OSGI

这一块暂时还没研究,先留个坑

posted @   苜蓿椒盐  阅读(185)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示