类加载机制

一个java类的main方法是如何执行的?第一步就是通过类加载器将其加载到JVM中。

类加载过程:加载 > 验证 > 准备 > 解析 > 初始化 > 使用 > 卸载

加载:通过IO读入字节码文件,在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

验证:验证字节码文件的正确性,比如验证这是不是一个class文件,格式正不正确。

准备:给静态变量分配内存和赋默认值。

解析:把一些静态方法替换为指针或者句柄等,是把符号引用替换为直接引用,称之为静态链接。

初始化:静态变量赋指定值,执行静态代码块。

类被加载到方法区之后,方法区包含哪些:运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用等。
 
注意一点,一个包里面的类不是一次性全部加载的,使用时才会加载,看下面的Demo示例:
public class DynamicLoad {
    static {
        System.out.println("测试使用时才会加载类");
    }
    public static void main(String[] args) {
        new A();
        B b = null;
    }
}
class A {
    static {
        System.out.println("加载A");
    }
    public A() {
        System.out.println("初始化A");
    }
}
class B {
    static {
        System.out.println("加载B");
    }
    public B() {
        System.out.println("初始化B");
    }
}

输出结果:

测试使用时才会加载类
加载A
初始化A

 

类加载器:

  • 引导类加载器,加载JRE的lib目录下的核心类库
  • 扩展类加载器,加载JRE的lib目录下的ext中的拓展类库
  • 应用程序类加载器,负责加载ClassPath路径下的类
  • 自定义加载器:负责加载用户自定义路径下的类

打印一下某个加载器能加载到哪些类:

# 引导类加载器加载文件
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
    System.out.println(urls[i]);
}
System.out.println("********************");
# 扩展类加载器加载文件
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("********************");
# 应用程序类加载器加载文件
System.out.println(System.getProperty("java.class.path"));

类加载器都是由com.misc.Launcher这个类负责创建的,可以看下这个类的构造方法:

// 这个类是单例的
private static Launcher launcher = new Launcher();    
public Launcher() {
  Launcher.ExtClassLoader var1;
    try {
    // 引导类加载器,其父加载器设置为null,因为父加载器是引导类加载器,这是虚拟机处理的,不属于java的范畴
    var1 = Launcher.ExtClassLoader.getExtClassLoader();
  } catch (IOException var10) {
    throw new InternalError("Could not create extension class loader", var10);
  }
  try {
    // 应用程序类加载器,父加载器设置为扩展类加载器
    this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
      throw new InternalError("Could not create application class loader", var9);
    }
  }
}

从上面构造方法,了解到了应用程序类加载器的父加载器是扩展类加载器,这其实是为了双亲委派的加载模式,加载某个类时,会先委托父加载器加载,如果父加载器在类加载路径下都加载不到,会再返回给自己的类加载器加载。

AppClassLoader的顶级父类是ClassLoader,在它的loadClass方法中实现了双亲委派模式:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
  synchronized (getClassLoadingLock(name)) {
    // 检查当前类加载器是否已经加载了当前类
    Class<?> c = findLoadedClass(name);
    if (c == null) {
      long t0 = System.nanoTime();
      try {
        // 判断父加载器是否为空,不为空的话向上委派
        if (parent != null) {
        c = parent.loadClass(name, false);
        } else {
          // 父加载器为空,交给引导类加载器
          c = findBootstrapClassOrNull(name);
        }
      } catch (ClassNotFoundException e) {
        // ClassNotFoundException thrown if class not found
        // from the non-null parent class loader
      }
      if (c == null) {
        // class还未被加载,调用findClass加载类
        long t1 = System.nanoTime();
        c = findClass(name);
        // this is the defining class loader; record the stats
        sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
        sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
        sun.misc.PerfCounter.getFindClasses().increment();
      }
    }
    if (resolve) {
      resolveClass(c);
    }
    return c;
  }
}    

设计双亲委派的意义在于:避免类库被随意加载,比如java.lang.Integer类只能由引导类加载器加载,如果任何一个加载器都能加载的话,不就可以篡改了么。第二点是避免重复加载。

一个类被加载之后,它所依赖的类也会被当前类的类加载器所加载,除非显示的指定使用其他的类加载器。

