类加载是什么

把磁盘中的java文件加载到内存中的过程叫做类加载

当咱们用java命令运行某个类的main函数启动程序时,首先须要通过类加载器把主类加载到JVM. 有如下 User 类

package dc.dccmmtop;public Class User {    public static void main(String[] args) {        System.out.println("hello");    }}

运行 java dc.dccmmtop.User 时, 先要找到 User.Class 文件,查找全门路就是 Class_PATH + {{package name}},对于User类来说,就是 {$Class_APTH}/dc/dccmmtop.User.Class

如果 User.javaF:\code, 并且不在Class_PATH 下,能够通过 java -Classpath "F:\code" 长期指定。

加载类之后还有后续的步骤:

  1. 验证
  2. 筹备
  3. 解析
  4. 初始化
  5. 应用
  6. 卸载

这篇文章次要来讲讲类加载

类加载器

不理解类加载机制的,可能就认为,只需找到java文件所在的磁盘地位,而后进行一次读文件的操作不就实现了加载嘛,其实远非如此。

总有一个加载类的工具,这个工具叫做类加载器,在java代码中能够通过如下形式获取以后类的类加载器是什么

package dccmmtop;    public Class User {      public static void main(String[] args) {          System.out.println("hello");          System.out.println(User.Class.getClassLoader());     }  }

如图能够看到类加载器的名字叫做 AppClassLoader

咱们全局搜寻一下这个类,会发现在 sun.misc.Launcher.java 文件中找到。

那么这个AppClassLoader 自身也是一个 java 文件,它又是什么时候被加载并初始化的呢?

咱们滚动到文件顶部,看到 Launcher 类的构造方法局部:

标记1 和标记2 实现了一个单例模式,在5 处获取到了 AppClassLoader 实例。也就是说在某一个中央通过调用 Launcher 类中的 getLauncher() 办法,会失去 AppClassLoader 实例, 那么 getLauncher() 办法又是在哪里调用的呢?追踪到这里曾经无奈在java代码中找到上一步了,其实这个办法是jvm (c++实现)调用的,如下图:

以上就是类加载的次要步骤了。上面看一下双亲委派机制

双亲委派机制

咱们持续看AppClassLoader 实例化的过程:

在5处,实例化了一个AppClassLoader的对象,同时传进去了一个参数 var1, 这个 var1 是另外一个类加载器ExtClassLoader , 咱们在进入 getAppClassLoader 办法看一看是怎么实现的:

先看一下 几个ClassLoad的继承关系:

有下面的继承关系图能够看进去,AppClassLoaderExtClassLoader 都是从 ClassLoader 继承来的。

Launcher() 中可知,调用 AppClassLoader.getAppClassLoader() 办法时, 把 ExtClassLoader 的实例作为参数传递进来,最终到4这一步,作为 var2 参数,调用父类的构造方法,持续追踪父类的构造方法直到 ClassLoader :

ClassLoader 构造方法中,保护了一个 parent 变量,到此咱们晓得了 AppClassLoader 中 parent 变量保留的是 ExtClassLoader的实例, 如下图示意

持续看Launcher 构造方法:

loadClass() 办法将 Class 文件加载到jvm中,咱们跟踪一下这个办法,会发现最初会调到 根类ClassLoader 中:

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;      }  }

下面代码块中的弟6行,findLoadedClass() , 先从已加载到的类汇合中查找有没有这个类,如果有的话,间接返回,没有再进行下一步, findLoadedClass 办法源码如下

native finnal Class<?> findLoadedClass0(String name); 这里曾经无奈在向后追踪了,看到 naive ,要明确 应用native关键字阐明这个办法是原生函数,也就是这个办法是用C/C++语言实现的,并且被编译成了DLL,由java去调用.

此时 User.Class 是第一次加载,AppClassLoader 中必定无奈在已加载的汇合中找到,所以持续向下走到第 10,11 行. 下面曾经剖析过,AppClassLoader 中的 parent 是 ExtClassLoader , 所以在11行由 ExtClassLoader 的实例执行 laodClass 办法。 ExtClassLoader 没有覆写根类ClassLoaderloaderClass 办法,所以也会到这里,只不过 ExtClassLoader 的 parent 是 NUll, 会走到13行,调用findBootstrapClassOrNull() 办法,再看一下这个办法的实现:


