链接指定的类
类加载器责任范围
上面我们提到每个加载器都有对应的加载搜索范围
-
Bootstrap ClassLoader:这个加载器不是一个Java类,而是由底层的c++实现,负责在虚拟机启动时加载Jdk核心类库(如:rt.jar、resources.jar、charsets.jar等)以及加载后两个类加载器。这个ClassLoader完全是JVM自己控制的,需要加载哪个类,怎么加载都是由JVM自己控制,别人也访问不到这个类
-
Extension ClassLoader:是一个普通的Java类,继承自ClassLoader类,负责加载{JAVA_HOME}/jre/lib/ext/目录下的所有jar包。
-
App ClassLoader:是Extension ClassLoader的子对象,负责加载应用程序classpath目录下的所有jar和class文件。
大家自行运行这个文件,就可以看到每个类加载器加载的文件了
两种类的加载方式
通常用这两种方式来动态加载一个 java 类,Class.forName() 与 ClassLoader.loadClass() 但是两个方法之间也是有一些细微的差别
Class.forName() 方式
查看Class类的具体实现可知,实质上这个方法是调用原生的方法:
形式上类似于Class.forName(name,true,currentLoader)。综上所述,Class.forName 如果调用成功会:
-
保证一个Java类被有效得加载到内存中;
-
类默认会被初始化,即执行内部的静态块代码以及保证静态属性被初始化;
-
默认会使用当前的类加载器来加载对应的类。
ClassLoader.loadClass方式
如果采用这种方式的类加载策略,由于双亲托管模型的存在,最终都会将类的加载任务交付给Bootstrap ClassLoader进行加载。跟踪源代码,最终会调用原生方法:
与此同时,与上一种方式的最本质的不同是,类不会被初始化,只有显式调用才会进行初始化。综上所述,ClassLoader.loadClass 如果调用成功会:
-
类会被加载到内存中;
-
类不会被初始化,只有在之后被第一次调用时类才会被初始化;
-
之所以采用这种方式的类加载,是提供一种灵活度,可以根据自身的需求继承ClassLoader类实现一个自定义的类加载器实现类的加载。(很多开源Web项目中都有这种情况,比如tomcat,struct2,jboss。原因是根据Java Servlet规范的要求,既要Web应用自己的类的优先级要高于Web容器提供的类,但同时又要保证Java的核心类不被任意覆盖,此时重写一个类加载器就很必要了)
双亲委派模型源码分析
Launcher
分析类加载器源码要从 sun.misc.Launcher.class 文件看起, 关键代码已添加注释,同时可以在此类中看到 ExtClassLoader 和 AppClassLoader 的定义,也验证了我们上文提到的他们不是继承关系,而是通过指定 parent 属性来形成的组合模型
进入上面第25行的 loadClass 方法中
我们看到方法有同步块(synchronized), 这也就解释了文章开头第2个问题,多线程情况不会出现重复加载的情况。同时会询问parent classloader是否有加载,如果没有,自己尝试加载。
URLClassLoader中的 findClass方法:
借用网友的一个加载时序图来解释整个过程更加清晰:
双亲委派模型的破坏
Java本身有一套资源管理服务JNDI,是放置在rt.jar中,由启动类加载器加载的。以对数据库管理JDBC为例,java给数据库操作提供了一个Driver接口:
然后提供了一个DriverManager来管理这些Driver的具体实现:
这里省略了大部分代码,可以看到我们使用数据库驱动前必须先要在DriverManager中使用registerDriver()注册,然后我们才能正常使用。
不破坏双亲委派模型的情况(不使用JNDI服务)
我们看下mysql的驱动是如何被加载的:
核心就是这句Class.forName()触发了mysql驱动的加载,我们看下mysql对Driver接口的实现:
可以看到,Class.forName()其实触发了静态代码块,然后向DriverManager中注册了一个mysql的Driver实现。这个时候,我们通过DriverManager去获取connection的时候只要遍历当前所有Driver实现,然后选择一个建立连接就可以了。
破坏双亲委派模型的情况
在JDBC4.0以后,开始支持使用spi的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver是哪个,然后使用的时候就直接这样就可以了:
可以看到这里直接获取连接,省去了上面的Class.forName()注册过程。现在,我们分析下看使用了这种spi服务的模式原本的过程是怎样的:
-
第一,从META-INF/services/java.sql.Driver文件中获取具体的实现类名“com.mysql.jdbc.Driver”
-
第二,加载这个类,这里肯定只能用class.forName(“com.mysql.jdbc.Driver”)来加载
好了,问题来了,Class.forName()加载用的是调用者的Classloader,这个调用者DriverManager是在rt.jar中的,ClassLoader是启动类加载器,而com.mysql.jdbc.Driver肯定不在/lib下,所以肯定是无法加载mysql中的这个类的。这就是双亲委派模型的局限性了,父级加载器无法加载子级类加载器路径中的类。
那么,这个问题如何解决呢?按照目前情况来分析,这个mysql的drvier只有应用类加载器能加载,那么我们只要在启动类加载器中有方法获取应用程序类加载器,然后通过它去加载就可以了。这就是所谓的线程上下文加载器。
文章前半段提到线程上下文类加载器可以通过 Thread.setContextClassLoaser() 方法设置,如果不特殊设置会从父类继承,一般默认使用的是应用程序类加载器
很明显,线程上下文类加载器让父级类加载器能通过调用子级类加载器来加载类,这打破了双亲委派模型的原则
现在我们看下DriverManager是如何使用线程上下文类加载器去加载第三方jar包中的Driver类的,先来看源码:
使用时,我们直接调用DriverManager.getConnection() 方法自然会触发静态代码块的执行,开始加载驱动然后我们看下ServiceLoader.load()的具体实现:
继续向下看构造函数实例化 ServiceLoader 做了哪些事情:
查看 reload() 函数:
继续查看LazyIterator构造器,该类同样实现了Iterator接口:
实例化到这里我们也将上下文得到的类加载器实例化到这里,来回看ServiceLoader 重写的 iterator() 方法:
上面next() 方法调用了lookupIterator.next(),这个lookupIterator 就是刚刚实例化的 LazyIterator(); 来看next方法
继续查看nextService 方法:
终于到这里了,在上面 nextService函数中第8行调用了c = Class.forName(cn, false, loader) 方法,我们成功的做到了通过线程上下文类加载器拿到了应用程序类加载器(或者自定义的然后塞到线程上下文中的),同时我们也查找到了厂商在子级的jar包中注册的驱动具体实现类名,这样我们就可以成功的在rt.jar包中的DriverManager中成功的加载了放在第三方应用程序包中的类了同时在第16行完成Driver的实例化,等同于new Driver(); 文章开头的问题在理解到这里也迎刃而解了
JAVA热部署实现
首先谈一下何为热部署(hotswap),热部署是在不重启 Java 虚拟机的前提下,能自动侦测到 class 文件的变化,更新运行时 class 的行为。Java 类是通过 Java 虚拟机加载的,某个类的 class 文件在被 classloader 加载后,会生成对应的 Class 对象,之后就可以创建该类的实例。默认的虚拟机行为只会在启动时加载类,如果后期有一个类需要更新的话,单纯替换编译的 class 文件,Java 虚拟机是不会更新正在运行的 class。如果要实现热部署,最根本的方式是修改虚拟机的源代码,改变 classloader 的加载行为,使虚拟机能监听 class 文件的更新,重新加载 class 文件,这样的行为破坏性很大,为后续的 JVM 升级埋下了一个大坑。
另一种友好的方法是创建自己的 classloader 来加载需要监听的 class,这样就能控制类加载的时机,从而实现热部署。
热部署步骤:
-
销毁自定义classloader(被该加载器加载的class也会自动卸载);
-
更新class
-
使用新的ClassLoader去加载class
JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):
-
该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
-
加载该类的ClassLoader已经被GC。
-
该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法
自定义类加载器
要创建用户自己的类加载器,只需要继承java.lang.ClassLoader类,然后覆盖它的findClass(String name)方法即可,即指明如何获取类的字节码流。
如果要符合双亲委派规范,则重写findClass方法(用户自定义类加载逻辑);要破坏的话,重写loadClass方法(双亲委派的具体逻辑实现)。
感谢与参考
非常感谢以下博文的作者,通过反复拜读来了解双亲委派模型的原理
- https://blog.csdn.net/u014634338/article/details/81434327
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
最后
现在其实从大厂招聘需求可见,在招聘要求上有高并发经验优先,包括很多朋友之前都是做传统行业或者外包项目,一直在小公司,技术搞的比较简单,没有怎么搞过分布式系统,但是现在互联网公司一般都是做分布式系统。
所以说,如果你想进大厂,想脱离传统行业,这些技术知识都是你必备的,下面自己手打了一份Java并发体系思维导图,希望对你有所帮助。
95%以上Java开发知识点,真正体系化!**
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-LduhzFSH-1711828881380)]
最后
现在其实从大厂招聘需求可见,在招聘要求上有高并发经验优先,包括很多朋友之前都是做传统行业或者外包项目,一直在小公司,技术搞的比较简单,没有怎么搞过分布式系统,但是现在互联网公司一般都是做分布式系统。
所以说,如果你想进大厂,想脱离传统行业,这些技术知识都是你必备的,下面自己手打了一份Java并发体系思维导图,希望对你有所帮助。
[外链图片转存中…(img-T0CBveKm-1711828881381)]