目录

类加载机制

目录

一、从java代码到运行经历哪些过程?

  • java代码 =》编译 =》加载 =》验证 =》准备 =》解析 =》初始化 =》使用 =》卸载

    • 编译:将 源码文件(.java) 编译成可被JVM执行的 二进制字节码文件(.class)

    • 加载:通过 classpath 以及类的全限定名来寻找 class 文件,并加载到内存

    • 验证:校验 class 文件的正确性

    • 准备:给静态变量分配内存,初始化为默认值

    • 解析:静态链接,将符号引用替换为直接引用

    • 初始化:对静态变量赋予指定的值,并执行静态代码块 (父类静态块——>子类静态块——>父类代码块——>父类构造器——>子类代码块——>子类构造器)

二、类加载器

  • 上面过程中加粗的五步是由类加载器来实现的
  • 类加载器类型如下:
    名称加载目录加载类描述
    启动类加载器 (BootstrapClassLoader ;C++实现)%JAVA_HOME%\jre\lib 或 -Xbootclasspath参数指定的路径支撑JVM运行的核心类库
    扩展类加载器 (ExtClassLoader)%JAVA_HOME%\jre\lib\ext 或-Djava.ext.dir指定位路径支撑JVM运行的扩展JAR类包
    应用程序类加载器(AppClassLoader)%CLASSPATH%一般是用户写的类
    自定义加载器用户自定义路径自定义路径下的类
    -Djava.library.path= 加载库文件jar

三、双亲委派模型

  1. 双亲委派模型的英文是 Parents Delegation Model

    • Parents表示为父辈,表达重点在于上层而不是“双”。
    • 据说是翻译问题,网上有说翻译为“父类委派模型”,但是实际上类加载器之间并不是继承关系,翻译为父类委派会误导大家,个人觉得翻译为上层委派模型比较合适。
  2. 双亲委派模型原理
      如果一个类加载器收到了类加载请求,它会先查看是否有上层的父加载器,如果有则委派给父加载器加载,父加载器也一样查看是否也有它的父加载器,有则依次向上委派,直至到达启动类加载器;倘若没有父加载器 或 自身已是启动类加载器,则尝试加载类,加载成功则直接返回,如果无法完成加载则返回,让委派请求的子加载器自行加载,还是无法完成则依次向下返回加载任务,直至开始受到类加载请求的类,都不能加载则抛异常。
      针对双亲委派模型的这一特点,有开发者戏称其为 “啃老模型” ,哈哈也有点道理。需要什么东西(类),先问问父辈有没有,有就用父辈的,没有才自己去找。

双亲委派模型

  1. 为什么要设计双亲委派模型?

    • 避免类的重复加载

      • 父加载器加载过了,无须另外加载
    • 保证加载类的唯一性

      • 加载类的全限定名相同,则以父加载器加载的为准
    • 防止核心类库被篡改

      • 因为启动类是最上层的加载器,启动类加载器会先加载核心类库,用户不能再加载自己编写的相同限定名的含恶意代码的类,否则会抛安全异常SecurityException

      • 特别的,在双亲委派模型中若用户想加载和核心类库相同的包名的类也是不行的。比如想加载类限定名为java.lang.Abcdefg的类,但java.lang是核心类库的包名,需要权限,否则会抛异常java.lang.SecurityException: Prohibited package name: java.lang,具体涉及java沙箱机制

  2. 双亲委派模型的局限性

    • 当业务需要,得加载同一个类库的不同版本时,因为双亲委派的限制,则只能加载一个版本。
    • 经典的例子就是Web容器:Tomcat,Tomcat可能需要同时运行两个web项目,两个项目很有可能会使用同一个类库的不同版本。
    • Tomcat通过自定义加载器突破了这个限制。

