上节回顾
1.类加载的七个过程清楚吗?
2.和类初始化相关的6个条件能说一下吗
3.数组的加载和引用类型的加载有哪些区别
这几个知识点还说不上来的,传送门
本章内容
话不多说,继续接着上一节的内容往下写,本章的内容主要是类加载器、双亲委派模型和“破坏”双亲委派模型这三点,并通过重写loadclss()和findclass()来实战加深印象。
类加载器
“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为类加载器(Class Loader)。
类加载器起到的作用不仅仅只是通过一个类的全限定名来获取描述该类的二进制字节流,他也是类唯一性的标志之一。判断两个类完全相等的前提就是两个类的类加载器是同个。不然就没有讨论的必要了。
一、双亲委派模型
相信大家都见过这个关系图片
这个就是比较经典的双亲委派模型的三层模型,除了最顶端的启动类加载器是jvm通过c++语言来实现的之外,其他的类加载器全部都是java语言实现的,并且是独立于虚拟机外部的,所以提供了我们很大的操作空间。
逻辑上这三层模型是自顶向下的一个父子类的继承关系,但是实际上他们之间是一种组合复用的关系,这一点需要大家区分开的。
通过他的工作模式就可以很好地来理解这句话了:当一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
也就是说当一个类提出了需要加载的需求之后,我们的类加载器会从最底层开始向上反馈,一旦上级反馈说自己无法加载这个类的时候,才会调用当前类加载器来加载这个类。所以说所有的类加载器是一种合作组合的关系,并不是严格意义上的父子类关系。
下面简单说下各个加载器的职责范围
1.启动类加载器
这个类加载器负责加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中。
不继承抽象类java.lang.ClassLoader,是jvm自带的,java代码中无法直接引用。
2.扩展类加载器
这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。
继承抽象类java.lang.ClassLoader,可以直接在java代码中使用扩展类加载器来加载类文件。
3.应用程序类加载器
这个类加载器由sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystemClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
二、双亲委派模型源码
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; } }
还记得高并发那个系列里给大家的方法吗?看源码先看方法注释
* <ol> * * <li><p> Invoke {@link #findLoadedClass(String)} to check if the class * has already been loaded. </p></li> * * <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method * on the parent class loader. If the parent is <tt>null</tt> the class * loader built-in to the virtual machine is used, instead. </p></li> * * <li><p> Invoke the {@link #findClass(String)} method to find the * class. </p></li> * * </ol>
这里我们着重看下这个
-
标签下的内容,这里介绍了这个方法的主要逻辑
- 1.调用findLoadedClass(String),判断类是否已经被加载过。
- 2.当parent不为空的时候,调用parent的loadClass(String),除了启动类加载器没有parent,其他都有parent,如果parent为空,则调用虚拟机自带的了加载器,也就是启动类加载器。
- 3.如果以上加载器都加载失败了,调用findClass(String),用用户自定义的查找方式来加载类。
- 通过这三部我们就可以比较清楚的看到,
双亲委派模型并不是强制的
,当双亲委派模型加载失败的时候,这里还提供给我们一个findClass(String)
的方法来加载类。
三、破坏双亲委派
- 从loadclass()源码也可以看出,双亲委派模型并不是强制约束的模型,在双亲委派模型加载失败的时候,我们可以通过自定义findclass()来查找类。
- 在一些特定的场景下,我们需要破坏这种模型,来达到我们的目的,
这也反过来证明了双亲委派模型的三层模型之间仅仅是看起来是父子继承关系,实际上是一种组合复用的关系。
- 双亲委派模型一共有三次被破坏的情况
第一次被破坏
- 由于双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类 java.lang.ClassLoader则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协,为了兼容这些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。
第二次被破坏
- 其实就是循环调用的问题,不管在哪一种架构中,不管你设计的多完美,总有那么一些需求和人会搞出一些循环调用的神奇操作。也就是A依赖于B,B又依赖于A的这种场景。
- 但是事实上,程序不是万能的,但是人是万能的~~作出妥协的永远是人,而不是程序。
- 讲个故事
- 双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类 加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被 称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变 的完美规则,如果有基础类型又要调用回用户的代码,那该怎么办呢?
- 这并非是不可能出现的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务, 它的代码由启动类加载器来完成加载(在JDK 1.3时加入到rt.jar的),肯定属于Java中很基础的类型 了。但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程 序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么办?
- 为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器 (Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
- 有了线程上下文类加载器,程序就可以做一些“舞弊”的事情了。JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,但也是无可奈何的事情。Java中涉及SPI的加载基本上都采用这种方式来完成,例如JNDI、JDBC、JCE、JAXB和JBI等。不过,当SPI的服务提供者多于一个的时候,代码就只能根据具体提供者的类型来硬编码判断,为了消除这种极不优雅的实现方式,在JDK 6时,JDK提供了java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式,这才算是给SPI的加载提供了一种相对合理的解决方案。
第三次被破坏
- 双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的,这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。说白了就是希望Java应用程序能像我们的电脑外设那样,接上鼠标、U盘,不用重启机器就能立即使用,鼠标有问题或要升级就换个鼠标,不用关机也不用重启。对于个人电脑来说,重启一次其实没有什么大不了的,但对于一些生产系统来说,关机重启一次可能就要被列为生产事故,这种情况下热部署就对软件开发者,尤其是大型系统或企业级软件开发者具有很大的吸引力。
四、实战:重写loadClass和findClass
1.重写localClass通过自定义加载器来加载
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException { ClassLoader myLoader = new ClassLoader() { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try { String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; //获取在类路径下的类文件 InputStream is = getClass().getResourceAsStream(fileName); if (is == null) { return super.loadClass(name); } byte[] b = new byte[is.available()]; is.read(b); //转换成对象 return defineClass(name, b, 0, b.length); } catch (IOException e) { throw new ClassNotFoundException(name); } } }; Object obj = myLoader.loadClass("day6.ExtendTestClass").newInstance(); //通过new来创建对象,此时对象是使用的 ExtendTestClass extendTestClass = new ExtendTestClass(); System.out.println("通过自定义加载器myLoader创建对象,类加载器为:" + obj.getClass().getClassLoader()); System.out.println("通过new来创建对象,类加载器为:" + extendTestClass.getClass().getClassLoader()); //通过不同的类加载器加载同一个类,查看instanceof结果是否一致 System.out.println(obj instanceof ExtendTestClass); System.out.println(extendTestClass instanceof ExtendTestClass); }
输出结果
通过自定义加载器myLoader创建对象,类加载器为:day6.ClassLoaders$1@60e53b93 通过new来创建对象,类加载器为:sun.misc.Launcher$AppClassLoader@18b4aac2 false true
这里通过重写loadclass来自定义了一个类加载器,通过对比传统的new()关键字来创建对
象和自定义的类加载器,效果已经很明显。这里也得出一种结论,那就是对于同一个类,通过不同的加载器是可以加载多次的。
只有在特殊的场景下我们才会考虑这么做,因为重写loadclass是打破了双亲委派模型的
2.findclass实现一个class文件热替换的功能
/** * Finds the class with the specified <a href="#name">binary name</a>. * This method should be overridden by class loader implementations that * follow the delegation model for loading classes, and will be invoked by * the {@link #loadClass <tt>loadClass</tt>} method after checking the * parent class loader for the requested class. The default implementation * throws a <tt>ClassNotFoundException</tt>. * * @param name * The <a href="#name">binary name</a> of the class * * @return The resulting <tt>Class</tt> object * * @throws ClassNotFoundException * If the class could not be found * * @since 1.2 */ protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
我们直接把自带的findclass这个方法拉出来,可以看到,源码中啥也没做,直接抛出ClassNotFoundException这个异常。也就是说一般情况下,类加载都是符合双亲委派模型的,但是不排除存在双亲委派模型也加载不了的类,又或者说我们需要加载一个指定路径下的指定类的时候就可以通过重写findclass来加载。
由于findclass正常情况下是需要在双亲委派模型都加载失败都情况下才会调用都一个方法,这里为了方便起见,我们直接破坏双亲委派重写loadclass来直接调用findclass,实现我们自己都逻辑。
1.定一个MyClassLoader
public class MyClassLoader extends ClassLoader { private final static Path DEFAULT_PATH = Paths.get("/Users/doudou/workspace", "jvm/jvm/src"); private final Path classdir; public MyClassLoader() { super(); classdir = DEFAULT_PATH; } public MyClassLoader(String classPath) { super(); this.classdir = Paths.get(classPath); } public MyClassLoader(ClassLoader parent, String classPath) throws ClassNotFoundException { super(parent); classdir = Paths.get(classPath); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] bytes = this.readClassBytes(name); if (null == bytes || bytes.length == 0) { throw new ClassNotFoundException("Can not load the class" + name); } return this.defineClass(name, bytes, 0, bytes.length); } private byte[] readClassBytes(String name) throws ClassNotFoundException { String classPath = name.replace(".", "/"); Path classPullPath = classdir.resolve(Paths.get(classPath) + ".class"); if (!classPullPath.toFile().exists()) { throw new ClassNotFoundException("The class" + name + "not found."); } try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { Files.copy(classPullPath, byteArrayOutputStream); return byteArrayOutputStream.toByteArray(); } catch (IOException e) { throw new ClassNotFoundException("load the class" + name + "occur error." + e); } } @Override public String toString() { return "My ClassLoader"; } }
这里主要看两个方法,分为3步来完成
1.首先第一步我们重写从ClassLoaer继承来的findclass()
2.在findclass内部调用readClassBytes(String name)来自定义文件加载的方式。这里我们通过try-with-resource来包裹我们的逻辑
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { Files.copy(classPullPath, byteArrayOutputStream); return byteArrayOutputStream.toByteArray(); } catch (IOException e) { throw new ClassNotFoundException("load the class" + name + "occur error." + e); }
开启一个字节流,然后把文件拷贝到这个字节流内部,返回给findclass
3.调用this.defineClass(name, bytes, 0, bytes.length);来把字节流转换成对象。
2.为了不被应用类加载器抢先加载了我们的类,我们重写loadclass,模拟双亲委派加载失败的情况。
/** * 破坏类加载机制的父委托机制 重写自定义加载器的loadClass方法,更改类加载的使用类加载器的顺序 首先查看缓存中有没有类信息,有则直接返回 没有的话在看类的名字 * 是以什么开头,若是以java或者javax开头则直接用系统类加载器 否则先用自定义类加载器,如果没有加载到则查看有没有父加载器,有则使用父加载器,没有则使用系统加载器 * 这样就破坏了加载顺序,先有自动以的加载器先加载而不是父类加载器 */ public class BrockdelegateClassLoader extends MyClassLoader { @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> loadedClass = findLoadedClass(name); if (loadedClass == null) { if (name.startsWith("java.") || name.startsWith("javax.")) { try { loadedClass = getSystemClassLoader().loadClass(name); } catch (Exception e) { System.out.println(e.getMessage()); } } else { loadedClass = this.findClass(name); if (loadedClass == null) { if (getParent() != null) { loadedClass = getParent().loadClass(name); } else { loadedClass = getSystemClassLoader().loadClass(name); } } } } else { System.out.println("缓存中获得了class"); return loadedClass; } if (loadedClass == null) { throw new ClassNotFoundException("the class " + name + " not found"); } if (resolve) { resolveClass(loadedClass); } return loadedClass; } } public BrockdelegateClassLoader(String name) { super(name); } public BrockdelegateClassLoader() { super(); } }
除非是java或者javax开头的类路径下的文件才调用系统类加载器,不然就会直接执行我们自定义的findclass()逻辑。这也是我们为什么去重写类loadclass来破坏双亲委派模型的目的。
3.我们之前讲过,一个类只有在类加载器是相同的情况下才能去判断类是否相同,我们这里也一样,只有在类加载器相同的情况下,类才不能够被重复加载,为了满足类文件热替换的效果,我们需要相同的类被重复加载,所以每次加载之前我们的类加载器就需要是重新new出来的。这一点大家应该很好就能理解。
public class Test { public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, InterruptedException { ////如果把类加载器对象放在循环外部,每次加载都是同一个类加载器的话就起不到类文件热替换的效果,因为他会从缓存中直接把之前已经加载过的类文件加载出来 // BrockdelegateClassLoader myClassLoader = new BrockdelegateClassLoader(); while (true) { BrockdelegateClassLoader myClassLoader = new BrockdelegateClassLoader(); // //如果开启就会看到我们的类加载器是sun.misc.Launcher$AppClassLoader,也就是应用类加载器,所以只会加载一次,做不到类文件热替换的效果 // MyClassLoader myClassLoader = new MyClassLoader(); Class<?> aClass1 = myClassLoader.loadClass("day3.HelloWorld"); System.out.println(aClass1.getClassLoader()); Object o = aClass1.newInstance(); Method say = aClass1.getMethod("say"); Object invoke = say.invoke(o); System.out.println(invoke); TimeUnit.SECONDS.sleep(3); } } }
通过我们的测试类的调试可以轻松的实现类文件的热替换。具体步骤是把main函数run起来,然后通过修改say()函数的返回值,通过javac指令重新生成class文件,因为这里设置了类加载的间隔是每3秒一次,所以替换之后的下一次输出就能看到效果。
具体效果如下:
My ClassLoader helloworld initialization hello world333 My ClassLoader helloworld initialization hello world333 My ClassLoader helloworld initialization hello world被修改了 My ClassLoader helloworld initialization hello world被修改了
好了到这里我们双亲委派模型、如何破坏双亲委派模型也讲完了,同时通过重写loadclass和findclass来加深了同学们的印象,不知道同学们对类加载这块内容清楚了没有呢。
到这里我们通过4个章节的篇幅从类文件结构的解析、类文件的加载过程和类文件加载器相关内容做了简单的介绍,下一篇开始我们会讲一下jvm的gc是怎么一回事。
最后让我看看还有哪个大聪明还没点赞呢👍