原创

java的类加载

温馨提示:
本文最后更新于 2023年02月03日,已超过 409 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

类加载机制

JVM类加载机制分为五个部分: 加载,验证,准备,解析,初始化

加载

加载是类加载过程中的一个阶段,会在内存中生成一个代表这个类的java.lang.Class 对象,作为方法区这个类的各种数据的入口

加载可以是从Class文件中获取,也可以从jar,war包读取获取,也可以在运行时生成(动态代理),以及JSP文件转换为Class类

验证

这个阶段主要是为了 确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身

准备

准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,在方法区分配这些变量所使用的内存空间
例如:

public static int v = 666;

此时会先给v分配内存,初始化变量为0值,在编译后,会将赋值指令存放与类构造器client方法中

但是,如果增加了final关键字:

public static final int a = 666;

将会在编译阶段生成ConstantValue属性,在准备阶段会根据ConstantValue赋值为666

解析

解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程

CONSTANT_Class_info
CONSTANT_Field_info
CONSTANT_Method_info
等常量
  • 符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
  • 直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。

初始化

初始化阶段是类加载的最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器之外,其他操作都有JVM主导

到了初始化阶段,才开始真正执行类中定义的java程序代码

初始化阶段是 执行类构造器<client>方法的过程

<client>方法是由编译器自动收集类中的变量赋值操作,静态语句块中的语句 合并而成的

虚拟机会保证 子<client>方法执行之前它的父类<client>方法已经执行完毕

如果一个类中没有对静态变量赋值,也没有静态语句快,则不会生成<client>方法

以下情况不会执行类的初始化:

  • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  • 定义对象数组,不会触发该类的初始化
  • 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触
    发定义常量所在的类
  • 通过类名获取 Class 对象,不会触发类的初始化。
  • 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初
    始化,其实这个参数是告诉虚拟机,是否要对类进行初始化
  • 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。

什么时候触发类加载

什么情况需要开始类加载过程的第一阶段(加载)呢?
Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。

但是对于初始化阶段,虚拟机规范则严格规定了以下几种情况必须立即对类进行初始化,如果类没有进行过初始化,则需要先触发其初始化。

  • new一个对象的时候
  • 访问类的静态变量(注意上面的,如果是访问父类的静态字段,不会触发子类的初始化)
  • 访问类的静态方法
  • 反射 Class.forName
  • 初始化一个类的子类(会先初始化父类)
  • 虚拟机启动时,定义了main方法的那个类

类加载器

虚拟机设计团队把加载动作放到 JVM 外部实现,以便让应用程序决定如何获取所需的类,JVM 提
供了 3 种类加载器:

启动类加载器(Bootstrap ClassLoader)

负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被
虚拟机认可(按文件名识别,如 rt.jar)的类。

扩展类加载器(Extension ClassLoader)

负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类
库。

应用程序类加载器(Application ClassLoader)

负责加载用户路径(classpath)上的类库

双亲委派

JVM通过 双亲委派模型进行类的加载,我们也可以通过继承 java.lang.ClassLoader实现自定义的类加载器

file

当一个类收到了类加载请求,它首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成
没一个层次的类加载器都是如此,因此所有的加载请求都会传送到启动类加载器中

只有当父类加载器反馈自己无法完成这个请求的时候,子类加载器才会尝试自己加载.

file

双亲委派机制可以保证不同地方引用的类,都是同一个.

自定义类加载器

在之前的文章中,我们有一个MathServiceImpl类,以这个类作为demo,进行演示,我们
先将add方法改为错误的减法,同时保存在target编译后的class文件:

    public Double add(double a, double b) {
        Double result=a-b;
        return result;
    }

将编译后的class文件放到当前项目目录中:
file

再将add改为正确的加号

此时,我们存在2个MathServiceImpl

  • 重新编译项目后,target会存在一个加号的MathServiceImpl
  • 项目当前目录下,一个减号的MathServiceImpl

在正常情况下,根据双亲委派机制,将加载编译目录中的MathServiceImpl,为正确的写法,现在我们需要实现一个ClassLoader类,重写findClass和loadClass方法:

package org.example;

import java.io.*;

public class MyClassLoader extends ClassLoader {
    private String codePath;
    public MyClassLoader(ClassLoader parent, String codePath) {
        super(parent);
        this.codePath = codePath;
    }
    public MyClassLoader(String codePath) {
        this.codePath = codePath;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        BufferedInputStream bis = null;
        ByteArrayOutputStream bos = null;
        codePath = codePath + name.replace(".", File.separator) + ".class";
        byte[] bytes = new byte[1024];
        int line = 0;
        try {
            //读取编译后的文件
            bis = new BufferedInputStream(new FileInputStream(codePath));
            bos = new ByteArrayOutputStream();
            while ((line = bis.read(bytes)) != -1) {
                bos.write(bytes, 0, line);
            }
            bos.flush();
            bytes = bos.toByteArray();
            return defineClass(null, bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                bis.close();
                bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return super.findClass(name);
    }


    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t1 = System.nanoTime();
                //如果包名是org.example.MathServiceImpl开头的,调用自定义类的findClass方法,否则调用父类的loadClass方法
                if (name.startsWith("org.example.MathServiceImpl")) {
                    c = this.findClass(name);
                } else {
                    c = this.getParent().loadClass(name);
                }
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
}

main方法进行调用:

package org.example;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        //通过默认的类加载器加载的类
        MathServiceImpl mathService = new MathServiceImpl();
        System.out.println(mathService.add(10,23));

        //类加载器加载的类
        String codePath = "/Users/tioncico/IdeaProjects/test-maven/test-maven/src/main/java/";
        MyClassLoader myClassLoader = new MyClassLoader(codePath);
        Class<?> aClass = myClassLoader.loadClass("org.example.MathServiceImpl");

        System.out.println("测试字节码是由" + aClass.getClassLoader().getClass().getName() + "加载的。。");
        //利用反射实例化对象,和调用TwoNum类里面的twoNum方法
        Object o = aClass.newInstance();
        Method add = aClass.getDeclaredMethod("add", double.class, double.class);
        Object invoke = add.invoke(o, 10, 23);
        System.out.println(invoke);

        //重新mathService
        MathServiceImpl mathServiceNew = new MathServiceImpl();
        System.out.println(mathServiceNew.add(10,23));
    }
}

输出:file

自己实现类加载器之后,可以违反双亲委派机制,强制要求自定义加载,所以出现了2个类的方法返回结果不一致的问题

正文到此结束
本文目录