-
2021-09-09 17:27:47
一、什么是类加载器
类加载器就是把类文件(.class)加载到虚拟机中,也就是说通过一个类的全限定名来获取描述该类的二进制字节流。
二、类加载器分类
1.启动(Bootstrap)类加载器:启动类加载器是用本地代码实现的类加载器,它负责将JAVA_HOME/lib下面的核心类库或-Xbootclasspath选项指定的jar包等虚拟机识别的类库加载到内存中。由于启动类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用。具体可由启动类加载器加载到的路径可通过System.getProperty(“sun.boot.class.path”)查看。
2.扩展(Extension)类加载器:扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责将JAVA_HOME/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器,具体可由扩展类加载器加载到的路径可通过System.getProperty(“java.ext.dirs”)查看。
3.系统(System)类加载器:系统类加载器是由 Sun 的AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将用户类路径(java-classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径,如第四节中的问题6所述)下的类库加载到内存中。开发者可以直接使用系统类加载器,具体可由系统类加载器加载到的路径可通过System.getProperty(“java.class.path”)查看。
类加载器的层次结构图:
三、什么是双亲委派模型
原理 :当一个类加载器收到类加载任务时,会先交给自己的父加载器去完成,因此最终加载任务都会传递到最顶层的BootstrapClassLoader,只有当父加载器无法完成加载任务时,才会尝试自己来加载。
具体: 根据双亲委派模式,在加载类文件的时候,子类加载器首先将加载请求委托给它的父加载器,父加载器会检测自己是否已经加载过类,如果已经加载则加载过程结束,如果没有加载的话则请求继续向上传递直Bootstrap ClassLoader。如果请求向上委托过程中,如果始终没有检测到该类已经加载,则Bootstrap ClassLoader开始尝试从其对应路劲中加载该类文件,如果失败则由子类加载器继续尝试加载,直至发起加载请求的子加载器为止。
四、为什么要使用双亲委派模型?
为什么需要双亲委派模型呢?假设没有双亲委派模型,试想一个场景:
黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。
或许你会想,我在自定义的类加载器里面强制加载自定义的java.lang.String类,不去通过调用父加载器不就好了吗?确实,这样是可行。但是,在JVM中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false。
举个简单例子:
ClassLoader1、ClassLoader2都加载java.lang.String类,对应Class1、Class2对象。那么Class1对象不属于ClassLoad2对象加载的java.lang.String类型。五、JDBC如何打破双亲委派模型
参考博客:聊聊JDBC是如何破坏双亲委派机制的
更多相关内容 -
双亲委派模型
2021-03-18 10:12:52今天大头菜打算讲双亲委派模型,重点关注:如何破坏双亲委派模型,你看完后,一定会获益匪浅哈哈哈。 广告时间:先点赞,先收藏,转粉不转路。 问题 大家思考一下这些问题: 为什么不能定义java.lang.Object的...前言
今天大头菜打算讲双亲委派模型,重点关注:如何破坏双亲委派模型,你看完后,一定会获益匪浅哈哈哈。
广告时间:先点赞,先收藏,转粉不转路。问题
大家思考一下这些问题:
- 为什么不能定义java.lang.Object的Java文件?
- 在多线程的情况下,类的加载为什么不会出现重复加载的情况?
- 以下代码,JVM是怎么初始化注册MySQL的驱动Driver?
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "root");
解答
以上这些问题,其实都和双亲委派模型有关,双亲委派模型,在面试中,是非常热的考察点。
首先,我们得知道什么是双亲委派模型?
简单点说,所谓的双亲委派模型,就是加载类的时候,先请求其父类加载器去加载,如果父类加载器无法加载类,再尝试自己去加载类。如果都没加载到,就抛出异常。
现在让我们回到第一个问题:为什么不能创建java.lang.Object的Java文件?
即使我们已经定义了java.lang.Object的Java文件,但其实也无法加载,因为java.lang.Object已经被启动类加载器加载了。
你可能会问:为什么JVM要使用双亲委派模型来加载类?
一,性能,避免重复加载;二,安全性,避免核心类被修改。
第一点,没法好说的。说说第二点安全性吧,你试想一下。假设我现在创建一个java.lang.Object的Java文件,然后在里面植入一些病毒木马,或者写一些死循环在构造方法中。对JVM来说,这是致命的。
接下来,简单介绍一下各种类加载器:
- 启动类加载器:它不是一个Java类,是C++写的。主要负责JDK的核心类库,比如rt.jar,resource.jar等类库。启动类加载器完全是JVM自己控制的,开发人员是无法访问的。
- 扩展类加载器:是一个继承ClassLoader类的Java类,负责加载{JAVA_HOME}/jre/lib/ext/目录下的所有jar包
- 应用程序类加载器:是一个继承ClassLoader类的Java类,负载加载classpath目录下的所有jar和class文件,基本上你写的类文件,都是被应用程序类加载器加载的。
可以用以下代码,打印出三个类加载器的加载的文件。
public class TestEnvironment { public static void main(String[] args) { //启动类加载器 System.out.println("1"+System.getProperty("sun.boot.class.path")); //扩展类加载器 System.out.println("2"+System.getProperty("java.ext.dirs")); //应用类加载器 System.out.println("3"+System.getProperty("java.class.path")); } }
补充一下:三个类加载器的关系,不是父子关系,是组合关系。
接下来我们看看类加载器的加载类的方法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 { //这里通过是否有parent来区分启动类加载器和其他2个类加载器 if (parent != null) { //先尝试请求父类加载器去加载类,父类加载器加载不到,再去尝试自己加载类 c = parent.loadClass(name, false); } else { //启动类加载器加载类,本质是调用c++的方法 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; } }
总结一下loadClass方法的大概逻辑:
- 首先加锁,防止多线程的情况下,重复加载同一个类
- 当加载类的时候,先请求其父类加载器去加载类,如果父类加载器无法加载类时,才自己尝试去加载类。
上面的源码解析,可以回答问题:在多线程的情况下,类的加载为什么不会出现重复加载的情况?
好,目前我们已经解决2个问题了。
接下来
就要开始破坏双亲委派模型了。首先声明哈,双亲委派模型,JVM并没有强制要求遵守,只是说推荐。
我们来总结一下,双亲委派模型就是子类加载器调用父类加载器去加载类。那如何来破坏呢?可以使得父类加载器调用子类加载器去加载类,这便破坏了双亲委派模型。
在讲解MySQL的驱动前,先补充一个知识点:
Class.forName() 与 ClassLoader.loadClass() 两种类的加载方式的区别
Class.forName()
-
实质是调用原生的forName0()方法
-
保证一个Java类被有效得加载到内存中;
-
类默认会被初始化,即执行内部的静态块代码以及保证静态属性被初始化;
-
默认会使用当前的类加载器来加载对应的类(先记住这个特点,下面会用到)
ClassLoader.loadClass()
- 实质是启动类加载器进行加载
- 与Class.forName()不同,类不会被初始化,只有显式调用才会进行初始化。
- 类会被加载到内存中
- 提供一种灵活度,可以根据自身的需求继承ClassLoader类实现一个自定义的类加载器实现类的加载。
我们继续讲一下关于MySQL的驱动,我们列举2种情况进行对比理解:
第一种:不破坏双亲委派模型
自定义的Java类 // 1.加载数据访问驱动 Class.forName("com.mysql.jdbc.Driver"); //2.连接到数据"库"上去 Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");
分析一下这2行代码:
第一行:进行类加载,还记得上面说过Class.forName()会使用当前的类加载器来加载对应的类。当前的类,就是用户写的Java类,用户写的Java类使用应用程序类加载器加载的。那现在问题就是应用程序类加载器是否能加载com.mysql.jdbc.Driver这个类,答案是可以的。因此这种方式加载类,是不会破坏双亲委派模型的。
第二行:就是通过遍历的方式,来获取MySQL驱动的具体连接。第二种:破坏双亲委派模型
在JDBC4.0以后,开始支持使用spi的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver是哪个,然后使用的时候就直接这样就可以了:Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");
这和不破坏双亲委派模型的代码有啥区别:就是少了Class.forName(“com.mysql.jdbc.Driver”)这一行。
public class Driver extends NonRegisteringDriver implements java.sql.Driver { public Driver() throws SQLException { } static { try { DriverManager.registerDriver(new Driver()); } catch (SQLException var1) { throw new RuntimeException("Can't register driver!"); } } }
因为少了Class.forName(),因为就不会触发Driver的静态代码块,进而少了注册的过程。
现在,我们分析下看使用了这种spi服务的模式原本的过程是怎样的:
第一,从META-INF/services/java.sql.Driver文件中获取具体的实现类名“com.mysql.jdbc.Driver”
第二,加载这个类,这里肯定只能用class.forName(“com.mysql.jdbc.Driver”)来加载。好了,问题来了,现在这个调用者是DriverManager,加载DriverManager在rt.jar中,rt.jar是被启动类加载器加载的。还记得上面Class.forName()会使用当前的类加载器来加载对应的类。也就是说,启动类加载器会去加载com.mysql.jdbc.Driver,但真的可以加载得到吗?很明显不可以,为什么?因为om.mysql.jdbc.Driver肯定不在<JAVA_HOME>/lib下,所以肯定启动类加载器是无法加载com.mysql.jdbc.Driver这个类。这就是双亲委派模型的局限性:父类加载器无法加载子类加载器路径中的类。
问题我们定位出来了,接下来该如何解决?
我们分析一下,列出来:
- 一,可以肯定com.mysql.jdbc.Driver,只能由应用程序类加载器加载。
- 二,我们需要使用启动类加载器去获取应用程序类加载器,进而通过应用程序类加载器去加载com.mysql.jdbc.Driver。
那么问题就变为了:如何让启动类加载器去获取应用程序类加载器?
为了解决上述的问题,我们需要引入一个新概念:线程上下文类加载器
线程上下文类加载器(ThreadContextClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。
线程上下文加载器:可以让父类加载器通过调用子类加载器去加载类。
这里得注意一下:我们之前定义的双亲委派模型是:子类加载器调用父类加载器去加载类。现在相反了,换句说,其实已经破坏了双亲委派模型。
如果你看到这里,相信你已经会解答问题3了吧。
今天就到这里结束了!明天见!
参考资料
《深入理解JAVA虚拟机》
低情商的大仙——以JDBC为例谈双亲委派模型的破坏絮叨
非常感谢你能看到这里,如果觉得文章写得不错 求关注 求点赞 求分享 (对我非常非常有用)。
如果你觉得文章有待提高,我十分期待你对我的建议,求留言。
如果你希望看到什么内容,我十分期待你的留言。
各位的捧场和支持,是我创作的最大动力! -
双亲委派模型的破坏
2022-04-23 16:07:00有了线程上下文加载器,JNDI服务就可以使用它去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器,实际上已经违背了双亲...一、类加载机制
Java虚拟机把描述类的数据从Class文件加载进内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这动作的代码模块成为“类加载器”。
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载他的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类命名空间。简单的说就是:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。
JVM的类加载机制有:
- 全盘负责:当一个类加载器加载某个Class时,该Class所依赖和引用的其它的Class也由该类加载器负责载入,除非显示的使用另一个类加载器来载入;
- 双亲委派模型;
- 按需加载:类的加载是按需进行的,只有使用了才会被加载;
- 缓存机制:所有被加载过的Class都会被缓存,当要使用某个Class时,会先去缓存查找,如果缓存中没有才会读取class文件进行加载。
二、双亲委派模型
1、类加载器的分类
从虚拟机的角度来说,只存在两种不同类加载器:
- 启动类加载器(Bootstrap ClassLoader):使用C++语言实现(只限HotSpot),是虚拟机自身的一部分;
- 所有其他的类加载器:由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
从开发人员的角度来看,类加载还可以划分的更细致一些:
- 启动类加载器(Bootstrap ClassLoader):负责将存放在 JAVA_HOME/lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录下也不会加载)。启动类加载器无法被java程序直接引用,如果需要把加载请求委派给启动类加载器去处理,可以直接使用 null 替代;
- 扩展类加载器(Extension ClassLoader):由 sun.misc.Launcher$ExtClassLoader 实现,它负责加载 JAVA_HOME/lib/ext 目录下的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库。开发者可以直接使用扩展类加载器;
- 应用程序类加载器(Application ClassLoader):由 sun.misc.Launcher$AppClassLoader 实现。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以也叫做系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库。开发者可以直接使用这个类加载器,如果应用中没有定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
2、双亲委派工作流程
上图中各个类加载器之间的关系称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当由自己的父类加载器加载(类加载器之间的父子关系一般都使用组合关系来复用父加载器的代码)。
双亲委派模型在JDK1.2 期间被引入并被广泛应用于之后的所有Java程序中,但并不是个强制性的约束模型,而是Java设计者推荐给开发者的一种类加载器实现方式。
双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派父类加载器去完成。每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个请求时,子加载器才会尝试自己去加载。
3、双亲委派好处
- 避免类的重复加载,确保一个类的全局唯一性。Class与加载它的类加载器一起具备了一种带有优先级的层次关系,当父亲已经加载了该类时,就没有必要子ClassLoader 再加载一次;
- 保护程序安全,防止核心API被随意篡改。
4、双亲委派实现
双亲委派模型的实现在 java.lang.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 // 父类加载器无法成功加载 } if (c == null) { // 父类加载器无法加载,调用自身的 findClass 方法进行加载 long t1 = System.nanoTime(); c = findClass(name); // 定义类加载器,记录相关数据 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父加载器的 loadClass() 方法, 如父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException 异常后,再调用自己的 findClass() 方法进行加载。
5、破坏双亲委派模型
到目前为止(JDK8),双亲委派模型有过3次大规模的“被破坏”的情况。
第一次破坏
由于双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类 java.lang.ClassLoader 则在JDK1.0时代就已经存在,用户去继承 java.lang.ClassLoader 的唯一目的就是为了重写 loadClass() 方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法 loadClassInternal(),而这个方法唯一逻辑就是去调用自己的 loadClass() 。为了兼容这些已有代码,Java设计者引入双亲委派模型时不得不做出一些妥协,在JDK1.2之后的 java.lang.ClassLoader 中添加了一个新的 protected 方法 findClass() ,并引导用户编写类加载逻辑时,尽可能去重写这个方法,而不是在 loadClass() 中编写代码。
第二次破坏
双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的,双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码继承,调用的API存在。但是如果基础类又要调用回用户的代码,那该么办?
一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时放进去的 rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的 ClassPath 下的JNDI接口提供者的代码,但启动类加载器不可能“认识”这些代码。
为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过 java.lang.Thread 类的setContextClassLoader() 方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
有了线程上下文加载器,JNDI服务就可以使用它去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
第三次破坏
双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简单的说就是机器不用重启,只要部署上就能用。
OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当受到类加载请求时,OSGi将按照下面的顺序进行类搜索:
- 将java.*开头的类委派给父类加载器加载;
- 否则,将委派列表名单内的类委派给父类加载器加载;
- 否则,将 Import 列表中的类委派给 Export 这个类的 Bundle 的类加载器加载;
- 否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载;
- 否则,查找类是否在自己的 Fragment Bundle 中,如果在,则委派给 Fragment Bundle 的类加载器加载;
- 否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载;
- 否则,类加载器失败。
6、如何破坏双亲委派模型?
- 使用SPI机制;
- 自定义类继承 ClassLoader,作为自定义类加载器,重写 loadClass() 方法,不让它执行双亲委派逻辑,从而打破双亲委派。但是遇到自定义类加载器和核心类重名或者篡改核心类内容,jvm会使用沙箱安全机制,保护核心类,防止打破双亲委派机制,防篡改,如果重名的话就报异常。
三、有哪些破坏双亲委派模型的例子?
1、SPI
spi机制是一种服务发现机制。它通过在 ClassPath 路径下的 META-INF/services 文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如在JDBC中就使用到了SPI机制。
原生的JDBC中 Driver 驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库厂商去实现的。原生的JDBC中的类是放在 rt.jar 包的,是由启动类加载器进行类加载的,在JDBC中的 Driver 类中需要动态加载不同数据库类型的 Driver 类,而 mysql-connector-.jar 中的 Driver 类是用户自己写的代码,那启动类加载器肯定是不能进行加载的,于是乎,这个时候就引入SPI,程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了。
2、Tomcat
2.1、Tomcat为什么不使用默认的双亲委派模型?
Tomcat 作为一个 web 容器,存在以下使用场景:
- 部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离;
- 部署在同一个 web 容器中相同的类库相同的版本可以共享;
- 容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来;
- 要支持 jsp 的热部署(jsp 文件最终也是编译成 class 文件才能在虚拟机中运行)。
对于第一种和第三种场景,如果使用默认的类加载器机制,是无法加载两个相同类库的不同版本的,默认的类加载器只关注全限定类名,不关注是什么版本的。
第二种场景,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。
第四种场景,要实现 jsp 文件的热更新(jsp 文件其实也就是 class 文件),使用默认类加载器,如果修改了,但类的全限定名还是一样,类加载器会直接取方法区中已经存在的,修改后的 jsp 是不会重新加载的。2.2、Tomcat 如何实现自己的类加载机制?
Tomcat 自己实现了自己的类加载器:
- CommonLoader:Tomcat最基本的类加载器,加载路径中的 class 可以被Tomcat容器本身以及各个 Webapp 访问;
- CatalinaLoader:Tomcat容器私有的类加载器,加载路径中的 class 对于 Webapp 不可见;
- SharedClassLoader:各个 Webapp 共享的类加载器,加载路径中的 class 对于所有 Webapp可见,但是对于Tomcat容器不可见;
- WebappClassLoader:各个 Webapp 私有的类加载器,加载路径中的 class 只对当前 Webapp可见;
- JspClassLoader:每一个JSP文件对应一个Jsp类加载器。
从图中的委派关系中可以看出:
- CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,从而实现了公有类库的共用;
- CatalinaClassLoader 和 Shared ClassLoader 自己能加载的类则与对方相互隔离;
- WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个WebAppClassLoader 实例之间相互隔离;
- JasperLoader 的加载范围仅仅是这个JSP文件所编译出来的那一个 .Class 文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 JasperLoader 来实现JSP文件的热插拔功能。
四、参考资料
《深入理解java虚拟机》
-
JVM的类加载过程以及双亲委派模型详解
2020-08-25 22:01:41主要介绍了JVM的类加载过程以及双亲委派模型详解,类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。,需要的朋友可以参考下 -
JVM 类加载机制及双亲委派模型
2022-05-23 15:01:44class 的,loadClass 使用双亲委派模型。 先解析一下这张图,图表示类的整个声明周期,类从被加载到虚拟机内存开始,到卸载出内存为止,包含 7 个阶段,其中验证、准备、解析 3 个阶段统称为连接。 加载、验证、...一 、整体的流程
Java 中的所有类,必须被装载到 jvm 中才能运行,这个装载工作是由 jvm 中的类加载器完成的,类加载器所做的工作实质是把类文件从硬盘读取到内存中,JVM 在加载类的时候,都是通过 ClassLoader 的 loadClass()方法来加载 class 的,loadClass 使用双亲委派模型。
先解析一下这张图,图表示类的整个声明周期,类从被加载到虚拟机内存开始,到卸载出内存为止,包含 7 个阶段,其中验证、准备、解析 3 个阶段统称为连接。
加载、验证、准备、初始化和卸载这 5 个阶段的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持 Java 语言的运行时绑定(动态绑定或晚期绑定)。1、 装载
装载两个字说起来简单,但是对于 JVM 来说,这是个复杂的流程,也就是虚拟机的类加载机制:虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。
2、加载
这里所说的「加载」是「类加载」过程的一个阶段,「类加载」描述的是整个过程,「加载」仅表示「类加载」的第一阶段,需要完成以下三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表该类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
说这么多其实就完成了一件事情:根据一个类的名字(全限定名)在内存中生成一个 Class 对象,注意 Class 对象不是关键字 new 出来的那个对象,Class 是一种类型,表示的是一个对象的运行时类型信息。
接下来的三个阶段,都属于连接(Linking)。加载阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。
3、连接 - 验证
验证是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。如果验证到输入的字节流不符合 Class 文件格式的约束,虚拟机就会抛出一个 java.lang.VerifyError 异常或其子类异常。
验证阶段大致完成 4 个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。4、连接 - 准备
准备阶段是正式为类变量(static 修饰的变量)分配内存并设置类变量初始值的极端,这些变量所使用的内存都将在方法区中进行分配。注意此时进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。
并且这里提到的初始值是指零值,每种基本数据类型都有对应的零值。
假设一个类变量的定义为:public static int value = 234
那这个变量在准备阶段过后的初始值是0而不是234,把value赋值为123的动作将在初始化阶段才会执行。
5、连接 - 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
符号引用:只包含语义信息,不涉及具体实现,以一组符号来描述引用目标,是字面量;符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
直接引用:与具体实现息息相关,是直接指向目标的指针;直接引用是可以直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
6、初始化
初始化阶段,才真正开始执行类中定义的 Java 程序代码(或者说是字节码)
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。
也就是我们通常理解的赋初始值以及执行静态代码块。
二、类加载器
1、类与类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
比较两个类是否「相等」,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
2、加载器的种类
- 启动类加载器(Bootstrap ClassLoader):负责将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。
- 扩展类加载器(Extension ClassLoader):负责加载 <JAVA_HOME>\lib\ext 目录中的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库。
- 应用程序类加载器(Application ClassLoader):也称为系统类加载器,负责加载用户类路径(ClassPath)上所指定的类库。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
三、 双亲委派模型
上图所示的类加载器之间的层次关系,称为类加载器的双亲委派模型。双亲委派模型除了要求顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系一般不会以继承的关系类实现,而是都使用组合关系来复用父加载器的代码。
1、双亲委派模型的工作原理
1)如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成;
2)如果父类加载器还存在父类加载器,则进一步向上委托,一次递归,请求最终将到达顶层的启动类加载器;
3)如果父类加载器可以完成类的加载任务,就成功返回,如果父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
举例1:
自定义一个String类,并且创建的包也是java.lang包public class String { static { System.out.println("我是自定义类的String类的静态代码块"); } }
在Test类中使用String类
public class Test { public static void main(String[] args) { java.lang.String s = new java.lang.String(); System.out.println("hello world!!!"); } }
打印结果:
通过结果发现,String类使用的还是java核心类库里面的String类,并没有使用到用户自定义的String类。这个执行的过程里面就使用到了双亲委派机制。举例2:
public class String { static { System.out.println("我是自定义类的String类的静态代码块"); } public static void main(String[] args) { System.out.println("hello,String"); } }
运行结果:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为: public static void main(String[] args) 否则 JavaFX 应用程序类必须扩展javafx.application.Application Process finished with exit code 1
为什么会报错呢?是因为启动类加载器加载的是java核心类库里面的String类,加载完成后,在执行main方法的时候就报错了,是由于加载的String类中没有mian这个方法,所有就出现了上面报错的信息。
2、 双亲委派机制的优势
1)避免类的重复加载
2)保护程序安全,防止核心API被随意篡改在java.lang包下创建一个Test类
运行结果:
报错原因:由于是java.lang包下,所有就会选用启动类加载器去加载,又因为Test这个类在java.lang包下不存在,启动类加载器为了安全考虑,避免破坏核心类库,所有抛出安全异常。为什么要使用双亲委派模型
借用一个例子:黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。
而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。
也就是说,无论哪一个类加载器去加载一个系统中已有的类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此系统里在程序的各种类加载器环境中都是同一个类。
双亲委派模型是如何实现的
实现双亲委派的代码都几种在 java.lang.ClassLoader 的 loadClass() 方法中:先检查是否已经被加载过,若没有加载则调用父加载器的 loadClass() 方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载器加载失败,抛出 ClassNotFoundException 异常后,再调用自己的 findClass() 方法进行加载。(看源码后发现这里的抛出异常是被吞了,catch 之后不会做任何操作)。
破坏双亲委派模型
双亲委派模型并不是一个强制性的约束模型,而是 Java 设计者推荐给开发者的类加载器的实现方式。大部分的类加载器都遵循这个模型,但双亲委派模型也可以被破坏,破坏并不是不好,而是在有足够意义和理由的情况下,突破已有的规则进行创建,实现特定的功能。
三种破坏双亲委派模型的方式
- 重写 loadClass() 方法
- 逆向使用类加载器,引入线程上下文类加载器
- 追求程序的动态性:代码热替换、模块热部署等技术
-
JVM双亲委派模型
2021-09-14 19:34:58双亲委派模型 类加载器用来把类加载到Java虚拟机中,从JDK1.2版本开始,类的加载过程采用双亲委派机制,这种机制能更好地保证Java平台的安全 定义:如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去... -
什么是双亲委派模型?
2022-04-24 16:50:20双亲委派模型 原理:当一个类加载器收到类加载任务时,会先交给自己的父加载器去完成,因此最终加载任务都会传递到最顶层的BootstrapClassLoader,只有当父加载器无法完成加载任务时,才会尝试自己来加载。 -
JVM问题(一) -- 如何打破双亲委派模型
2022-04-13 20:23:411. 如何打破双亲委派模型 我们知道类的加载方式默认是双亲委派,如果我们有一个类想要通过自定义的类加载器来加载这个类,而不是通过系统默认的类加载器,说白了就是不走双亲委派那一套。即: 自定义类加载器 ,... -
Java双亲委派模型:为什么要双亲委派?如何打破它?破在哪里?
2021-06-01 23:06:591. 双亲委派就是类加载器之间的层级关系,加载类的过程是一个递归调用的过程,首先一层一层向上委托父类加载器加载,直到到达最顶层启动类加载器,启动类加载器无法加载时,再一层一层向下委托给子类加载器加载。... -
类加载器和双亲委派模型
2022-05-29 15:14:34双亲委派模型的工作过程: 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最... -
JVM 类加载器与双亲委派模型
2022-06-14 14:30:11这时候就需要双亲委派机制来告诉 JVM 使用哪个类加载器加载。在讲解什么是双亲委派机制之前,我们先看一下有哪些加载器。从 Java 虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器 Bootstrap ... -
类加载器以及双亲委派模型
2022-03-20 14:27:05自JDK1.2以来,Java一直保持着三层类加载器、双亲委派的类加载架构。 一.存放位置 1.启动类加载器(Bootstrap Class Loader): 存放在jre\lib 目录下的 rt.jar,启动类加载器无法被Java程序直接引用。 2.... -
什么是双亲委派模型
2022-03-12 22:45:48什么是双亲委派模型: 我们了解jvm是三层架构. 当需要加载一个类时, 自己不加载 让上一层加载 一直到最上层. 最上层 加载不到, 下层加载, 然后在下层 app - ext bootstrap 双亲委派模型有什么好处: 1.确保安全... -
为什么要破坏JVM的双亲委派模型
2021-09-28 09:16:47本文来说下为什么要破坏JVM的双亲委派模型 文章目录概述 概述 我原来面试的时候被问过一个这样的问题,「如果在你项目中建一个java.lang.String的类,那系统中用的String类是你定义的String类,还是原生api中的... -
关于双亲委派模型的几个问题
2021-08-29 13:39:19本文来说下关于双亲委派模型的几个问题 文章目录概述定义 概述 本文浅析了双亲委派的基本概念、实现原理、和自定义类加载器的正确姿势。 对于更细致的加载loading过程、初始化initialization顺序等问题,文中暂不... -
Java双亲委派模型是什么、优势在哪、双亲委派模型的破坏
2020-01-08 10:06:31Java双亲委派模型是什么、优势在哪、双亲委派模型的破坏 前言 双亲委派模型是Java加载类的机制.采用双亲委派模型的好处是Java类随着它的类加载器一起具备了一种带有优先级的层级关系,通过这种层级关系可以避免类... -
Java 双亲委派模型机制
2021-01-04 10:57:11再比如,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,在没有双亲委派模型的情况下,将会用自定义的类加载器加载,那系统中将会出现多个不同的... -
类加载的过程和双亲委派模型
2022-06-08 22:48:03安全性:使用双亲委派模型也可以保证了 Java 的核心 API 不被篡改,如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的... -
浅显易懂的带你掌握双亲委派模型
2021-06-17 16:44:11只有当父加载器无法加载这个类时,才会把加载请求传递给它的子加载器去尝试加载,流程如下: 双亲委派模型的作用 使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java中的类随着它的加载器一起... -
为什么要采用双亲委派模型
2022-07-08 18:22:06为什么要采用双亲委派模型 -
JVM之双亲委派模型
2020-06-15 08:44:57文章目录 双亲委派模型(Parents Delegation Model) 启动类加载器(Bootstrap Class Loader) 扩展类加载器(Extension Class Loader) 应用程序类加载器(Application Class Loader) 双亲委派模型的工作过程 参考... -
类加载器、双亲委派模型
2022-05-19 09:43:24双亲委派模型 一般来说java有着三层类加载器、双亲委派的类加载架构,今天我们来介绍一下是哪三层类加载器。 我们自定义一个类,然后通过类。class.getClassLoader查看我们自定类的类加载器,可以发现结果是... -
【JVM】双亲委派模型、优势及如何破坏双亲委派模型
2020-04-28 19:03:09** 一、基本概念 一个类是由加载它的类加载器和这个类本身来共同确定其在Java虚拟机中的唯一性。...双亲委派模型要求除了最顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器,在双亲委派模型中... -
破坏双亲委派模型的情况
2021-08-31 09:59:18破坏双亲委派模型 双亲委派主要出现过三次较大规模的“被破坏”的情况。 JDK1.2之后,为避免loadClass()被子类覆盖的可能,建议用户重写findClass()方法 JNDI作为java的标准服务,需要调用启动类加载器不认识的...