跳过正文
  1. 文章/
  2. Java/
  3. JavaSE/
  4. JavaSE高级/

9、类加载器

·3574 字·8 分钟· loading · loading · ·
Java JavaSE JavaSE高级
GradyYoung
作者
GradyYoung
JavaSE高级 - 点击查看当前系列文章
§ 9、类加载器 「 当前文章 」

JVM执行加载结构
#

image-20230519151749400

类加载器
#

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

类加载器的执行过程
#

  • 加载:根据全类名查找和导入.class文件;
  • 链接(与加载交叉进行)
    • 校验:检查载入.class文件数据的正确性和代码逻辑
    • 准备:给类的静态变量分配存储空间,设置默认值(只是赋数据类型的零值)
      • 例如static Integer num = 111:此时num的值只是赋了零值0(初始化阶段才会赋值)
      • 如果static final Integer num = 111:那么当前准备阶段就会直接赋值111
    • 解析:虚拟机将常量池内的符号引用替换为直接引用的过程
  • 初始化:对类的静态变量、静态代码块执行初始化工作
    • 此阶段不是所有的类都会被初始化,只有在以下五种情况才会初始化(主动使用类才会初始化)
      • 当遇到newgetstaticputstaticinvokestatic这 4 条直接码指令时,比如 new一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
        • 当 jvm 执行 new 指令时会初始化类。即当程序创建一个类的实例对象
        • 当 jvm 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)
        • 当 jvm 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值
        • 当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法
      • 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname("..."), newInstance() 等等。如果类没初始化,触发其初始化。
      • 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
      • 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
      • 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
  • 卸载:卸载需要满足要求如下
    • 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象
    • 该类没有在其他任何地方被引用
    • 该类的类加载器的实例已被 GC
    • 综上,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的;但由我们自定义的类加载器加载的类有可能被卸载

类加载器的分层和分类
#

image-20230522093110376

  • 启动类加载器(根类加载器、引导类加载器)(BootstrapClassLoader)
    • 最顶层的加载类,由 C++实现,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的rt.jarresources.jarcharsets.jar等jar包和类)以及被-Xbootclasspath参数指定的路径下的所有类。
      • rt.jar是Java基础类库,包含Java doc里面看到的所有的类的类文件。也就是说,我们常用内置库 java.xxx.* 都在里面,比如java.util.*java.io.*java.nio.*java.lang.*java.sql.*java.math.*
  • 扩展类加载器(ExtClassLoader)
    • 主要负责加载%JRE_HOME%/lib/ext目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
  • 系统类加载器(AppClassLoader)
    • 面向用户的加载器,负责加载当前应用classpath下的所有 jar 包和类。

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

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

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

加载顺序(双亲委派)
#

  • 类加载器加载存在委托机制,此机制的目的是保证了 Java 程序的稳定运行,可以避免类的重复加载
  • 模型要求除顶层的启动类加载器外,其余的类加载器必须有自己的父类加载器
  • 在查找类或资源之前,搜索类和资源的任务会委托给父类加载器

具体流程如下:

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

image-20230522093135499

自定义类加载器
#

为什么自定义类加载器
#

  • 隔离加载类

    • 在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如:阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。再比如:Tomcat这类Web应用服务器,内部自定义了好几种类加载器,用于隔离同一个Web应用服务器上的不同应用程序。

    • 两个jar包内都存在相同类名且包名相同,如果没有隔离加载类,则会报错,如:两个版本的jar

  • 修改类加载方式

    • 类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载
  • 扩展加载源

    • 比如从数据库、网络、甚至是电视机机顶盒进行加载
  • 防止源码泄露

    • Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码。

    • 通常Java系统想增加License(授权),就可以通过自定义类加载器实现。

实现自定义类加载器
#

自定义加载器的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);
JavaSE高级 - 点击查看当前系列文章
§ 9、类加载器 「 当前文章 」