java的类加载器 classloader
以下内容转自 http://blog.csdn.net/coslay/article/details/40709921
ClassLoader顾名思义就是类加载器,负责将Class加载到JVM中,它就好比开会时 门口的接待员,负责给进入会场的嘉宾发放入会证明,入会的嘉宾分为VIP会员、黄金会 员、白金会员和普通会员等。对应的接待室也会分为VIP会员接待室、黄金会员接待室、 白金会员接待室和普通接待室,不同等级的会员会被分到不同的接待室接待。所有的会员 要想进入会场得有入会证明才行,一旦会员进入会场就会根据接待室的等级标识他们,也 就是会员的身份由接待室决定。如果你是一位大佬但是你不是VIP接待室接待的,那么对 不起,你仍然不是VIP会员。当然对你是不是VIP会员会有严格的审查规定,如果你是也 不会冤枉你,但是如果你想混进来那就另当别论了。
事实上,ClassLoader除了能将Clas加载到JVM之外,还有一个重要的作用就是 审查每个类该由谁加载,它是一种父优先的等级加载机制,为何是这种加载机制我们将 在后面详细分,ClassLoader除了上述两个作用外还有一个任务就是将Class字节码重新 JVM统一要求的对象格式。
本章主要分析ClassLoader的前两个作用,也就是ClassLoader的加栽机制和加栽类的过程,另外还将着重介紹在Java Web中常用的ClassLoader是如何实现的,理解它们将帮助我们在日常的开发过程中更好在理解程序是如何工作的。
ClassLoader类结构分析
我们经常会用到或扩展ClassLoader,主要会用到如图6-1所示的几个方法,以及它们的重载方法。
defineClass通常是和findClass方法一起使用的,我们通过直接覆盖ClassLoader父类的findClass方法来实现类的加载规则,从而取得要加载类的字节码。然后调用defineClass方法生成类的Class对象,如果你想在类被加载到JVM中时就被链接(Link),那么可以接着调用另办一个 resolveClass方法,当然你也可以选择让JVM来解决什么时候才链接这个类。
如果你不想重新定义加载类的规则,也没有复杂的处理逻辑,只想在运行时能够加载自己指定的一个类,那么你可以用 this.getClass().getClassLoader().loadClass("class")调用ClassLoader的loadClass方法以获取这个类的Class对象,这个loadClass还有重载方法,你同样可以决定在什么时候解析这个类。
ClassLoader是个抽象类,它还有很多子类,我们如果要实现自己的ClassLoader,一般都会继承URLClassLoader这个字类,因为这个类已经帮我们实现了大部分工作,我们只需要在适当的地方做些修改就好了,就像我们要实现Servlet时通常会直接继HttpServlet —样 。
前面介绍的这几个方法都是我们在扩展ClassLoader时需要用到的,ClassLoader 还提供了 另外一些辅助方法, 如获取 class 文件的法 getResource ,getResourceAsStream 等,还有就是获取SystemClassLoader的方法等。对这些方法我们在后面再详细介绍。
ClassLoader的等级加载机制
ClassLoader就设计了这样—种接待机制,这个机制就是上级委托接待机制。它是这样的:任何—个会员到达任何一个会员接待室时,这个接待室会检查这个会员是否已经被自己接待过,如果已经接待过,则拒绝本次接待,也就是不再发入场证明了,如果没有接待过,那么会向上询问这个会员是否应该在上一级的更髙级别的接待室接待,上级接待室会根据它们的接待规则,检查这个会员是否已经被接待过,如果已经接待过,同样的处理方法,将已经接待的结果反馈给下一级,如果也没有接待过,则向更高一级(如果有更高一级的话)接待室转发接待请求,更髙一级也是同样的处理方法,直到有—级接待室接待或者诗诉它下一级这个会员不是自己接待的这个结果,如果这个会员来到的这个接待室得到它上一级的接待室反馈认为这个会员没有被接待,并且也不应该由它们接待,这个接待室将会正式接待这个会员,并发给它入会证明,这个会员就被定义为这个接待室等级的会员。
这种接待规则看上去有点麻零,佴是它®能够%揉所耷都被正确的筆待室接待,会员的身份也不会错,也不存在冒充身份的会员。
整个JVM平台提供三层ClassLoader,这三层ClassLoader可分为两种类型,可以理解为接待室服务的接待室和为会员服务的接待室两种。
(2) ExtClassLoader,这个类加载器有点特殊,它是JVM自身的 一部分,但是它的血统也不是很纯正,它并不是JVM亲自实现的,我们可以理解为这个类加载器是那些与这个大会合作单位的员工会员,这些会员既不是JVM内部的,也和普通的外部会员不同,
所以就由这个类加载器来加载。它服务的特定目标在System.getProperty("java.ext.dirs”)目录下。
(3) AppClassLoader,这个类加载器就是专门为接待会员服务的,它的父类是ExtClassLoader。它服务的目标是广大普通会员,所有在 System.getProperty("java.class.path”)目录下的类都可以被这个类加载器加载,这个目录就是我们经常用到的classpath。
通常一个应用中的类加载器的等级结构如图6-2所示。
ExtClassLoader 和AppClassLoader都位于sun.misc.Launcher 类中,它们是Launcher类的内部类,如图6-3所示。
getClassLoader()方法获取的ClassLoadiet就是AppClassLoader对象。所以如果在Java应用中没有定义其他ClassLoader,那么除了 System,getPropeirty("java.ext..dirs")目录下的类是由ExtClassLoader加载外,其他类都由AppClassLoader来加载。
JVM加载class文件到内存有两种方式。
◎隐式加载:所谓隐式加载就是不通过在代码里调用ClassLoader来加载需要的类,而是通过JVM来自动加载需要的类到内存的方式。例如,当我们在类中继承或者引用某个类时,JVM在解析这个类时发现引用的类不在内存中,那么就会自动将这些类加载到内存中。
◎显式加载:相反的显式加载就是我们在代码中通过调用ClassLoader类来加载个类一个类的方式,例如,调用this.getClass.getClassloader().loadClass()或者 Class.forName(),或者我们自己实现ClassLoader的findClass()方法等。
如何加载class文件
- 第一个阶段是找到.class文件并把这个文件包含的字节码加载到内存中。
- 第二个阶段又可以分为3个步骤,分别是字节码验征、Class类数据结构分析及相应的内存分配和最后的符号表的链接。
- 第三个阶段是类中静态属性和初始化赋值,以及静态块的执行等。
加载字节码到内存
我们再看看URLClassLoader类的构造函数,如图6-5所示,我们可以发现必须 要指定—个URL数据才能够创建URLClassLoader对象,也就是必须要指定这个ClassLoader默认到哪个目录下去杳找class 文件。
在创建URLClassPath对象时会根据传过来的URL数组中的路径来判断是文件还是jar包,根据路径的不同分别创建FileLoader或者JarLoader,或者使用默认的加载器。当JVM调用findClass时由这几个加载器来将class文件的字节码加载到内存中。
如何设置每个ClassLoader的搜索路径呢?如表6-1所示是Bootstrap ClassLoader、ExtClassLoader 和 AppClassLoader 的参数形式。
ClassLoader 类型 |
参数选项 |
说 明 |
|
-Xbootclasspath: |
设置Bootstrap ClassLoader的搜索路径 |
Bootstrap ClassLoader |
-Xbootclasspath/a: |
把路径添加到己存在Bootstrap ClassLoader搜索路径的后面 |
|
-Xbootclasspath/p: |
把路径添加到已存在Bootstrap ClassLoader搜索路径的前面 |
ExtClassLoader |
-Djava.ext.dirs |
设置ExtClassLoader的搜索路径 |
ClassLoader 类型 |
参数选项 |
说 明 |
|
-Xbootclasspath: |
设置Bootstrap ClassLoader的搜索路径 |
Bootstrap ClassLoader |
-Xbootclasspath/a: |
把路径添加到己存在Bootstrap ClassLoader搜索路径的后面 |
|
-Xbootclasspath/p: |
把路径添加到已存在Bootstrap ClassLoader搜索路径的前面 |
ExtClassLoader |
-Djava.ext.dirs |
设置ExtClassLoader的搜索路径 |
ClassLoader的出错分析在后面会详细介绍。
◎类准备,在这个阶段准备代表每个类中定义的字段、方法和实现接口所必需的数据结构。
◎解析,在这个阶段类装入器装类所用的其他所有类。可以用许多方式引用类,如超类、接口、字段、方法签名、方法、方法中使用的本地变量。
初始化Class对象
常见加载器类错误分柝
ClassNotFoundException
这个异常通常发生在显式加载类的时候,例如,用如下方式调用加载一个类时就报这个错了 :
- public class notfountexception {
- public static void main(String[] args) {
- try {
- Class.forName("notFountClass”);
- }catch (ClassNotFoundException e) {
- e.printStackTrace();
- }
- }
- }
显式加载一个类通常有如下方式:
◎通过类Class中的forName()方法。
◎ 通过类 ClassLoader 中的 loadClass()方法。
◎通过类 ClassLoader 中的 findSystemClass()方法。
出现这类错误也很好理解,就是当JVM要加载指定文件的字节码到内存时,并没有找到这个文件对应的字节码,也就是这个文件并不存在。解决的办法就是检査在当前的classpath目录下有没有指定的文件存在。如果不知道当前的classpath路径,就可以通过如
下命令来获取:
- java -cp example.jar Example
在这个jar包里面只有一个类,这个类是net.xulingbo.Example,可能让你感到郁闷的是,明明在这个jar包里有这个类为啥会报如下错误呢?
- Exception in thread "main" java.lang.NoClassDefFoundError:example/jar
- Caused by: java.lang.ClassNotFoundException: example.jar
- at java. net. URLClassLoader$l. run (URLClassLoader .java: 200)
- at java.security.AccessController.doPrivileged (Native Method)
- at java.net .URLClassLoader. findClass (URLClassLoader. java: 188)
- at java. lang. Class Loader. loadClass (ClassLoader. java : 306)
- at sun.misc. Launcher$AppClassLoader . loadClass (Launcher .java: 276)
- at java.lang.ClassLoader.loadClass(ClassLoader.java ; 251)
- at java. lang. ClassLoader. loadClass Internal (ClassLoader. java : 319)
这是因为你在命令行中没有加类的包名,正确的写法是这样的:
- java -cp example.jar net.xulingbo.Example
这里同时报了NoClassDefFoundError和 ClassNotFoundException异常,原因是 Java虚拟机隐式加载了exanple.jar后显式加载Example时没有找到这个类,所以是ClassNotFoundException 引发了 NoClassDefFoundError 异常。
在JVM的规范中描述了出现NoClassDefFoundError可能的情况就是使用new关键字、属性引用某个类、继承个接口或类,以及方法的某个参数中引用了某个类,这时会触发JVM隐式加载这些类时发现这些类不存在的异常。
解决这个错误的办法就是确保每个类引用的类都在当前的classpath路径下面。
UnsatisfiedLinkError
- public class NoLibException {
- public native void nativeMethod();
- static {
- System.loadLibrary("NoLib");
- }
- public static void main(String[] args) {
- new NoLibException().nativeMethod();
- }
- }
这个错误通常是在解析native标识的方法时JVM找不到对应的本机库文件时出现,代码如下:
ClassCastException
- public class CastExceptlon {
- public static Map m = new HashMap(){{
- put ("a","2");
- } };
- public static void main(String[] args) {
- Integer islnt =(Integer)m.get("a”);
- System.out.print(islnt);
- }
- }
当强制将本来不是Integer类型的字符串转成Integer类型时会报如下错误:
- Exception in thread "main” java .lang .ClassCastException: j ava. lang. String csnnot be cast to java.lang.Integer
- at EmptyProject.classloader.CastException.main((Cast Exception.java: 17)
- at sun.reflect.NativeMethodAccessorlmpl.invoke0(Native Method)
- at sun.reflect.NativeMethodAccessorlmpl.invoke (NativeMethodAccessorImpl.java:57)
- at sun.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorlmpl.java:43)
- at java.lang.reflect.Method.invoke(Method.java:613)
- at com.intellij.rt.execution.application.AppMain .main (AppMain .java: 110)
JVM在做类型转换时会按照如下规侧进行检査:
- 对于普通对象,对象必须是貝标类的实例或目标类的子类的实例。如果目标类是接口,那么会把它当作实现了该接口的一个子类。
- 对于数组类型,目标类必须是数组类型或java.lang.Object 、java.lang.Cloneable或java.io.Serializable。
- 在容器类型中显式地指明这个容器所包含的对象类型,如在上面的Map中可以写为 Map<String,Integer> m = new HashMap<String,Integer>(),.这祥上面的代码在编译阶段就会检査通过。
- 先通过instanceof检查是不是目标类型,然后再进强制类型转换。
ExceptionInInitializerError
这个错误在JVM规范中是这样定义的:- 如果Java虚拟机试图创建类ExceptionlnInitializerError的新实例,但是因为出现Out-Of-Memory-Error而无法创建新实倒.,那么就抛出OutOfMemoryError对象作为替代。
- 如果初始化器抛出一些Exception,而县Exception类不是Error或者它的某个子类,那么就会创建ExceptionlnlnitializeiError类的一个新实例,并用Exception作为参数,用这个实例代替Exception。
将上面的代码例子稍微改了一下:
- public class CastExceptlon {
- public static Map m = new HashMap(){{
- m.put ("a","2");
- } };
- public static void main(String[] args) {
- Integer islnt =(Integer)m.get("a”);
- System.out.print(islnt);
- }
- }
这段代码在执行时就会报如下错误:
常用的ClassLoader分析
创建一个简单的Web应用,里面有一个HelloWorldServlet,然后在这个Servlet中打印加载它的ClassLoader,代码如下:
上面这段代码打印出来的结果如下:
这里有一点一定要说明下,我们前面说Tomcat容器的加载ClassLoader是StandardClassLoader,但是如果你调用Tomcat中任何一个类拿,如StandardContext类,通过 getClass().getClassLoader()方法返回的 ClassLoader 并不是 StandardClassLoader,而是AppClassLoader,为什么呢?原因是StandardClassLoader虽然是加载 StandardContext的类,但是可以看StandardClassLoader的实现方法可以发现StandardClassLoader只是一个代理类,并没有覆盖ClassLoader的loadClass()方法,StandardClassLoader仍然沿用委托加载器,它首先会父加载器来加载,所以真正加载类仍然是是通过其父类AppclassLoader来完成的,加载Tomcat容器本身仍然是AppclassLoader。
但是如果Tomcat的ClassPath没有被设置,那么AppClassLoader就将加载不到Tomcat容器的类,这时就要通过StandardClassLoader来加载了。其实不管是StandardClassLosder还是AppClassLoader加载,都没有任何影响;因为它们的加载规则一模—样,唯一不同的就是加载的路径不同。
其实我们真正关心的不是Tomcat容器本身是谁加载的,而是我们的应用是怎么加载的,也就是一个Web应用需要Tomcat执行时,这应用中的类是通过什么规则加载起来的?
- if (getLoader() == null)[
- WebappLaader webappLoader = new WebappLaader (getParentClassLoader());
- webappLoader.setDelegate(getDelegate());
- setLoader(webappLoader);
- }
这段代码清楚地表示将创建WebappLoader对象,而WebappLoader对象将创建WebappClassLoader 作为其 ClassLoader。再翻阅 StandardWrapper 类的 loadServlet()方法可以发现,所有的Servlet都是InstanceManager实例化的,那么InstanceManager类使用的ClassLoader 是不是 WebappClassLoader 呢?
再看一下InstanceManager构造函数,代码如下:
(1) 首先检查在WebappClassLoader中是否已经加载过了,如果请求的类以前是被WebappClassLoader 加载的,那么肯定在 WebappClassLoader的缓存容器 resourceEntries 中。
(2) 如果不在WebappClassLoader的resourceEntries中,则继续检查在JVM虚拟机中是否已经加载过,也就是调用ClassLoader的findLoadedClass方法。
(3) 如果在前两个缓存中都没有,则先调用SystemClassLoader加栽请求的类,SystemClassLoader在这里是AppClassLoader,也就是在当前的JVM的ClassPath路径下査找请求的类。
(4) 检查请求的类是否在packageTriggers定义的包名下,如果在这个设置的包目录下,则将通过StandardClassLoader类来加载。
从上面的分析来看,Tomcat仍然沿用了 JVM的类加载规范,也就是委托式加载,保证核心类通过AppCIassLoader来加载。但是Tomcat会优先检查WebappClassLoader己经加载的缓存,而不是JVM的findLoadedClass缓存,这一点需要注意。
◎在自定义路径下查找自定义的class类文件,也许我们需要的class文件并不总是在己经设置好的ClassPath下面,那么我们必须想办法来找到这个类,在这种情况下我们需要自己实现一个ClassLoader。
◎对我们自己的要加载的类做特殊处理,如保证通过网络传输的类的安全性,可以将类经过加密后再传输,在加载到JVM之前需要对类的字节码再解密,这个过程就可以在自定义的ClassLoader中实现。
◎可以定义类的实现机制,如果我们可以检查已经加载的class文件是否被修改,如果修改了可以重新加载这个类,从而实现类的热部署。
加载自定义格式的class文件
defineClassO方法创建这个类的实例,最后完成类的加载工作,如下代码所示:
实现类的热部署
完整类名是否一样,这个类名包括类所在的包名。 二是看加载这个类的ClassLoader是否是同一个,这里所说的同一个是指ClassLoader的实例是杏是同一个实例。即使是同一个ClassLoader类的两个实例,加同一个类也会不一样。所以要实现类的热部署可以创建不同的ClassLoader的实例对象,然后通过这个不同的实例对象来加载同名的类,如下面的代码所示:
答案是否定的,因为我们的Classloader对象也和其他对象一样,当没有对象再引用它以后,也会被JVM回收,但是需要注意的一点是,被这个Classloader加载的类的字节码会保存在JVM的PermGen区,这个数据一般只是在执行Full GC时才会被回收的,所以如
果在你的应用中都是大量的动态类加载,FullGC又不是太频繁,也要注意PermGen区的大小,防止内存溢出。
java应不应该动态加载类
Java的优势正是基于共享对象的机制,达到信息的髙度共享,也就是通过保存并持有对象的状态而省去类信息的重复创建和回收。我们知道对象一旦被创建,途个对象就可以被人持有和利用。
假如,我只是说假如,如我们能够动态加载一个对象进入JVM,但是如何做到JVM中对象的平滑过渡?几乎不可能!虽然在JVM中对象只有一份,在理论上可以直接替换这个对象,然后更新Java栈中所有对原对象的引用关系。看起来好像对象可以被替换了,但是这仍然不可行,因为它违反了 JVM的设计原则,对象的引用关系只有对象的创建者持有和使用,JVM不可以干预对象的引用关系,因为JVM并不知道对象是怎么被使用的,这就涉及JVM并不知道对象的运行时类型而只知道编译时类型。
假如一个对象的属性结构被修改,但是在运行时其他对象可能仍然引用该属性。虽然完全的无障碍的替换是不现实的,但是如果你非要那样做,也还是有一些“旁门左道”的。前面的分析造成不能动态提供类对象的关键是,对象的状态被保存了,并且被其他对象引用了,一个简单的解决办法就是不保存对象的状态,对象被创建使用后就被释放掉,下次修改后,对象也就是新的了。