JVM字节码与类的加载(二):类加载器
前面讲解了类的装载过程,其中第一个阶段是加载环节。
在Java语言中,实现该环节的工具就是类加载器(ClassLoader
)。
(一)概述
类加载器从文件系统或者网络中加载class文件到JVM内部,至于class文件是否可以运行,则由执行引擎决定,类加载器将加载的类信息存放到方法区。
类加载器在整个装载阶段,只能影响到类的加载,而无法改变类的链接和初始化行为。它最早出现在Java 1.0版本中,当时主要为了满足Java Applet应用的需要,虽然目前JavaApplet应用极少,但类加载器并没有随之消失不见,相反类加载器在OSGi、热部署等领域依然应用广泛。
这主要是因为JVM没有将所有的类加载器绑定在JVM内部,这样做的好处就是能够更加灵活和动态地执行类加载操作。
1.类加载的分类
类的加载分为显式加载和隐式加载两种类型。
显式加载指的是在代码中通过类加载器的方法加载class对象,如直接使用Class.forName(name)
或this.getClass().getClassLoader().loadClass()
加载class对象。
隐式加载则是不直接在代码中调用类加载器的方法加载class文件,而是通过JVM自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。
在日常开发中以上两种方式一般会混合使用。
2.类加载器的必要性
一般情况下,Java开发人员并不需要在程序中显式地使用类加载器,但是了解类加载器的加载机制却显得至关重要。
主要原因有以下几个方面。
- (1)避免在开发中遇到
java.lang.ClassNotFoundException
异常或java.lang.NoClassDefFoundError
异常时手足无措。 - (2)只有了解类加载器的加载机制,才能够在出现异常的时候快速地根据错误异常日志定位并解决问题。
- (3)需要支持类的动态加载或需要对编译后的class文件进行加解密操作时,就需要与类加载器打交道。
- (4)开发人员可以在程序中编写自定义类加载器来重新定义类的加载规则,以便实现一些自定义的处理逻辑。
3.命名空间
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在JVM中的唯一性。
每个类加载器都有自己的命名空间,命名空间由该类加载器及所有的父类加载器组成,在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类;在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。
4.类加载机制的基本特征
(1)双亲委派模型
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载。
(2)可见性
子类加载器可以访问父类加载器加载的类型,但是反过来是不允许的。不然,因为缺少必要的隔离,就没有办法利用类加载器去实现容器的逻辑。
(3)单一性
由于父类加载器的类型对于子类加载器是可见的,所以父类加载器中加载过的类型,就不会在子加载器中重复加载。但是注意,同一个类仍然可以被同级别的类加载器加载多次,因为互相并不可见。
(二)类加载器分类
JVM支持两种类型的类加载器,分别为启动类加载器(Bootstrap ClassLoader
)和自定义类加载器(User-Defined ClassLoader
)。
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
无论类加载器的类型如何划分,在程序中我们最常见的类加载器结构如图所示,其中扩展类加载器和应用程序类由抽象类ClassLoader派生而来。
除了顶层的启动类加载器,其余的类加载器都应当有自己的“父类”加载器。不同类加载器看似是继承关系,实际上是聚合关系。
在下层加载器中,包含着上层加载器的引用,也就是说应用程序类加载器并不是扩展类加载器的子类。
如代码清单所示,展示了类加载器直接的包含关系,定义了ParentClassLoader
和ChildClassLoader
两个类继承抽象类ClassLoader
。
/*** 类加载器的包含关系*/
public abstract class ClassLoader {ClassLoader parent; // 父类加载器public ClassLoader(ClassLoader parent) {this.parent = parent;}
}class ParentClassLoader extends ClassLoader {public ParentClassLoader(ClassLoader parent) {super(parent);}
}class ChildClassLoader extends ClassLoader {public ChildClassLoader(ClassLoader parent) {// parent = new ParentClassLoader();super(parent);}
}
习惯上把ChildClassLoader
称为子类加载器,ParentClassLoader
称为父类加载器,但是它们之间并不是继承关系,而是在构造子类的时候以参数的形式传入ParentClassLoader
而已,在实例化ChildClassLoader
时,构造器形参使用ParentClassLoader
实例进行赋值,给属性初始化。
虽然说法上称为父类加载器,但是却不是继承关系,需要注意这一点。
1.引导类加载器
引导类加载器(BootstrapClassLoader,又称启动类加载器)使用C/C++语言实现,嵌套在JVM内部。
引导类加载器不继承java.lang.ClassLoader
,没有父类加载器。
出于安全考虑,引导类加载器主要用来加载Java的核心库,也就是“JAVA_HOME/jre/lib/rt.jar”
或“sun.boot.class.path”
路径下的内容,指定为扩展类和应用程序类加载器的父类加载器。
使用-XX:+TraceClassLoading
参数可以得到类加载器加载了哪些类,注意该参数只能得到所有加载器加载的全部类文件,不能得到各个加载器加载了什么类。查看引导类加载器加载的类文件。
/*** 查看引导类加载器加载范围*/
public class ClassLoaderTest {public static void main(String[] args) {String pathBoot = System.getProperty("sun.boot.class.path");System.out.println("BootStrapClassLoader 加载范围 " + "开始 -------");System.out.println(pathBoot.replaceAll(";", System.lineSeparator()));System.out.println("BootStrapClassLoader 加载范围 " + "结束 -------");}
}
输出结果如下:
BootStrapClassLoader 加载范围 开始 -------
E:\Environment\jdk1.8\jre\lib\resources.jar
E:\Environment\jdk1.8\jre\lib\rt.jar
E:\Environment\jdk1.8\jre\lib\sunrsasign.jar
E:\Environment\jdk1.8\jre\lib\jsse.jar
E:\Environment\jdk1.8\jre\lib\jce.jar
E:\Environment\jdk1.8\jre\lib\charsets.jar
E:\Environment\jdk1.8\jre\lib\jfr.jar
E:\Environment\jdk1.8\jre\classes
BootStrapClassLoader 加载范围 结束 -------
引导类加载器加载了如上路径的类文件。
2.扩展类加载器
扩展类加载器(ExtensionClassLoader
)由Java语言编写,该类的全路径名为sun.misc.Launcher$ExtClassLoader,ExtClassLoader
是Launcher
类的内部类,间接继承于ClassLoader
类,父类加载器为启动类加载器,类的继承关系如图所示。
扩展类加载器主要负责从java.ext.dirs
系统属性所指定的目录或者JDK的安装目录的jre/lib/ext
子目录下加载类库。
如果用户创建的类放在上述目录下,也会自动由扩展类加载器加载。简言之扩展类加载器主要负责加载Java的扩展库。
/*** 扩展类加载器加载范围*/
public class ClassLoaderTest {public static void main(String[] args) {System.out.println("ExtClassLoader 加载范围开始 -----");String pathExt = System.getProperty("java.ext.dirs");System.out.println(pathExt.replaceAll(";",System.lineSeparator()));System.out.println("ExtClassLoader 加载范围结束 -----");}
}
运行结果如下:
ExtClassLoader 加载范围开始 -----
E:\Environment\jdk1.8\jre\lib\ext
C:\Windows\Sun\Java\lib\ext
ExtClassLoader 加载范围结束 -----
扩展类加载器加载了如上路径的类文件,如下所示。
access-bridge-64.jar
cldrdata.jar
dnsns.jar
jaccess.jar
localedata.jar
meta-index
nashorn.jar
sunec.jar
sunjce_provider.jar
sunmscapi.jar
sunpkcs11.jar
zipfs.jar
3.应用程序类加载器
应用程序类加载器(AppClassLoader
)和扩展类加载器一样也是由Java语言编写,该类的全路径名为sun.misc.Launcher$AppClassLoader
,间接继承于ClassLoader类,父类加载器为扩展类加载器,应用程序类加载器也称系统类加载器。
它负责加载环境变量classpath
或系统属性java.class.path
指定路径下的类库,应用程序中的类加载器默认是应用程序类加载器。
它是用户自定义类加载器的默认父类加载器,通过ClassLoader
的getSystemClassLoader()
方法可以获取到该类加载器。
查看应用类加载器加载的类文件,如下所示
/*** 应用类加载器加载范围*/
public class ClassLoaderTest {public static void main(String[] args) {System.out.println("AppClassLoader 加载范围开始 ------------");String pathApp = System.getProperty("java.class.path");System.out.println(pathApp.replaceAll(";", System.lineSeparator()));System.out.println("AppClassLoader 加载范围结束 ------------");}
}
输出如下所示:
AppClassLoader 加载范围开始 ------------
C:\Users\Administrator\AppData\Local\Temp\classpath2016029688.jar
D:\Program Files\IntelliJ IDEA 2024.2.2\lib\idea_rt.jar
AppClassLoader 加载范围结束 ------------
4.自定义类加载器
在Java的日常应用程序开发中,类的加载几乎是由前面讲解的3种类加载器相互配合执行的。必要时,还可以自定义类加载器来定制类的加载方式。
体现Java语言强大生命力和巨大魅力的关键因素之一便是Java开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地的JAR包,也可以是网络上的远程资源。
以下是自定义类加载器的好处。
(1)插件机制。
通过类加载器可以实现非常绝妙的插件机制,这方面的实际应用案例不胜枚举。
例如,著名的OSGi组件框架,再如Eclipse的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制,这种机制无须重新打包发布应用程序就能实现。
(2)隔离加载类。
在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境中。
比如Tomcat这类Web应用服务器,内部自定义了好几种类加载器,用于隔离同一个Web应用服务器上的不同应用程序。再比如两个模块依赖某个类库的不同版本,如果分别被不同的类加载器加载,就可以互不干扰。
(3)修改类加载的方式。
类的加载模型并非强制,除引导类加载器外,其他的类加载器并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载。
(4)扩展加载源。
应用需要从不同的数据源获取类定义信息,例如网络数据源,而不是本地文件系统。或者是需要自己操纵字节码,动态修改或者生成类型。
(5)提高程序安全性。
在一般情况下,使用不同的类加载器去加载不同的功能模块,会提高应用程序的安全性。
但是,如果涉及Java类型转换,则加载器反而容易产生不美好的事情。
在做Java类型转换时,只有两个类型都是由同一个加载器所加载,才能进行类型转换,否则转换时会发生异常。用户通过定制自己的类加载器,可以重新定义类的加载规则,以便实现一些自定义的处理逻辑。
(三)获取不同的类加载器
每个对象都会包含一个定义它的类加载器的一个引用。
public class ClassLoaderTest {public static void main(String[] args) {// 获取当前类的类加载器ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();System.out.println("获取当前类的类加载器 = " + classLoader);// 获取当前线程的上下文加载器ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();System.out.println("获取当前线程的上下文加载器 = " + contextClassLoader);// 获取系统类加载器ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();System.out.println("获取系统类加载器 = " + systemClassLoader);// 获取扩展类加载器ClassLoader extClassLoader = systemClassLoader.getParent();System.out.println("获取扩展类加载器 = " + extClassLoader);// 获取引导类加载器ClassLoader bootstrapClassLoader = extClassLoader.getParent();System.out.println("获取引导类加载器 = " + bootstrapClassLoader);}
}
输出结果如下
获取当前类的类加载器 = sun.misc.Launcher$AppClassLoader@18b4aac2
获取当前线程的上下文加载器 = sun.misc.Launcher$AppClassLoader@18b4aac2
获取系统类加载器 = sun.misc.Launcher$AppClassLoader@18b4aac2
获取扩展类加载器 = sun.misc.Launcher$ExtClassLoader@7ca48474
获取引导类加载器 = null
需要注意的是,引导类加载器结果为null,原因是引导类加载器是C++语言编写,并不是一个java对象,所以这里用null展示。
数组类的Class对象,不是由类加载器创建的,而是在Java运行期JVM根据需要自动创建的。
数组类的类加载器可以通过Class.getClassLoader()
方法返回,如果数组元素是引用数据类型,类加载器与数组当中元素类型相同,如果数组元素类型是基本数据类型,数组类没有类加载器。
/*** <pre>* desc : 获取数组类加载器* </pre>*/
public class ClassLoaderTest {public static void main(String[] args) {String[] strArr = new String[6];System.out.println(strArr.getClass().getClassLoader());ClassLoaderTest[] classLoaderTests = new ClassLoaderTest[1];System.out.println(classLoaderTests.getClass().getClassLoader());int[] intArr = new int[2];System.out.println(intArr.getClass().getClassLoader());}
}
输出如下:
null
sun.misc.Launcher$AppClassLoader@18b4aac2
null
要注意的是第一个null表示引导类加载器,第二个null表示当数组中元素为基本数据类型时,结果也为null,但是第二个null表示的含义是没有类加载器,而不是引导类加载器。
(四)类加载器源码解析
类加载器源码解析前面我们多次提到了抽象类java.lang.ClassLoader,该类在类加载中起着至关重要的作用,除了引导类加载器之外,其他的类加载器都需要继承它,所以对该类的源码学习就显得尤为重要。
ClassLoader与JVM提供的类加载器关系如图所示。
从图中可以看到ClassLoader类位于所有类加载器的顶层,前面讲述了通过ClassLoader.getSystemClassLoader()
来获得系统类加载器,下面讲述其获取过程。
(1)通过ClassLoader.getSystemClassLoader()
进入源码分析,如下所示。
public static void main(String[] args) {// 1. 获取 classloader 的方法ClassLoader classLoader = ClassLoader.getSystemClassLoader();
}
(2)进入getSystemClassLoader()
方法,如下所示,其中需要重点关注initSystemClassLoader()方法。
public static ClassLoader getSystemClassLoader() {initSystemClassLoader();if (scl == null) {return null;}SecurityManager sm = System.getSecurityManager();if (sm != null) {checkClassLoaderPermission(scl, Reflection.getCallerClass());}return scl;
}
(3)initSystemClassLoader()
方法的作用是对应用程序类加载器进行初始化,代码如下。
// 初始化系统加载器,包括初始化父类加载器
private static synchronized void initSystemClassLoader() {// boolean 类型的静态变量,标记是否被初始化了,解决并发问题if (!sclSet) {if (scl != null) {throw new IllegalStateException("recursive invocation");}// 获取 Launcher 类实例,加载器都是它的内部类,直接去看 Launcher 源码sun.misc.Launcher l = sun.misc.Launcher.getLauncher();if (l != null) {Throwable oops = null;// scl 为 classloader 内部的缓存静态变量,存储系统类加载器scl = l.getClassLoader();try {// 是否用户指定了默认的加载类scl = AccessController.doPrivileged(new SystemClassLoaderAction(scl));} catch (PrivilegedActionException pae) {oops = pae.getCause();if (oops instanceof InvocationTargetException) {oops = oops.getCause();}}if (oops != null) {if (oops instanceof Error) {throw (Error) oops;} else {// wrap the exceptionthrow new Error(oops);}}}// 初始化完毕sclSet = true;}
}
通过获取Launcher
类实例的代码直接跳转到Launcher类的源码,如下。
// 包含部分源代码
public class Launcher {private static URLStreamHandlerFactory factory = new Factory();private static Launcher launcher = new Launcher();public static Launcher getLauncher() {return launcher;}// 定义类加载器private ClassLoader loader;// 返回类加载器public ClassLoader getClassLoader() {return loader;}// 构造方法public Launcher() {// 1. 创建 ExtClassLoaderClassLoader extcl;try {extcl = ExtClassLoader.getExtClassLoader();} catch (IOException e) {throw new InternalError("Could not create extension class loader");}// 2. 用 ExtClassLoader 作为 parent 去创建 AppClassLoadertry {loader = AppClassLoader.getAppClassLoader(extcl);} catch (IOException e) {throw new InternalError("Could not create application class loader");}// 3. 设置 AppClassLoader 为 ContextClassLoaderThread.currentThread().setContextClassLoader(loader);// ...}// 定义内部类扩展类加载器 ExtClassLoaderstatic class ExtClassLoader extends URLClassLoader {private File[] dirs;public static ExtClassLoader getExtClassLoader() throws IOException {final File[] dirs = getExtDirs();return new ExtClassLoader(dirs);}public ExtClassLoader(File[] dirs) throws IOException {super(getExtURLs(dirs), null, factory);this.dirs = dirs;}private static File[] getExtDirs() {String s = System.getProperty("java.ext.dirs");File[] dirs;// ...return dirs;}}// 定义内部类系统类加载器 AppClassLoaderstatic class AppClassLoader extends URLClassLoader {public static ClassLoader getAppClassLoader(final ClassLoader extcl)throws IOException {final String s = System.getProperty("java.class.path");final File[] path = (s == null) ? new File[0] : getClassPath(s);URL[] urls = (s == null) ? new URL[0] : pathToURLs(path);return new AppClassLoader(urls, extcl);}AppClassLoader(URL[] urls, ClassLoader parent) {super(urls, parent, factory);}public synchronized Class loadClass(String name, boolean resolve)throws ClassNotFoundException {int i = name.lastIndexOf('.');if (i != -1) {SecurityManager sm = System.getSecurityManager();if (sm != null) {// sm.checkPackageAccess(name.substring(0, i));}}return (super.loadClass(name, resolve));}}
}
Launcher源码里定义了static类型的扩展类加载器ExtClassLoader
和static类型的系统类加载器AppClassLoader
。
如下面代码所示,在ExtClassLoader构造器里,并没有指定parent,或者说ExtClassLoader
的parent为null。
因为ExtClassLoader
的parent是BootstrapLoader
,而BootstrapLoader不存在于Java API里,只存在于JVM里,我们是看不到的,所以请正确理解“ExtClassLoader的parent为null”的含义。
public ExtClassLoader(File[] dirs) throws IOException {super(getExtURLs(dirs), null, factory);this.dirs = dirs;
}
如下面代码所示,在 AppClassLoader 构造器里有了 parent。实例化 AppClassLoader 的时候,传入的 parent 就是一个 ExtClassLoader 实例。
AppClassLoader(URL[] urls, ClassLoader parent) {super(urls, parent, factory);
}
Launcher 的构造方法如下。
public Launcher() {// 1. 创建 ExtClassLoaderClassLoader extcl;try {extcl = ExtClassLoader.getExtClassLoader();} catch (IOException e) {throw new InternalError("Could not create extension class loader");}// 2. 用 ExtClassLoader 作为 parent 去创建 AppClassLoadertry {loader = AppClassLoader.getAppClassLoader(extcl);} catch (IOException e) {throw new InternalError("Could not create application class loader");}// 3. 设置 AppClassLoader 为 ContextClassLoaderThread.currentThread().setContextClassLoader(loader);// ...
}
首先,实例化ExtClassLoader,从java.ext.dirs系统变量里类加载路径,也说明了为什么扩展类加载器加载的路径是java.ext.dirs,如下。
private static File[] getExtDirs() {// 这里说明了为什么扩展类加载器加载的路径是“java.ext.dirs”String s = System.getProperty("java.ext.dirs");File[] dirs;if (s != null) {StringTokenizer st = new StringTokenizer(s, File.pathSeparator);int count = st.countTokens();dirs = new File[count];for (int i = 0; i < count; i++) {dirs[i] = new File(st.nextToken());}} else {dirs = new File[0];}return dirs;
}
通过ExtClassLoader
作为parent去实例化AppClassLoader
,从java.class.path
系统变量里获得类加载路径,如下所示。
public static ClassLoader getAppClassLoader(final ClassLoader extcl)throws IOException {// 应用类加载器获取加载目录final String s = System.getProperty("java.class.path");final File[] path = (s == null) ? new File[0] : getClassPath(s);return AccessController.doPrivileged(new PrivilegedAction<AppClassLoader>() {public AppClassLoader run() {URL[] urls = (s == null) ? new URL[0] : pathToURLs(path);return new AppClassLoader(urls, extcl);}});
}/** Creates a new AppClassLoader*/
AppClassLoader(URL[] urls, ClassLoader parent) {super(urls, parent, factory);
}
最终Launcher getClassLoader()返回的就是AppClassLoader。以上便是获取类加载器源码的分析。
(五)自定义类加载器
Java提供了抽象类java.lang.ClassLoader
,所有用户自定义的类加载器都应该继承ClassLoader
类。
(1)重写findClass()
方法
前面讲了在自定义ClassLoader
子类的时候,常见的有两种做法,即重写loadClass()
方法或者重写findClass()
方法,重写findClass()
方法是比较推荐的方式。
这两种方法本质上差不多,毕竟loadClass()
也会调用findClass()
,但是从逻辑上讲最好不要直接修改loadClass()
的内部逻辑,建议的做法是只在findClass()
里重写自定义类的加载方法,根据参数指定类的名字,返回对应的Class对象的引用。
loadClass()
这个方法是实现双亲委派模型逻辑的地方,擅自修改这个方法会导致模型被破坏,容易造成问题。因此最好是在双亲委派模型框架内进行小范围的改动,不破坏原有的稳定结构。
同时,也避免了自己重写loadClass()
方法的过程中必须写双亲委托的重复代码,从代码的复用性来看,不直接修改这个方法始终是比较好的选择。
当编写好自定义类加载器后,便可以在程序中调用loadClass()
方法来实现类加载操作。
需要注意的是自定义类加载器的父类加载器是应用程序类加载器。
(1)创建自定义类加载器MyClassLoader
如代码清单所示,重写findClass()
方法即可。
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;public class MyClassLoader extends ClassLoader {private String rootDir;public MyClassLoader(String rootDir) {this.rootDir = rootDir;}protected Class<?> findClass(String className) {Class clazz = this.findLoadedClass(className);FileChannel fileChannel = null;WritableByteChannel outChannel = null;if (null == clazz) {try {String classFile = getClassFile(className);FileInputStream fis = new FileInputStream(classFile);fileChannel = fis.getChannel();ByteArrayOutputStream baos = new ByteArrayOutputStream();outChannel = Channels.newChannel(baos);ByteBuffer buffer = ByteBuffer.allocateDirect(1024);while (true) {int i = fileChannel.read(buffer);if (i == 0 || i == -1) {break;}buffer.flip();outChannel.write(buffer);buffer.clear();}byte[] bytes = baos.toByteArray();clazz = defineClass(className, bytes, 0, bytes.length);} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {try {if (fileChannel != null) {fileChannel.close();}} catch (IOException e) {e.printStackTrace();}try {if (outChannel != null) {outChannel.close();}} catch (IOException e) {e.printStackTrace();}}}return clazz;}/*** 类文件的完全路径*/private String getClassFile(String className) {return rootDir + "\\" + className.replace('.', '\\') + ".class";}
}
(2)测试加载类
接下来进行类加载测试
public class MyClassLoaderTest {public static void main(String[] args) {MyClassLoader loader = new MyClassLoader("d:/");try {Class clazz = loader.loadClass("com.example.Demo1");System.out.println("加载此类的类的加载器为: " + clazz.getClassLoader().getClass().getName());System.out.println("加载当前 Demo1 类的类的加载器的父类加载器为: " + clazz.getClassLoader().getParent().getClass().getName());} catch (ClassNotFoundException e) {e.printStackTrace();}}
}
需要注意的是,自定义类加载器在调用clazz =defineClass(className,bytes,0,bytes.length)
方法的过程中,如果传入className
类的全路径名称,比如Demo1的全路径名称为com.example.Demo1
,
比如加载下面的自定义类:
package com.example;public class Demo1 {public void say(){System.out.println("hello world");}
}
如果通过IDE启动的话,输出结果如下:
加载此类的类的加载器为: sun.misc.Launcher$AppClassLoader
加载当前 Demo1 类的类的加载器的父类加载器为: sun.misc.Launcher$ExtClassLoader
因为根据双亲委派模型可知,该类会优先去找父类的sun.misc.Launcher$AppClassLoader
类加载器加载。
如果想让自定义类加载器加载类Demo1,需要将编译后的Demo1.class
文件放在d:/com/example/
目录下,并确保不在IDE的classpath
中。
删除classpath里的Demo1.class类后,输出结果如下。
加载此类的类的加载器为: MyClassLoader
加载当前 Demo1 类的类的加载器的父类加载器为: sun.misc.Launcher$AppClassLoader
这样就实现用自定义加载器来加载类了。
(六)双亲委派模型
1.定义与本质
类加载器用来把类文件加载到JVM内存中。从JDK1.2版本开始,类的加载过程采用双亲委派模型,这种机制能更好地保证Java平台的安全。
(1)双亲委派定义
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载,如图所示。
(2)双亲委派本质
规定了类加载的顺序。首先是引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由应用程序类加载器或自定义的类加载器进行加载,如图所示。
2.双亲委派模型的优势与劣势
下面我们谈谈双亲委派模型的优势与劣势。
(1)双亲委派模型优势
避免类的重复加载,确保一个类的全局唯一性,Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父类加载器已经加载了该类时,子类加载器就没有必要再加载一次。
这样做可以保护程序安全,防止核心API被随意篡改,比如JVM不允许定义一个java.lang.String
的类,会出现java.lang.SecurityException
,类加载器会做安全检查。
(2)代码支持
双亲委派模型在java.lang.ClassLoader.loadClass(String,boolean)
接口中体现。
该接口的逻辑如下。
- (1)在当前加载器的缓存中查找有无目标类,如果有,直接返回。
- (2)判断当前加载器的父类加载器是否为空,如果不为空,则调用
parent.loadClass(name,false)
接口进行加载。 - (3)反之,如果当前加载器的父类加载器为空,则调用
findBootstrapClassOrNull(name)
接口,让引导类加载器进行加载。 - (4)如果通过以上3条路径都没能成功加载,则调用
findClass(name)
接口进行加载。该接口最终会调用java.lang.ClassLoader
接口的defineClass
系列的native接口加载目标Java类。
假设当前加载的是java.lang.Object
这个类,很显然,该类属于JDK中核心得不能再核心的一个类,因此一定只能由引导类加载器进行加载。
当JVM准备加载java.lang.Object时,JVM默认会使用应用程序类加载器去加载,按照上面4步加载的逻辑,在第1步从系统类的缓存中肯定查找不到该类,于是进入第2步。
由于从应用程序类加载器的父类加载器是扩展类加载器,于是扩展类加载器继续从第1步开始重复。由于扩展类加载器的缓存中也一定查找不到该类,因此进入第2步,最终通过引导类加载器进行加载。
需要注意的是如果在自定义的类加载器中重写java.lang.ClassLoader#loadClass(String)
或java.lang.ClassLoader#loadClass(String,boolean)
方法,抹去其中的双亲委派机制,仅保留上面这4步中的第1步与第4步。
虽然可以这样操作,但是这样却不能加载核心类库,因为JDK还为核心类库提供了一层保护机制,不管是自定义的类加载器,还是应用程序类加载器抑或扩展类加载器,最终都必须调用java.lang.ClassLoader#defineClass(String,byte[],int,int,ProtectionDomain)
方法,该方法会执行java.lang.ClassLoader#preDefineClass()
方法,该方法中提供了对JDK核心类库的保护。
(3)双亲委派模型劣势
检查类加载的委派过程是否为单向的,这个方式虽然从结构上说比较清晰,使各个类加载器的职责非常明确,但同时会带来一个问题,即顶层的类加载器无法访问底层的类加载器所加载的类。
通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器加载的类为应用类。
按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问题。
比如在系统类中提供了一个接口,该接口需要在应用类中实现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。
这时,就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。
所以Java虚拟机规范并没有明确要求类加载器的加载机制一定要使用双亲委派模型,只是建议采用这种方式而已,比如在Tomcat中,类加载器所采用的加载机制就和传统的双亲委派模型有一定区别,当缺省的类加载器接收到一个类的加载任务时,首先会由它自行加载,当它加载失败时,才会将类的加载任务委派给它的超类加载器去执行,这同时也是Servlet规范推荐的一种做法。
3.破坏双亲委派模型
双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型,但也有例外的情况,如下所示,注意破坏双亲委派模型并不一定就是一件坏事,如果有特殊需求,完全可以主动破坏双亲委派模型。
(1)破坏双亲委派机制一
重写loadClass()方法破坏双亲委派模型,我们前面讲过双亲委派模型就是通过这个方法实现的,这个方法可以指定类通过什么加载器来加载,所以如果我们改写它的规则,就相当于打破了双亲委派模型。重写这个方法以后就能自己定义使用什么加载器了,也可以自定义加载委派机制。
其实JDK在早期版本中已经发生过一次破坏了,双亲委派模型是在JDK 1.2之后引入的,但是类加载器的概念和抽象类java.lang.ClassLoader在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协,为了兼容这些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),findClass()方法就是为了让开发人员在自定义类加载器的时候不要重写loadclass()方法以免破坏双亲委派模型,但是loadClass()方法比双亲委派模型出现得早,有很多程序已经重写了loadClass()方法,这已经是没有办法避免的事情了。
(2)破坏双亲委派机制二
双亲委派模型有一定的局限性,父类加载器无法访问子类加载器路径中的类。
双亲委派模型最典型的不适用场景就是SPI的使用,Java中所有涉及SPI的加载动作基本都采用这种方式,例如JNDI
、JDBC
等。所以提供了一种线程上下文类加载器,能够使父类加载器调用子类加载器进行加载。
简单来说就是接口定义在了启动类加载器中,而实现类定义在了其他类加载器中,当启动类加载器需要加载其他子类加载器路径中的类时,需要使用线程上下文类加载器(默认是应用程序类加载器),这样以上下文加载器为中介,使得启动类加载器中的代码也可以访问应用类加载器中的类。
比如Java中的核心jar包rt.jar
,该包下面的类是由引导类加载器加载的,但是如果rt.jar包想要访问jdbc.jar
中的类该怎么办呢?
我们知道jdbc.jar
是由应用程序类加载器加载的,此时引导类加载器就会委托线程上下文类加载器去加载jdbc.jar
,间接访问子类加载器中的类了,如图所示。
(3)破坏双亲委派机制三
双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的,如代码热替换(Hot Swap
)、模块热部署(Hot Deployment
)等。
IBM公司实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。
在OSGi环境下,类加载器不再是双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构。
4.热替换的实现
热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为。热替换的关键需求在于服务不能中断,修改必须立即表现正在运行的系统之中。基本上大部分脚本语言都是天生支持热替换的,比如PHP,只要替换了PHP源文件,这种改动就会立即生效,而无须重启Web服务器。
但对Java来说,热替换并非天生就支持,如果一个类已经加载到系统中,通过修改类文件,并无法让系统再来加载并重定义这个类。因此,在Java中实现这一功能的一个可行的方法就是灵活运用类加载器。
由不同类加载器加载的同名类属于不同的类型,不能相互转换和兼容。
即两个不同的类加载器加载同一个类,在虚拟机内部,会认为这两个类是完全不同的。根据这个特点,可以用来模拟热替换的实现,基本思路如图所示,首先创建自定义的类加载器,在服务不重启的条件下动态替换类文件,这样就可以直接执行新的类文件了。
根据上面的流程来模拟热替换,代码如下所示。要想实现同一个类的不同版本的共存,那么这些不同版本必须由不同的类加载器进行加载,因此就不能把这些类的加载工作委托给系统类加载器来完成,因为它们只有一份。
为了做到这一点,就不能采用系统默认的类加载器委托规则,也就是说定制的类加载器的父类加载器必须设置为null
或者重写findClass()方
法,加载类的时候调用findClass()
方法即可,不去调用loadClass()
方法,当通过loadClass()
方法进行类的加载时,如果该类没有加载过,会委托给应用程序类加载器进行加载,这样就不会实现热部署了。
委托给应用程序类加载器进行加载,这样就不会实现热部署了。
(1)创建自定义类加载器
如上所示。
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;public class MyClassLoader extends ClassLoader {private String rootDir;public MyClassLoader(String rootDir) {this.rootDir = rootDir;}protected Class<?> findClass(String className) {Class clazz = this.findLoadedClass(className);FileChannel fileChannel = null;WritableByteChannel outChannel = null;if (null == clazz) {try {String classFile = getClassFile(className);FileInputStream fis = new FileInputStream(classFile);fileChannel = fis.getChannel();ByteArrayOutputStream baos = new ByteArrayOutputStream();outChannel = Channels.newChannel(baos);ByteBuffer buffer = ByteBuffer.allocateDirect(1024);while (true) {int i = fileChannel.read(buffer);if (i == 0 || i == -1) {break;}buffer.flip();outChannel.write(buffer);buffer.clear();}byte[] bytes = baos.toByteArray();clazz = defineClass(className, bytes, 0, bytes.length);} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {try {if (fileChannel != null) {fileChannel.close();}} catch (IOException e) {e.printStackTrace();}try {if (outChannel != null) {outChannel.close();}} catch (IOException e) {e.printStackTrace();}}}return clazz;}/*** 类文件的完全路径*/private String getClassFile(String className) {return rootDir + "\\" + className.replace('.', '\\') + ".class";}
}
(2)创建需要热替换的类
Demo1类实现很简单,仅包含一个方法hot()
。
public class Demo1 {public void hot(){System.out.println("oldDemo1");}
}
(3)创建测试类LoopRun
把编译好的Demo1.class文件放在当前目录中。接下来要使用我们前面编写的MyClassLoader来实现该类的热替换。
具体的做法为写一个死循环,每隔5秒钟执行一次。
循环体中会创建新的类加载器实例加载Demo1类,生成实例,并调用hot()方法。
接下来修改Demo1
类中hot()方法的打印内容,重新编译,并在系统运行的情况下替换掉原来的Demo1.class
,会看到系统会打印出更改后的内容,如代码清单所示。
public class LoopRun {public static void main(String args[]) {while (true) {try {//1. 创建自定义类加载器的实例MyClassLoader loader = new MyClassLoader("D:/");//2. 加载指定的类Class clazz = loader.findClass("com.example.Demo1");//3. 创建运行时类的实例Object demo = clazz.newInstance();//4. 获取运行时类中指定的方法Method m = clazz.getMethod("hot");//5. 调用指定的方法m.invoke(demo);Thread.sleep(5000);} catch (Exception e) {System.out.println("not find");try {Thread.sleep(5000);} catch (InterruptedException ex) {ex.printStackTrace();}}}}
}
没有替换之前输出结果如下。
oldDemo1
oldDemo1
修改Demo1类中hot()方法输出内容为NewDemo1之后,重新编译,程序不重启的情况下,输出结果如下。
NewDemo1