一. classloader基础

1. 概念

程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的。

每个class都有一个reference,指向自己的ClassLoader。Class.getClassLoader();array的ClassLoader就是其元素的ClassLoader,若是基本数据类型,则这个array没有ClassLoader

2. 获取classloader的方式

  • this.getClass.getClassLoader(); // 使用当前类的ClassLoader
  • Thread.currentThread().getContextClassLoader(); // 使用当前线程的ClassLoader
  • ClassLoader.getSystemClassLoader(); // 使用系统ClassLoader,即系统的入口点所使用的ClassLoader。(注 意,system ClassLoader与根ClassLoader并不一样。JVM下system ClassLoader通常为App ClassLoader)

注:

  • 方法一得到的Classloader是静态的,表明类的载入者是谁;方法二得到的Classloader是动态的,谁执行(某个线程),就是那个执行者的Classloader。
  • 对于单例模式的类,静态类等,载入一次后,这个实例会被很多程序(线程)调用,对于这些类,载入的Classloader和执行线程的Classloader通常都不同。

3. 线程中的ClassLoader

每个运行中的线程都有一个成员contextClassLoader,用来在运行时动态地载入其它类,可以使用方法Thread.currentThread().setContextClassLoader(...);更改当前线程的contextClassLoader,来改变其载入类的行为;也可以通过方法Thread.currentThread().getContextClassLoader()来获得当前线程的ClassLoader。

实际上,在Java应用中所有程序都运行在线程里,如果在程序中没有手工设置过ClassLoader,对于一般的java类如下两种方法获得的ClassLoader通常都是同一个

二. java默认类加载器

1. BootStrap ClassLoader

称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等,若某类A通过该类加载器加载,调用类A.getClassLoader(),获取到null,因为该类加载器为c++实现

String.class.getClassLoader();

2. Extension ClassLoader

称为扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。

3. App ClassLoader

称为系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件。

HelloWord.class.getClassLoader();

注意: 除了Java默认提供的三个ClassLoader之外,用户还可以根据需要定义自已的ClassLoader,而这些自定义的ClassLoader都必须继承自java.lang.ClassLoader类,也包括Java提供的另外二个ClassLoader(Extension ClassLoader和App ClassLoader)在内,但是Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。
通过classloader获取某个类的路径:System.out.println(ClassLoader.getSystemResource("java/lang/String.class"));

三. classloader主要方法

  • loadClass:ClassLoader 的入口点,加载某个类
  • defineClass:ClassLoader 的主要诀窍。该方法接受由原始字节组成的数组并把它转换成 Class 对象。原始数组包含如从文件系统或网络装入的数据。
  • findSystemClass:从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用 defineClass 将原始字节转换成 Class 对象,以将该文件转换成类。当运行 Java 应用程序时,这是 JVM 正常装入类的缺省机制。
  • resolveClass:可以不完全地(不带解析)装入类,也可以完全地(带解析)装入类。当编写我们自己的 loadClass 时,可以调用 resolveClass,这取决于 loadClass 的 resolve 参数的值
  • findLoadedClass :充当一个缓存:当请求 loadClass 装入类时,它调用该方法来查看ClassLoader 是否已装入这个类,这样可以避免重新装入已存在类所造成的麻烦。

注:通过下面的例子,加载当前类的类加载器及它所有的父类加载器

//获得加载ClassLoaderTest.class这个类的类加载器
ClassLoaderloader = ClassLoaderTest.class.getClassLoader();

while(loader != null) {
    System.out.println(loader);
    loader = loader.getParent(); //获得父类加载器的引用
}
System.out.println(loader);

四.loadClass方法调用过程

    1. 调用 findLoadedClass 来查看是否存在已装入的类。
    1. 如果没有,那么采用某种特殊的神奇方式来获取原始字节。(通过IO从文件系统,来自网络的字节流等)
    1. 如果已有原始字节,调用 defineClass 将它们转换成 Class 对象。
    1. 如果没有原始字节,然后调用 findSystemClass 查看是否从本地文件系统获取类。
    1. 如果 resolve 参数是 true,那么调用 resolveClass 解析 Class 对象。
    1. 如果还没有类,返回 ClassNotFoundException。 否则,将类返回给调用程序。

