`
ssydxa219
  • 浏览: 610206 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
文章分类
社区版块
存档分类
最新评论

ClassLoader

 
阅读更多

ClassLoader

  类加载器( class loader)用来加载 Java 类到 Java 虚拟机中。 Java 源程序( .java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码( .class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。

2. ClassLoader Hierarchy
  JVM在加载类时,使用的是双亲委托模式 (delegation model),也就是说除了 Bootstrap ClassLoader之外,每个 ClassLoader都有一个 Parent ClassLoader ClassLoader是按需进行加载 class文件。当 ClassLoader试图加载一个类时,首先检查本地缓冲,查看类是 否已被加载,如果类没有被加载,尝试委托给父 ClassLoader进行加载,如果父 ClassLoader加载失败,才会由该 ClassLoader 行加载,从而避免了重复加载的问题。一下为类装载器层次图:


<!-- [if !supportLineBreakNewLine]-->
<!-- [endif]-->

<!-- [if !supportLists]-->a.       <!-- [endif]-->Boostrap ClassLoader:启动类加载器,它用来加载一些 jdk的核心类,主要负责 核心 api JAVA_HOME/jre/lib or JAVA_HOME/bin  下的类的加载,可以通过参数 -Xbootclasspath制定需要装入的 jar包。 它本身不是用 java实现的,所以肯定不是 ClassLoader的子类了。

<!-- [if !supportLists]-->b.      <!-- [endif]-->Extendsion ClassLoader:扩展类加载器,用来加载一些扩展类,主要负责     JAVA_HOME/jre/lib/ext下类的加载 -Djava.ext.dirs 指定目录下的类 。此类是 ClassLoader的一个子类。

<!-- [if !supportLists]-->c.       <!-- [endif]-->System ClassLoader:系统类加载器 也叫 Application ClassLoader。是离我们最近的 ClassLoader了,它负责加载 CLASSPATH里指定的那些类。我们要实现自己的 ClassLoader也是继承自该类。 SystemClassLoader的父类是 Extension ClassLoader
 如果类 App1在本地缓冲中没有 class文件 (没有被加载 ),那么它会自底向上依次查找是否已经加载了类,如果已经加载,则直接返回该类实例的引用。如 BootstrapClassLoader也未成功加载该类,那么会抛出异常,然后自顶向下依次尝试加载,如果到 App1 ClassLoader还没有加载成功,那么会抛出 ClassNotFoundException异常给调用者。

<!-- [if !supportLists]-->d.      <!-- [endif]-->User Custom ClassLoader/用户自定义类加载器 (java.lang.ClassLoader的子类 )
在程序运行期间 , 通过 java.lang.ClassLoader的子类动态加载 class文件 , 体现 java动态实时类装入特性 .

类加载器的特性:

1 每个 ClassLoader都维护了一份自己的名称空间, 同一个名称空间里不能出现两个同名的类。
2 为了实现 java安全沙箱模型顶层的类加载器安全机制 , java默认采用了双亲委派的加载链结构
如下图 :

Class Diagram

类图中, BootstrapClassLoader是一个单独的 java类, 其实在这里, 不应该叫他是一个 java类。
因为, 它已经完全不用 java实现了。

它是在 jvm启动时, 就被构造起来的, 负责 java平台核心库。(具体上面已经有介绍)

自定义类加载器加载一个类的步骤 :

ClassLoader 类加载逻辑分析, 以下逻辑是除 BootstrapClassLoader 外的类加载器加载流程 :

// 检查类是否已被装载过

Class c = findLoadedClass(name);

if (c == null ) { // 指定类未被装载过

try {

         if (parent != null ) {  // 如果父类加载器不为空, 则委派给父类加载

             c = parent.loadClass(name, false );

         } else {// 如果父类加载器为空, 则委派给启动类加载加载

             c = findBootstrapClass0(name);

         }

     } catch (ClassNotFoundException e) {

         // 启动类加载器或父类加载器抛出异常后, 当前类加载器将其

         // 捕获, 并通过 findClass 方法, 由自身加载

         c = findClass(name);

     }

}

Class.forName 加载类
Class.forName使用的是被调用者的类加载器来加载类的 .
这种特性 , 证明了 java类加载器中的名称空间是唯一的 , 不会相互干扰 .

即在一般情况下 , 保证同一个类中所关联的其他类都是由当前类的类加载器所加载的 .

public static Class forName(String className)

     throws ClassNotFoundException {

     return forName0(className, true , ClassLoader.getCallerClassLoader());

}

/** Called after security checks have been made. */

private static native Class forName0(String name, boolean initialize,

ClassLoader loader)

     throws ClassNotFoundException;

上图中 ClassLoader.getCallerClassLoader 就是得到调用当前 forName方法的类的类加载器

线程上下文类加载器
java默认的线程上下文类加载器是 系统类加载器 (AppClassLoader).

// Now create the class loader to use to launch the application

try {

    loader = AppClassLoader.getAppClassLoader(extcl);

} catch (IOException e) {

    throw new InternalError(

"Could not create application class loader" );

}

// Also set the context class loader for the primordial thread.

Thread.currentThread().setContextClassLoader(loader);

以上代码摘自 sun.misc.Launch的无参构造函数 Launch()

使用线程上下文类加载器 , 可以在执行线程中 , 抛弃双亲委派加载链模式 , 使用线程上下文里的类加载器加载类 .
典型的例子有 , 通过线程上下文来加载第三方库 jndi实现 , 而不依赖于双亲委派 .

大部分 java app服务器 (jboss, tomcat..)也是采用 contextClassLoader来处理 web服务。
还有一些采用 hotswap 特性的框架 , 也使用了线程上下文类加载器 , 比如 seasar (full stack framework in japenese).

线程上下文从根本解决了一般应用不能违背双亲委派模式的问题 .

使 java类加载体系显得更灵活 .

随着多核时代的来临 , 相信多线程开发将会越来越多地进入程序员的实际编码过程中 . 因此 ,
在编写基础设施时, 通过使用线程上下文来加载类 , 应该是一个很好的选择 .

当然 , 好东西都有利弊 . 使用线程上下文加载类 , 也要注意 , 保证多根需要通信的线程间的类加载器应该是同一个 ,
防止因为不同的类加载器 , 导致类型转换异常 (ClassCastException).

 自定义的类加载器实现
defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)
java.lang.Classloader提供给开发人员 , 用来自定义加载 class的接口 .

使用该接口 , 可以动态的加载 class文件例如 ,
jdk , URLClassLoader是配合 findClass方法来使用 defineClass, 可以从网络或硬盘上加载 class.

而使用类加载接口 , 并加上自己的实现逻辑 , 还可以定制出更多的高级特性 .

 比如 ,一个简单的 hot swap 类加载器实现 :

import java.io.File;

import java.io.FileInputStream;

import java.lang.reflect.Method;

import java.net.URL;

import java.net.URLClassLoader;

/**

* 可以重新载入同名类的类加载器实现 , 放弃了双亲委派的加载链模式 .

* 需要外部维护重载后的类的成员变量状态 .

*/

public class HotSwapClassLoader extends URLClassLoader {

    public HotSwapClassLoader(URL[] urls) {

        super (urls);

    }

    public HotSwapClassLoader(URL[] urls, ClassLoader parent) {

        super (urls, parent);

    }

    public Class load(String name)

          throws ClassNotFoundException {

        return load(name, false );

    }

    public Class load(String name, boolean resolve)

          throws ClassNotFoundException {

        if ( null != super .findLoadedClass(name))

            return reload(name, resolve);

        Class clazz = super .findClass(name);

        if (resolve)

            super .resolveClass(clazz);

        return clazz;

    }

    public Class reload(String name, boolean resolve)

          throws ClassNotFoundException {

        return new HotSwapClassLoader( super .getURLs(), super .getParent()).load(

            name, resolve);

    }

}

public class A {

    private B b;

    public void setB(B b) {

         this .b = b;

    }

    public B getB() {

         return b;

    }

}

public class B {}

这个类的作用是可以重新载入同名的类, 但是, 为了实现 hotswap, 老的对象状态
需要通过其他方式拷贝到重载过的类生成的全新实例中来。 (A类中的 b实例 )

而新实例所依赖的 B类如果与老对象不是同一个类加载器加载的, 将会抛出类型转换异常 (ClassCastException).为了解决这种问题, HotSwapClassLoader自定义了 load方法 . 即当前类是由自身 classLoader加载的, 而内部依赖的类

还是老对象的 classLoader加载的 .

public class TestHotSwap {

public static void main(String args[]) {

    A a = new A();

    B b = new B();

    a.setB(b);

    System.out.printf("A classLoader is %s n" , a.getClass().getClassLoader());

    System.out.printf("B classLoader is %s n" , b.getClass().getClassLoader());

    System.out.printf("A.b classLoader is %s n" ,   a.getB().getClass().getClassLoader());

    HotSwapClassLoader c1 = new HotSwapClassLoader( new URL[]{ new URL( "file:\e:\test\")} , a.getClass().getClassLoader());

    Class clazz = c1.load(" test.hotswap.A ");

    Object aInstance = clazz.newInstance();

    Method method1 = clazz.getMethod(" setB ", B.class);

    method1.invoke(aInstance, b);

  Method method2 = clazz.getMethod(" getB ", null);

    Object bInstance = method2.invoke(aInstance, null);

    System.out.printf(" reloaded A.b classLoader is %s n", bInstance.getClass().getClassLoader());

}

}

 

public static void main(String[] args) {

                                ClassLoader cl = ClassLoader.getSystemClassLoader();

                                while(cl != null){

                                                System.out.println(cl);

                                                System.out.println("parent class loader: " + cl.getParent());

                                                cl = cl.getParent();

                                }

                }

sun.misc.Launcher$AppClassLoader@19821f

parent class loader: sun.misc.Launcher$ExtClassLoader@addbf1

sun.misc.Launcher$ExtClassLoader@addbf1

parent class loader: null

我们看到,当前系统类装载器为 AppClassLoader,AppClassLoader的父类装载器是 ExtClassLoader ExtClassLoader的父装载器为 null,表示为 BootstrapClassLoader BootstrapClassLoader JVM采用本地代码实现,因此没有对应的 Java类,所以 ExtClassLoader getParent()返回 null
  ClassLoader的职责之一是保护系统名字空间。以下为 ClassLoader类部分代码:

private ProtectionDomain preDefineClass(String name,

                                                                                    ProtectionDomain protectionDomain)

    {

                if (!checkName(name))

                    throw new NoClassDefFoundError("IllegalName: " + name);

 

                if ((name != null) && name.startsWith("java.")) {

                    throw new SecurityException("Prohibited package name: " +

                                                                                name.substring(0, name.lastIndexOf('.')));

                }

                if (protectionDomain == null) {

                    protectionDomain = getDefaultDomain();

                }

 

                if (name != null)

                    checkCerts(name, protectionDomain.getCodeSource());

 

                return protectionDomain;

}

那么,当我们定义如下类 Foo,虽然能够通过编译,但是会报 java.lang.SecurityException: Prohibited package name: java.lang异常,因为我们试图将 Foo类写入到 java.lang包下。

package java.lang;

 

public class Foo {

   

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

        Foo f = new Foo();

        System.out.println(f.toString());

    }

}

<!-- [if !supportLists]-->3.       <!-- [endif]-->定制 ClassLoader
  Java自带的 ClassLoader类的定义为:

public abstract class ClassLoader{

}

启动类加载器是 JVM通过调用 ClassLoader.loadClass()方法。

public Class<?> loadClass(String name) throws ClassNotFoundException {

                return loadClass(name, false);

    }

 

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) {

                                    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;

    }

 

protected Class<?> findClass(String name) throws ClassNotFoundException {

                throw new ClassNotFoundException(name);

}

 

loadClass(String name, boolean resolve)方法中的 resolve如果为 true,表示分析这个 Class对象,包括检查 Class Loader是否已经初始化等。 loadClass(String name) 在加载类之后不会对该类进行初始化,直到第一次使用该类时,才会对该类进行初始化。
那么,我们在定制 ClassLoader的时候,通常只需要覆写 findClass(String name)方法。在 findClass(String name)方法内,我们可以通过文件、网络 (URL)等形式获取字节码。以下为获取字节码的方法 :

public InputStream getResourceAsStream(String name);

public URL getResource(String name);

public InputStream getResourceAsStream(String name);

public Enumeration<URL> getResources(String name) throws IOException;

在取得字节码后,需要调用 defineClass()方法将字节数组转换成 Class对象,该方法签名如下:

protected final Class<?> defineClass(String name, byte[] b, int off, int len,

                                                                              ProtectionDomain protectionDomain)

            throws ClassFormatError

对于相同的类, JVM最多会载入一次。如果同一个 class文件被不同的 ClassLoader载入(定义),那么载入后的两个类是完全不同的

public class Foo{

    //

    private static final AtomicInteger COUNTER = new AtomicInteger(0);

 

    public Foo() {

        System.out.println("counter: " + COUNTER.incrementAndGet());

    }

   

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

        URL urls[] = new URL[]{new URL("file:/c:/")};

        URLClassLoader ucl1 = new URLClassLoader(urls);

        URLClassLoader ucl2 = new URLClassLoader(urls);

        Class<?> c1 = ucl1.loadClass("Foo");

        Class<?> c2 = ucl2.loadClass("Foo");

        System.out.println(c1 == c2);

        c1.newInstance();

        c2.newInstance();

    }

}

以上程序需要保证 Foo.class 文件不在 classpath 路径下。从而使 AppClassLoader 无法加载 Foo.class
输出结果:

false

counter: 1

counter: 1

 

4. Web应用的 ClassLoader
  绝大多数的 EJB容器, Servlet容器等都会提供定制的 ClassLoader,来实现特定的功能。但是通常情况下,所有的 servlet filter使用一个 ClassLoader。每个 jsp都使用一个独立的 ClassLoader

5. 隐式 (implicit)和显示 (explicit)的加载
  隐式加载:我们使用 new关键字实例化一个类,就是隐身的加载了类。
  显示加载分为两种:
     java.lang.Class forName()方法;
     java.lang.ClassLoader loadClass()方法。
  Class.forName()方法有两个重载的版本:

public static Class<?> forName(String className)

                throws ClassNotFoundException {

        return forName0(className, true, ClassLoader.getCallerClassLoader());

    }

 

public static Class<?> forName(String name, boolean initialize,

                                                                   ClassLoader loader)

        throws ClassNotFoundException

可以看出, forName(String className)默认以 true ClassLoader.getCallerClassLoader()调用了三参数的重载方法。 ClassLoader.getCallerClassLoader()表示以 caller class loader加载类,并会初始化类 (即静态变量会被初始化,静态初始化块中的代码也会被执行 )。如果以 false ClassLoader.getCallerClassLoader()调用三参数的重载方法,表示加载后的类不会被初始化。
ClassLoader.loadClass()方法在类加载后,也同样不会初始化类。

6. 两个异常 (exception)
  NoClassDefFoundError: java源文件已编译成 .class文件 ,但是 ClassLoader在运行期间搜寻路径 load某个类时 ,没有找到 .class文件则抛出这个异常。
  ClassNotFoundException: 试图通过一个 String变量来创建一个 Class类时不成功则抛出这个异常

 

 

 

package com.fnk.classloader;

import java.io.ByteArrayOutputStream;

import java.io.FileInputStream;

import java.io.IOException;

import com.fnk.TestClass;

public class MultyClassLoader  {

                public static void main(String[] args) {

                                FileClassLoader classLoader1 = new FileClassLoader("./");

                                FileClassLoader classLoader2 = new FileClassLoader("./");

                                try {

/*

*在同一个类加载器中加载了类 com.fnk.classloader.TestClass com.fnk.TestClass虽然类名相同,但是不同的包下面所以可以加载。但如果加载同包同类名就会抛出重复加载的错误。

*/          Class c1 = classLoader1.loadClass("com.fnk.classloader.TestClass");

                classLoader1.setDirectory("./src/");

                Class c11 = classLoader1.findClass("com.fnk.TestClass");

/*TestClass是定义在 class path下面的类,虽然类名也是 com.fnk.TestClass。但是因为 TestClass App classloader加载的,而 c11 classLoader1加载的。这样将 c11实例化的对象的引用赋值给 TestClass时就抛出 ClassCastException异常

*/

TestClass tc = (TestClass)c11.newInstance();

tc.test();                                             

/*

* 不同的 classLoader中可以加载同包同类名的类。而且会被认为是不同的类。

* 如下面当我调用 tc1.equals(tc2)时返回了 false,如下我重载了 equals方法。

                * public boolean equals(Object obj) {

                *  // TODO Auto-generated method stub

                *   if(obj instanceof TestClass)

                *    return true;

                *   else

                *    return super.equals(obj);

                *  }

*  我们可以让 TestClass类实现 Testf接口,那样就将 TestClass

对象赋值给 Testf接口,然后调用方法

*/

                                                Class c2 = classLoader2.loadClass("com.fnk.classloader.TestClass");

                                                Testf tc1 = (Testf)c1.newInstance();

                                                Testf tc2 = (Testf)c2.newInstance();

                                                tc1.test();tc2.test();

                                                if(tc1.equals(tc2)){System.out.println("true");

                                                }else{System.out.println("false");

                                                }

                                } catch (InstantiationException e) {

                                                e.printStackTrace();

                                } catch (IllegalAccessException e) {

                                                e.printStackTrace();

                                } catch (ClassNotFoundException e) {

                                                e.printStackTrace();

                                }

                }

}

 

java class的加载采用所谓的委托模式( Delegation Modle),当调用一个 ClassLoader.loadClass()加载一个类的时候,将遵循以下的步骤:  
1)检查这个类是否已经被加载进来了?  
2)如果还没有加载,调用父对象加载该类  
3)如果父对象无法加载,调用本对象的 findClass()取得这个类。  

所以当创建自己的 Class Loader时,只需要重载 findClass()这个方法。  

Unloading? Reloading? 

一个 java class被加载到 JVM之后,它有没有可能被卸载呢?我们知道 Win32 FreeLibrary()函数, Posix dlclose()函数可以被调用来卸载指定的动态连接库,但是 Java并没有提供一个 UnloadClass()的方法来卸载指定的类。  

Java中, java class的卸载仅仅是一种对系统的优化,有助于减少应用对内存的占用。既然是一种优化方法,那么就完全是 JVM自行决定如何实现,对 Java开发人员来说是完全透明的。  

什么时候一个 java class/interface会被卸载呢? Sun公司的原话是这么说 的: "class or interface may be unloaded if and only if its class loader is unreachable. Classes loaded by the bootstrap loader may not be unloaded." 

事实上我们关心的不是如何卸载类的,我们关心的是如何更新已经被加载了的类从而更新应用的功能。 JSP则是一个非常典型的例子,如果一个 JSP文件被更改了,应用服务器则需要把更改后的 JSP重新编译,然后加载新生成的类来响应后继的请求。  

实一个已经加载的类是无法被更新的,如果你试图用同一个 ClassLoader再次加载同一个类,就会得到异常 java.lang.LinkageError: duplicate class definition),我们只能够重新创建一个新的 ClassLoader实例来再次加载新类。至于原来已经加载的类,开发人员不必去管它,因为它可能还有实例正在被使用,只要相关的实例都被内存回收了, 那么 JVM就会在适当的时候把不会再使用的类卸载

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics