ClassLoader
每个class都有一个reference,指向自己的ClassLoader。Class.getClassLoader()
array的ClassLoader就是其元素的ClassLoader,若是基本数据类型,则这个array没有ClassLoader
1.2 主要方法和工作过程Java1.1及从前版本中,ClassLoader主要方法:
Class loadClass( String name, boolean resolve ); ClassLoader.loadClass() 是 ClassLoader 的入口点
defineClass 方法是 ClassLoader 的主要诀窍。该方法接受由原始字节组成的数组并把它转换成 Class 对象。原始数组包含如从文件系统或网络装入的数据。
findSystemClass 方法从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用 defineClass 将原始字节转换成 Class 对象,以将该文件转换成类。当运行 Java 应用程序时,这是 JVM 正常装入类的缺省机制。
resolveClass可以不完全地(不带解析)装入类,也可以完全地(带解析)装入类。当编写我们自己的 loadClass 时,可以调用 resolveClass,这取决于 loadClass 的 resolve 参数的值
findLoadedClass 充当一个缓存:当请求 loadClass 装入类时,它调用该方法来查看 ClassLoader 是否已装入这个类,这样可以避免重新装入已存在类所造成的麻烦。应首先调用该方法
一般load方法过程如下:
调用 findLoadedClass 来查看是否存在已装入的类。
如果没有,那么采用某种特殊的神奇方式来获取原始字节。(通过IO从文件系统,来自网络的字节流等)
如果已有原始字节,调用 defineClass 将它们转换成 Class 对象。
如果没有原始字节,然后调用 findSystemClass 查看是否从本地文件系统获取类。
如果 resolve 参数是 true,那么调用 resolveClass 解析 Class 对象。
如果还没有类,返回 ClassNotFoundException。
否则,将类返回给调用程序。
1.3 委托模型自从JDK1.2以后,ClassLoader做了改进,使用了委托模型,所有系统中的ClassLoader组成一棵树,ClassLoader在载入类库时先让Parent寻找,Parent找不到才自己找。
JVM在运行时会产生三个ClassLoader,Bootstrap ClassLoader、Extension ClassLoader和App ClassLoader。其中,Bootstrap ClassLoader是用C++编写的,在Java中看不到它,是null。它用来加载核心类库,就是在lib下的类库,Extension ClassLoader加载lib/ext下的类库,App ClassLoader加载Classpath里的类库,三者的关系为:App ClassLoader的Parent是Extension ClassLoader,而Extension ClassLoader的Parent为Bootstrap ClassLoader。加载一个类时,首先BootStrap进行寻找,找不到再由Extension ClassLoader寻找,最后才是App ClassLoader。
将ClassLoader设计成委托模型的一个重要原因是出于安全考虑,比如在Applet中,如果编写了一个java.lang.String类并具有破坏性。假如不采用这种委托机制,就会将这个具有破坏性的String加载到了用户机器上,导致破坏用户安全。但采用这种委托机制则不会出现这种情况。因为要加载java.lang.String类时,系统最终会由Bootstrap进行加载,这个具有破坏性的String永远没有机会加载。
委托模型还带来了一些问题,在某些情况下会产生混淆,如下是Tomcat的ClassLoader结构图:
Bootstrap
|
System
|
Common
/
Catalina Shared
/
Webapp1 Webapp2 ...
由 Common 类装入器装入的类决不能(根据名称)直接访问由 Web 应用程序装入的类。使这些类联系在一起的唯一方法是通过使用这两个类集都可见的接口。在这个例子中,就是包含由 Java servlet 实现的 javax.servlet.Servlet。
如果在lib或者lib/ext等类库有与应用中同样的类,那么应用中的类将无法被载入。通常在jdk新版本出现有类库移动时会出现问题,例如最初我们使用自己的xml解析器,而在jdk1.4中xml解析器变成标准类库,load的优先级也高于我们自己的xml解析器,我们自己的xml解析器永远无法找到,将可能导致我们的应用无法运行。
相同的类,不同的ClassLoader,将导致ClassCastException异常
1.4 线程中的ClassLoader每个运行中的线程都有一个成员contextClassLoader,用来在运行时动态地载入其它类,可以使用方法Thread.currentThread().setContextClassLoader(...);更改当前线程的contextClassLoader,来改变其载入类的行为;也可以通过方法Thread.currentThread().getContextClassLoader()来获得当前线程的ClassLoader。
实际上,在Java应用中所有程序都运行在线程里,如果在程序中没有手工设置过ClassLoader,对于一般的java类如下两种方法获得的ClassLoader通常都是同一个
this.getClass.getClassLoader();
Thread.currentThread().getContextClassLoader();
方法一得到的Classloader是静态的,表明类的载入者是谁;方法二得到的Classloader是动态的,谁执行(某个线程),就是那个执行者的Classloader。对于单例模式的类,静态类等,载入一次后,这个实例会被很多程序(线程)调用,对于这些类,载入的Classloader和执行线程的Classloader通常都不同。
1.5 Web应用中的ClassLoader回到上面的例子,在Tomcat里,WebApp的ClassLoader的工作原理有点不同,它先试图自己载入类(在ContextPath/WEB-INF/...中载入类),如果无法载入,再请求父ClassLoader完成。
由此可得:
对于WEB APP线程,它的contextClassLoader是WebAppClassLoader
对于Tomcat Server线程,它的contextClassLoader是CatalinaClassLoader
1.6 获得ClassLoader的几种方法可以通过如下3种方法得到ClassLoader
this.getClass.getClassLoader(); // 使用当前类的ClassLoader
Thread.currentThread().getContextClassLoader(); // 使用当前线程的ClassLoader
ClassLoader.getSystemClassLoader(); // 使用系统ClassLoader,即系统的入口点所使用的ClassLoader。(注意,system ClassLoader与根ClassLoader并不一样。JVM下system ClassLoader通常为App ClassLoader)
1.7 几种扩展应用用户定制自己的ClassLoader可以实现以下的一些应用
安全性。类进入JVM之前先经过ClassLoader,所以可以在这边检查是否有正确的数字签名等
加密。java字节码很容易被反编译,通过定制ClassLoader使得字节码先加密防止别人下载后反编译,这里的ClassLoader相当于一个动态的解码器
归档。可能为了节省网络资源,对自己的代码做一些特殊的归档,然后用定制的ClassLoader来解档
自展开程序。把java应用程序编译成单个可执行类文件,这个文件包含压缩的和加密的类文件数据,同时有一个固定的ClassLoader,当程序运行时它在内存中完全自行解开,无需先安装
动态生成。可以生成应用其他还未生成类的类,实时创建整个类并可在任何时刻引入JVM
2.0 资源载入
所有资源都通过ClassLoader载入到JVM里,那么在载入资源时当然可以使用ClassLoader,只是对于不同的资源还可以使用一些别的方式载入,例如对于类可以直接new,对于文件可以直接做IO等。 2.1 载入类的几种方法假设有类A和类B,A在方法amethod里需要实例化B,可能的方法有3种。对于载入类的情况,用户需要知道B类的完整名字(包括包名,例如"com.rain.B")
1. 使用Class静态方法 Class.forName
Class cls = Class.forName("com.rain.B");
B b = (B)cls.newInstance();
2. 使用ClassLoader
/* Step 1. Get ClassLoader */
ClassLoader cl; // 如何获得ClassLoader参考1.6
/* Step 2. Load the class */
Class cls = cl.loadClass("com.rain.B"); // 使用第一步得到的ClassLoader来载入B
/* Step 3. new instance */
B b = (B)cls.newInstance(); // 有B的类得到一个B的实例
3. 直接new
B b = new B();
2.2 文件载入(例如配置文件等)假设在com.rain.A类里想读取文件夹 /com/rain/config 里的文件sys.properties,读取文件可以通过绝对路径或相对路径,绝对路径很简单,在Windows下以盘号开始,在Unix下以"/"开始
对于相对路径,其相对值是相对于ClassLoader的,因为ClassLoader是一棵树,所以这个相对路径和ClassLoader树上的任何一个ClassLoader相对比较后可以找到文件,那么文件就可以找到,当然,读取文件也使用委托模型
1. 直接IO
/**
* 假设当前位置是 "C:/test",通过执行如下命令来运行A "java com.rain.A"
* 1. 在程序里可以使用绝对路径,Windows下的绝对路径以盘号开始,Unix下以"/"开始
* 2. 也可以使用相对路径,相对路径前面没有"/"
* 因为我们在 "C:/test" 目录下执行程序,程序入口点是"C:/test",相对路径就
* 是 "com/rain/config/sys.properties"
* (例子中,当前程序的ClassLoader是App ClassLoader,system ClassLoader = 当前的
* 程序的ClassLoader,入口点是"C:/test")
* 对于ClassLoader树,如果文件在jdk lib下,如果文件在jdk lib/ext下,如果文件在环境变量里,
* 都可以通过相对路径"sys.properties"找到,lib下的文件最先被找到
*/
File f = new File("C:/test/com/rain/config/sys.properties"); // 使用绝对路径
//File f = new File("com/rain/config/sys.properties"); // 使用相对路径
InputStream is = new FileInputStream(f);
如果是配置文件,可以通过java.util.Properties.load(is)将内容读到Properties里,Properties默认认为is的编码是ISO-8859-1,如果配置文件是非英文的,可能出现乱码问题。
2. 使用ClassLoader
/**
* 因为有3种方法得到ClassLoader,对应有如下3种方法读取文件
* 使用的路径是相对于这个ClassLoader的那个点的相对路径,此处只能使用相对路径
*/
InputStream is = null;
is = this.getClass().getClassLoader().getResourceAsStream(
"com/rain/config/sys.properties"); //方法1
//is = Thread.currentThread().getContextClassLoader().getResourceAsStream(
"com/rain/config/sys.properties"); //方法2
//is = ClassLoader.getSystemResourceAsStream("com/rain/config/sys.properties"); //方法3
如果是配置文件,可以通过java.util.Properties.load(is)将内容读到Properties里,这里要注意编码问题。
3. 使用ResourceBundle
ResourceBundle bundle = ResourceBundle.getBoundle("com.rain.config.sys");
这种用法通常用来载入用户的配置文件,关于ResourceBunlde更详细的用法请参考其他文档
总结:有如下3种途径来载入文件
1. 绝对路径 ---> IO
2. 相对路径 ---> IO
---> ClassLoader
3. 资源文件 ---> ResourceBundle
2.3 如何在web应用里载入资源在web应用里当然也可以使用ClassLoader来载入资源,但更常用的情况是使用ServletContext,如下是web目录结构
ContextRoot
|- JSP、HTML、Image等各种文件
|- [WEB-INF]
|- web.xml
|- [lib] Web用到的JAR文件
|- [classes] 类文件
用户程序通常在classes目录下,如果想读取classes目录里的文件,可以使用ClassLoader,如果想读取其他的文件,一般使用ServletContext.getResource()
如果使用ServletContext.getResource(path)方法,路径必须以"/"开始,路径被解释成相对于ContextRoot的路径,此处载入文件的方法和ClassLoader不同,举例"/WEB-INF/web.xml","/download/WebExAgent.rar"
再次深入理解~
这篇文章翻译自zeroturnaround.com的 Do You Really Get Classloaders? ,融入和补充了笔者的一些实践、经验和样例。本文的例子比原文更加具有实际意义,文字内容也更充沛一些,非常感谢作者 Jevgeni Kabanov 能够共享如此优秀的文档。
1. 为什么你需要了解和敬畏ClassLoader
ClassLoader在Java语言中占据了核心地位,Java应用服务器,OSGi,以及大量的网络框架,它们大多数都用到了ClassLoader。如果在使用过程中出现了类加载错误,你能解决它吗?
我们将从JVM和开发者两个角度讲述ClassLoader,将会选择一些典型的案例,然后演示如何解决它们。NoClassDefFoundError,LinkageError等很多错误都会有特定的表征,我们分析每个例子,然后进行解决。
2. 进入ClassLoader
每个ClassLoader对象都是一个java.lang.ClassLoader的实例。每个Class对象都被这些ClassLoader对象所加载,通过继承java.lang.ClassLoader可以扩展出自定义ClassLoader,并使用这些自定义的ClassLoader对类进行加载。
先大体了解一下ClassLoader的API:
01 | package java.lang; |
02 |
03 | public abstract class ClassLoader { |
04 | public Class loadClass(String name); |
05 |
06 | protected Class defineClass( byte [] b); |
07 |
08 | public URL getResource(String name); |
09 |
10 | public Enumeration getResources(String name); |
11 |
12 | public ClassLoader getParent(); |
13 | } |
最重要的是ClassLoader的loadClass
方法,它接受一个全类名,然后返回一个Class类型的实例。
defineClass
方法接受一组字节,然后将其具体化为一个Class类型实例,它一般从磁盘上加载一个文件,然后将文件的字节传递给JVM,通过JVM(native 方法)对于Class的定义,将其具体化,实例化为一个Class类型实例。
getParent
方法返回其parent ClassLoader。
getResource
和getResources
方法,从给定的repository中查找URLs,同时它们也具备类似loadClass
一样的代理机制,我们可以将loadClass
视为:defineClass(getResource(name).getBytes())
。
Java由于其晚绑定和“解释型”的特性,类型的加载是到最晚才进行,一个类型直到被调用构造函数、静态方法或者在字段上使用时才会被加载。
考虑如下代码:
1 | public class A { |
2 | public void doSomething() { |
3 | B b = new B(); |
4 | b.doSomethingElse(); |
5 | } |
6 | } |
代码:B b = new B();等同于B b = Class.forName(“B”, false, A.class.getClassLoader()).newInstance();
这代表着,在类型A中使用到的类型,将由加载了类型A的类加载器来进行加载。
3. ClassLoader继承体系
当启动一个JVM时,bootstrap 类加载器就会加载java的核心类,例如:rt.jar中的类。bootstrap 类加载器是其他类加载器的parent,它使唯一一个没有parent的类加载器。
接下来是extension 类加载器,它以bootstrap 类加载器作为parent,它用来从Java系统变量java.ext.dir中的jar包中加载类的。
第三个,也是最重要的一个就是开发者使用的system classpath 类加载器 。它是extension 类加载器 的child,它用来从Java系统变量java.class.path下面加载类,可以通过 -classpath 来指定这个位置。
注意类加载器的体系并不是“继承”体系,而是一个“委派”体系。大多数类加载器首先会到自己的parent中查找类或者资源,如果找不到,才会在自己的本地进行查找。事实上,类加载器被定义加载哪些在parent中无法加载到的类,这样在较高层级的类加载器上的类型能够被“赋值”为较低类加载器加载的类型。
类加载器的委托行为动机是为了避免相同的类被加载多次。回到1995年,Java的主要方向被放在Applet上,那时候网络带宽优先,所以程序中的类直到用时才会被加载。但是事实上,Java在服务器端展示了强劲的能力,但是服务器端要求类加载器能够反转委派原则,也就是先加载本地的类,如果加载不到,再到parent中加载。
JavaEE的 委派模型
每个方块都是一个类加载器,JavaEE规范推荐每个模块的类加载器先加载本类加载的内容,如果加载不到才回到parent类加载器中尝试加载。
反转委派原则的原因是应用服务器中所携带的类库并不是应用所期待的,也许不适合应用开发者,一个常见的例子就是log4j的依赖在容器和不同的应用中都存在,但是它们的版本大都不同。
Tomcat的 类加载顺序(开启了delegate模式)
在Tomcat中,默认的行为是先尝试在Bootstrap和Extension中进行类型加载,如果加载不到则在WebappClassLoader中进行加载,如果还是找不到则在Common中进行查找。在Alibaba使用的Tomcat开启了delegate模式,因此加载类型时会以parent类加载器优先。
4. NoClassDefFoundError
NoClassDefFoundError是在开发JavaEE程序中常见的一种问题。该问题会随着你所使用的JavaEE中间件环境的复杂度以及应用本身的体量变得更加复杂,尤其是现在的JavaEE服务器具有大量的类加载器。
在JavaDoc中对NoClassDefFoundError的产生是由于JVM或者类加载器实例尝试加载类型的定义,但是该定义却没有找到,影响了执行路径。换句话说,在编译时这个类是能够被找到的,但是在执行时却没有找到。
这一刻IDE是没有出错提醒的,但是在运行时却出现了错误。
看看如下示例:
01 | /** |
02 | * @author weipeng2k 2015年3月27日 下午5:15:15 |
03 | */ |
04 | @WebServlet (name = "NoClassDefFoundErrorServlet" , urlPatterns = "/noClassDefFoundError.do" ) |
05 | public class NoClassDefFoundErrorServlet extends HttpServlet { |
06 |
07 | private static final long serialVersionUID = 61585757018374721L; |
08 |
09 | @Override |
10 | protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { |
11 | resp.getWriter().println(TestCase. class .toString()); |
12 | } |
13 | } |
在看pom.xml中对于依赖的定义:
01 | <dependencies> |
02 | <dependency> |
03 | <groupId>junit</groupId> |
04 | <artifactId>junit</artifactId> |
05 | <version> 3.8 . 1 </version> |
06 | <scope>provided</scope> |
07 | </dependency> |
08 | <dependency> |
09 | <groupId>javax.servlet</groupId> |
10 | <artifactId>servlet-api</artifactId> |
11 | <version> 3.0 </version> |
12 | <scope>provided</scope> |
13 | </dependency> |
14 | <dependency> |
15 | <groupId>org.springframework</groupId> |
16 | <artifactId>spring</artifactId> |
17 | <version> 2.5 . 6 </version> |
18 | </dependency> |
19 | </dependencies> |
其中对于junit的依赖是provided级别的,这里是为了能简化错误出现的条件。可以看到,在NoClassDefFoundErrorServlet中,使用了junit.jar中的TestCase,但是junit.jar在WEB-INF/lib中却没有,从而导致WebappClassLoader在进行加载TestCase时无法找到,从而抛出NoClassDefFoundError。我们需要从最终的war包中确定是否存在这个类,而不是在IDE中进行搜索。
5. NoSuchMethodError
在另一个场景中,我们可能遇到了另一个错误,也就是NoSuchMethodError。
NoSuchMethodError代表这个类型确实存在,但是一个不正确的版本被加载了。为了解决这个问题我们可以使用 ‘-verbose:class’ 来判断该JVM加载的到底是哪个版本。
看如下示例:
01 | import org.springframework.beans.factory.BeanFactoryUtils; |
02 |
03 | /** |
04 | * @author weipeng2k 2015年3月31日 上午9:09:58 |
05 | */ |
06 | @WebServlet (name = "NoSuchMethodErrorServlet" , urlPatterns = { "/noSuchMethodError.do" }) |
07 | public class NoSuchMethodErrorServlet extends HttpServlet { |
08 |
09 | private static final long serialVersionUID = 1699609060417354821L; |
10 |
11 | @Override |
12 | protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { |
13 | BeanFactoryUtils.isGeneratedBeanName( "xxx" ); |
14 |
15 | resp.getWriter().println( "done." ); |
16 | } |
17 | } |
在doGet方法中调用了BeanFactoryUtils.isGeneratedBeanName(”xxx“);,看一下项目的pom依赖。
01 | < dependencies > |
02 | < dependency > |
03 | < groupId >junit</ groupId > |
04 | < artifactId >junit</ artifactId > |
05 | < version >4.11</ version > |
06 | < scope >provided</ scope > |
07 | </ dependency > |
08 | < dependency > |
09 | < groupId >javax.servlet</ groupId > |
10 | < artifactId >servlet-api</ artifactId > |
11 | < version >3.0</ version > |
12 | < scope >provided</ scope > |
13 | </ dependency > |
14 | < dependency > |
15 | < groupId >org.springframework</ groupId > |
16 | < artifactId >org.springframework.context</ artifactId > |
17 | < version >3.0.5.RELEASE</ version > |
18 | < scope >provided</ scope > |
19 | </ dependency > |
20 | < dependency > |
21 | < groupId >org.apache.mina</ groupId > |
22 | < artifactId >mina-core</ artifactId > |
23 | < version >2.0.7</ version > |
24 | </ dependency > |
25 | < dependency > |
26 | < groupId >com.alibaba.external</ groupId > |
27 | < artifactId >sourceforge.spring</ artifactId > |
28 | < version >2.0.7</ version > |
29 | </ dependency > |
30 | </ dependencies > |
这里为了方便观察到结果,将org.springframework.context的 scope 改为了 provided ,目的是不将其打包入war包,而只是使用了sourceforge.spring中定义的2.0.7版本,这个版本肯定没有isGeneratedBeanName(String name)方法,但是在IDE中,由于应用依赖到了高版本的spring从而能够编译通过,但是在运行时却没有那么好运了。这种错误,常见于 Maven坐标 的变动,使得应用依赖了多个 相同内容,不同版本 的jar包,以致在运行时选择了非期望的版本。
6. ClassCastException
NoClassDefFoundError和NoSuchMethodError是两个在 JavaEE 环境中经常出现的问题,这些问题需要 开发人员了解问题的本质,才能够被 从容 的处理。
下面我们看一下ClassCastException,在一个类加载器的情况下,一般出现这种错误都会是在转型操作时,比如:A a = (A) method();,很容易判断出来method()方法返回的类型不是类型A,但是在 JavaEE 多个类加载器的环境下就会出现一些难以定位的情况。
看如下示例:
01 | package com.murdock.classloader.servlet; |
02 |
03 | import java.io.File; |
04 | import java.io.IOException; |
05 | import java.net.URL; |
06 |
07 | import javax.servlet.ServletException; |
08 | import javax.servlet.annotation.WebServlet; |
09 | import javax.servlet.http.HttpServlet; |
10 | import javax.servlet.http.HttpServletRequest; |
11 | import javax.servlet.http.HttpServletResponse; |
12 |
13 | import org.apache.mina.proxy.utils.MD4; |
14 |
15 | import com.murdock.classloader.CachedClassLoader; |
16 |
17 | /** |
18 | * @author weipeng2k 2015年4月4日 下午6:00:54 |
19 | */ |
20 | @WebServlet (name = "ClassCastExceptionServlet" , urlPatterns = "/classCastException.do" ) |
21 | public class ClassCastExceptionServlet extends HttpServlet { |
22 | private static final long serialVersionUID = -8959000121057369987L; |
23 |
24 | @Override |
25 | protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { |
26 | String localFirst = req.getParameter( "localFirst" ); |
27 | CachedClassLoader cl = null ; |
28 | cl = new CachedClassLoader( |
29 | new URL[] { new File( |
30 | "/Users/weipeng2k/.m2/repository/org/apache/mina/mina-core/2.0.7/mina-core-2.0.7.jar" ).toURI() |
31 | .toURL() }, this .getClass().getClassLoader()); |
32 | if ( "false" .equals(localFirst)) { |
33 | cl.setLocalFirst( false ); |
34 | } |
35 | try { |
36 | Class<?> klass = cl.loadClass( "org.apache.mina.proxy.utils.MD4" ); |
37 | MD4 md4 = (MD4) klass.newInstance(); |
38 |
39 | resp.getWriter().println(md4); |
40 |
41 | } catch (Exception ex) { |
42 | throw new RuntimeException(ex); |
43 | } finally { |
44 | cl.close(); |
45 | } |
46 |
47 | } |
48 | } |
在ClassCastExceptionServlet中,构建了一个CachedClassLoader,利用这个ClassLoader加载org.apache.mina.proxy.utils.MD4
,然后反射调用构造该类的实例,将其赋给MD4
,最后将其打印到浏览器。
请求URL:http://localhost:8080/classCastException.do
响应页面,出现错误:
1 | java.lang.RuntimeException: java.lang.ClassCastException: org.apache.mina.proxy.utils.MD4 cannot be cast to org.apache.mina.proxy.utils.MD4 |
2 | com.murdock.classloader.servlet.ClassCastExceptionServlet.doGet(ClassCastExceptionServlet.java: 42 ) |
3 | javax.servlet.http.HttpServlet.service(HttpServlet.java: 622 ) |
4 | javax.servlet.http.HttpServlet.service(HttpServlet.java: 729 ) |
5 | org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java: 52 ) |
请求URL :http://localhost:8080/classCastException.do?localFirst=false
响应页面,输出正常:
1 | org.apache.mina.proxy.utils.MD4 @401c8af5 |
请求的URL加上了localFirst=false
就可以正常的输出,而它也就是在CachedClassLoder上设置了一下,为什么有这么大的差别。org.apache.mina.proxy.utils.MD4
全类名一致,为什么会出现ClassCastException呢?
在JVM中,如何确定一个类型实例?答:全类名吗?不是,是类加载器加上全类名。在JVM中,类型被定义在一个叫SystemDictionary 的数据结构中,该数据结构接受类加载器和全类名作为参数,返回类型实例。
SystemDictionary 如图所示:
类型加载时,需要传入类加载器和需要加载的全类名,如果在 SystemDictionary 中能够命中一条记录,则返回class 列上对应的类型实例引用,如果无法命中记录,则会调用loader.loadClass(name);
进行类型加载。
这里不会更加深入的介绍 SystemDictionary 如何进行类型加载的过程,而是需要指出 JVM中确定一个类型的坐标是通过类加载器和全类名做到的 。回想一下MD4 md4 = (MD4) klass.newInstance();
,是不是代表着等式两边的MD4是不同的类加载器加载的呢?那问题一定出在 CachedClassLoader 上。这里贴一下loadClass(String name)
方法的部分逻辑。
CachedClassLoader 的loadClass逻辑:
01 | if (localFirst) { |
02 | try { |
03 | clazz = findClass(name); |
04 | if (clazz != null ) { |
05 | return clazz; |
06 | } |
07 | } catch (ClassNotFoundException ex) { |
08 |
09 | } |
10 | return super .loadClass(name); |
11 | } else { |
12 | return super .loadClass(name); |
13 | } |
可以看到在 localFirst 为true时,该类加载器会首先加载自身 repository 中的类型,如果加载不到,则会尝试默认的加载机制进行加载,也就是parent优先加载。这样就可以解释MD4 md4 = (MD4) klass.newInstance();,等式左边MD4 md4,这个类型是WebappClassLoader.org.apache.mina.proxy.utils.MD4,等式右边klass.newInstance()返回的类型是CachedClassLoader.org.apache.mina.proxy.utils.MD4,二者并不是同一个类型,所以无法完成类型转换,最终抛出 ClassCastException 。而当 localFirst 为false时,该类加载器遵循parent优先,从而会先委派给WebappClassLoader进行加载,当然转型也就不会有问题了。
在传统的双亲委派模型下,这种 ClassCastException 是不会发生的,因为它的加载顺序杜绝了出现这种问题的可能,而在 JavaEE 环境下,每个资源模块(比如一个war包)都优先使用自身的资源,正因为突破了双亲委派模型, 奇怪的问题 就发生了。
7. LinkageError
有时候事情会变得更糟,和 ClassCastException 本质一样,加载自不同位置的*相同类*在同一段逻辑(比如:方法)中交互时,会出现 LinkageError 。
我们先看一下出错的异常信息,然后分析一下它产生的条件和原因:
01 | java.lang.LinkageError: loader constraint violation: when resolving overridden method "com.murdock.classloader.linkageerror.Param2.generate()Lcom/murdock/classloader/linkageerror/Param2;" the class loader (instance of com/murdock/classloader/linkageerror/LinkageErrorTest$ 1 ) of the current class , com/murdock/classloader/linkageerror/Param2, and its superclass loader (instance of sun/misc/Launcher$AppClassLoader), have different Class objects for the type com/murdock/classloader/linkageerror/Param2 used in the signature |
02 | at java.lang.Class.getDeclaredConstructors0(Native Method) |
03 | at java.lang.Class.privateGetDeclaredConstructors(Class.java: 2671 ) |
04 | at java.lang.Class.getConstructor0(Class.java: 3075 ) |
05 | at java.lang.Class.newInstance(Class.java: 412 ) |
06 | at com.murdock.classloader.linkageerror.LinkageErrorTest.test(LinkageErrorTest.java: 34 ) |
07 | at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) |
08 | at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java: 62 ) |
09 | at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java: 43 ) |
10 | at java.lang.reflect.Method.invoke(Method.java: 497 ) |
11 | at org.junit.runners.model.FrameworkMethod$ 1 .runReflectiveCall(FrameworkMethod.java: 47 ) |
12 | at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java: 12 ) |
13 | at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java: 44 ) |
14 | at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java: 17 ) |
看到一堆出错信息,但是不要紧张,慢慢的读一下出错信息,这种错误一般会让你直觉感觉不会出现。loader constraint violation
表示类加载器冲突了,这句话暗示: 相同的类,由不同的ClassLoader加载,但是在这里遇到了。when resolving overridden method "com.murdock.classloader.linkageerror.Param2.generate()Lcom/murdock/classloader/linkageerror/Param2;"
表示在解析那条语句出现了问题,这里表示在Param2.generate()
方法的解析过程中出现了问题。the class loader (instance of com/murdock/classloader/linkageerror/LinkageErrorTest$1) of the current class, com/murdock/classloader/linkageerror/Param2,
表示解析的语句所在的类型Param2
是LinkageErrorTest$1
类加载器加载的。and its superclass loader (instance of sun/misc/Launcher$AppClassLoader), have different Class objects for the type com/murdock/classloader/linkageerror/Param2 used in the signature
表示Param2
的超类Param
中被覆盖的方法返回的类型Param2
为Launcher$AppClassLoader
加载。
Linkage在常规情况下非常难以制造,只有在多个类加载器交互时才有可能出现,下面看一下问题代码。出现问题的类和参数:
01 | package com.murdock.classloader.linkageerror; |
02 |
03 | /** |
04 | * @author weipeng2k 2015年4月28日 上午10:04:26 |
05 | */ |
06 | public class HandleUtils { |
07 | public void m(Param param) { |
08 | param.generate(); |
09 | } |
10 |
11 | } |
12 |
13 | package com.murdock.classloader.linkageerror; |
14 |
15 | public class Param { |
16 | public Param2 generate() { |
17 | return new Param2(); |
18 | } |
19 | } |
20 |
21 | package com.murdock.classloader.linkageerror; |
22 |
23 | public class Param2 extends Param { |
24 | public Param2 generate() { |
25 | return new Param2(); |
26 | } |
27 | } |
测试用例如下:
01 | @Test |
02 | public void test() throws Exception { |
03 |
04 | // cl1在加载HandleUtils和Param时将会使用AppClassLoader |
05 | URLClassLoader cl1 = new URLClassLoader( new URL[] { new File( "target/test-classes" ).toURI().toURL()}, null ) { |
06 |
07 | @Override |
08 | public Class<?> loadClass(String name) throws ClassNotFoundException { |
09 | if ( "com.murdock.classloader.linkageerror.HandleUtils" .equals(name)) { |
10 | return ClassLoader.getSystemClassLoader().loadClass(name); |
11 | } |
12 |
13 | if ( "com.murdock.classloader.linkageerror.Param" .equals(name)) { |
14 | return ClassLoader.getSystemClassLoader().loadClass(name); |
15 | } |
16 |
17 | return super .loadClass(name); |
18 | } |
19 |
20 | }; |
21 |
22 | ClassLoader.getSystemClassLoader().loadClass( "com.murdock.classloader.linkageerror.Param2" ); |
23 | HandleUtils hu = (HandleUtils) cl1.loadClass( "com.murdock.classloader.linkageerror.HandleUtils" ).newInstance(); |
24 | hu.m((Param) cl1.loadClass( "com.murdock.classloader.linkageerror.Param2" ).newInstance()); |
25 | } |
LinkageError 需要观察哪个类被不同的类加载器加载了,在哪个方法或者调用处发生(交汇)的,然后才能想解决方法,解决方法无外乎两种。第一,还是不同的类加载器加载,但是相互不再交汇影响,这里需要针对发生问题的地方做一些改动,比如更换实现方式,避免出现上述问题;第二,冲突的类需要由一个Parent类加载器进行加载。**LinkageError** 和**ClassCastException** 本质是一样的,加载自不同类加载器的类型,在同一个类的方法或者调用中出现,如果有转型操作那么就会抛 ClassCastException ,如果是直接的方法调用处的参数或者返回值解析,那么就会产生 LinkageError 。
8. 类加载器问题对照表
遇到类加载器问题时,可以尝试使用下面的表格进行问题排查。
类找不到 | 加载了不正确的类 | 多于一个类被加载 |
ClassNotFoundException NoClassDefFoundError | IncompatibleClassChangeError NoSuchMethodError NoSuchFieldError IllegalAccessError | ClassCastException LinkageError |
IDE class lookup (Ctrl+Shift+T in Eclipse)find . -name “*.jar” -exec jar -tf {} \; | grep DateUtils
使用middleware-detector | 通过在启动参数中加 -verbose:class,观察加载的类来自哪个jar包使用middelware-detector | 通过`-verbose:class`观察 |
再举个例子,加深一下影响:
JAVA启动后,是经过JVM各级ClassLoader来加载各个类到内存。为了更加了解加载过程,我通过分析和写了一个简单的ClassLoader来粗浅的分析它的原理。
JVM的ClassLoader分三层,分别为Bootstrap ClassLoader,Extension ClassLoader,System ClassLoader,他们不是类继承的父子关系,是逻辑上的上下级关系。
Bootstrap ClassLoader是启动类加载器,它是用C++编写的,从%jre%/lib目录中加载类,或者运行时用-Xbootclasspath指定目录来加载。
Extension ClassLoader是扩展类加载器,从%jre%/lib/ext目录加载类,或者运行时用-Djava.ext.dirs制定目录来加载。
System ClassLoader,系统类加载器,它会从系统环境变量配置的classpath来查找路径,环境变量里的.表示当前目录,是通过运行时-classpath或-Djava.class.path指定的目录来加载类。
一般自定义的Class Loader可以从java.lang.ClassLoader继承,不同classloader加载相同的类,他们在内存也不是相等的,即它们不能互相转换,会直接抛异常。java.lang.ClassLoader的核心加载方法是loadClass方法,如:
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {//直接判断父加载器classloader,所以默认是双亲委派加载机制
c = parent.loadClass(name, false);
} else {
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
通过上面加载过程,我们能知道JVM默认是双亲委托加载机制,即首先判断缓存是否有已加载的类,如果缓存没有,但存在父加载器,则让父加载器加载,如果不存在父加载器,则让Bootstrap ClassLoader去加载,如果父类加载失败,则调用本地的findClass方法去加载。
可以通过下面三条语句,输入现在加载的各个classloader的加载路径:
System.out.println("sun.boot.class.path:" + System.getProperty("sun.boot.class.path"));
System.out.println("java.ext.dirs:" + System.getProperty("java.ext.dirs"));
System.out.println("java.class.path:" +System.getProperty("java.class.path"));
ClassLoader cl = Thread.currentThread().getContextClassLoader();//ClassLoader.getSystemClassLoader()
System.out.println("getContextClassLoader:" +cl.toString());
System.out.println("getContextClassLoader.parent:" +cl.getParent().toString());
System.out.println("getContextClassLoader.parent2:" +cl.getParent().getParent());
输出结果为:
sun.boot.class.path:C:\Program Files\Java\jre7\lib\resources.jar;C:\Program Files\Java\jre7\lib\rt.jar;C:\Program Files\Java\jre7\lib\sunrsasign.jar;C:\Program Files\Java\jre7\lib\jsse.jar;C:\Program Files\Java\jre7\lib\jce.jar;C:\Program Files\Java\jre7\lib\charsets.jar;C:\Program Files\Java\jre7\classes
java.ext.dirs:C:\Program Files\Java\jre7\lib\ext;C:\Windows\Sun\Java\lib\ext
java.class.path:E:\MyProjects\workspace\TestConsole\bin
getContextClassLoader:sun.misc.Launcher$AppClassLoader@19dbc3b
getContextClassLoader.parent:sun.misc.Launcher$ExtClassLoader@b103dd
getContextClassLoader.parent2:null
从上面的运行结果可以看出逻辑上的层级继承关系。双亲委托机制的作用是防止系统jar包被本地替换,因为查找方法过程都是从最底层开始查找。 因此,一般我们自定义的classloader都需要采用这种机制,我们只需要继承java.lang.ClassLoader实现findclass即可,如果需要更多控制,自定义的classloader就需要重写loadClass方法了,比如tomcat的加载过程,这个比较复杂,可以通过其他文档资料查看相关介绍。
各个ClassLoader加载相同的类后,他们是不互等的,这个当涉及多个ClassLoader,并且有通过当前线程上线文获取ClassLoader后转换特别需要注意,可以通过线程的setContextClassLoader设置一个ClassLoader线程上下文,然后再通过Thread.currentThread().getContextClassLoader()获取当前线程保存的Classloader。但是自定义的类文件,放到Bootstrap ClassLoader加载目录,是不会被Bootstrap ClassLoader加载的,因为作为启动类加载器,它不会加载自己不熟悉的jar包的,并且类文件必须打包成jar包放到加载器加载的根目录,才可能被扩展类加载器所加载。
下面我自定义一个简单的classloader:
public class TestClassLoader extends ClassLoader {
//定义文件所在目录
private static final String DEAFAULTDIR="E:\\MyProjects\\workspace\\TestConsole\\bin\\";
public Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = null;
try {
b = loadClassData(GetClassName(name));
} catch (Exception e) {
e.printStackTrace();
}
return defineClass(name, b, 0, b.length);
}
@Override
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if(name.startsWith("java.")){try {
return super.loadClass(name, false);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
byte[] b = null;
try {
b = loadClassData(GetClassName(name));
} catch (Exception e) {
e.printStackTrace();
}
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String filepath) throws Exception {
int n =0;
BufferedInputStream br = new BufferedInputStream(
new FileInputStream(
new File(filepath)));
ByteArrayOutputStream bos= new ByteArrayOutputStream();
while((n=br.read())!=-1){
bos.write(n);
}
br.close();
return bos.toByteArray();
}
public static String GetClassName(String name){
return DEAFAULTDIR+name.replace('.','/')+".class";
}
}
这个自定义的ClassLoader重写了loadclass方法,但不用默认的双亲委托,比如java.lang包下面的都无法解析,这里我简单的判断如果是java.开始的包则用父类去解析,能简单的满足双亲委托机制,但是其他相关非系统类加载也没有用父类加载了。
测试代码如:
TestClassLoader liuloader = new TestClassLoader();
Myrunner runner = new Myrunner();
runner.setContextClassLoader(liuloader);
runner.start();
Myrunner是我自定义继承自Thread的线程,通过设置线程上下文的classloader后,线程内部测试代码如:
ClassLoader cl1 = Thread.currentThread().getContextClassLoader();
System.out.println(cl1);
它将会输出:
com.liu.ClassLoader.TestClassLoader@347cdb,说明已经为当前线程上下文设置了自定义的Classloader了,如果这个线程内部通过这个classloader加载一个类,再转换成当前的类,如代码:
Class c = cl1.loadClass("com.liu.ClassLoader.TestLoader2"); TestLoader2 tloader = (TestLoader2)c.newInstance();
则为抛java.lang.ClassCastException异常: com.liu.ClassLoader.TestLoader2 cannot be cast to com.liu.ClassLoader.TestLoader2。
因为cl1当前是 TestClassLoader加载的,而这个TestLoader2的类还是默认由AppClassLoader加载,因此它们不能隐式转换,Classloader加载相同的类,内存认为它们是没有关系的对象。
如果把我自定义的TestClassLoader里的LoadClass方法去掉,则采用了双亲委托机制,这样我们除了指定的类以外,其他都会优先用父类来加载。这样可以解决刚才的java.lang.ClassCastException异常问题,为加载的对象建立一个抽象父类,自定义的Classloader负责加载子类,父类统一交给AppClassLoader或父加载器来加载,这样线程内部可以使用类试:
Class c = cl1.loadClass("com.liu.ClassLoader.TestLoader2");
BaseTest tloader = (BaseTest)c.newInstance();
BaseTest是TestLoader2的父类,因为BaseTest都是AppClassLoader或父加载器加载的,因此可以达到成功隐式转换的目的。
对于Tomcat等几个处理的Classloader都是自定义并重写了loadclass方法,内部会更复杂处理。