00-JAVA基础-JVM类加载机制及自定义类加载器

JVM 类加载机制

JVM类加载机制是Java运行时环境的核心部分,它负责将类的.class文件加载到JVM中,并将其转换为可以被JVM执行的数据结构。

类加载的整体流程

类加载的整体流程可以分为五个阶段:加载(Loading)、链接(Linking)、初始化(Initialization)、使用和卸载(Unloading)。其中,链接阶段又可以细分为验证(Verification)、准备(Preparation)和解析(Resolution)三个阶段。

加载阶段

  • 系统提供的类加载器或自定义类加载器:首先,通过类加载器(可以是系统提供的,也可以是用户自定义的)根据类的全名(包括包名)找到对应的.class文件。
  • 读取.class文件:类加载器从文件系统或网络等位置读取.class文件的二进制数据。
  • 创建Class对象:将读取到的二进制数据转换为方法区的运行时数据结构,并在堆区创建一个java.lang.Class对象,这个Class对象作为该类在JVM中的元数据表示。

链接阶段

  • 验证阶段:
    • 文件格式验证:验证.class文件是否符合JVM规范,是否是一个有效的字节码文件。
    • 元数据验证:验证字节码中的元数据是否符合Java语言规范。
    • 字节码验证:验证字节码的执行是否符合Java虚拟机规范。
    • 符号引用验证:验证类中的符号引用是否有效,能否被正确解析。
    • 准备阶段:为类的静态变量分配内存空间,并设置初始值(注意,这里的初始值不是代码中显式赋予的值,而是根据变量的数据类型赋予的默认值,如int为0,引用类型为null)。
    • 解析阶段:将常量池中的符号引用转换为直接引用。符号引用是一个抽象的概念,如字段名、方法名等,而直接引用则是指向内存中的具体地址。

初始化阶段

  • 初始化阶段是执行类构造器()方法的过程。类构造器()方法是由编译器自动收集类中的所有变量的赋值动作和静态代码块(static代码块)中的语句合并产生的。
  • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先初始初始化其父类。
  • JVM会保证一个类的()方法在多线程环境中被正确的加锁和同步。因此JVM中一个类的class是线程安全的。
  • 只有当类或接口的静态变量被首次主动使用时,JVM才会初始化这个类或接口。
类的主动引用(一定会发生类的初始化)
  • new一个类的对象
  • 调用类的静态成员(除了final常量)和静态方法
  • 使用java.lang.reflect包的方法对类进行反射调用
  • 当JVM启动,java Hello,则一定会初始化Hello类(即先启动main方法说在的类)
  • 当初始化一个类,如果其父类没有被初始化,则会先初始化它的父类

注意:

一个类被初始化后,不会重复进行被初始化

类的被动引用(不会发生类的初始化)
  • 当访问一个静态属性是,只有真正声明这个域的类才会被初始化
    • 通过子类引用父类的静态变量,不会导致子类初始化
  • 通过数组定义引用,不会触发此类的初始化
  • 引用常量不会触发此类的初始化(因为常量在编译节点就存入调用类的常量池中了)

使用和卸载阶段

  • 类被初始化后,就可以被JVM使用,创建实例对象、调用方法等。
  • 当类不再被使用时,JVM会将其从内存中卸载,释放其占用的资源。

测试类的加载机制

创建一个A对象和一个B对象,使B对象继承A,测试时初始化A,查看JVM类的加载过程

A.java

java 复制代码
package demo;

public class A {

    /** 定义一个静态属性 */
    public static String NAME = "A";

    /** 定义一个final 静态常量 */
    public static final int AGE = 20;

    static {
        System.out.println("A static 代码块被调用了");
        NAME = "static 修改了 NAME";
    }

    public A(){
        System.out.println("A 默认构造方法被调用了");
    }

}

B.java

java 复制代码
package demo;

public class B extends A {

    /** 定义一个静态属性 */
    private static String TYPE = "A";

    /** 定义一个final 静态常量 */
    private static final int WIDTH = 20;

    static {
        System.out.println("B static 代码块被调用了");
        TYPE = "static 修改了 TYPE";
    }

    public B(){
        System.out.println("B 默认构造方法被调用了");
    }

}

ClassLoadDemo.java

java 复制代码
package demo;

/**
 * 测试JVM类的加载机制
 *
 * @author Anna.
 * @date 2024/4/5 14:14
 */
public class ClassLoadDemo {

