曹县商城网站建设,中国网站优化公司,怎么做网站宣传,天津市城乡建设网1、类加载器
1.1 类加载的概念 要了解双亲委派模型#xff0c;首先我们需要知道java的类加载器。所谓类加载器就是通过一个类的全限定名来获取描述此类的二进制字节流#xff0c;然后把这个字节流加载到虚拟机中#xff0c;获取响应的java.lang.Class类的一个实例。我们把实…1、类加载器
1.1 类加载的概念 要了解双亲委派模型首先我们需要知道java的类加载器。所谓类加载器就是通过一个类的全限定名来获取描述此类的二进制字节流然后把这个字节流加载到虚拟机中获取响应的java.lang.Class类的一个实例。我们把实现这个动作的代码模块称为“类加载器”。
1.2 类与类加载器 对于任意的一个类都需要由加载它的类加载器和这个类本身一同建立其在Java虚拟机中的唯一性每个类加载器都拥有一个独立的类名称空间即比较两个类是否“相等”只有在这两个类是由同一个类加载器加载的前提下才有意义否则即使这两个类源于同一个Class文件被同一个虚拟机加载只要加载它们的类加载器不同那这两个类就必定不想等。
package com.demo.test;import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;/*** author lxc* createTime 2023-02-09 10:20* description*/
public class ClassLoaderTest {public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {ClassLoader myloader new ClassLoader() {Overridepublic Class? loadClass(String name) throws ClassNotFoundException {String fileName name.substring(name.lastIndexOf(.) 1) .class;InputStream is getClass().getResourceAsStream(fileName);if (is null) {return super.loadClass(name);}try {byte[] b new byte[is.available()];is.read(b);return defineClass(name,b,0,b.length);} catch (IOException e) {throw new RuntimeException(e);}}};Object obj myloader.loadClass(com.demo.test.ClassLoaderTest).newInstance();System.out.println(obj.getClass());System.out.println(obj instanceof ClassLoaderTest);Class? clazz Class.forName(com.demo.test.ClassLoaderTest);Constructor? constructor clazz.getConstructor();Object obj1 constructor.newInstance();System.out.println(obj1 instanceof ClassLoaderTest);}
}运行结果如下
obj对象的Class:class com.demo.test.ClassLoaderTest
obj1对象的Class:class com.demo.test.ClassLoaderTest
false
true从运行结果来看第一句和第二句来看两个对象都是由类class com.demo.test.ClassLoaderTest实例化出来的对象 从第三句可以看出这个对象与类class com.demo.test.ClassLoaderTest做所属类型检查时却返回了false,这是因为虚拟机中存在了两个ClassLoaderTest一个是由我们自定义的类加载器加载的 另一个是由系统应用程序类加载器加载的虽然都来自同一个Class文件但依然是两个独立的类做对象所属类型检查时结果为false。
2、双亲委派模型
2.1 类加载器的分类 从虚拟机的角度来看存在两种不同的类加载器一种是启动类加载器Bootstrap ClassLoader,这个类加载器使用C语言实现是虚拟机自身的一部分另一个就是所有的其他类加载器这些类加载器都是由Java语言实现独立于虚拟机外部并且全都继承字抽象类java.lang.ClassLoader。 从开发者角度来看类加载器可以分为以下四类启动类加载器Bootstrap ClassLoader、扩展类加载器Extension ClassLoader、应用程序类加载Application ClassLoader和自定义类加载器。
启动类加载器Bootstrap ClassLoader这个类加载器是加载核心java库负责将JAVA_HOME/jre/lib目录中的或者被-Xbootclasspath参数所指定的路径中的并且是虚拟机识别的仅仅按照文件名识别如rt.jar,名字不符合的类库即使放在lib目录下也不会被加载类库加载到虚拟机内存中。开发者不能直接使用启动类加载器。扩展类加载器Extension ClassLoader这个类加载器是由sun.misc.Launcher$ExtClassLoader实现它负责加载JAVA_HOME/jre/lib/ext目录中的或者被java.ext.dirs系统变量所指定的路径中的所有类库开发者可以直接使用该类加载器。应用程序类加载器Application ClassLoader这个类加载器是由sun.misc.Launcher$AppClassLoader实现。这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值所以一般也称之为系统类加载器。它负责加载用户类路径ClassPath上所指定的类库开发者可以直接使用这个类加载器如果应用程序中没有定义过自己的类加载器一般情况下这就是程序中默认的类加载器。 通过上面的分类我们可以看到这三种类加载器只能加载各自所负责的目录下的类库而不能加载超过其目录范围的类库这也就是我们常常说的双亲委派模型中的可见性原则。 我们平时所写的应用程序都是由这三种类加载器相互配合进行加载的如果有必要还可以加上自己定的类加载器。
2.2 双亲委派模型中各类加载器之间的层次关系 类加载器之间这种层次关系我们称之为类加载的双亲委派模型。双亲委派模型中除了顶层的启动类加载器外其余的类加载器都应当有自己的父-类加载。这里的父子关系不是以继承关系来实现的而都是使用组合的关系来复用父-类加载的代码。
2.3 双亲委派模型中类加载的工作过程 如果一个类加载器收到了类加载的请求它首先不会自己去尝试加载这个类而是把这个请求委派给父-类加载去完成每一个层次的类加载器启动类加载器除外都是如此因此所有的加载请求最终都应该传送到顶层的启动类加载器中只有当父-类加载器反馈自己无法完成这个加载请求时子-类加载器才会尝试自己去加载。 下面我们看段源码从代码角度看一下这个工作过程
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.//在父-类加载器无法完成加载的时候再调用本身的findClass方法来进行类加载long t1 System.nanoTime();c findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}双亲委派模型对于保证java的稳定运行很重要但从上面的源码来看实现还是比较简单的双亲委派模型的核心代码主要都在java.lang.ClassLoader的loadClass()方法中大体逻辑如下 先检查是否已经被加载过若没有加载则调用父-类加载的loadClass()方法若父类加载为空则默认使用启动类加载器做父-类加载器加载。如果父-类加载器加载失败抛出ClassNotFoundException异常后再调用自己的findClass()方法进行加载。
2.4 双亲委派模型的目的 那么我们思考一下java为什么采用双亲委派模型呢从上面双亲委派模型的工作过程我们看出java类随着它的类加载器一起具备了带有优先级的层次关系。例如类java.lang.Integer,它存放在核心包rt.jar中那么无论哪一个类加载器要加载这个类最终都 是要委派给处于最顶端的启动类加载器进行加载从而是的Integer类在程序的各中类加载器环境中都是同一个类;相反如果没有使用双亲委派模型而是由各个类加载器自行去加载的话当用户自己编写了一个名为java.lang.Integer类并放到ClassPath中那么系统将会出现多个不同的Integer类这样就会造成java体系中最基础的行为都无法保证连最基本的类型都不唯一程序将变得一片混乱。你可能会说我自定义一个类加载去加载java.lang.Integer,直接重写loadClass方法从而破坏掉双亲委派模型不就行了。 我们写个简单的例子试下。
public class MyClassLoader extends ClassLoader {Overridepublic Class? loadClass(String name) throws ClassNotFoundException {String className null;if (name.startsWith(java.lang)) {className / name.replace(., /) .class;} else {className name.substring(name.lastIndexOf(.) 1) .class;}InputStream is getClass().getResourceAsStream(className);if (is null) {return super.loadClass(name);}try {byte[] bytes new byte[is.available()];is.read(bytes);return defineClass(name, bytes, 0, bytes.length);} catch (IOException e) {throw new RuntimeException(e);}}public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {ClassLoader myLoader new MyClassLoader();Object obj myLoader.loadClass(java.lang.Integer).newInstance();System.out.println(obj);}
}结果输出
Exception in thread main java.lang.SecurityException: Prohibited package name: java.langat java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)at java.lang.ClassLoader.defineClass(ClassLoader.java:761)at java.lang.ClassLoader.defineClass(ClassLoader.java:642)at com.demo.test.MyClassLoader.loadClass(MyClassLoader.java:28)at com.demo.test.MyClassLoader.main(MyClassLoader.java:36)从源码分析来看主要是defineClass方法调用的preDefineClass方法异常在preDefineClass这个方法中我们看到 if ((name ! null) name.startsWith(java.)) {throw new SecurityException(Prohibited package name: name.substring(0, name.lastIndexOf(.)));}即如果是以java.开头的包下的类都只能用启动类加载器来加载。
2.5 双亲委派模型的三个原则 双亲委派模型有三个基本原则委托性、可见性和唯一性原则。
委托性原则当子类加载器收到类加载请求时会将加载请求向上委托给父类加载器可见性原则每种类加载器都有自己可加载类库的范围超出这个范围是不可见的即无法加载的唯一性原则这是双亲委派模型的核心也是最重要的目的。
2.6 为什么要打破双亲委派模型 我们这里主要说一下JDBC为什么要打破双亲委派模型其他的方面我后续再分析。 我们以mysql数据库驱动为例来说明。最早我们使用mysql数据库驱动的时候一般是这样写代码 Class.forName(com.mysql.jdbc.Driver);Connection conn DriverManager.getConnection(jdbc:mysql://host:port/dbname?useUnicodetruecharacterEncodingutf-8useSSLfalse, username, password);其中com.mysql.jdbc.Driver下的Driver.class的源码如下 package com.mysql.jdbc;import java.sql.DriverManager;import java.sql.SQLException;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(Cant register driver!);}}}从com.mysql.jdbc的Driver.java源码中看到,在Driver类中向DriverManager注册了对应的驱动实现类。 而从JDBC4.0以后开始支持使用SPI的方式来注册这个Driver,这样当我们使用不同jdbc驱动时就不用手动修改Class.forName加载的驱动类只需要加入相关的jar包就行了。所以上面的数据库连接代码可以简写成如下
Connection conn DriverManager.getConnection(jdbc:mysql://host:port/dbname?useUnicodetruecharacterEncodingutf-8useSSLfalse, username, password);这就不需要Class.forName(“com.mysql.jdbc.Driver”)了。 了解SPI的同学都知道在DriverManager中这时候对应的驱动类大体是这么加载的 1.通过从META-INF/services/java.sql.Driver文件中获取具体的实现类”com.mysql.jdbc.Driver“ 2.通过Class.forName(“com.mysql.jdbc.Driver”)将这个类加载进来。 但是DriverManager是在java.sql中在rt.jar包中这个包中的类只能使用启动类加载器进行加载那么根据类加载的机制当被装载的类引用了另外一个类的时候虚拟机就会使用装载第一个类的类装载器装载被引用的类。即启动类加载器还要去加载mysql驱动jar中的类com.mysql.jdbc.Driver,这显然是不可能的根据双亲委派模型的可见性原则启动类加载器找不到这个mysql类库所以无法加载。 这个问题更加有适用性的说法应该是JAVA核心包中的类去调用开发者实现的类的方法这时候就会出现启动类加载器无法加载到具体实现类的问题。所以想让启动类加载器顶层类加载器加载可见范围之外的类库只能破坏双亲委派模型中的可见性原则让启动类加载器可以”加载“到可见范围之外的类库。主要这里我加了个引号因为这个地方并不是真的是由启动类加载器加载了com.mysql.jdbc.Driver这个类库其实还是由Application ClassLoader系统类加载器加载完成的只不过从表面上看起来是破坏了可见行原则实质上并没有破坏双亲委派原则。 下面我们看DriverManager是怎么实现的。 DriverManager加载时会执行静态代码块在静态代码块中会执行loadInitialDrivers方法。而这个方法中会加载对应的驱动类。
public class DriverManager {static {loadInitialDrivers();println(JDBC DriverManager initialized);}private static void loadInitialDrivers() {String drivers;try {drivers AccessController.doPrivileged(new PrivilegedActionString() {public String run() {return System.getProperty(jdbc.drivers);}});} catch (Exception ex) {drivers null;}AccessController.doPrivileged(new PrivilegedActionVoid() {public Void run() {// 根据配置文件加载驱动实现类下面这个方法中说明了所使用的类加载器ServiceLoaderDriver loadedDrivers ServiceLoader.load(Driver.class);IteratorDriver driversIterator loadedDrivers.iterator();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 ServiceLoaderS load(ClassS service) {//使用了一个线程上下文类加载器ClassLoader cl Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);} ExtClassLoader和AppClassLoader都是通过Launcher类来创建的在Launcher类的构造函数中我们可以看到线程上下文类加载器默认是AppClassLoader。Launcher类中无参构造方法
public Launcher() {ExtClassLoader var1;try {var1 Launcher.ExtClassLoader.getExtClassLoader();} catch (IOException var10) {throw new InternalError(Could not create extension class loader, var10);}try {this.loader Launcher.AppClassLoader.getAppClassLoader(var1);} catch (IOException var9) {throw new InternalError(Could not create application class loader, var9);}//设置当前线程的上下文类加载器就是AppClassLoaderThread.currentThread().setContextClassLoader(this.loader);String var2 System.getProperty(java.security.manager);if (var2 ! null) {SecurityManager var3 null;if (!.equals(var2) !default.equals(var2)) {try {var3 (SecurityManager)this.loader.loadClass(var2).newInstance();} catch (IllegalAccessException var5) {} catch (InstantiationException var6) {} catch (ClassNotFoundException var7) {} catch (ClassCastException var8) {}} else {var3 new SecurityManager();}if (var3 null) {throw new InternalError(Could not create SecurityManager: var2);}System.setSecurityManager(var3);}2.7 如何打破双亲委派模型 在ClassLoader中有几个核心方法上面我们已经展示了loadClass的基本源码下面我们再简略看一下去掉了一些代码细节 package java.lang;public abstract class ClassLoader {protected Class defineClass(byte[] b); protected Class? findClass(String name); protected Class? loadClass(String name, boolean resolve) {synchronized (getClassLoadingLock(name)) {// 1. 检查类是否已经被加载过Class? c findLoadedClass(name);if (c null) {try {if (parent ! null) {//2. 委托给父类加载c parent.loadClass(name, false);} else {//3. 父类不存在的交给启动类加载器c findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) { }if (c null) {//4. 父类加载器无法完成类加载请求时调用自身的findClass方法来完成类加载c findClass(name);}}return c;}
}defineClass 方法调用 native 方法将 字节数组解析成一个 Class 对象;findClass 方法抽象类ClassLoader中默认抛出ClassNotFoundException需要继承类自己去实现目的是通过文件系统或者网络查找类;loadClass 方法 首先根据类的全限定名检查该类是否已经被加载过如果没有被加载那么当子加载器持有父加载器的引用时那么委托给父加载器去尝试加载如果父类加载器无法完成加载再交给子类加载器进行加载。loadClass方法 就是实现了双亲委派机制。 ClassLoader 的三个重要方法那么如果需要自定义一个类加载器的话直接继承 ClassLoader类一般情况只需要重写 findClass 方法即可自己定义加载类的路径可以从文件系统或者网络环境。但是如果想打破双亲委派机制那么还要重写 loadClass 方法。