会发现这个办法也是C++实现的,尽管咱们无奈看到源码,然而依据正文能够晓得,这个是保留了启动类加载器加载过的类。

到此为止,咱们曾经见识过3中不同的类加载器了:

  • AppClassLoader
  • ExtClassLoader
  • BootStrapClassLoader

咱们先不论这个前面两个类加载器是什么, 假设他们也找不到 User.Class. 持续向下看:

执行到第21行findClas()这里,再看源码

在A-2 这一步,ucp 其实保留的就是以后 ClassLoader 的类加载门路,就不再开展。要记住此时的 ClassLoader 是 ExtClassLoader, 如果依然找不到User.Class 会执行到 A-3.而后返回到 loadClass 办法中, 此时 c 是空,继续执行到33行,返回到 AppClassLoader 调用 parent.getAppClassLoader 处,在 AppClassLoader 实例的范畴下持续向后执行,而后再持续调用 findClass 办法,如果在AppClassLoader的类加载门路中找到User.Class 文件,就会 执行 defindClass(name,res) 办法去加载类文件了。

整个过程用文字描述起来比较复杂,来张图就很分明了,为什么叫做双亲委派:

把 loadedClassList 汇合称作缓存

  1. 先在 AppClassLoader 中缓存中找,如果找不到向 ExtClassLoader 找,如果能找到,间接返回
  2. 在 ExtClassLoader 中缓存找,如果找不到向 BootStrapClassLoader 找,如果能找到,间接返回
  3. 在 BootStrapClassLoader 找,如果找不到, 在 ExtClassLoader 类门路汇合中找,
  4. 如果在 ExtClassLoader 类门路汇合找不到,在 AppClassLoader 类门路汇合找
  5. 如果在 AppClassLoader 类门路汇合中能找到,加载该类,并放入缓存。找不到则报错

双亲指的是 ExtClassLoaderBootStrapClassLoader, AppClassLoader 先不加载,而是向上让其“父”加载,父加载不到时,本人再加载。这里的父不是父类,而是调用层级的关系。

是时候介绍一下 这三个类加载器

BootStrapClassLoader

疏导类加载器

负责加载撑持JVM运行的位于JRE的lib目录下的外围类库,比方 rt.jar、charsets.jar等

ExtClassLoader

扩大类加载器

负责加载撑持JVM运行的位于JRE的lib目录下的ext扩大目录中的JAR 类包

AppClassLoader

应用程序加载器

负责加载ClassPath门路下的类包,次要就是加载你本人写的那些类

咱们能够写代码验证一下:

