9、类加载器

JVM执行加载结构

image-20230519151749400

类加载器

用来加载.class字节码文件,加载为Class对象存储到JVM内存中

类加载器的执行过程

类加载器的分层和分类

image-20230522093110376

除了BootstrapClassLoader是 JVM 自身的一部分(无需加载)之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自ClassLoader抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。

扩展类加载器和系统类加载器由于也是类,也是JDK中提供的类,所以是由引导类加载器进行加载

每个 ClassLoader 可以通过getParent()获取其父 ClassLoader,如果获取到 ClassLoader 为null的话,那么该类是通过BootstrapClassLoader加载的。

加载顺序(双亲委派)

具体流程如下:

  1. 系统类加载器:任何类一开始都是由系统类加载器来加载,也就说最下层来加载的。但是由于类加载器具备委托机制。所以,它不会马上去自己加载,而是委托给上一层类加载器来加载,即扩展类加载器来加载。如果扩展类加载器加载到了就加载进内存,如果扩展类加载器没有加载到,就自己再去加载,如果它自己也没有加载到,就会报异常ClassNotFoundException
  2. 扩展类加载器:如果轮到扩展类加载器来加载的话,由于类加载器具备委托机制,它会让上一层类加载器来加载,即引导类加载器来加载,如果引导类加载器加载到了,就进内存,如果引导类加载器加载不到,再由自己来加载,如果自己也没有加载到,就返回到系统类加载器来加载
  3. 引导类加载器:如果轮到引导类加载器来加载的话,由于它没有上一层,所以自己来加载。如果加载到,就进内存;如果它自己没有加载到,就会返回到扩展类加载器来加载

image-20230522093135499

自定义类加载器

为什么自定义类加载器

实现自定义类加载器

自定义加载器的parent为AppClassLoader

Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类。

在自定义ClassLoader的子类时候,我们常见的会有两种做法

方式一:重写loadClass()方法

方式二:重写findClass()方法

这两种方法本质上差不多,毕竟loadClass()也会调用findClass(),但是从逻辑上讲我们最好不要直接修改loadClass()的内部逻辑。

loadClass()这个方法是实现双亲委派模型逻辑的地方,擅自修改这个方法会导致模型被破坏,容易造成问题。

MyClassLoader
public class MyClassLoader extends ClassLoader{

    private String classPath;

    public MyClassLoader(String classPath){
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 解析字节码文件路径
        String s = name.replaceAll("\\.", Matcher.quoteReplacement(File.separator));
        String filePath = classPath + s + ".class";
        // 读取字节码文件
        FileInputStream inputStream = null;
        ByteArrayOutputStream outputStream = null;
        try {
            inputStream = new FileInputStream(filePath);
            outputStream = new ByteArrayOutputStream();
            int len;
            byte[] bytes = new byte[1024];
            while ((len = inputStream.read(bytes)) != -1){
                outputStream.write(bytes,0,len);
            }
            byte[] outBytes = outputStream.toByteArray();
            // 使用defineClass根据字节码创建Class实例
            return defineClass(name,outBytes,0,outBytes.length);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }finally {
            if (outputStream != null){
                try {
                    outputStream.flush();
                    outputStream.close();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
            if (inputStream != null){
                try {
                    inputStream.close();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }
}
top.ygang.Student
package top.ygang;

public class Student {

    private String name;

    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

使用命令javac -encoding utf-8 Student.java编译,并且将Student.class拷贝到D:\\

注意:删除项目中的Student.java!不然由于双亲委派机制,自定义类加载器向上委托,AppClassLoader会在项目classPath中找到该类并执行进行加载

main
public static void main(String[] args) throws Exception{
    MyClassLoader myClassLoader = new MyClassLoader("D:\\");
    Class<?> aClass = myClassLoader.loadClass("top.ygang.Student");
    System.out.println(aClass.getClassLoader());
    // classloader.MyClassLoader@4dd8dc3

    Constructor<?> constructor = aClass.getConstructor(String.class, int.class);
    Object lucy = constructor.newInstance("lucy", 12);
    System.out.println(lucy);
    // Student{name='lucy', age=12}
}

相关API

获取类加载器对象

// 先获取类的字节码文件对象
Class clazz = Person.class;
            
// 通过字节码文件对象获取类加载器对象
ClassLoader classLoader = clazz.getClassLoader();

// 获取类加载器的上一层类加载器
ClassLoader classLoader1 = classLoader.getParent();

使用类加载器来读取配置文件

原始方法,通过Properties类API进行读取

// 在src的路径下,有一个jdbc.properties的配置文件
Properties p = new Properties();
p.load(new FileInputStream("src/jdbc.properties"));
String driver = p.getProperty("driver");
System.out.println(driver);

这种方法的弊端是如果配置文件在jar包内,也就是classpath下,则无法读取

使用类加载器,实际上就是AppClassLoader,由于这个类加载器加载的是classpath下的类,所以同样也就可以用来读取classpath下的文件

方式一

// 获取类的字节码文件对象
Class clazz = Demo2.class;

// 获取类加载器的对象
ClassLoader classLoader = clazz.getClassLoader();

// 使用类加载器对象读取配置文件
// 注意:使用类加载器的方式读取配置文件,默认的根目录是相对于classpath目录
InputStream is = classLoader.getResourceAsStream("jdbc.properties");

Properties p = new Properties();
p.load(is);

String driver = p.getProperty("driver");
System.out.println(driver);

方式二

// 获取类的字节码文件对象
Class clazz = Demo1.class;

// 获取类加载器对象
ClassLoader classLoader = clazz.getClassLoader();

// 使用类加载器对象读取配置文件
// 注意:使用类加载器的方式读取配置文件,默认的根目录是相对于classpath目录
URL url = classLoader.getResource("jdbc.properties");
String path = url.getPath();

FileInputStream fis = new FileInputStream(path);

Properties p = new Properties();
p.load(fis);

String driver = p.getProperty("driver");
System.out.println(driver);