  static {
    System.out.println("mian方法所在类的 static 代码块被调用了");
  }

  public static void main(String[] args) {
    new B();

    System.out.println("初始化完成后-第二次调用不会重复进行初始化");
    new B();
  }
}

执行结果:

结论:

一个类初始化时,如果其父类没有被初始化,则先初始化其父类。

一个类被初始化后,再次被引用时,则不会重复初始化。

new 对象会默认调用类的无参构造方法

测试类的主动引用,一定会发生类的初始化

类定义使用上述A.java及B.java

  • 通过new一个对象
java 复制代码
public class ClassLoadDemo1 {

  public static void main(String[] args) {
    System.out.println("========1 通过new========");
    new A();
  }
}

执行结果:

  • 调用类的静态成员(除了final常量)和静态方法
java 复制代码
package demo;

public class ClassLoadDemo1 {

  public static void main(String[] args) {
    System.out.println("========2 调用类的静态成员(除了final常量)和静态方法========");
    System.out.println("调用final常量:");
    System.out.println("A.AGE:" + A.AGE);
    System.out.println("调用非final静态成员常量:");
    System.out.println("A.NAME:" + A.NAME);
  }
}

执行结果:

  • 使用java.lang.reflect包的方法对类进行反射调用
java 复制代码
package demo;
public class ClassLoadDemo1 {

    public static void main(String[] args) throws Exception {
        System.out.println("========3 反射调用========");
        Class.forName("demo.A");
    }
}

执行结果:

类的被动引用,不会发生类的初始化

  • 通过子类引用父类的静态变量,不会导致子类初始化
java 复制代码
package demo;
public class ClassLoadDemo2 {

    public static void main(String[] args) {
        System.out.println("========通过子类引用父类的静态变量,不会导致子类初始化========");
        System.out.println("========子类引用父类final常量,既不初始化父类,也不初始化子类========");
        System.out.println("B.AGE" + B.AGE);
        System.out.println("========子类引用父类非final常量,初始化父类,但不初始化子类========");
        System.out.println("B.NAME" + B.NAME);
    }
}

执行结果:

  • 通过数组定义引用,不会触发此类的初始化
java 复制代码
package demo;
public class ClassLoadDemo2 {

    public static void main(String[] args) {
        System.out.println("========2 通过数组定义引用,不会触发此类的初始化========");
        A[] arr = new A[10];

    }
}

执行结果:

类加载器

  • Java类加载器(Java Classloader)是Java运行时环境(Java Runtime Environment)的重要组成部分,负责动态加载Java类到Java虚拟机的内存空间中。
  • 类加载器在Java程序中扮演着至关重要的角色,它确保了类的正确加载、链接和初始化,为程序的执行提供了基础。

类加载器的层次结构

Java类加载器的层次结构是一个有序的组织形式,它定义了类加载器之间的父子关系和加载范围,确保了Java程序的正确运行。

Java类加载器的层次结构通常包括四种主要类型的类加载器:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader)和自定义类加载器。这些类加载器按照父子关系组织,形成了一个有序的加载链。

  • 启动类加载器(Bootstrap ClassLoader):
    • 是Java虚拟机的一部分,通常由本地代码实现(C语言),负责加载Java核心类库,如java.lang包中的类。
    • 由于是由本地代码实现的,因此并不基础自java.lang.classLoader。在Java代码中无法直接获取其引用。
    • 负责加载JVM运行时环境所需的基础类库,是所有类加载器的根加载器。
  • 扩展类加载器(Extension ClassLoader):
    • 是由Java语言实现的,用于加载Java扩展类库,如javax包中的类。
    • 它的父加载器是启动类加载器(但无法通过java获取其父类)。
    • 可以通过系统属性"java.ext.dirs"来指定扩展类库的路径。
  • 应用程序类加载器(Application ClassLoader):
    • 也称为系统类加载器,是默认的类加载器,负责加载应用程序的类路径(classpath,java.lang.path)下的类文件。
    • 它的父加载器是扩展类加载器。
    • 可以通过ClassLoader类的getSystemClassLoader()方法获取到它的引用。
    • 有sum.misc.Launcher$AppClassLoader实现
  • 自定义类加载器
    • 开发者可以通过基础java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊需求。