四、类加载器核心源码

  • 以下几个核心方法在ClassLoader抽象类中。
  1. loadClass方法

    • 用途:加载指定全限定名的类,其中包含双亲委派模式的逻辑实现
        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 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) {
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        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;
            }
        }
    
  2. findClass方法

  • 作用:默认是抛ClassNotFoundException异常,需要自己重写类加载逻辑
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
    
  1. defineClass方法

    • 作用:将byte字节流解析成JVM能够识别的Class对象
  2. resolveClass方法

    • 作用:链接一个 Class 对象?

五、示例代码

例一、获取类加载器名称

  • 代码
    import sun.net.spi.nameservice.dns.DNSNameService;
    
    public class MyClass {
        public static void main(String[] args) {
            //获取核心类库中Integer的类加载器
            System.out.println(Integer.class.getClassLoader());
            //获取拓展类库中DNSNameService的类加载器
            System.out.println(DNSNameService.class.getClassLoader().getClass().getName());
            //获取当前类的类加载器
            System.out.println(MyClass.class.getClassLoader().getClass().getName());
            //获取系统的类加载器,一般都会返回ApplicationClassLoader
            System.out.println(ClassLoader.getSystemClassLoader().getClass().getName());
            //获取线程上下文的类加载器
            System.out.println(Thread.currentThread().getContextClassLoader().getClass().getName());
        }
    }
    
  • 运行结果
    • 需要指出的是,因为启动类加载器BootstrapClassLoader是通过C++语言实现的,在java中没有对应的类,所以输出是null
      null
      sun.misc.Launcher$ExtClassLoader
      sun.misc.Launcher$AppClassLoader
      sun.misc.Launcher$AppClassLoader
      sun.misc.Launcher$AppClassLoader
      

例二、自定义类加载器

  • 自定义类加载器若通过继承ClassLoader抽象类来实现的话,需要两个核心函数,一个是loadClass(String, boolean),另一个是findClass(String)。
    • ClassLoader抽象类中loadClass(String, boolean)方法实现了双亲委派的具体逻辑,直接拿来用就行,但是findClass(String)方法在抽象类中是直接抛ClassNotFoundException,需要自己重写方法才行。
  • 代码
    import java.io.FileInputStream;
    import java.lang.reflect.Method;
    
    public class MyClassLoaderTest {
    
        /**
        * 自定义类加载器
        */
        static class MyClassLoader extends ClassLoader {
    
    
            /**
            * 自定义类加载路径
            */
            private String classPath;
    
    
            /**
            * 构造类
            * @param classPath 自定义类加载路径
            */ 
            public MyClassLoader(String classPath) {
                this.classPath = classPath;
            }
    
    
            /**
            * 获取class文件并转化为字节数组
            * @param name 类全限定名
            * @return class文件字节数组
            * @throws Exception
            */
            private byte[] loadByte(String name) throws Exception {
                name = name.replaceAll("\\.", "/");
                FileInputStream fileInputStream = new FileInputStream(classPath + "/" + name + ".class");
                byte[] data = new byte[fileInputStream.available()];
                fileInputStream.read(data);
                fileInputStream.close();
                return data;
            }
    
    
            /**
            *
            * @param name
            * @return
            * @throws ClassNotFoundException
            */
            @Override
            protected Class<?> findClass(String name) throws ClassNotFoundException {
                try {
                    byte[] data = loadByte(name);
                    //defineClass将一个字节数组转为Class对象。
                    return defineClass(name, data, 0, data.length);
                } catch (Exception e) {
                    e.printStackTrace();
                    throw new ClassNotFoundException();
                }
            }
    
        }
    
        public static void main(String args[]) throws Exception {
            //实例化自定义类加载器
            MyClassLoader classLoader = new MyClassLoader("D:/project");
            //导入目标类
            Class clazz = classLoader.loadClass("com.abing.blog.MyHelloWorld");
    
            //调用目标类的方法,并输出classloader
            Object obj = clazz.newInstance();
            Method method= clazz.getDeclaredMethod("print", null);
            method.invoke(obj, null);
        }
    }
    

