类加载器的分类
类加载的过程主要又类加载器完成。java里的类加载器主要分为以下内容:
- 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等
- 扩展类加载器:负责加载JRE下ext目录里的jar包
- 应用程序类加载器:负责加载设置的CLASSPATH路径下的类包,主要就是你写的类。
- 自定义加载器:负责加载用户自定义路径下的类。
上代码
public class TestJdkClassLoader { public static void main(String[] args) { System.out.println(String.class.getClassLoader()); System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader()); System.out.println(TestJdkClassLoader.class.getClassLoader()); System.out.println(); ClassLoader appClassLoader = ClassLoader.getSystemClassLoader(); ClassLoader extClassLoader = appClassLoader.getParent(); ClassLoader bootstrapLoader = extClassLoader.getParent(); System.out.println("the bootstrapLoader : " + bootstrapLoader); System.out.println("the extClassloader : " + extClassLoader); System.out.println("the appClassLoader : " + 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 sun.misc.Launcher$ExtClassLoader@5ca881b5 sun.misc.Launcher$AppClassLoader@18b4aac2 the bootstrapLoader : null the extClassloader : sun.misc.Launcher$ExtClassLoader@5ca881b5 the appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2 bootstrapLoader加载以下文件: file:/C:/Program%20Files/Java/jdk1.8.0_121/jre/lib/resources.jar file:/C:/Program%20Files/Java/jdk1.8.0_121/jre/lib/rt.jar file:/C:/Program%20Files/Java/jdk1.8.0_121/jre/lib/sunrsasign.jar file:/C:/Program%20Files/Java/jdk1.8.0_121/jre/lib/jsse.jar file:/C:/Program%20Files/Java/jdk1.8.0_121/jre/lib/jce.jar file:/C:/Program%20Files/Java/jdk1.8.0_121/jre/lib/charsets.jar file:/C:/Program%20Files/Java/jdk1.8.0_121/jre/lib/jfr.jar file:/C:/Program%20Files/Java/jdk1.8.0_121/jre/classes extClassloader加载以下文件: C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext;C:\Windows\Sun\Java\lib\ext appClassLoader加载以下文件: C:\Program Files\Java\jdk1.8.0_121\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_121\jre\lib\rt.jar;D:\demo\juc-yang\target\classes;D:\repository\org\projectlombok\lombok\1.18.20\lombok-1.18.20.jar;D:\repository\org\springframework\boot\spring-boot-starter\2.5.4\spring-boot-starter-2.5.4.jar;D:\repository\org\springframework\boot\spring-boot\2.5.4\spring-boot-2.5.4.jar;D:\repository\org\springframework\spring-context\5.3.9\spring-context-5.3.9.jar;D:\repository\org\springframework\spring-aop\5.3.9\spring-aop-5.3.9.jar;D:\repository\org\springframework\spring-beans\5.3.9\spring-beans-5.3.9.jar;D:\repository\org\springframework\spring-expression\5.3.9\spring-expression-5.3.9.jar;D:\repository\org\springframework\boot\spring-boot-autoconfigure\2.5.4\spring-boot-autoconfigure-2.5.4.jar;D:\repository\org\springframework\boot\spring-boot-starter-logging\2.5.4\spring-boot-starter-logging-2.5.4.jar;D:\repository\ch\qos\logback\logback-classic\1.2.5\logback-classic-1.2.5.jar;D:\repository\ch\qos\logback\logback-core\1.2.5\logback-core-1.2.5.jar;D:\repository\org\apache\logging\log4j\log4j-to-slf4j\2.14.1\log4j-to-slf4j-2.14.1.jar;D:\repository\org\apache\logging\log4j\log4j-api\2.14.1\log4j-api-2.14.1.jar;D:\repository\org\slf4j\jul-to-slf4j\1.7.32\jul-to-slf4j-1.7.32.jar;D:\repository\jakarta\annotation\jakarta.annotation-api\1.3.5\jakarta.annotation-api-1.3.5.jar;D:\repository\org\springframework\spring-core\5.3.9\spring-core-5.3.9.jar;D:\repository\org\springframework\spring-jcl\5.3.9\spring-jcl-5.3.9.jar;D:\repository\org\yaml\snakeyaml\1.28\snakeyaml-1.28.jar;D:\repository\org\slf4j\slf4j-api\1.7.32\slf4j-api-1.7.32.jar;C:\Program Files\JetBrains\IntelliJ IDEA 2021.3.1\lib\idea_rt.jar
从上面输出结果我们可以观察到:
- 引导类加载器又c++创建,所以对象为空。
- 扩展类加载器的parent属性为引导类加载器。
- 应用程序类加载器的parent属性为扩展类加载器。
类加载器初始化过程
类运行加载全过程
又上图可知:
- 创建JVM启动器实例sun.misc.Launcher。
- 类加载器由Launcher通过getClassLoader()获得。
查看源码
可知Launcher默认返回appClassLoader类加载器
双亲委派机制
概念:加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。
通过appClassLoader的loadClass方法中,看到都是super,最终会走到根类ClassLoader的loadClass方法,双亲委派机制也是在此实现的。
//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) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); //都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类 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; } }
- 当super.loadClass方法执行到根节点时首先执行findLoadedClass,看看是否在加载的类中有。此时第一次加载,所以返回null。
- 这时执行到此方法parent.loadClass。我们知道在上面说到的类加载器初始化时,系统默认使用的是appClassLoader(应用程序加载器),其parent为extClassLoader(扩展类加载器)。那这行的执行结果就是由extClassLoader在次来到根节点ClassLoader执行loadClass
- 同样的第一次加载找不到要加载的类进入parent != null的判断,此时extClassLoader的parent属性为引导类加载器,引导类加载器由c++创建不存在对象,所以进入c = findBootstrapClassOrNull(name)。
- findBootstrapClassOrNull就是查询引导类加载器是否加载此类,我们自己的类肯定是没有的所以返回为null。
- 最后进入c=null这个判断,执行findClass(name)进行类加载。最后由URLClassLoader中的findClass方法进行具体执行。
urlClassLoader中的findClass方法
可以看到真正执行加载类的是defineClass(name, res)方法。此方法中有太多的native方法,就不深入看了,加载过程大致分为几步:
- 加载:在硬盘上查找并通过IO读入字节码文件,使用到该类时才会加载,比如new对象,调用改类的main方法。
- 验证:安全校验,比如字节码文件开头的cafe babe
- 准备:给类的静态变量分配内存,并赋予默认值。
- 解析:将符号引用替换为直接引用。简单来说就是把静态方法替换为内存指向的地址,这步也叫静态链接。
- 初始化:对类的静态变量初始化为指定的值,执行静态代码块。
public class TestDynamicLoad { static { System.out.println("*************load TestDynamicLoad************"); } public static void main(String[] args) { new A(); System.out.println("*************load test************"); B b = null; //B不会加载,除非这里执行 new B() } } class A { static { System.out.println("*************load A************"); } public A() { System.out.println("*************initial A************"); } } class B { static { System.out.println("*************load B************"); } public B() { System.out.println("*************initial B************"); } } 运行结果: *************load TestDynamicLoad************ *************load A************ *************initial A************ *************load test************
自定义类加载器
自定义类加载器只需要继承根类ClassLoader类,改类有两个核心方法。loadClass,实现了双亲委派机制。另一个是findClass,默认是实现的空方法,所以我们自定义重新该方法即可。
public class MyClassLoaderTest { 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(); } } } public static void main(String args[]) throws Exception { //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader MyClassLoader classLoader = new MyClassLoader("D:/test"); //D盘创建 test/com/tuling/jvm 几级目录,将User类的复制类User1.class丢入该目录 Class clazz = classLoader.loadClass("com.tuling.jvm.User1"); Object obj = clazz.newInstance(); Method method = clazz.getDeclaredMethod("sout", null); method.invoke(obj, null); System.out.println(clazz.getClassLoader().getClass().getName()); } } 运行结果: =======自己的加载器加载类调用方法======= com.yang.jvm.MyClassLoaderTest$MyClassLoader
在来个打破双亲委派机制,就是loadClass中去掉用parent进行loadClass方法即可
public class MyClassLoaderTest { 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); return defineClass(name, data, 0, data.length); } catch (Exception e) { e.printStackTrace(); throw new ClassNotFoundException(); } } /** * 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载 * @param name * @param resolve * @return * @throws 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(); c = findClass(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 { MyClassLoader classLoader = new MyClassLoader("D:/test"); //尝试用自己改写类加载机制去加载自己写的java.lang.String.class Class clazz = classLoader.loadClass("java.lang.String"); Object obj = clazz.newInstance(); Method method= clazz.getDeclaredMethod("sout", null); method.invoke(obj, null); System.out.println(clazz.getClassLoader().getClass().getName()); } } 运行结果: java.lang.SecurityException: Prohibited package name: java.lang at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659) at java.lang.ClassLoader.defineClass(ClassLoader.java:758)