层次结构的优势

  • 安全性:通过双亲委派机制,确保了Java核心类库的安全性,防止了恶意代码通过自定义类加载器来篡改或替换核心类库。
  • 有序性:层次结构确保了类的加载是有序的,避免了类的重复加载。
  • 灵活性:通过自定义类加载器,可以实现热加载、隔离加载环境、加载加密类文件等高级功能。

ClassLoader介绍

作用
  • java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对于的字节代码,然后从这些字节代码中定义出第一个Java类,即java.lang.Class类的一个实例。
相关方法
方法 描述
loadClass(String name) 这是 ClassLoader 的核心方法,用于加载指定的类。当应用程序请求加载一个类时,ClassLoader 首先会检查这个类是否已经被加载过。如果已经加载过,就直接返回该类的 Class 对象。否则,会尝试加载该类。 如果 ClassLoader 自己无法找到类,它会根据双亲委派模型将请求委托给父类加载器。如果父类加载器也无法加载类,那么 ClassLoader 会尝试自己加载类。
findClass(String name) 这是一个受保护的方法,用于查找指定名称的类。当 ClassLoader 需要自己加载类时,它会调用该方法。具体的类加载逻辑通常会在该方法中实现。 在自定义 ClassLoader 时,开发人员可以重写该方法以实现自己的类加载逻辑。
findLoadedClass(String name) 该方法用于查找已经由当前 ClassLoader 加载的类。如果该类已经被加载,则返回对应的 Class 对象;否则返回 null。
findSystemClass(String name) 该方法用于查找由系统 ClassLoader(通常是 AppClassLoader)加载的类。它不会委托给父类加载器,而是直接在当前 ClassLoader 或系统 ClassLoader 中查找类。
defineClass(String name, byte[] b, int off, int len) 该方法用于将字节码数组转换为 Class 对象。它允许从字节码数组直接定义类,而不需要从文件系统或网络加载类文件。 在某些高级场景中,如动态代理或代码生成,开发人员可能会使用该方法动态地创建类。
getResource(String name) 和 getResources(String name) 这两个方法用于查找资源(如文件、图像等)。它们根据类加载器的类路径查找资源,并返回 URL 对象或 URL 对象的枚举。
getParent() 该方法返回当前 ClassLoader 的父类加载器。通过该方法,可以访问双亲委派模型中的父级加载器。

案例

java 复制代码
package demo2;
public class ClassLoadDemo {
    public static void main(String[] args) {
        System.out.printf("获取当前应用程序类加载器:%s%n", ClassLoader.getSystemClassLoader());
        System.out.printf("获取应用程序类加载器父类加载器:%s%n", ClassLoader.getSystemClassLoader().getParent());
        System.out.printf("获取根加载器:%s%n", ClassLoader.getSystemClassLoader().getParent().getParent());

        System.out.printf("获取应用类路径:%s%n", System.getProperty("java.class.path"));
    }
}

执行结果:

类加载器的代理模式

  • 代理模式
    • 交给其他加载器来加载指定的类
  • 双亲委派机制
    • 在代理模式下,当一个类加载器收到类加载请求时,它首先会检查这个类是否已经被加载过。如果已经加载过,就直接返回这个类的Class对象。如果没有加载过,它会将这个请求委派给父类加载器去完成。这种机制称为双亲委派模型。通过逐级向上委派,最终会到达顶层的启动类加载器。如果启动类加载器无法加载该类,那么请求会逐级向下传递,直到找到能够加载该类的类加载器为止。
    • 双亲委派机制是为了保证java核心库的类型安全。这种机制保证了不会出现用户自己定义java.lang.Object类的情况
    • 类加载器除了用于加载类,也是安全的最基本的屏障
  • 双亲委派机制是代理模式的一种
    • 并不是所有的类加载器都采用双亲委派机制
    • tomcat服务器加载器也使用代理模式,所不同的是它是首先尝试去加载这个类,如果找不到在代理给父类加载器。

自定义类加载器

自定义类加载器流程:

  • 继承:java.lang.ClassLoader
  • 首先检查请求的类型是否已经被这个加载装载到了命名空间,如果已加载,则直接返回
  • 委派类加载器请求给父类加载器,如果父类加载器能够完成加载,则直接返回加载器加载的Class实例
  • 调用本类加载器的findClass()方法,试图获取对应的字节码,如果获取的到,则调用defineClass()导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(),
  • 测试案例调用:loadClass()加载class

UserDo.java

java 复制代码
package demo3;

/**
 * UserDo 实体
 * @author Anna.
 * @date 2024/4/5 16:31
 */