五.定义自己的classloader

既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?

因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时,比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的业务逻辑。在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader。

定义自已的类加载器分为两步:
1、继承java.lang.ClassLoader
2、重写父类的findClass方法

详见
附录1(网上找的自定义classloader例子)
附录2(自己写的例子)
附录2证明:

    1. 不同类加载器加载到的同一class文件,在内存中存在两份
    1. 自定义类加载器的父类加载器为appclassloader
    1. 不能将两个对象互转

六. ClassLoader加载类的原理

1、原理介绍

ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。

2、为什么要使用双亲委托这种模型呢?

因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

3、 但是JVM在搜索类的时候,又是如何判定两个class是相同的呢?

JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。比如网络上的一个Java类org.classloader.simple.NetClassLoaderSimple,javac编译之后生成字节码文件NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB这两个类加载器并读取了NetClassLoaderSimple.class文件,并分别定义出了java.lang.Class实例来表示这个类,对于JVM来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个Class实例生成具体的对象进行转换时,就会抛运行时异常java.lang.ClassCaseException,提示这是两个不同的类型。

4. 几种扩展应用用户定制自己的ClassLoader可以实现以下的一些应用

  • 安全性:类进入JVM之前先经过ClassLoader,所以可以在这边检查是否有正确的数字签名等
  • 加密:java字节码很容易被反编译,通过定制ClassLoader使得字节码先加密防止别人下载后反编译,这里的ClassLoader相当于一个动态的解码器
  • 归档:可能为了节省网络资源,对自己的代码做一些特殊的归档,然后用定制的ClassLoader来解档
  • 自展开程序:把java应用程序编译成单个可执行类文件,这个文件包含压缩的和加密的类文件数据,同时有一个固定的ClassLoader,当程序运行时它在内存中完全自行解开,无需先安装
  • 动态生成:可以生成应用其他还未生成类的类,实时创建整个类并可在任何时刻引入JVM

5. Web应用中的ClassLoader

在Tomcat里,WebApp的ClassLoader的工作原理有点不同,它先试图自己载入类(在ContextPath/WEB-INF/...中载入类),如果无法载入,再请求父ClassLoader完成。
由此可得:
对于WEB APP线程,它的contextClassLoader是WebAppClassLoader
对于Tomcat Server线程,它的contextClassLoader是CatalinaClassLoader

七. 知识扩展

文件载入(例如配置文件等)假设在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种途径来载入文件

● 绝对路径 ---> IO 
● 相对路径 ---> IO 
          ---> ClassLoader 
● 资源文件 ---> ResourceBundle 
  1. 如何在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"

附录1:

package classloader;import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
/**
 * 加载网络class的ClassLoader
 */