下面自定义一个类加载器:

// 定义一个User类,之后通过自定义类加载器进行加载
public class User {

    private String id;

    private String name;
    
    public void print() {
        System.out.println("CustomClassLoader user");
    }
}

要自定义类加载器,需要继承ClassLoader,重写findClass即可

public class CustomClassLoader extends ClassLoader {

    String classPath;

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

    /**
     * 读取class文件,将文件读取为字节数组
     *
     * @param name
     * @return
     * @throws Exception
     */
    private byte[] loadByte(String name) throws Exception {
        // 包名替换成路径
        name = name.replaceAll("\\.", "/");
        // 获取class文件路径
        FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }

    /**
     * 重写findClass,将字节数组转为Class对象,只要调用ClassLoader的defineClass方法即可
     *
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadByte(name);
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }

    public static void main(String[] args) throws Exception {
        CustomClassLoader customClassLoader = new CustomClassLoader("/Users/project/JVM/src/main/java");
        Class<?> clazz = customClassLoader.findClass("com.dluo.loader.User");
        Object instance = clazz.newInstance();
        Method print = clazz.getDeclaredMethod("print");
        print.invoke(instance, null);
        System.out.println(instance.getClass().getClassLoader().getClass());
    }
    
}

看输出结果,成功执行了User的print方法,打印类加载器的类名,是我们自定义的类加载器

CustomClassLoader user
class com.dluo.loader.CustomClassLoader

常规场景下类加载机制都会使用双亲委派模式,但有些情况下双亲委派会产生限制。比如我们使用tomcat时,它可能会部署多个程序,不同的程序可能使用不同的类库版本,比如一个项目是spring4,而另一个项目是spring5,这样就不能要求一个类只能有一份了,需要使其隔离。所以,从这个角度出发,tomcat类加载器,至少要实现多个版本的类库实现。再拓展来说,同一个容器中的类库应实现共享,要不就会重复,容器自己依赖的类库和应用程序的类库也应该隔离。还有一点值得注意,jsp文件需要编译成class文件运行,而且jsp经常要修改,应该保证能动态更新,所以每个jsp应该都是独一无二的类加载器,加载完即卸载,更新时再重新创建类加载器加载jsp文件。综上,tomcat这种类加载的方式一定违背了双亲委派模式。

重写类加载方法,实现自己的加载逻辑,不委派给双亲加载

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t1 = System.nanoTime();
                // 非自定义的类还是走双亲委派加载
                if (!name.startsWith("com.dluo.loader")) {
                    c = this.getParent().loadClass(name);
                } else {
                    c = findClass(name);
                }
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

现在测试一下,看一下能不能加载两个User类。

创建两个目录:

mkdir -p temp1/com/dluo/loader
mkdir -p temp2/com/dluo/loader

将User的print方法输出不同的内容,分别编译为.class文件,copy到对应的temp文件中测试使用

    public static void main(String[] args) throws Exception {
        CustomClassLoader customClassLoader1 = new CustomClassLoader("/Users/dluo/temp1");
        Class<?> clazz1 = customClassLoader1.loadClass("com.dluo.loader.User");
        Object instance1 = clazz1.newInstance();
        Method method1 = clazz1.getDeclaredMethod("print");
        method1.invoke(instance1, null);
        System.out.println(clazz1.getClassLoader());

        CustomClassLoader customClassLoader2 = new CustomClassLoader("/Users/dluo/temp2");
        Class<?> clazz2 = customClassLoader2.loadClass("com.dluo.loader.User");
        Object instance2 = clazz2.newInstance();
        Method method2 = clazz2.getDeclaredMethod("print");
        method2.invoke(instance2, null);
        System.out.println(clazz2.getClassLoader());
    }

输出结果:

版本一类加载器
com.dluo.loader.CustomClassLoader@58372a00
版本二类加载器
com.dluo.loader.CustomClassLoader@568db2f2

从这里可以看到,虽然都是使用CustomClassLoader,但是已经是不同的类加载器实例初始化的了,一个JVM程序中同时存在两个User类,打破了双亲委派加载模式。

 

posted @ 2020-12-23 14:21  以战止殇  阅读(64)  评论(0编辑  收藏  举报