`

Java核心代码(二)Class loader

 
阅读更多

 

需要动态加载很多类和资源时(经常出现在编写框架代码) .至少有三个 ClassLoader 可以选择 : 

        1、系统类加载器或叫作应用类加载器 (system classloader or application classloader) 

        2、当前类加载器 

        3、当前线程类加载器 

        system classloader这个类加载器处理 -classpath 下的类加载工作 , 可以通过 ClassLoader.getSystemClassLoader() 方法调用 . ClassLoader 下所有的 getSystemXXX() 的静态方法都是通过这个方法定义的 . 在你的代码中 , 你应该尽量少地调用这个方法 , 以其它的类加载器作为代理 . 否则你的代码将只能工作在简单的命令行应用中 , 这个时候系统类加载器 (system classloader) 是 JVM 最后创建的类加载器 . 一但你把代码移到 EJB, Web 应用或 Java Web Start 应用中 , 一定会出问题 . 

        当前类加载器就是你当前方法所属的类的加载器 . 在运行时类之间动态联编 , 及调用 Class.forName,() Class.getResource() 等类似方法时 , 这个类加载器会被隐含地使用

    线程上下文类型加载器是在Java 2平台上被引入的. 每一个线程都有一个类加载器与之对应(除非这个线程是被本地代码创建的). 这个类加载器是通过Thread.setContextClassLoaser()方法设置的. 如果你不在线程构造后调用这个方法, 这个线程将从它的父线程中继承相应的上下文类加载器. 如果在整个应用中你不做任何特殊设置, 所有的线程将都以系统类加载器(system classloader)作为自己的线程上下文类加载器. 自从Web和J2EE应用服务器使用成熟的类加载器机制来实现诸如JNDI, 线程池, 组件热部署等功能以来, 这种在整个应用中不做任何线程类加载器设置的情况就很少了. 

     上下文类加载器提供了类加载机制的后门, 这一点也在J2SE中被引入了. 通常, 在JVM中的所有类加载器被组织成了有继承层次的结构, 每一个类加载器(除了引导JVM的原始类加载器)都有一个父加载器. 每当被请示加载类时, 类加载器都会首先请求其父类加载器, 只有当父类加载器不能加载时, 才会自己进行类加载. 

   有时候这种类加载的顺序安排不能正常工作, (此处的意思是:正常情况下都是从子类加载器到根类加载器请求,万一有根类里需要加载子类时,这种顺序就不能满足要求,就要有一条反向的通道,即得到子类加载器,这样就用到了thread context classloader,因为通过thread.getcontextclassloader()可以得到子类加载器).通常当必须动态加载应用程序开发人员提供的资源的时候. 

   以JNDI为例: 它的内容(从J2SE1.3开始)就在rt.jar中的引导类中实现了, 但是这些JNDI核心类需要动态加载由独立厂商实现并部署在应用程序的classpath下的JNDI提供者. 这种情况就要求一个父classloader(本例, 就是引导类加载器)去加载对于它其中一个子classloader(本例, 系统类加载器)可见的类. 这时通常的类加载代理机制不能实现这个要求. 解决的办法(workaround)就是, 让JNDI核心类使用当前线程上下文的类加载器, 这样, 就基本的类加载代理机制的相反方向建立了一条有效的途径. 

    另外, 上面一段可能让你想起一些其它的事情: XML解析Java API(JAXP). 是的, 当JAXP只是J2SE的扩展进, 它很自然地用当前类加载器来引导解析器的实现. 而当JAXP被加入到J2SE1.4的核心类库中时, 它的类加载也就改成了用当前线程类加载器, 与JNDI的情况完全类似(也使很多程序员很迷惑). 明白为什么我说来自Sun的指导很缺乏了吧? 

   在以上的介绍之后, 我们来看关键问题: 这两种选择(当前类加载器和当前线程类加载器)都不是在所有环境下都适用. 有些人认为当前线程类加载器应该成为新的标准策略. 但是, 如果这样, 当多个线程通过共享数据进行交互的时, 将会呈现出一幅极其复杂的类加载的画面, 除非它们全部使用了同一个上下文的类加载器. 进一步说, 在某些遗留下来的解决方案中, 委派到当前类加载器的方法已经是标准. 比如对Class.forName(String)的直接调用(这也是我为什么推荐尽量避免对这个方法进行调用的原因). 即使你努力去只调用上下文相关的类加载器, 仍然会有一些代码会不由你控制. 这种不受控制的类加载委派机制是混入是很危险的. 

    更严重的问题, 某些应用服务器把环境上下文及当前类加载器设置到不同的类加载器实例上, 而这些类加载器有相同的类路径但却没有委派机制中的父子关系. 想想这为什么十分可怕. 要知道类加载器定义并加载的类实例会带有一个JVM内部的ID号. 如果当前类加载器加载一个类X的实例, 这个实例调用JNDI查找类Y的实例, 些时的上下文的类加载器也可以定义了加载类Y实例. 这个类Y的定义就与当前类加载器看到的类Y的定义不同. 如果进行强制类型转换, 则产生异常. 

   这种混乱的情况还将在Java中存在一段时间. 对于那些需要动态加载资源的J2SE的API, 我们来猜想它们的类加策略. 例如: 

          JNDI 使用线程上下文类加载器 

          Class.getResource() 和Class.forName()使用当前类加载器 

          JAXP(J2SE 1.4 及之后)使用线程上下文类加载器 2012/11/11 10:26:55

          java.util.ResourceBundle 使用调用者的当前类加载器 

          URL protocol handlers specified via java.protocol.handler.pkgs system property are looked up in the bootstrap and system classloaders only 

          Java 序列化API默认使用调用者当前的类加载器 

这些类及资源的加载策略问题, 肯定是J2SE领域中文档最及说明最缺乏的部分了.

 

 

当执行java命令的时候,JVM会先使用bootstrap classloader载入并初始化一个Launcher,Launcher源代码:

        extclassloader = ExtClassLoader.getExtClassLoader();

        loader = AppClassLoader.getAppClassLoader(extclassloader);

        Thread.currentThread().setContextClassLoader(loader);

当此线程运行的时候,我们可以通过getContextClassLoader方法来获得此context classloader,就可以用它来载入我们所需要的Class。默认的是system classloader。利用这个特性,我们可以“打破”classloader委托机制了,父classloader可以获得当前线程的context classloader,而这个context classloader可以是它的子classloader或者其他的classloader,那么父classloader就可以从其获得所需的Class,这就打破了只能向父classloader请求的限制了。这个机制可以满足当我们的classpath是在运行时才确定,并由定制的classloader加载的时候,由system classloader(即在jvm classpath中)加载的class可以通过context classloader获得定制的classloader并加载入特定的class(通常是抽象类和接口,定制的classloader中是其实现),例如web应用中的servlet就是用这种机制加载的.

 

出于安全性的考虑,试想如果system classloader“亲自”加载了一个具有破坏性的“java.lang.System”类的后果吧。这种委托机制保证了用户即使具有一个这样的类,也把它加入到了类路径中,但是它永远不会被载入,因为这个类总是由bootstrap classloader来加载的

 

每个ClassLoader加载Class的过程是:

1.检测此Class是否载入过(即在cache中是否有此Class),如果有到8,如果没有到2

2.如果parent classloader不存在(没有parent,那parent一定是bootstrap classloader了),到4

3.请求parent classloader载入,如果成功到8,不成功到5

4.请求jvm从bootstrap classloader中载入,如果成功到8

5.寻找Class文件(从与此classloader相关的类路径中寻找)。如果找不到则到7.

6.从文件中载入Class,到8.

7.抛出ClassNotFoundException.

8.返回Class.

 

classloader加载类用的是全盘负责委托机制。

        全盘负责:当一个classloader加载一个Class的时候,这个Class所依赖的和引用的所有Class也由这个classloader负责载入,除非是显式的使用另外一个classloader载入;

        委托:先让parent(委托父)类加载器(而不是super,它与parent classloader类不是继承关系)寻找,只有在parent找不到的时候才从自己的类路径中去寻找。

                System ClassLoader(AppClassLoader)-->ExtClassLoader-->BootstrapClassLoader。三者只是运行时委托父子关系,无任何继承关系

                loadClass,findClass,defineClass

        类加载还采用了cache机制,也就是如果cache中保存了这个Class就直接返回它,如果没有才从文件中读取和转换成Class,并存入cache,这就是为什么我们修改了Class但是必须重新启动JVM才能生效的原因。

 

ClassLoader Tree & Delegation Model不要翻译成双亲委托,直接叫委托。

每个ClassLoader都维护了一份自己的名称空间, 同一个名称空间里不能出现两个同名的类

 

.

 

大部分java app服务器(jboss, tomcat..)也是采用contextClassLoader来处理web服务。通过线程上下文来加载第三方库jndi实现, 而不依赖于委派模型

 

 

Webapp1,Webapp2 ... -->Common --> System --> Bootstrap

一个树状结构,相信大家差不多都知道tomcat默认是以child first装载class,优先载入web中的class类,找不到才会去装载公用类。

    tomcat启动入口,通过分析StandardContext.start()

    Java代码  收藏代码

        public synchronized void start() {  

        .....  

 

        if (getLoader() == null) {  

                    WebappLoader webappLoader = new WebappLoader(getParentClassLoader());  

                    webappLoader.setDelegate(getDelegate());  

                    setLoader(webappLoader);  

                }  

        .....  

        }  

    如果getLoader为空,则创建一个新的WebappLoader,注意这也是jboss设置tomcat loader的入口,从而替换默认的tomcat classloader。

    WebappLoader通过在startInternal启动了一个新的WebappClassLoader

    Java代码  收藏代码

        // Construct a class loader based on our current repositories list  

                try {  

 

                    classLoader = createClassLoader();  

                    classLoader.setResources(container.getResources());  

                    classLoader.setDelegate(this.delegate);  

                    classLoader.setSearchExternalFirst(searchExternalFirst);  

                    .....  

                    for (int i = 0; i < repositories.length; i++) {  

                        classLoader.addRepository(repositories[i]);  

                    }  

        }<span style="white-space: normal;"> </span>  

    最后就是WebappClassLoader的loadClass方法了,具体的类装载策略。

 

下面来看一下,loadClass的相关逻辑: 

 

    (0.1)先检查class是否已经被装载过,findLoadedClass

    (0.2)通过system classloader装载系统class,所以这里会优先装载jdk的相关代码,这个很重要,也很特别。

    如果是delegate=true并且不是filterPackage列表,则采用parent first,否则采用child first。

tomcat类加载:http://yjhexy.iteye.com/blog/668334

public class MyCalssLoader {

        public static void main(String[] args) throws IOException {

                //引导(原始)类加载器BootstrapClassLoader,不是ClassLoader的子类,JVM自身用C++实现的。

                System.out.println(System.getProperty("sun.boot.class.path"));

                URLClassPath urlClassPath = Launcher.getBootstrapClassPath();

                URL[] urls = urlClassPath.getURLs();

                for(URL url : urls){

                        //我机器上(和平台相关的)jre/lib/下的6个包和一个calsses。

                        //这就是不需要在系统属性CLASSPATH中指定这些类库的原因

                        System.out.println(url.toExternalForm()+"--"+url.getProtocol());

                }

                //类加载器sun.misc.Launcher$ExtClassLoader@addbf1

                System.out.println(System.getProperty("java.ext.dirs"));

                System.out.println(ClassLoader.getSystemClassLoader().getParent());

                //类加载器sun.misc.Launcher$AppClassLoader@19821f (即SystemClassLoader)

                System.out.println(System.getProperty("java.class.path"));

                System.out.println(ClassLoader.getSystemClassLoader());

                //得到本类的类加载器

                System.out.println(System.class.getClassLoader());//null,bootstrap classloader(不是ClassLoader实例)

                System.out.println(Config.class.getClassLoader());

                System.out.println(MyCalssLoader.class.getClassLoader()); 

        }

}

分享到:
评论
1 楼 xlshl43 2013-08-21  
流比呀。。。但这排版看的有点蛋疼。。。

相关推荐

Global site tag (gtag.js) - Google Analytics