public class NetworkClassLoader extends ClassLoader { 
        private String rootUrl; 
        public NetworkClassLoader(String rootUrl) {
                  this.rootUrl = rootUrl;
        } 
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
                  Class clazz = null;//this.findLoadedClass(name); // 父类已加载     
                  //if (clazz == null) { //检查该类是否已被加载过  
                  byte[] classData = getClassData(name); //根据类的二进制名称,获得该class文件的字节码数组  
                   if (classData == null) {
                         throw new ClassNotFoundException();
                  }
                  clazz = defineClass(name, classData, 0, classData.length); //将class的字节码数组转换成Class类的实例  
                  //}   
                  return clazz;
        } 
        private byte[] getClassData(String name) {
                  InputStream is = null;
                 try {
                     String path = classNameToPath(name);
                     URL url = new URL(path);
                     byte[] buff = new byte[1024*4];
                     int len = -1;
                     is = url.openStream();
                     ByteArrayOutputStream baos = new ByteArrayOutputStream();
                     while((len = is.read(buff)) != -1) {
                          baos.write(buff,0,len);
                     }
                    return baos.toByteArray();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (is != null) {
                          try {
                              is.close();
                          } catch(IOException e) {
                              e.printStackTrace();
                          }
                   }
              }
              return null;
       } 
      private String classNameToPath(String name) {
        return rootUrl + "/" + name.replace(".", "/") + ".class";
    }
}
测试类:
package classloader;
public class ClassLoaderTest { 
      public static void main(String[] args) {
      try {  
            /*ClassLoader loader = ClassLoaderTest.class.getClassLoader(); //获得ClassLoaderTest这个类的类加载器 
            while(loader != null) { 
                System.out.println(loader); 
                loader = loader.getParent(); //获得父加载器的引用 
            } 
            System.out.println(loader);*/
            String rootUrl = "http://localhost:8080/httpweb/classes";
            NetworkClassLoader networkClassLoader = new NetworkClassLoader(rootUrl);
            String classname = "org.classloader.simple.NetClassLoaderTest";
            Class clazz = networkClassLoader.loadClass(classname);
            System.out.println(clazz.getClassLoader()); } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

附录2.

package com.kevin;
import java.io.File;
import java.io.FileInputStream;
public class ClassLoaderTest extends ClassLoader{ 
    public static void main(String[] args) { 
        ClassLoaderTest test = new ClassLoaderTest();
        try {
            //通过该加载器获取到某个类(包含包名及类名)
            Class c = test.findClass("com.kevin.ClassLoaderTest");
            System.err.println("正常加载到的class:"+test.getClass()+"通过自定义类加载器加载到的class:"+c);
            System.err.println("两个不同classloader加载到的同一class文件是否为同一class:"+(test.getClass()==c)); //获取到该类的方法
            //Method[] ms = c.getMethods();
            //for(Method m : ms){
            // System.err.println(m);
            //} System.err.println("正常加载类的classloader:"+ClassLoaderTest.class.getClassLoader()+";通过自定义classloader加载类的classloader:"+c.getClassLoader()); //自定义classloader的父类加载器为appclassloader
            System.err.println("自定义classloader的父类加载器:"+c.getClassLoader().getParent());

            //将自定义类加载器加载到的对象实例强赋给正常类加载器加载到的对象的实例
            ClassLoaderTest test2 = (ClassLoaderTest)c.newInstance();
        }catch (Exception e){
            e.printStackTrace();
        } } @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException { FileInputStream input = null;
        try {
            String path = "/Users/liukai/soft/tmp/com/kevin/ClassLoaderTest.class";//加载类的目录,包名不重要
            File f = new File(path);
            input = new FileInputStream(f);
            byte[] bs = new byte[(int)f.length()];
            //byte[] bs = new byte[1024];
            int i=0;
            //将文件读取到byte数组中
            while ((i = input.read(bs))!=-1) {
                System.err.println("文件长度:"+bs.length);
            }
            //将字节数组转换为class对象
           return this.defineClass(name,bs,0,bs.length);
        }catch (Exception e){
            e.printStackTrace();
            throw new ClassNotFoundException();
        }finally {
            try {
                 input.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

输出:
2547---114----2547
正常加载到的class:class com.kevin.ClassLoaderTest
通过自定义类加载器加载到的class:class com.kevin.ClassLoaderTest
两个不同classloader加载到的同一class文件是否为同一class:false
正常加载类的classloader:sun.misc.Launcher$AppClassLoader@4b67cf4d;
通过自定义classloader加载类的classloader:com.kevin.ClassLoaderTest@60e53b93
自定义classloader的父类加载器:sun.misc.Launcher$AppClassLoader@4b67cf4d
java.lang.ClassCastException: com.kevin.ClassLoaderTest cannot be cast to com.kevin.ClassLoaderTest
 at com.kevin.ClassLoaderTest.main(ClassLoaderTest.java:33)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:498)
 at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)
证明:1. 不同类加载器加载到的同一class文件,在内存中存在两份
            2. 自定义类加载器的父类加载器为appclassloader
    3. 不能将两个对象互转