0
点赞
收藏
分享

微信扫一扫

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

心如止水_c736 04-09 19:00 阅读 2

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

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

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

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();
  }
}

执行结果:

在这里插入图片描述

结论:

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

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

  • 通过new一个对象
public class ClassLoadDemo1 {

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

执行结果:
在这里插入图片描述

  • 调用类的静态成员(除了final常量)和静态方法
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包的方法对类进行反射调用
package demo;
public class ClassLoadDemo1 {

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

执行结果:

在这里插入图片描述

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

  • 通过子类引用父类的静态变量,不会导致子类初始化
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);
    }
}

执行结果:

在这里插入图片描述

  • 通过数组定义引用,不会触发此类的初始化
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 的父类加载器。通过该方法,可以访问双亲委派模型中的父级加载器。

案例

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

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 + '\'' +
                '}';
    }
}

CustomClassLoader.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

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());
    }

}

执行结果:

在这里插入图片描述

加密解密类加载器

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

EncrptUtils.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

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

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源码

举报

相关推荐

0 条评论