public class UserDo1 {
    private String name;
    public UserDo1(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "UserDo{" +
                "name='" + name + '\'' +
                '}';
    }
}

注意: 该类编译完成后.class文件不要放在应用目录下。JVM双亲委派机制会先使用应用类加载器进行加载,这样会导致无法测试自定义类加载器

CustomClassLoader.java

java 复制代码
package demo3;

import java.io.*;

/**
 * 自定义类加载器
 *
 * 首先检查请求的类型是否已经被这个加载装载到了命名空间,如果已加载,则直接返回
 * + 委派类加载器请求给父类加载器,如果父类加载器能够完成加载,则直接返回加载器加载的Class实例
 * + 调用本类加载器的findClass()方法,试图获取对应的字节码,如果获取的到,则调用defineClass()导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(),loadClass()转抛异常,终止加载过程
 * @author Anna.
 * @date 2024/4/5 16:07
 */
public class CustomClassLoader extends ClassLoader{

    /** 定义一个加载根路径 */
    private String rootDir;

    public CustomClassLoader(String rootDir){
        this.rootDir = rootDir;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 加载该类是否已经加载到命名空间
        Class<?> clazz = findLoadedClass(name);
        if (clazz != null) {
            return clazz;
        }

        // 尝试自己加载类
        byte[] classData;
        try {
            classData = getClassData(name);
        } catch (IOException e) {
            throw new ClassNotFoundException();
        }

        // 如果自己也加载不到该类,则抛出异常
        if(classData == null){
            throw new ClassNotFoundException();
        }

        // 调用defineClass()导入类型到方法区
        clazz = defineClass(name, classData, 0, classData.length);

        return clazz;
    }

    /**
     * 根据路径读取.class文件
     *
     * @param name
     * @return byte[]
     * @author Anna.
     * @date 2024/4/5 16:24
     */
    private byte[] getClassData(String name) throws IOException {
        // 将包路径转换为类路径
        String path = rootDir + File.separator + name.replace(".", File.separator) + ".class";
        // 使用字节流读取class文件
        InputStream is  = null;
        ByteArrayOutputStream baos  = null;
        try{
            is = new FileInputStream(path);
            baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int temp = -1;
            while ((temp = is.read(buffer)) != -1){
                baos.write(buffer,0,temp);
            }
        }
        catch (Exception e){
            throw e;
        }
        finally {
            if(is != null){
                is.close();
            }
            if(baos != null){
                baos.close();
            }
        }

        return baos.toByteArray();
    }
}

ClassLoaderDemo.java

java 复制代码
package demo3;

/**
 * 自定义类加载器 测试
 *
 * @author Anna.
 * @date 2024/4/5 16:33
 */
public class ClassLoaderDemo {

    public static void main(String[] args) throws ClassNotFoundException {

        // 获取根路径
        String path = "D:";
        System.out.println(path);

        // 获取自定义类加载器
        CustomClassLoader customClassLoader = new CustomClassLoader(path);

        // 使用同一个类加载器加载UserDo
        Class<?> clazz1 = customClassLoader.loadClass("demo3.UserDo");
        Class<?> clazz2 = customClassLoader.loadClass("demo3.UserDo");
        System.out.printf("clazz1 hashCode:%s%n", clazz1.hashCode());
        System.out.printf("clazz2 hashCode:%s%n", clazz2.hashCode());
        System.out.printf("判断同一个类加载器加载同一个对象,class是否相同:%s%n", clazz1 == clazz2);
        System.out.printf("获取clazz1的类加载器:%s%n", clazz1.getClassLoader());

        // 重新创建一个自定义类加载器
        CustomClassLoader customClassLoader2 = new CustomClassLoader(path);

        // 使用不同类加载器加载UserDo
        Class<?> clazz3 = customClassLoader2.loadClass("demo3.UserDo");
        System.out.printf("clazz3 hashCode:%s%n", clazz3.hashCode());
        System.out.printf("判断不同类加载器加载同一个对象,class是否相同:%s%n", clazz1 == clazz3);
        System.out.printf("获取clazz3的类加载器:%s%n", clazz1.getClassLoader());
    }

}

执行结果:

注意:被两个类加载器加载同一个类,JVM不认为是相同的类。

加密解密类加载器

可以通过取反操作或者DES对称秘钥进行加密解密

EncrptUtils.java

