ClassLoader原理解析
类加载器概述
Java类加载器(class loader)是Java运行时环境(Java Runtime Environment)的一部分,负责装载Java类到Jvm的内存空间,类通常是按需加载,并不是一次性全部加载。每个Java类如果需要使用的话必须要某个类加载器加载到内存中,Java运用类加载器来加载使用第三方类库。
类加载器基本概念
Java虚拟机使用一个Java类的方式如下: 使用编译器编译.Java文件成.class文件,使用类加载器加载对应class文件至内存,对应的class文件指代一个Class对象,使用Class类的newInstance() 方法新建一个对象,接下来就可以使用该对象的指定功能了,然而实际情况有可能更复杂,class文件不一定是编译器生成,也有可能是工具动态的生成,也可能是通过网络远程下载,但是无论class文件来自于哪里,最终都需要类加载器加载到JVM内存中。
Java中类加载器大致可以分成两种,一种是内建的类加载器也就是Java自带的类加载器,一种是用户编写的类加载器。除了系统提供的类加载器意外,开发人员可以通过继承 java.lang.ClassLoader类的方式来实现自己的类加载器。
系统提供的类加载器有下面三个:
-
- 引导类加载器(bootstrap class loader):用来加载Java核心类,并不继承java.lang.ClassLoader(一般一种语言的引导类加载器都不是本语言写的,否则就陷入了鸡生蛋蛋生鸡的问题,例如JAVA的引导类加载器就是C++编写),这个类不遵守下面的类加载器的规则,没有父类,也没有子类,外部也无法访问,属于纯粹JVM自己使用。他加载的目录是<JAVA_HOME>/jre/lib/rt.jar 或者使用配置-Xbootclasspath指定目录。
- 扩展类加载器(extension class loader):用来加载JAVA的扩展类,JAVA虚拟机的实现会提供一个扩展目录,该加载器加载指定目录下的所有的类。目录在
<JAVA_HOME>/jre/lib/ext
或java.ext.dirs变量中指定,也可以使用 System.getProperty('java.ext.dirs')来获取加载的目录,可以使用-Djava.ext.dirs配置来指定加载目录,该类由sun.misc.Launcher$ExtClassLoader 来实现。
- 系统类加载器(system class loader):也称为Apps类加载器,根据Java的类路径(环境的classpath) 来加载Java类。一般Java应用的类都是由他来加载的,可以通过ClassLoader.getSystemClassLoader() 来获取,这个加载器由sun.misc.Launcher$AppClassLoader来实现,可以更改classpath或者使用配置 -Djava.class.path=-cp 或者-Djava.class.path=-classpath来更改加载目录。
以下是ClassLoader类的方法列表
返回 | 方法列表 | 方法说明 |
ClassLoader | getParent() | 返回委托的父类加载器。 |
protected Class<?> | findClass(String name) | 使用指定的二进制名称查找类 |
protected Class<?> | loadClass(String name) | 使用指定的二进制名称查找类 |
protected final Class<?> | findLoadedClass(String name) | 如果 Java 虚拟机已将此加载器记录为具有给定二进制名称的某个类的启动加载器,则返回该二进制名称的类。否则,返回 null。 |
protected final Class<?> | final defineClass(String name,byte[]b,int offset,int len) | 把字节数组 b 中的内容转换成 Java 类,返回的结果是 java.lang.Class 类的实例。这个方法被声明为 final 的。 |
ClassLoader | static getSystemClassLoader() |
返回委托的系统类加载器。 |
类加载器的树状组织结构
可以使用以下代码来查看类加载器的树状结构。
1 /** 2 * 测试classloader 的继承树 3 */ 4 public class ClassLoaderTreeTest { 5 public static void main(String[] args) { 6 testTree(); 7 } 8 /** 9 * 演示了类加载器的树状组织结构 10 */ 11 private static void testTree() { 12 ClassLoader classLoader = ClassLoaderTreeTest.class.getClassLoader(); 13 14 int i = 0; 15 while (classLoader != null){ 16 System.out.println("第"+i+++"个:"+classLoader.toString()); 17 classLoader = classLoader.getParent(); 18 } 19 } 20 } 21 22 //运行结果 23 //第0个:sun.misc.Launcher$AppClassLoader@7b7035c6 24 //第1个:sun.misc.Launcher$ExtClassLoader@3da997a
第一输出的是加载了ClassLoaderTreeTest类的类加载器,也就指向了AppClassLoader的实例,第二个输出的也就是ExtClassLoader类的实例,这里没有输出bootstrap class loader是因为不同的JDK的实现对于父类加载器是引导类加载器的情况,getParent()方法返回的是null
类加载器的工作机制
类加载器的工作机制称为代理模式或者双亲委托机制或者等级加载机制。
类加载器在尝试自己去查找某个类的字节码并定义他的时候,会先代理给自己的父类加载器,由父类加载器去先尝试加载这个类,如果父类加载器还有父类加载器依次类推。当最顶层的加载器尝试开始加载class文件的时候,如果无法加载该类,那么会转交子类加载该类,最终到达树的底层,如果还是无法加载将抛出ClassNotFoundException.那么类加载器为什么要这样做呢?因为在JVM中判断两个类是否相同并不是单纯比较类的名称(全路径),而且还要比较加载该类的加载器是否一样,只有满足这2个条件,才会认为两个类是相同的。所以如果一个相同的类被不同的类加载器加载到JVM中,那么JVM会认为这两次加载的是不同的对象(虽然加载的类全包名相同的,但是加载他的类加载器是不同的)!代理模式是为了保证 Java 核心库的类型安全。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间,这种技术在许多框架中都被用到。
JVM加载class文件到内存中主要存在两种方式:
- 隐式加载:所谓隐式加载就是不再代码里直接调用某个classloader来加载这些需要的类,而是通过JVM来自动加载这些类到内存的方式。例如在我们在类中继承或者引用到其他类的时候,JVM发现这些类不在内存中,就会自动把这些类加载进来
- 显示加载:显示加载指的就是在代码中通过主动调用classloader来加载一个类的方式,例如调用 this.getClass().getClassLoader().loadClass()或者Class.forName() 等。
其实这两种方式并没有严格的区分,大多数情况下我们都是在混合使用,比如自己定义了一个classloader在加载某个类的时候,发现这个类有引用其他的对象,那么这个引用的对象就会被隐式加载。
ClassLoader加载一个class文件所经历的步骤:
- 将指定路径的class文件加载到内存中。在抽象类ClassLoader中其实没有定义如何去查找class文件,如何加载class文件,所以这些都是在子类中实现,也就是实现ClassLoader的findClass() 方法,然而再调用defineClass来实现创建类对象
- 验证与解析。验证与解析又分为三步,第一步字节码验证,类装入器对于类的字节码要做许多检测用来保证格式,行为的正确性。第二步类准备,这个阶段准备代表每个类中定义的字段,方法以及实现接口需要的数据结构。第三步解析,在这阶段类装入器装入类所引用的其他所有类。可以有多种方式引用类,如超类,接口,字段,方法签名,方法中使用的本地变量等。
- 初始化class对象。在类中包含的静态初始化执行,这一阶段类的静态字段被初始化为默认值。
在前面介绍类加载器的代理模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用 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
方法不会被重复调用。
Tomcat加载器分析
大量的项目中都使用了类加载器,tomcat也不例外,但是tomcat中的类加载机制与JVM中的略有不同,JVM类加载机制与tomcat中类加载器的加载机制对比如下图
当tomcat启动的时候会创建如图琐所示的几种类加载器:
- BootStrap:bootstrap ClassLoader包含了JVM运行的基本环境和基本扩展目录下的类($JAVA_HOME/jre/lib/ext)。
- System:这个加载器理论上是加载classpath目录下的所有的文件,但是tomcat的启动脚本(cataline.bat/sh) 中完全没有使用环境变量来加载,而是加载了$CATALINA_HOME/bin/ 目录下的三个jar包,分别是bootstrap.jar(包含了启动tomcat的main方法以及相关的classloader和实现类),tomcat-juli.jar(jdk的logging模块的增强实现类),commons-daemon.jar(在bootstrap.jar加载的同时会同时加载这个模块)
- Common:这个类加载器加载的是在tomcat内部以及每个WebApplication共享的jar包
- WebappX:这个类加载器加载的是每个webapplication中自己特有的jar包,包含的目录是/WEB-INF/class,这个类加载器的生命周期与webapplication相同。
而tomcat中类加载器加载的顺序是和JVM中不同的,按照以下顺序加载。
- bootstrap 类加载器加载JVM相关的类以及相关扩展类。
- WebappX 类加载器加载/WEB-INF/class 目录下
- Webappx 类加载器加载/WEB-INF/lib 目录下jar
- System 类加载器加载$CATALINA_HOME/bin/ 目录下3个指定jar
- Common 类加载器先加载 $CATALINA_BASE/lib 下的class和jar文件,再$CATALINA_HOME/lib 下的class和jar文件
所以需要注意两点:
1:自己写的类会优先于lib中引用的jar包加载
2:如果想在不同的webapplication中共享一些jar包的话,可以放到Tomcat下的lib目录中但是如果单独有一个webapplication中含有同样的jar包话,webapplication中的类会覆盖共享的类,所以需要同时删除该webapplication中相同的jar包
总结就是tomcat中的类加载顺序不是双亲委托机制,而是大体上自上而下的一种顺序,这样做的目的一方面是为了实现servlet中的相关规范,另外一方面的目的是为了隔离不同webapplication中的类。
实现自己类加载器
一般情况下我们是不需要自己定义自己的classloader实现的,但是有些情况我们可以尝试自己去实现一个classloader来完成一些需求。比如有些不再指定路径下的class文件,或者需要远程加载某些class,需要加密解密class文件,或者实现类似热部署那样的功能。
从上面的表述可以看到,如果要实现自己定义的classloader需要实现类ClassLoader,然后覆盖实现findClass方法,最后调用defineClass方法来实现加载Class。之所以不去实现loadClass方法是因为loadClass方法内部封装了委托机制,比较复杂。
自己实现的在特殊路径加载指定的class文件,代码如下。
1 import java.io.ByteArrayOutputStream; 2 import java.io.File; 3 import java.io.FileInputStream; 4 import java.io.InputStream; 5 6 public class MyfirstClassloader extends ClassLoader { 7 8 private static final String WEB_ROOT = System.getProperty("user.dir") + "/HardWorking/target/classes"; 9 10 /** 11 * 一般自己定义classloader 只需要实现findclass即可 12 * @param name 全包名 13 * @return Class 14 * @throws ClassNotFoundException 15 */ 16 @Override 17 protected Class<?> findClass(String name) throws ClassNotFoundException { 18 byte[] classData = getClassData(name); 19 if (classData == null || classData.length <= 0) { 20 throw new ClassNotFoundException(); 21 } 22 return defineClass(name, classData, 0, classData.length); 23 } 24 25 /** 26 * 使用流读取指定目录下的class文件 27 * @param name 28 * @return 29 */ 30 public byte[] getClassData(String name) { 31 String path = getClassPath(name); 32 byte[] container = new byte[4096]; 33 try { 34 int i; 35 InputStream inputStream = new FileInputStream(path); 36 ByteArrayOutputStream out = new ByteArrayOutputStream(); 37 while ((i = inputStream.read(container)) != -1) { 38 out.write(container,0,i); 39 } 40 return out.toByteArray(); 41 } catch (Exception e) { 42 } 43 return null; 44 } 45 46 /** 47 * 获取class文件所在路径 48 * @param name 49 * @return 50 */ 51 private String getClassPath(String name) { 52 return WEB_ROOT + File.separator + name.replace(".", File.separator) + ".class"; 53 } 54 55 public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException { 56 MyfirstClassloader classloader = new MyfirstClassloader(); 57 Class<?> myClass = classloader.findClass("learn.classloader.TestClass"); 58 Object o = myClass.newInstance(); 59 System.out.println(o); 60 } 61 }
在网络指定位置加载class文件,代码如下
1 import java.io.ByteArrayOutputStream; 2 import java.io.File; 3 import java.io.InputStream; 4 import java.net.URL; 5 6 public class NetClassLoader extends ClassLoader { 7 private static final String urlAddress = "http://localhost:8080/"; 8 /** 9 * @param name 全包名 10 * @return 11 * @throws ClassNotFoundException 12 */ 13 @Override 14 protected Class<?> findClass(String name) throws ClassNotFoundException { 15 byte[] data = getData(name); 16 if (data != null || data.length == 0) { 17 throw new ClassNotFoundException(); 18 } 19 data = decodeByte(data); 20 return super.defineClass(name,data, 0, data.length); 21 } 22 23 private byte[] decodeByte(byte[] data) { 24 //解码 25 return data; 26 } 27 28 private byte[] getData(String name) { 29 String path = getPath(name); 30 try { 31 URL url = new URL(path); 32 InputStream inputStream = url.openStream(); 33 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 34 byte[] cbuf = new byte[4096]; 35 int len; 36 while ((len = inputStream.read(cbuf)) != -1) { 37 outputStream.write(cbuf, 0, len); 38 } 39 return outputStream.toByteArray(); 40 } catch (Exception e) { 41 } 42 return null; 43 } 44 45 private String getPath(String name) { 46 String packName = name; 47 packName = packName.replace(".", "/"); 48 return urlAddress + File.separator + packName + ".class"; 49 } 50 }
总结
上面我们说了,ClassLoader工作的机制原理,以及如何创建自己的ClassLoader。所以现在我们有了个问题,JVM到底应不应该动态的去加载class文件?假如JVM可以动态的加载class文件,那么每次请求使用的class都需要重新加载,首先运行效率肯定受影响,第二个在替换了class以后必然需要重新更改对象的映射信息,这个就完全违反了JVM的设计规则。因为对象的引用信息只有对象的创建者才可以使用,而JVM并不可以干预,因为JVM并不知道对象是如何被使用的,也就是JVM不清楚对象的编译时期和运行时期的类型。那么我们开发的时候肯定也遇到过每次修改一个类都需要重新启动,严重影响开发效率,那是不是就没有办法了呢?然而并不是,可以每次请求都不缓存class文件不就行了,这个也就是不需要编译,只不过运行效率多多少少会受影响,但是开发效率肯定会上升,说到这里大家大概都想到了解释性语言。是的,python是最好的编程语言!
参考:
http://tomcat.apache.org/tomcat-8.0-doc/class-loader-howto.html
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/
http://www.cnblogs.com/xing901022/p/4574961.html
深入分析java web技术内幕 classloader 相关章节