package dccmmtop;    import sun.misc.Launcher;    import java.net.URL;    public Class User {      public static void main(String[] args) {          System.out.println(String.Class.getClassLoader()); // null          System.out.println(com.sun.crypto.provider.DESKeyFactory.Class.getClassLoader().getClass().getName()); //sun.misc.Launcher$ExtClassLoader          System.out.println(User.Class.getClassLoader().getClass().getName()); // sun.misc.Launcher$AppClassLoader          System.out.println();          System.out.println("bootstrapLoader加载以下文件:");          URL[] urls = Launcher.getBootstrapClassPath().getURLs();          for (int i = 0; i < urls.length; i++) {              System.out.println(urls[i]);          }          System.out.println();          System.out.println("extClassloader加载以下文件:");          System.out.println(System.getProperty("java.ext.dirs"));          System.out.println();          System.out.println("appClassLoader加载以下文件:");          System.out.println(System.getProperty("java.Class.path"));      }  }

输出如下:

null // 因为调用了 c++ 实现。无奈获取到java对象sun.misc.Launcher$ExtClassLoadersun.misc.Launcher$AppClassLoaderthe bootstrapLoader : nullthe extClassloader : sun.misc.Launcher$ExtClassLoader@77459877the appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2bootstrapLoader加载以下文件:file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/resources.jarfile:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/rt.jarfile:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/sunrsasign.jarfile:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jsse.jarfile:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jce.jarfile:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/charsets.jarfile:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jfr.jarfile:/C:/Program%20Files/Java/jdk1.8.0_261/jre/ClassesextClassloader加载以下文件:C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\extappClassLoader加载以下文件:C:\Program Files\Java\jdk1.8.0_261\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_261\jre\lib\ext\jaccess.jar;...省略

为什么应用双亲委派机制

  1. 沙箱平安机制:
    本人写的java.lang.String.Class类不会被加载,这样便能够避免外围 API库被随便篡改
  2. 防止类的反复加载:当父亲曾经加载了该类时,就没有必要子ClassLoader再加载一 次,保障被加载类的唯一性

全盘负责委托机制

“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的应用另外一个ClassLoder,该类
所依赖及援用的类也由这个ClassLoder载入

自定义类加载器

从上述源码的形容可知,类加载器的外围办法是 findClass , 和 defineClass 。

defindClass 将class文件从磁盘加载文件到内存,defineClass 开始解析class文件:

所以自定义类加载器只需继承 ClassLoader,而后从写 findClass 文件就行了:

目录如下:

App.java:

import java.io.FileInputStream;import java.lang.reflect.Method;public class App {    static class MyClassLoader extends ClassLoader {        private String classPath;        public MyClassLoader(String classPath) {            this.classPath = classPath;        }        // 从磁盘加载文件        private byte[] loadByte(String name) throws Exception {            name = name.replaceAll("\\.", "/");            FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");            int len = fis.available();            byte[] data = new byte[len];            fis.read(data);            fis.close();            return data;        }        // 重写        protected Class<?> findClass(String name) throws ClassNotFoundException {            try {                byte[] data = loadByte(name);                // defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节 数组。                return defieClass(name, data, 0, data.length);            } catch (Exception e) {                e.printStackTrace();                throw new ClassNotFoundException();            }        }    }    public static void main(String args[]) throws Exception {        // 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载 器设置为应用程序类加载器AppClassLoader        MyClassLoader classLoader = new MyClassLoader("D:/dc_code/java"); // D盘创立        // 创立 io/dc 几级目录,将User类的复制类User.class丢入该目录        Class clazz = classLoader.loadClass("io.dc.User");        Object obj = clazz.newInstance();        // 应用反射调用 User 类的 sout 办法        Method method = clazz.getDeclaredMethod("sout", null);        method.invoke(obj, null);        System.out.println(clazz.getClassLoader().getClass().getName());    }}

突破双亲委派机制

通过下面的源码剖析发现,次要是 ClassLoader 类中的laodClass 办法来实现的双亲委派机制,本人不加载而是先让其父加载。

所以间接复写 loadClass 办法即可,不再指定父级加载,以后类间接加载,如下:

import java.io.FileInputStream;import java.lang.reflect.Method;public class App {    static class MyClassLoader extends ClassLoader {        private String classPath;        public MyClassLoader(String classPath) {            this.classPath = classPath;        }        // 从磁盘加载文件        private byte[] loadByte(String name) throws Exception {            name = name.replaceAll("\\.", "/");            FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");            int len = fis.available();            byte[] data = new byte[len];            fis.read(data);            fis.close();            return data;        }        protected Class<?> findClass(String name) throws ClassNotFoundException {            try {                byte[] data = loadByte(name);                // defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节 数组。                return defineClass(name, data, 0, data.length);            } catch (Exception e) {                e.printStackTrace();                throw new ClassNotFoundException();            }        }        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) {                    // If still not found, then invoke findClass in order                    // to find the class.                    long t1 = System.nanoTime();                    if (name.startsWith("io.dc")) {                        // 间接查找, 限定包名                        c = findClass(name);                    } else {                        // 其余包中的类还是应用双亲委派机制                        // 否则会报找不到 Object 类                        c = this.getParent().loadClass(name);                    }                    // this is the defining class loader; record the stats                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);                    sun.misc.PerfCounter.getFindClasses().increment();                }                if (resolve) {                    resolveClass(c);                }                return c;            }        }    }    public static void main(String args[]) throws Exception {        // 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载 器设置为应用程序类加载器AppClassLoader        MyClassLoader classLoader = new MyClassLoader("D:/dc_code/java"); // D盘创立        // 创立 io/dc 几级目录,将User类的复制类User.class丢入该目录        Class clazz = classLoader.loadClass("io.dc.User");        Object obj = clazz.newInstance();        // 应用反射调用 User 类的 sout 办法        Method method = clazz.getDeclaredMethod("sout", null);        method.invoke(obj, null);        System.out.println(clazz.getClassLoader().getClass().getName());    }}