当我们new一个对象时,jvm会去判断对应的类是否被加载到内存中,如果没有的话,就会启动类加载器去加载对应的类。下面我讲解下java中的类加载器。
1. JVM类加载器的种类
JVM预定义了三种类型类加载器,当一个 JVM启动的时候,Java缺省开始使用如下三种类型类装入器:
启动类加载器(Bootstrap ClassLoader):引导类装入器是用本地代码实现的类装入器,它负责将<JAVA_HOME>/jre/lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,其实就是用C++语言开发的,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。(仅按照文件名识别,如rt.jar,名称不符合的类库即使放在lib目录中也不会被加载。)
扩展类加载器(Extension CLassLoader):扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将<JAVA_HOME>/jre/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$ApplicationClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
我们的应用程序都是由这三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。
除了以上列举的三种类加载器,还有一种比较特殊的类型就是线程上下文类加载器。
2.类加载器的双亲委派模型
在这里,需要着重说明的是,JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。具体模型是这样的:
这样看的不是很直接,那么我们就从源码看,类加载是如何实现双亲委派模型的:
public Class loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { // 首先判断该类型是否已经被加载 Class c = findLoadedClass(name); if (c == null) { //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载 try { if (parent != null) { //如果存在父类加载器,就委派给父类加载器加载 c = parent.loadClass(name, false); } else { //父加载器为null,说明this为扩展类加载器的实例,父加载器为启动类加载器//通过调用本地方法native findBootstrapClass0(String name) c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; }
其中:
//加载指定名称(包括包名)的二进制类型,供用户调用的接口 public Class loadClass(String name) throws ClassNotFoundException{ … } //加载指定名称(包括包名)的二进制类型,同时指定是否解析(但是这里的resolve参数不一定真正能达到解析的效果),供继承用 protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException{ … }
下面我们用代码看看,Application ClassLoader的父类加载器是不是Extension CLassLoader,Extension CLassLoader的父类加载器是不是Bootstrap ClassLoader。
代码:
public class LoaderTest { public static void main(String[] args) { try { System.out.println(ClassLoader.getSystemClassLoader()); System.out.println(ClassLoader.getSystemClassLoader().getParent()); System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent()); } catch (Exception e) { e.printStackTrace(); } }}
结果输出:
sun.misc.Launcher$AppClassLoader@38da9246
sun.misc.Launcher$ExtClassLoader@15b94ed3null
通过以上的代码输出,我们可以判定系统类加载器的父加载器是标准扩展类加载器,但是我们试图获取标准扩展类加载器的父类加载器时确得到了null,就是说标准扩展类加载器本身强制设定父类加载器为null。
根据上面的loadClass()方法可以知道,并没有定义扩展类加载器的父类加载器为启动类加载器,而是定义其父加载器为null。(我觉得是防止你人为的将jar文件放到启动类加载器的地址里面,他是拒绝外来文件的加载的,因为如果你将java文件放到扩展类加载器的地址,jvm会用扩展类加载器去加载你的java文件的)
举例说明我的猜想:
测试一:
首先定义一个类
package classloader.test.bean;public class TestBean { public TestBean() { }}
测试一下如果加载这个类使用的是什么加载器,
package classloader.test.bean;public class ClassLoaderTest { public static void main(String[] args) { try { //查看当前系统类路径中包含的路径条目 System.out.println(System.getProperty("java.class.path")); //调用加载当前类的类加载器(这里即为系统类加载器)加载TestBean Class typeLoaded = Class.forName("classloader.test.bean.TestBean"); //查看被加载的TestBean类型是被那个类加载器加载的 System.out.println(typeLoaded.getClassLoader()); } catch (Exception e) { e.printStackTrace(); } }}
输出结果:
C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes
sun.misc.Launcher$AppClassLoader@73d16e93
说明这个类的加载器是应用程序类加载器,也就是我们自己编写类的默认加载器。
但如果将classloader.test.bean这个文件打包到扩展类加载器的地址后,还会用应用程序类加载器加载吗?
测试二:
将当前工程输出目录下的TestBean.class打包进test.jar剪贴到<JAVA_HOME>/jre/lib/ext目录下(现在工程输出目录下和JRE扩展目录下都有待加载类型的class文件)。再运行测试一测试代码,
结果如下:
C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes
- sun.misc.Launcher$ExtClassLoader@15db9742
我们明显可以验证前面说的双亲委派机制,应用程序类加载器在接到加载classloader.test.bean.TestBean类型的请求时,首先将请求委派给父类加载器(标准扩展类加载器),标准扩展类加载器抢先完成了加载请求。并且应用程序类加载器只加载对应地址里的jar文件。
但如果将classloader.test.bean这个文件打包到启动类加载器的文件目录下,还会用启动类加载器加载吗?
测试三:
将test.jar拷贝一份到<Java_Home>/jre/lib下,运行测试代码,输出如下:
C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes
sun.misc.Launcher$ExtClassLoader@15db9742
输出结果和上面一致。那就是说,放置到<Java_Home>/jre/lib目录下的TestBean对应的class字节码并没有被加载,这其实和前面讲的双亲委派机制并不矛盾。虚拟机出于安全等因素考虑,不会加载<Java_Home>/lib存在的陌生类,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的。做个进一步验证,删除<Java_Home>/lib/ext目录下和工程输出目录下的TestBean对应的class文件,然后再运行测试代码,则将会有ClassNotFoundException异常抛出。有关这个问题,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中设置相应断点运行测试三进行调试,会发现findBootstrapClass0()会抛出异常,然后在下面的findClass方法中被加载,当前运行的类加载器正是扩展类加载器(sun.misc.Launcher$ExtClassLoader),这一点可以通过JDT中变量视图查看验证。
3.自定义类加载器
类加载器的代理委派模式会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在Java虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。
方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。 类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输Java类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在Java虚拟机中运行的类来。下面将通过两个具体的实例来说明类加载器的开发。
如何加载存储在文件系统上的Java字节代码?
首先定义自己的加载器:
package classloader;import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStream;// 文件系统类加载器,所有自定义的加载器都必须继承自ClassLoaderpublic class FileSystemClassLoader extends ClassLoader { private String rootDir; public FileSystemClassLoader(String rootDir) { this.rootDir = rootDir; } // 获取类的字节码 @Override protected Class findClass(String name) throws ClassNotFoundException { byte[] classData = getClassData(name); // 获取类的字节数组 if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] getClassData(String className) { // 读取类文件的字节 String path = classNameToPath(className); try { InputStream ins = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; // 读取类文件的字节码 while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } private String classNameToPath(String className) { // 得到类文件的完全路径 return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; }}
如上所示,类 FileSystemClassLoader继承自类java.lang.ClassLoader。在java.lang.ClassLoader类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可。java.lang.ClassLoader类的方法loadClass()封装了前面提到的代理委派模式的实现。该方法会首先调用findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写 findClass()方法。
类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。
加载本地文件系统上的类,示例如下:
定义需要加载的类:
package com.example;public class Sample { private Sample instance; public void setSample(Object instance) { System.out.println(instance.toString()); this.instance = (Sample) instance; }}
下面是加载过程:
package classloader;import java.lang.reflect.Method;public class ClassIdentity { public static void main(String[] args) { new ClassIdentity().testClassIdentity(); } public void testClassIdentity() { //你需要加载的类在电脑上的根地址 String classDataRootPath = "C:\\Users\\JackZhou\\Documents\\NetBeansProjects\\classloader\\build\\classes"; FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath); FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath); String className = "com.example.Sample"; try { Class class1 = fscl1.loadClass(className); // 加载Sample类 Object obj1 = class1.newInstance(); // 创建对象 Class class2 = fscl2.loadClass(className); Object obj2 = class2.newInstance(); Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class); setSampleMethod.invoke(obj1, obj2); } catch (Exception e) { e.printStackTrace(); } }}
输出结果:
com.example.Sample@7852e922
总结:
最后根据源码我们看看类加载器到底是怎么运行的?
不管你是Class.forName() 或者是object.getClass()或者是类.class。前面都是一下本地方法的调用,最终还是调用ClassLoader的loadClass()方法加载类。
ClassLoader中的loadClass()方法:
public Class loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { // 首先判断该类型是否已经被加载 Class c = findLoadedClass(name); if (c == null) { //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载 try { if (parent != null) { //如果存在父类加载器,就委派给父类加载器加载 c = parent.loadClass(name, false); } else { //父加载器为null,说明this为扩展类加载器的实例,父加载器为启动类加载器//通过调用本地方法native findBootstrapClass0(String name) c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; }
其中findLoadedClass()方法:
protected final Class findLoadedClass(String name) { //判断全限定名是否符合规范 if (!checkName(name)) return null; //本地方法判断是否能找到被加载的类,是的话就会返回,否者返回null return findLoadedClass0(name); } private native final Class findLoadedClass0(String name);
其中checkName()方法:
private boolean checkName(String name) { if ((name == null) || (name.length() == 0)) return true; if ((name.indexOf('/') != -1) || (!VM.allowArraySyntax() && (name.charAt(0) == '['))) return false; return true; }
如果父类都无法加载此类,就会调用findClassss()方法加载:
protected Class findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
一般我们自己定义加载器就可以用classLoader的defineClass()方法加载你传入的文件格式,形成class对象。
defineClass()方法:
protected final Class defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain) throws ClassFormatError { protectionDomain = preDefineClass(name, protectionDomain); String source = defineClassSourceLocation(protectionDomain); Class c = defineClass1(name, b, off, len, protectionDomain, source); postDefineClass(c, protectionDomain); return c; }
参考文献: