-
2021-06-11 17:40:49
双亲委派机制时JVM类加载的默认使用的机制,其原理是:当一个类加载器收到类加载任务时,会先交给自己的父加载器去完成,因此最终加载任务都会传递到最顶层的BootstrapClassLoader,只有当父加载器无法完成加载任务时,才会尝试自己来加载。按照由父级到子集的顺序,类加载器主要包含以下几个:
- BootstrapClassLoader(启动类加载器):主要负责加载核心的类库(java.lang.*等),JVM_HOME/lib目录下的,构造ExtClassLoader和APPClassLoader。
- ExtClassLoader (拓展类加载器):主要负责加载jre/lib/ext目录下的一些扩展的jar
- AppletClassLoader(系统类加载器):主要负责加载应用程序的主函数类
- 自定义类加载器:主要负责加载应用程序的主函数类
了解类加载器的基本原理和基本概念之后,进入我们今天的主题:
- 双亲委派机制有什么缺陷?
- 如何打破双亲委派机制?
问题1:通过双亲委派机制的原理可以得出一下结论:由于BootstrapClassloader是顶级类加载器,BootstrapClassloader无法委派AppClassLoader来加载类,也就是说BootstrapClassloader中加载的类中无法使用由AppClassLoader加载的类。可能绝大部分情况这个不算是问题,因为BootstrapClassloader加载的都是基础类,供AppClassLoader加载的类调用的类。但是万事万物都不是绝对的比如经典的JAVA SPI机制。
首先我们先了解下JAVA SPI机制:
SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种 服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。
SPI具体约定:
Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader。以mysql-conneator-java-5.1.37Java包说明SPI机制:
以上截图展示了SPI使用的三要素:
- 实现类的java包位置要放在主程序的classpath中;
- 在实现类的jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
- 主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
这就引申出来我们对双亲委派机制的缺陷的讨论,接口:java.sql.Driver,定义在java.sql包中,包所在的位置是:jdk\jre\lib\rt.jar中,java.sql包中还提供了其它相应的类和接口比如管理驱动的类:DriverManager类,很明显java.sql包是由BootstrapClassloader加载器加载的;而接口的实现类com.mysql.jdbc.Driver是由第三方实现的类库,由AppClassLoader加载器进行加载的,我们的问题是DriverManager再获取链接的时候必然要加载到com.mysql.jdbc.Driver类,这就是由BootstrapClassloader加载的类使用了由AppClassLoader加载的类,很明显和双亲委托机制的原理相悖,那它是怎么解决这个问题的?这就引申了我们第二个问题:如何打破双亲委派机制?
首先看下手动获取数据库连接的代码:
// 加载Class到AppClassLoader(系统类加载器),然后注册驱动类 // Class.forName("com.mysql.jdbc.Driver").newInstance(); String url = "jdbc:mysql://localhost:3306/testdb"; // 通过java库获取数据库连接 Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");
我们很惊喜的发现:加载JDBC驱动程序实现的代码Class.forName("com.mysql.jdbc.Driver").newInstance();被注释掉,代码依然能够正常运行,这很奇怪, 继续查看DriverManager.getConnection(url,"name","password");重点就是DriverManager类的静态代码块,我们都是知道调用类的静态方法会初始化该类,然后执行该类静态代码块,DriverManager的静态代码块如下:
static { loadInitialDrivers(); println("JDBC DriverManager initialized"); }
继续查看 loadInitialDrivers();如下:
private static void loadInitialDrivers() { String drivers; try { drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { //获取环境变量中jdbc.drivers的列表 return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } // If the driver is packaged as a Service Provider, load it. // Get all the drivers through the classloader // exposed as a java.sql.Driver.class service. // ServiceLoader.load() replaces the sun.misc.Providers() //如果按照spi的约定在jar包中的META-INF/services设置了文件,将会加载为服务 AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); /* Load these drivers, so that they can be instantiated. * It may be the case that the driver class may not be there * i.e. there may be a packaged driver with the service class * as implementation of java.sql.Driver but the actual class * may be missing. In that case a java.util.ServiceConfigurationError * will be thrown at runtime by the VM trying to locate * and load the service. * * Adding a try catch block to catch those runtime errors * if driver not available in classpath but it's * packaged as service and that service is there in classpath. */ try{ while(driversIterator.hasNext()) { //依次加载所有驱动 driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); println("DriverManager.initialize: jdbc.drivers = " + drivers); //如果环境变量中没有设置的驱动程序,就可以结束了 //否则就将环境变量中的驱动程序加载一下 if (drivers == null || drivers.equals("")) { return; } String[] driversList = drivers.split(":"); println("number of Drivers:" + driversList.length); for (String aDriver : driversList) { try { println("DriverManager.Initialize: loading " + aDriver); Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); } } } ------------------------------------------------------------------------------------ public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
对于以上源码有几点说明:
- 我们前文提过JAVA SPI使用的扫描服务实现类的工具类是ServiceLoader,很凑巧我们在源码中发现了这个方法,这说明DriverManager.getConnection()方法在被调用的时候就已经从classpath中去加载由第三方实现的java.sql.Driver接口的实现类了。继续查看ServiceLoader.load(Driver.class);方法发现类加载器使用的是线程上下文类加载器,这是打破双亲委托机制的关键。
- 按照loadedDrivers.iterator()->next()->nextService()调用连查看源码最终发现c = Class.forName(cn, false, loader);这个方法是文章开头的Class.forName()被注释掉但是文章仍然能够继续运行的关键。因为在DriverManager中的初始化代码中已经注册过了。但在这里我有一个疑问1:既然驱动类已经已经在ServiceLoader.load(Driver.class)方法中被加载过了,为什么在Class.forName(cn, false, loader);方法中注册驱动类的时候还要传递一个类加载器的参数,这样做由什么意义?但是我们可以大胆的推测loader一定不是启动类加载器,因为启动类加载器没法加载classpath下的类。
- loadInitialDrivers()加载里两个位置的驱动程序(代码中已有注释),环境变量中jdbc.drivers的列表和类路径下符合SPI规范的jar包,前者使用的是Class.forName(aDriver,true,ClassLoader.getSystemClassLoader());进行加载,而类加载器使用的是:ClassLoader.getSystemClassLoader();后者使用的是load(Driver.class)方法中的线程上下文类加载器。接下来我们看下ClassLoader.getSystemClassLoader();源码,按照ClassLoader.getSystemClassLoader()->initSystemClassLoader();>scl= l.getClassLoader()发现ClassLoader.getSystemClassLoader()的返回值是类Launcher的一个成员变量并且在Launcher的构造方法中进行初始化,最终的返回值是this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);应用类加载器(查看下方截图1处代码),在这里我有一个疑问2:Class.forName(aDriver,true,ClassLoader.getSystemClassLoader());这段代码所在类的类加载器是启动类加载器,但是代码中使用了应用类加载器,这样可以使用吗?如果可以那在启动类加载器加载的类中使用应用类的时候直接指定应用类加载器去加载就可以了,为什么还要使用线程上下文类加载器?
以上两个疑问后续解决,不影响我们对如何打破双亲委托机制的讨论,现在我们已经知道,在DriverManager中去加载SPI中配置的java.sql.Driver接口的实现类使用的是线程上下文类加载器。ContextClassLoader默认存放了AppClassLoader的引用(查看下方截图2处代码),由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成加载类的操作,简单来说:在BootstrapClassLoader或ExtClassLoader加载的类A中如果使用到AppClassLoader类加载器加载的类B,由于双亲委托机制不能向下委托,那可以在类A中通过线上线程上下文类加载器获得AppClassLoader,从而去加载类B,这不是委托,说白了这是作弊,也是JVM为了解决双亲委托机制的缺陷不得已的操作!
拓展:
简单介绍一下Class.forName();这个方法由两个作用:
- 装载一个类并对其进行实例化
- Class.forName();使用的类加载器默认是当前类加载器,但是可以为之传递一个加载器
源码如下:
public static Class<?> forName(String className) throws ClassNotFoundException { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); } ------------------------------------------------------------------------------------------ public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException { Class<?> caller = null; SecurityManager sm = System.getSecurityManager(); if (sm != null) { // Reflective call to get caller class is only needed if a security manager // is present. Avoid the overhead of making this call otherwise. caller = Reflection.getCallerClass(); if (sun.misc.VM.isSystemDomainLoader(loader)) { ClassLoader ccl = ClassLoader.getClassLoader(caller); if (!sun.misc.VM.isSystemDomainLoader(ccl)) { sm.checkPermission( SecurityConstants.GET_CLASSLOADER_PERMISSION); } } } return forName0(name, initialize, loader, caller); }
更多相关内容 -
双亲委派机制以及打破双亲委派机制
2021-07-31 15:38:30双亲委派机制以及打破双亲委派机制 双亲委派机制 Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存中生成class对象,而且加载某个类的class文件时,Java虚拟机...双亲委派机制以及打破双亲委派机制
双亲委派机制
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存中生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式.
双亲委派机制的优点:
- 1、避免类的重复加载
- 2、保护程序的安全性,防止核心API随意被篡改
沙箱安全机制:保证对Java信息源代码的保护
何为沙箱安全机制?即如果我们自己建立和源代码相同的包,例如java/lang/String.Class,在我们去使用类加载器去加载此类时,为了防止你自定义的类对源码的破坏,所以他默认不是使用你的String类的本身的系统加载器去加载它,而是选择率先使用引导类加载器去加载,而引导类在加载的过程中会先去加载JDK自带的文件(rt.jar包中的java/lang/String.class),而不是你自己定义的String.class,报错信息会提示没有main方法 ,就是因为加载的是rt.jar包下的String类,这样就可以做到保证对java核心源代码的保护,这即是沙箱保护机制.
那么双亲委派机制存在哪些缺点,又有哪些打破双亲委派机制的例子呢?
通过上面双亲委派机制的特点,我们知道了以下结论:
由于BootStrapClassLoader是顶级类加载器,并且它不能使用AppClassLoader加载器加载的类,因为BootstrapClassloader无法委派AppClassLoader来加载类。故当我们的上层类加载器需要下层类加载器帮忙加载类,就会出现问题,这便是双亲委派机制的缺点,那么哪些场景发生了这些事情,而Java又是如何打破这种双亲委派机制的呢?
我们来举一个经典的打破双亲委派机制的例子:Java SPI机制
SPI(Service Provider Interface),主要是应用于厂商自定义组件或插件中。
java SPI提供一种服务发现的机制,即为某个接口寻找服务实现的机制,我们在需要在jar包的META-INF/services/目录里面创建一个一服务接口命令的文件,而该文件就是实现这个接口的具体实现类,当有外部程序需要装配我们的这个模块接口的时候,就可以通过META-INF/services/目录的配置文件找到对应的实现类的类名,并将其实例化,完成该接口模块的注入。而在JDK中,也提供了一个服务实现查找的一个工具类:java.util.ServiceLoader。
我们以数据库连接jar包为例:mysql-connector-java-5.1.37.jar,它遵循SPI的规范
我们的java.sql.Driver接口定义在java.sql包下,而该包的位置位于jdk\jre\lib\rt.jar,其中java.sql包中还提供了其它相应的类和接口比如管理驱动的类:DriverManager类等等,这样我们就很清楚地知道了java.sql是由BootStrapClassLoader加载器加载的
而我们的接口的实现类com.mysql.jdbc.Driver是第三方实现的类库,是由AppClassLoader加载器加载的
那么问题来了:我们使用DriverManager获取Connection的时候,必然会加载到com.mysql.jdbc.Driver类,此时即是BootstrapClassloader加载的类使用了由AppClassLoader加载的类,很明显和双亲委托机制的原理相悖!!!
JVM采取了一种作弊的方式打破了双亲委派机制,从而完成了上层类加载器使用下层类加载器进行加载的类:
在BootstrapClassLoader或ExtClassLoader加载的类A中,如果使用到AppClassLoader类加载器加载的类B,由于双亲委托机制不能向下委托,那可以在类A中通过线程上下文类加载器获得AppClassLoader,从而去加载类B
我们通过源码来看一下:
首先我们创建一个web项目,然后导入mysql的驱动包,我们编写一下测试代码:
public class TestClassLoader { public static void main(String[] args) throws SQLException, ClassNotFoundException { String driverClassName = "com.mysql.jdbc.Driver"; String url = "jdbc:mysql://localhost:3306/mybatis"; String username = "root"; String password = "root"; Connection connection = null; PreparedStatement pstmt = null; ResultSet rs = null; //执行驱动"com.mysql.jdbc.Driver" Class.forName("com.mysql.jdbc.Driver"); //获取连接 connection = DriverManager.getConnection(url,username,password); } }
此时我们知道这段代码肯定是能正常执行了,第一步加载我们的mysql的驱动,然后利用DriverManager获取连接
那么我们将第一步的执行驱动代码注释掉,你猜猜还会执行成功吗??
根据上面我们说的,答案是肯定的!!!我们进入源码探究一下在DriverManager类的内部存在一个静态代码块,其中有一个**loadInitialDrivers()**方法:
我们点进去,里面有一行代码:
//JDK中提供的ServiceLoader来实现服务实现查找功能 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
我们点进去这个方法看看:
public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
这段代码便是打破双亲委派机制,实现com.mysql.jdbc.Driver驱动类加载的核心,即我们获取线程上下文加载器Thread.currentThread().getContextClassLoader(),然后利用线程上下文类加载器进行mysql驱动的类加载,以达到打破双亲委派机制的作用
PS:此时如果我们改变线程上下文类加载器,就会使得此时程序出现错误,大家也就明白了其中的流程和原理了
public class TestClassLoader { public static void main(String[] args) throws SQLException, ClassNotFoundException { String driverClassName = "com.mysql.jdbc.Driver"; String url = "jdbc:mysql://localhost:3306/mybatis"; String username = "root"; String password = "root"; Connection connection = null; PreparedStatement pstmt = null; ResultSet rs = null; //改变当前线程上下文类加载器为当前类的扩展类加载器 Thread.currentThread().setContextClassLoader(TestClassLoader.class.getClassLoader().getParent()); //执行驱动"com.mysql.jdbc.Driver" //Class.forName("com.mysql.jdbc.Driver"); //获取连接 connection = DriverManager.getConnection(url,username,password); } }
执行结果:
Exception in thread "main" java.sql.SQLException: No suitable driver found for jdbc:mysql://localhost:3306/mybatis at java.sql.DriverManager.getConnection(DriverManager.java:689) at java.sql.DriverManager.getConnection(DriverManager.java:247) at it.feng.test.TestClassLoader.main(TestClassLoader.java:25)
-
双亲委派机制
2021-08-25 20:17:26双亲委派机制 JVM类加载器是有亲子层级结构的,如下图 这里类加载其实就有一个双亲委派机制,加载某个类时会先委托父加载器寻找目标类,找不到再 委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找...双亲委派机制
JVM类加载器是有亲子层级结构的,如下图
这里类加载其实就有一个双亲委派机制,加载某个类时会先委托父加载器寻找目标类,找不到再 委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的 类加载路径中查找并载入目标类。 比如我们的Math类,最先会找应用程序类加载器加载,应用程序类加载器会先委托扩展类加载 器加载,扩展类加载器再委托引导类加载器,顶层引导类加载器在自己的类加载路径里找了半天 没找到Math类,则向下退回加载Math类的请求,扩展类加载器收到回复就自己加载,在自己的 类加载路径里找了半天也没找到Math类,又向下退回Math类的加载请求给应用程序类加载器, 应用程序类加载器于是在自己的类加载路径里找Math类,结果找到了就自己加载了。
双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载为什么要设计双亲委派机制?
沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改。
避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,保证被加载类的唯一性。全盘负责委托机制
全盘负责是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类 所依赖及引用的类也由这个ClassLoder载入。自定义类加载器示例(沿用双亲委派机制):
自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是 loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空 方法,所以我们自定义类加载器主要是重写findClass方法。import java.io.FileInputStream; import java.lang.reflect.Method; 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; } @Override 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(); } } }// MyClassLoader public static void main(String args[]) throws Exception{ // 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器 // 的父加载器设置为应用程序类加载器AppClassLoader MyClassLoader classLoader = new MyClassLoader("D:\\111"); // 如果运行环境也有User类,就不会用自定义ClassLoader加载 Class clazz = classLoader.loadClass("User"); Object obj = clazz.newInstance(); Method method = clazz.getDeclaredMethod("sout",null); method.invoke(obj,null); System.out.println(clazz.getClassLoader().getClass().getName()); } }
运行结果:
打破双亲委派机制
尝试打破双亲委派机制,用自定义类加载器加载我们自己实现的java.lang.String.class,除了重写findClass方法还要重写loadClass方法。import java.io.FileInputStream; import java.lang.reflect.Method; import java.lang.String; 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; } @Override 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(); } } /** * 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载 * @param name * @param resolve * @return */ @Override 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 t1 = System.nanoTime(); //非自定义的类还是走双亲委派加载 if (!name.startsWith("com.tom")) { c = this.getParent().loadClass(name); }else{ c = findClass(name); } // this is the defining class loader;record the stats sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); }// if c == null if(resolve){ resolveClass(c); } return c; } } }// MyClassLoader public static void main(String args[]) throws Exception{ // 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器 // 的父加载器设置为应用程序类加载器AppClassLoader MyClassLoader classLoader = new MyClassLoader("D:/111"); // java.lang.SecurityException: Prohibited package name: java.lang 限制包名, // 不能自定义这个包名,与java类库冲突,安全管理器不通过 // Class clazz = classLoader.loadClass("java.lang.String"); // 但是换个包名就可以 Class clazz = classLoader.loadClass("com.tom.String"); Object obj = clazz.newInstance(); Method method = clazz.getDeclaredMethod("sout",null); method.invoke(obj,null); System.out.println(clazz.getClassLoader().getClass().getName()); } }
Tomcat打破双亲委派机制
以Tomcat类加载为例,Tomcat 如果使用默认的双亲委派类加载机制行不行? 我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:- 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的 不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是 独立的,保证相互隔离。
- 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程 序,那么要有10份相同的类库加载进虚拟机。
- web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的 类库和程序的类库隔离开来。
- web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中 运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启。
再看看我们的问题:Tomcat 如果使用默认的双亲委派类加载机制行不行? 答案是不行的。为什么?
第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认 的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。
第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。
第三个问题和第一个问题一样。
我们再看第四个问题,我们想我们要怎么实现jsp文件的热加载,jsp 文件其实也就是class文 件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp 是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想 到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载 器。重新创建类加载器,重新加载jsp文件。
Tomcat自定义加载器详解
tomcat的几个主要类加载器:- commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容 器本身以及各个Webapp访问;
- catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不 可见; -sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有 Webapp可见,但是对于Tomcat容器不可见;
- WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前 Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本, 这样实现就能加载各自的spring版本;
从图中的委派关系中可以看出:
CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用, 从而实现了公有类库的共用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则 与对方相互隔离。
WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader 实例之间相互隔离。
而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的 就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例, 并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。
tomcat 这种类加载机制违背了java 推荐的双亲委派模型了吗?答案是:违背了。
很显然,tomcat 不是这样实现,tomcat 为了实现隔离性,没有遵守这个约定,每个 webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委 派机制。
模拟实现Tomcat的webappClassLoader加载自己war包应用内不同版本类实现相互共存与隔离import java.io.FileInputStream; import java.lang.reflect.Method; import java.lang.String; 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; } @Override 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(); } } /** * 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载 * @param name * @param resolve * @return */ @Override 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 t1 = System.nanoTime(); //非自定义的类还是走双亲委派加载 if (!name.startsWith("com.tom")) { c = this.getParent().loadClass(name); }else{ c = findClass(name); } // this is the defining class loader;record the stats sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); }// if c == null if(resolve){ resolveClass(c); } return c; } } }// MyClassLoader public static void main(String args[]) throws Exception{ // 初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器 // 的父加载器设置为应用程序类加载器AppClassLoader MyClassLoader classLoader = new MyClassLoader("D:/111"); // java.lang.SecurityException: Prohibited package name: java.lang 限制包名, // 不能自定义这个包名,与java类库冲突,安全管理器不通过 // Class clazz = classLoader.loadClass("java.lang.String"); // 但是换个包名就可以 Class clazz = classLoader.loadClass("com.tom.String"); Object obj = clazz.newInstance(); Method method = clazz.getDeclaredMethod("sout",null); method.invoke(obj,null); System.out.println(clazz.getClassLoader()); MyClassLoader classLoader1 = new MyClassLoader("D:/111"); Class clazz1 = classLoader1.loadClass("com.tom.String"); Object obj1 = clazz1.newInstance(); Method method1 = clazz1.getDeclaredMethod("sout",null); method1.invoke(obj1,null); System.out.println(clazz1.getClassLoader()); } }
-
双亲委派机制和破坏双亲委派机制
2021-04-02 21:29:20双亲委派机制 要了解双亲委派,必须得知道类加载器,类加载器是用来把class文件加载进内存的 类加载器3层架构 启动类加载器:C++实现,是虚拟机的一部分,java虚拟机能够识别,无法被java程序直接引用(null)。负责...双亲委派机制
要了解双亲委派,必须得知道类加载器,类加载器是用来把class文件加载进内存的
类加载器3层架构
- 启动类加载器:C++实现,是虚拟机的一部分,java虚拟机能够识别,无法被java程序直接引用(null)。负责加载放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,只加载指定的文件,如:rt.jar、tools.jar,按照文件名识别,名称不符合的类库即使放在lib目录下也不会被加载
- 扩展类加载器:负责加载<JAVA_HOME>\lib\ext目录中、或者被java.ext.dirs系统变量所指定的路径中的所有类库。允许用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能
- 应用程序(系统)类加载器:负责加载用户类路径(ClassPath)上所有类库,和扩展类加载器一样,开发者同样可以直接在代码中使用这个类加载器。如果程序中没有自定义类加载器,一般情况下这个就是程序中默认的类加载器
从虚拟机的角度看,扩展类加载器和应用程序类加载器相比启动类加载器属于其他类加载器,由Java语言实现,独立存在于虚拟机外部,并且全部继承抽象类java.lang.ClassLoader
类加载过程
双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当由自己的父类加载器加载,破坏双亲委派就是打破这种关系破坏双亲委派机制
如果想打破双亲委派模型则需要重写loadClass()方法(当然其中的坑也不会少)。典型的打破双亲委派模型的框架和中间件有tomcat与osgi
tomcat破坏双亲委派机制
上面三部分满足双亲委派模型,但是下面的自定义类加载器不满足:不满足由自己的父类加载器加载这种关系,是为了实现隔离性,下面的自定义类加载器只会加载自己目录下的class文件,而不传递给父加载器加载
osgi破坏双亲委派机制
动态模块化规范,没有固定的委派关系,模块Bundle之间的依赖关系从传统的上层模块依赖底层模块转变为平级模块之间的依赖,如下图所示:
-
JVM类加载机制、双亲委派机制、自定义类加载器、打破双亲委派机制
2022-03-17 21:38:01中对应的类加载器就是AppClassLoader,满足双亲委派机制 4、打破双亲委派机制 例如在Tomact中部署多个项目,每个项目使用的相同但不用版本的组件 自定义类加载器,在加载类时,没有遵循双亲委派机制(先委托父加载器... -
Java双亲委派机制
2021-09-09 15:29:31这就不得不提到"双亲委派机制"。 首先,我们需要知道的是,Java语言系统中支持以下4种类加载器: Bootstrap ClassLoader 启动类加载器 Extention ClassLoader 标准扩展类加载器 Application ClassLoader 应用类加载... -
双亲委派机制及其弊端
2021-09-22 16:34:55目录标题双亲委派机制?优点:双亲委派机制有什么缺陷?详解:如何打破双亲委派机制?上下文类加载器的使用? 双亲委派机制? 参考: https://zhuanlan.zhihu.com/p/185612299 ...双亲委派机制时JVM类加载的默认使用的... -
双亲委派机制及打破双亲委派示例
2021-01-01 20:22:03双亲委派机制 在加载类的时候,会一级一级向上委托,判断是否已经加载,从自定义类加载器-》应用类加载器-》扩展类加载器-》启动类加载器,如果到最后都没有加载这个类,则回去加载自己的类。 双亲委托有个弊端... -
【Code皮皮虾】带你盘点双亲委派机制【原理、优缺点】,以及如何打破它?
2021-10-06 13:02:30前提知识:线程上下文类加载器双亲委派出现之前JDBC打破双亲委派机制Tomcat如何打破双亲委派机制?1.自定义类加载器2. 使用线程上下文类加载器????福利???? Java入门到就业学习路线规划???? 小白快速入门Python爬虫... -
Tomcat打破双亲委派机制
2021-11-30 13:22:39沙箱安全机制示例,尝试打破双亲委派机制,用自定义类加载器加载自己实现的 java.lang.String.class public class MyClassLoaderTest { static class MyClassLoader extends ClassLoader { private String ... -
通俗易懂的双亲委派机制
2018-09-27 23:24:08在介绍双亲委派机制的时候,不得不提ClassLoader。说ClassLoader之前,我们得先了解下Java的基本知识。 Java是运行在Java的虚拟机(JVM)中的,但是它是怎么就运行在JVM中了呢?我们在IDE中编写的Java源代码被编译器... -
java类加载-ClassLoader双亲委派机制
2021-03-08 18:28:19“类加载体系”及ClassLoader双亲委派机制。java程序中的 .java文件编译完会生成 .class文件,而 .class文件就是通过被称为类加载器的ClassLoader加载的,而ClassLoder在加载过程中会使用“双亲委派机制”来加载 .... -
为什么使用双亲委派机制以及如何破坏双亲委派
2021-05-30 15:46:50一、什么是双亲委派机制? 当某个特定的类加载器它在接到需要加载类的请求时,这个类会首先查看自己已加载完的类中是否包含这个类,如果有就返回,没有的话就会把加载的任务交给父类加载器加载,以此递归,父类加载... -
如何打破双亲委派机制
2021-02-21 22:57:28双亲委派机制 第一次知道何为打破双亲委派机制是通过阅读周志明的《深入理解Java虚拟机》,我们知道双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给... -
类加载器及双亲委派机制
2021-06-10 00:48:58一. 类的加载 java class是由class loader 类加载器加载到JVM内存中的。 看下下面的demo,java中有三种类... 双亲委派机制 2. 为什么采用双亲委派机制 这种机制能保证系统级别的类不会被用户覆盖,防止危险代码的植入。 -
摸个鱼的功夫,搞懂双亲委派机制
2022-03-14 23:50:53五分钟,搞懂双亲委派机制。八股文扛把子,岂能不会! -
JVM - 彻底理解打破双亲委派机制
2020-06-12 00:11:41JVM-白话聊一聊JVM类加载和双亲委派机制源码解析 JVM - 自定义类加载器 何为打破双亲委派 举个例子 有个类 Artisan 我们希望通过自定义加载器 直接从某个路径下读取Artisan.class . 而不是说 通过自定义加载器 委托... -
【Tomcat框架】Tomcat如何打破双亲委派机制?
2021-08-02 14:23:40Tomcat如何打破双亲委派机制? Tomcat是一个Web框架,并且可以支持部署多个web项目,web项目在Tomcat被抽象的称为Context,即每一个web项目是一个Context。而每个Context是独立的,比如项目A可以引用spring1.0,而... -
[五]类加载机制双亲委派机制 底层代码实现原理 源码分析 java类加载双亲委派机制是如何实现的
2021-03-08 18:28:12Launcher启动类本文是双亲委派机制的源码分析部分,类加载机制中的双亲委派模型对于jvm的稳定运行是非常重要的不过源码其实比较简单,接下来简单介绍一下我们先从启动类说起有一个Launcher类 sun.misc.Launcher;... -
双亲委派机制的局限有哪些?
2021-03-15 16:23:23双亲委派机制大家都知道吧,其实它也是有缺陷的,这次我们要聊的就是双亲委派机制的局限性,一起来了解一下吧。一、因为双亲委派模型是JDK1.2之后才被引入,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0时就... -
双亲委派机制的原理和作用是什么?
2022-01-07 14:53:43说到双亲委派机制,就必须要先要弄清楚Java的类加载器。 什么是类加载器? Java类加载器(ClassLoader)是Java运行时环境(JRE)的一部分,负责动态的将Java类加载到Java虚拟机的内存空间。 类加载器主要有三个: ... -
什么是双亲委派机制?
2022-03-19 23:31:26简介 Java虚拟机对class文件...双亲委派机制的优点 优势 避免类的重复加载 保护程序安全,防止核心API被随意篡改 自定义类: java. lang. String(有引导类加载器加载,不会加载自定义String) 自定义类: java. lang. -
Java中的双亲委派机制以及如何打破
2020-07-31 18:51:55Java中的双亲委派机制以及如何打破 什么是双亲委派机制 当一个类收到了类的加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一层的类加载器都是如此,因此所有的加载请求都应该传送到... -
双亲委派机制的原理?为什么要使用双亲委派机制?有什么缺点,可以打破这个机制吗,如何打破?
2022-03-08 15:28:14类加载器是分层级的,遵循**双亲委派**机制: 最上层是**Bootstrap ClassLoder**,加载java的**核心类库**,加载java安装目录下的**lib目录**的class文件 第二层是**Ext ClassLoder**,加载一些java的**其他类库**...