• 类加载机制


    一个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类,打破了双亲委派加载模式。

  • 相关阅读:
    VUE vue和element框架搭配实现导航跳转,底部导航跳转页面
    【HDFS篇14】HA高可用 --- Federation架构设
    【HDFS篇13】HA高可用 --- YARN-HA集群配置
    【HDFS篇12】HA高可用 --- HDFS-HA集群配置
    【HDFS篇11】HA高可用
    【HDFS篇10】DataNode相关概念
    【HDFS篇09】集群安全模式
    【HDFS篇08】NameNode故障处理
    【HDFS篇07】NameNode和SecondearyNameNode
    【HDFS篇06】HDFS数据读写流程
  • 原文地址:https://www.cnblogs.com/dlcode/p/14178387.html
Copyright © 2020-2023  润新知