例三、安全模型验证

  1. example_01

    • 代码
      package java.lang;
      
      public class String {
          public static void main(String[] args) {
              System.out.println("This is my String");
          }
      }
      
    • 运行结果
      错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
          public static void main(String[] args)
      否则 JavaFX 应用程序类必须扩展javafx.application.Application
      
    • 分析
      • 明明定义了main方法,为什么却提示找不到呢?因为限定名为java.lang.String的类,在启动类加载器(BootstrapClassLoader)里已经加载过了,所以不会再加载用户编写的java.lang.String,而运行的时候运行的是核心类库的String类,固然没有main方法
  2. example_02

    • 代码(修改例二的main方法)

          public static void main(String args[]) throws Exception {
              //实例化自定义类加载器
              MyClassLoader classLoader = new MyClassLoader("D:/project");
              //导入目标类
              Class clazz = classLoader.loadClass("java.lang.Virus");
      
              //调用目标类的方法,并输出classloader
              Object obj = clazz.newInstance();
              Method method= clazz.getDeclaredMethod("print", null);
              method.invoke(obj, null);
          }
      
    • 运行结果

      java.lang.SecurityException: Prohibited package name: java.lang
          at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
          at java.lang.ClassLoader.defineClass(ClassLoader.java:758)
      
    • 分析

      • 当用自定义类加载器强制去加载一个自己写的包名为java.lang的类时,这个自己写的类的包名虽然和核心类库包名一致,但是他们是由不同的类加载器加载的,不属于同一个运行时包,所以java安全沙箱机制规定了自己写的类没有权限去访问已加载的核心类库java.lang包,就抛了SecurityException。(运行时包:指由同一个类装载器装载的、属于同一个包的、多个类型的集合。)

六、Q&As

  1. 类加载过程中验证一步验证什么?

    • 主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。
    • 参考链接:https://www.cnblogs.com/fengbs/p/7082262.html
  2. 类加载器什么时候会加载类?

    • 对于单java文件,运行时即加载主类。而对于多个java文件的工程项目编译成jar文件后,运行jar时,类加载器先加载主类。当主类运行使用到其他类时,才加载其他类,比如显式加载时:Class.forName(name)或this.getClass().getClassLoader().loadClass(); 或者隐式加载new方法等时。

    • 即jar包里的类不是一次性全部加载的,是使用到时才加载。

  3. 类加载过程中解析一步的静态链接是什么?

    • 静态链接是将符号引用替换为直接引用的过程。比如将静态方法(main方法等)替换为指向数据所存内存的指针或句柄等。
    • 在编译时,并不知道类所依赖的其他东西,会在内存中的哪个位置,只能用符号来表示。而所有东西被加载到这里后会有自己的真实空间地址,那么就可以用真实地址去替换符合引用,这样运行时就能找到它们所依赖的东西了。
  4. 在JVM中两个class对象是否为同一个类由什么决定?

    • 加载这个类的ClassLoader实例对象 和 这个类的全限定名来一起决定,有一个不同则不是同一个类
  5. 加载前期做了什么?

    • java.exe调用底层jvm.dll文件中的方法,创建了一个Java虚拟机实例
    • 创建一个引导类加载器实例
    • C++调用创建JVM启动器实例sun.misc.Launcher(该类由引导类加载器负责加载,并由它创建其他类加载器)
    • sun.misc.Launcher.getLauncher()
    • 获取运行类自己的加载器ClassLoader.一般是AppClossLoader实例
    • 调用loadClass(“com.xxx.xxx.ClassName”)
    • 加载完成后,调用类的main方法。
  6. 如何查看class文件对应的字节码?

    • 可以通过 javap -v 命令,反编译class文件

参考链接

  • Java字节码指令大全:https://www.cnblogs.com/longjee/p/8675771.html

  • 深入理解Java类加载器(ClassLoader):https://blog.csdn.net/javazejian/article/details/73413292

← 返回技术