java 复制代码
package demo4;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * 取反加密工具
 *
 * @author Anna.
 * @date 2024/4/5 17:36
 */
public class EncrptUtils {

    /**
     * 文件字节取反加密
     *
     * @param src  原路径
     * @param dest 目标路径
     * @return void
     * @author Anna.
     * @date 2024/4/5 17:39
     */
    public static void inversion(File src, File dest) throws IOException {
        try (FileInputStream is = new FileInputStream(src); FileOutputStream baos = new FileOutputStream(dest)) {
            int temp = -1;
            while ((temp = is.read()) != -1) {
                // 取反
                baos.write(temp ^ 0xff);
            }
        } catch (Exception e) {
            throw e;
        }
    }
}

DecrptClassLoader.java

java 复制代码
package demo4;

import java.io.*;

/**
 * 自定义解密类加载器
 *
 * @author Anna.
 * @date 2024/4/5 16:07
 */
public class DecrptClassLoader extends ClassLoader {

    /**
     * 定义一个加载根路径
     */
    private String rootDir;

    public DecrptClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 加载该类是否已经加载到命名空间
        Class<?> clazz = findLoadedClass(name);
        if (clazz != null) {
            return clazz;
        }

        // 尝试自己加载类
        byte[] classData;
        try {
            classData = getClassData(name);
        } catch (IOException e) {
            throw new ClassNotFoundException();
        }

        // 如果自己也加载不到该类,则抛出异常
        if (classData == null) {
            throw new ClassNotFoundException();
        }

        // 调用defineClass()导入类型到方法区
        clazz = defineClass(name, classData, 0, classData.length);

        return clazz;
    }

    /**
     * 根据路径读取.class文件
     *
     * @param name
     * @return byte[]
     * @author Anna.
     * @date 2024/4/5 16:24
     */
    private byte[] getClassData(String name) throws IOException {
        // 将包路径转换为类路径
        String path = rootDir + File.separator + name.replace(".", File.separator) + ".class";
        // try-with-resources
        try (InputStream is = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream();) {
            // 使用字节流读取class文件 字节取反
            int temp = -1;
            while ((temp = is.read()) != -1) {
                // 字节取反
                baos.write(temp ^ 0xff);
            }
            return baos.toByteArray();
        }
    }
}

ClassLoadDemo.java

java 复制代码
package demo4;

import java.io.File;
import java.io.IOException;

/**
 * 自定义解密类加载器
 * * 1 加密UserDo.class文件
 * * 2 使用上一案例中类加载器加载加密后的class
 * * 3 使用解密类加载器加载
 *
 * @author Anna.
 * @date 2024/4/5 17:36
 */
public class ClassLoadDemo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 1 加密.class文件
        EncrptUtils.inversion(new File("D://demo3/UserDo.class"), new File("D://temp/demo3/UserDo.class"));

        // 获取根路径
        String path = "D:/temp";

        // 2 使用上一案例中类加载器加载加密后的class
        // 获取自定义类加载器    报错ClassNotFoundException
//        CustomClassLoader customClassLoader = new CustomClassLoader(path);
//        Class<?> clazz1 = customClassLoader.loadClass("demo3.UserDo");

        // 3 使用解密类加载器加载
        DecrptClassLoader decrptClassLoader = new DecrptClassLoader(path);
        Class<?> clazz2 = decrptClassLoader.loadClass("demo3.UserDo");
        System.out.printf("获取clazz3的类加载器:%s%n", clazz2.getClassLoader());
    }
}

执行结果:

gitee源码

git clone https://gitee.com/dchh/JavaStudyWorkSpaces.git

相关推荐
重生之我是数学王子3 分钟前
QT基础 编码问题 定时器 事件 绘图事件 keyPressEvent QT5.12.3环境 C++实现
开发语言·c++·qt
xmh-sxh-13143 分钟前
jdk各个版本介绍
java
Ai 编码助手4 分钟前
使用php和Xunsearch提升音乐网站的歌曲搜索效果
开发语言·php
学习前端的小z8 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
神仙别闹16 分钟前
基于C#和Sql Server 2008实现的(WinForm)订单生成系统
开发语言·c#
XINGTECODE17 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
天天扭码22 分钟前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶23 分钟前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺27 分钟前
Spring Boot框架Starter组件整理
java·spring boot·后端
zwjapple33 分钟前
typescript里面正则的使用
开发语言·javascript·正则表达式