解析Java的ClassLoader机制
Java的ClassLoader机制解析
1.类加载器概述
类加载器是一个对象,是负责加载类.在JVM是通过类加载器的调用LoadClass方法加载类对象.
类加载器结构:
1. 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的[null]
2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类[ExtClassLoader]
3. 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader() 来获取它[AppClassLoader]
2.类加载器的工作流程
当classLoader有类需要载入时,先让其parent查找载入,如果parent找不到,再由自己搜索路径进行载入。ClassLoader在运行期会以父/子的层次结构存在,每个classLoader 实例都有其父ClassLoader的引用,而父ClassLoader并没有持有子ClassLoader的引用,从而形成一条单向链,当一个类装载请求提交到某个ClassLoader时,默认的类装载过程如下:
1. 检查这个类有没有被装载过,如果已经装载过,返回
2. 调用父ClassLoader去装载类,如果装载成功返回.
3. 调用自身的装载类方法,如果装载成功则返回
4. 如查都没有成功,抛出ClassNotFoundException.
简单说,当ClassLoader链上的某一ClassLoader收到类装载请求时,会按顺序向上询问其所有父节点,直到boot classLoader.任何一个节点成功受理了此请求,则返回,如果所有父节点都不能受理,这个时候才由请求的ClassLoader自身来装载这个类,如果仍不能装载,则抛出异常.
3.Class的类加载器到底是哪个.
类加载器在其应用场景的不同又可以分为如下类加载器:
1. 系统 ClassLoader
2. 调用者 ClassLoader
3. 线程上下文ClassLoader
这些类加载器主要是用于动态加载资源,也可以解决架包的重复问题.
调用者类加载器是指当前所在的类装载时所使用的ClassLoader,它可能是SystemClassLoader, 也可能是一个自定义的ClassLoader.可以通过getClass().getClassLoader()来得到Caller ClassLoader.例如,存在类A,是被AClassLoader所加载,A.class.getClassloader()为AClassLoader的实例,它就是A.class的Caller Classloader.
如果在A类中new一个B类,那么B类的类加载器就一定是AClassLoader吗。答案是错的。因为new一个对象,loadClass(B.class)可能在其父ClassLoader中就已经完成.
决定一个类的类加载器是defineClass,而判断两个类是否为同一对象的标准里面有一条是类加载器必须为相同.
现在有一个问题,如何使用指定的ClassLoader去完成类和资源的加载呢,或者说,当需要去实例化一个调用者ClassLoader和它的父ClassLoader都不能加载的类时,怎么办.
一个典型的一例子是Jaxp,当使用xerces的Sax实现时,我们首先需要通过rt.jar中的java.xml.parsers.SaxparserFactory.getinstance()得到xeceImpl.jar中的org.apache.xerces.jaxp.SAXParserFactory.Impl的实例,由于Jaxp的框架接口的类位于Java_hom/lib/rt.jar中,由bootStrap ClassLoader装载,处于ClassLoader层次结构中的最顶层,而xecesImpl.jar由低怪的ClassLoader装载,也就是说SaxParserFactoryImpl是在SaxParserFactory中实例化的,如前所述,使用SaxParserFactory的CallerClassLoader(boot)是完成不了这个任务的.这里我们需要理解下线程上下文ClassLoader.
线程上下文ClassLoader. 每一个线程都有一个关联的上下文ClassLoader.如果使用new Thread()方式生成新的线程,新线程将继承其父线程的上下文ClassLoader.如果程序对线程上下文ClassLoader没有任何改动的话,程序的所有线程将都使用System ClassLoader作为上下文ClassLoader.当使用Thread.currentThread().setContextClassLoader(classLoader)时,线程上下文ClassLoader就变成了指定的ClassLoader了。此时,在本线程的任意一处地方,调用Thread.currentThread().getContextClassLoader().都可以得到前面设置的ClassLoader.
一个线程来了,第一件事情便是设置ClassLoader来设置线程上下文类加载器,模块内部都使用线程上下文类加载器[比如某一接口处于AClassLoader,我设置AClassLoader为线程上下文类加载器,那么我通过getContextClassLoader就可以得到A类的类加载器,以后使用A类的类加载器,这样就可以统一调用了]
(有人可能会问了,我总不能每加载一个类,都使用上下文线程去加载吧,我笑了,其实这大可不必,只要你的类是你私有的[在当前类加载器父级加载器未加载过],就不需要重新加载)
4.JVM工作流程
Class Loader 加载流程
Jvm 建立=>初始化工作=>产生第一个ClassLoader,即boot
Boot ClassLoader在sum.misc.Launcher类里面的ExtClassLoader,并设置其Parent为Boot.
Boot ClassLoader载入sun.misc.Launcher$AppClassLoader,设定其parent为ExtClassLoader(但是AppClassLoader也是boot所载入)
AppClassLoader载入各个xx.class,xx.class也有可能被ExtClassLoader或者boot载入.
自定义的ClassLoader的getparent()是AppClassLoader.parent和他的加载器没有关系.
ExtClassLoader和AppClassLoader都是URLClassLoader的子类。
AppClassLoader的URL是由系统参数java.class.path取出的字符串决定,而java.class.path由运行机制java.exe时的-cp或-classpath或CLASSPATH环境变量决定
ExtClassLoader查找的url是系统变量java.ext.dirs,java.ext.dirs默认为jdk\jre\lib\ext
Bootstrap loader的查找url是sun.boot.class.path
5.独立应用的类加载器
由于系统类加载器是JVM最后创建的类加载器,这样代码只会适应于简单命令行启动的程序。一旦代码移植到EJB、Web应用或者Java Web Start应用程序中,程序肯定不能正确执行。
因此一般只有两种选择,当前类加载器和线程上下文类加载器。当前类加载器是指当前方法所在类的加载器。这个类加载器是运行时类解析使用的加载器,Class.forName(String)和Class.getResource(String)也使用该类加载器。代码中X.class的写法使用的类加载器也是这个类加载器。
Web应用和Java企业级应用中,应用服务器经常要使用复杂的类加载器结构来实现JNDI(Java命名和目录接口)、线程池、组件热部署等功能,因此理解这一点尤其重要。
通常JVM中的类加载器是按照层次结构组织的,目的是每个类加载器(除了启动整个JVM的原初类加载器)都有一个父类加载器。当类加载请求到来时,类加载器通常首先将请求代理给父类加载器。只有当父类加载器失败后,它才试图按照自己的算法查找并定义当前类。[如何覆盖父类的加载机制]
有时这种模式并不能总是奏效。这通常发生在JVM核心代码必须动态加载由应用程序动态提供的资源时。拿JNDI为例,它的核心是由JRE核心类(rt.jar)实现的。但这些核心JNDI类必须能加载由第三方厂商提供的JNDI实现。这种情况下调用父类加载器(原初类加载器)来加载只有其子类加载器可见的类,这种代理机制就会失效[双亲委托机制]。
解决办法就是让核心JNDI类使用线程上下文类加载器,从而有效的打通类加载器层次结构,逆着代理机制的方向使用类加载器。
顺便提一下,XML解析API(JAXP)也是使用此种机制。当JAXP还是J2SE扩展时,XML解析器使用当前累加载器方法来加载解析器实现。但当JAXP成为J2SE核心代码后,类加载机制就换成了使用线程上下文加载器,这和JNDI的原因相似。
但这在不同JVM线程共享数据来沟通时,就会使类加载器的结构乱七八糟。除非所有线程都使用同一个上下文类加载器。而且,使用当前类加载器已成为缺省规则,它们广泛应用在类声明、Class.forName等情景中。即使你想尽可能只使用上下文类加载器,总是有这样那样的代码不是你所能控制的。这些代码都使用代理到当前类加载器的模式。混杂使用代理模式是很危险的。
这种混乱的状况还将在Java中存在很长时间。在J2SE中还包括以下的功能使用不同的类加载器:
JNDI使用线程上下文类加载器
Class.getResource()和Class.forName()使用当前类加载器
JAXP使用上下文类加载器
Java.unit.ResourceBundle使用调用者的当前类加载器
URL协议处理器使用java.protocol.handler.pkgs系统属性并只使用系统类加载器
Java序列化API缺省使用调用者当前的类加载器.
6.Tomcate类加载器的代理模式
下面是对每个类加载器的定义:
1.Bootstrap加载器在这里是Java里的Bootstrap和ExtClassLoader的总称,负责加载Java核心包的类,和<Java_Home>/jre/lib/ext目录下的类.通常我们开发人员并不关心.我想只要是java程序这些肯定是必要的
2.System就是系统加载器,一般是AppClassLoader,负责加载ClassPath环境变量设置目录下的值,这个我们开发人员会非常关注,但是在Tomcat里面,虽然用AppClassLoader类加载器,但我们设置的ClassPath对它没有影响(如果有影响,那就麻烦了,将会导致Tomcat运行不稳定),为什么呢,因为tomcat每次启动的时候都会在命令行窗口中都会重新设置Classpath值为:<catalina_Home>/bin/bootstrap.jar和<java_Home>/lib/tools.jar,所以这里面的类一般对应用程序不可见的.除非你设置了
3.Common类加载器负责加载TomcatHOME/common/class下的.Class文件和common/lib中的jar包,这些类可以被Tomcat内核和每个Web应用程序都可以看见,一般放公用的一些重要的类,如servlet.jar等
4.Catalina类加载器从server/classes和server/lib下加载类,Catalina加载的类只对Tomcat服务器内核可见,对Web应用程序不可见,对于运行Tomcat内核的线程,它的上下文类加载器就是Catalina类加载器
5.Shared类加载器负责从share/classes和share/lib中加载类,它加载的类只对所有Web应用程序有效,对Tomcat不可见.
6.WebappX类加载器负责加载Web应用程序的/web-INF/classes和lib目录下的类,只对当前Web应用程序有效,对其他Web应用程序无效,对于运行每个Web应用程序的线程,他们的上下文类加载器就是它们各自的WebappX类加载器
7.自定义类加载器
通过ClassLoader的子类动态加载class文件,体现java动态实时类装入特性:
ClassLoader有两种载入方式:
Pre-loading 预先载入,载入的基础类
Load-on-demand 按需载入,只有实例化一个类才会被classloader载入,仅仅声有不会被载入.
Static 何时执行:
当调用forName(String)载入class时执行,如果调用ClassLoader.loadClass不会执行,forName(String,false,ClassLoader)也不会执行.
如果在载入class时没有执行static块,则在第一次实例化时执行,比如new,Class.newInstance()操作.
Static块仅执行一次
当使用java 去执行一个类的时候,JVM使用applicationClassLoader加载这个类,如果A类引用了B类,不管是直接引用,还是class.forName()引用,JVM会找到加载A类的classLoader,并使用这个ClassLoader加载B类.
注意:JVM加载类A,并使用A的ClassLoader去加载B,但B的类加载器并不一定和A的类加载器一致.
使用java –verbos:class Main运行一个程序,加载如下:
1. 加载java*下的类
2. 加载自定义的类
只要当程序运行到B了,需要加载B了,JVM才会去加载这个类
隐式加载
这里的B就是引用类,发生由于引用,实例化或继承导致需要装载类的时候,隐式类装载是在幕后启动的,JVM会解析必要的引用并装载类.
显式加载
1. Java.lang.Class.forName()方法加载
a) Public static Class forName(String classname)
b) Public static Class forName(String className,Boolean ini,ClassLoader loader)
参数说明:
className 所需类的完全限定名
ini 是否必须初始化类(静态代理初始化)
loader – 用于加载类的类加载器
调用只有一个参数的相当于Class.forName(className,true,loader);
这里的loader 为callerClassLoader,就是调用者类加载器.
不管使用的是new来实例化某个类,或是使用只有一个参数的forName()方法,内部矛盾都隐含了“载入类+运行静态代码块”的步骤。而使用三个参数的,如果第二个参数为false,那么类加载器只会加载类,而不会初始化静态代码块.只有实例化类时,才会执行.不过静态代码只执行一次.
上一篇: 深入探讨 Java 类加载器
下一篇: was SystemOut.log乱码