classloader

一.什么是ClassLoader

Java中的所有类,必须被装载到jvm中才能运行,这个装载工作是由jvm中的类装载器完成的,类装载器所做的工作实质是把类文件从硬盘读取到内存中

程序运行时,经常需要从一个Class文件中调用另一个Class文件中的方法,如果另一个Class文件不存在那么就会引发异常,在程序启动的时候,并不会一次性加载所有的Class文件,而是根据需要,通过ClassLoader来动态加载某个Class文件到内存中,而只有Class被加载到内存中之后,才能被其他Class所引用,所以ClassLoader就是动态加载class文件到内存中用的

二 .java默认提供的三个ClassLoader

2.1BootStrap ClassLoader

启动类加载器,是最顶层的类加载器,负责加载jdk的核心类库,可通过如下代码获取启动类加载器从哪些地方加载了相关的jar或者class

URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
    System.out.println(urls[i].toExternalForm());
}

也可以通过 如下代码获取

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

2.2扩展类加载器Extension ClassLoader

负责加载java的扩展类库,默认加载JAVA_HOME/jre/lib/ext目录下的所有jar


public class ClassLoaderTest { 
    public static void main(String[] args) {
        try {
            URL[] extURLs = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
            for (int i = 0; i < extURLs.length; i++) {
                System.out.println(extURLs[i]);
            }
        } catch (Exception e) {
            //…
        }
    }
}

2.3App ClassLoader系统类加载器

负责加载classpath目录下的jar和class

系统类加载器加载的路径
一是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器(系统类加载器和扩展类加载器本身都派生自URLClassLoader),调用URLClassLoader中的getURLs()方法可以获取到。  
二是可以直接通过获取系统属性java.class.path来查看当前类路径上的条目信息 :System.getProperty("java.class.path")。

2.4注意:

除了默认的三种ClassLoader之外,用户可以自定义类加载器,自定义类加载器需要继承ClassLoader,ExtensionClassLoader和App ClassLoader也是继承了ClassLoader实现的;
BootStrap ClassLoader不是继承自ClassLoader,因为它不是一个普通的java类,底层由C++实现,已经嵌入在jvm中,jvm启动后,BootStrap ClassLoader也会随之启动,负责加载完核心类库,并构造Extension Classloader和App ClassLoader

ExtensionClassLoader的继承层次.png
AppClassLoader的继承层次.png

三 ClassLoader加载类的原理

3.1原理

ClassLoader采用的是双亲委托模型,每个ClassLoader实例都有一个父类加载器的引用,BootStrap ClassLoader没有父类加载器,但它可以作为其他类加载器的父类加载器,当一个ClassLoader实例需要加载某个类时,在它试图自己搜索这个类之前,先把这个任务交给父类加载器,这个过程是由上到下依次检查的,首先由最顶层的BootStrap ClassLoader试图加载,如果没加载到就把任务转给Extension ClassLoader,如果也没加载到则转交给App ClassLoader,如果App ClassLoader也没加载到,则返回给委托的发起者,由它到指定的文件系统或者网络中去加载该类,如果都没有加载到,就抛出异常,如果找到就将它加载到内存,并返回这个类在内存中的实例对象。

note:同一个加载器:类A引用到类B,则由类A的加载器去加载类B,保证引用到的类由同一个加载器加载。

public class A{
  public void doSth(){
    B b = new B();
    b.doSth(); 
  }
}

B b = new B();相当于B b = Class.forName(“B”, false, A.class.getClassLoader()).newInstance();在类型A中使用到的类型,将由加载了类型A的类加载器来进行加载

双亲委派模型的实现:注意其中的parent

// 检查类是否已被装载过    
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);    
     }    
}    

一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可。java.lang.ClassLoader类的方法loadClass()封装了前面提到的代理模式的实现。该方法会首先调用findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写 findClass()方法。

ClassLoader类的构造方法中会初始化parent,parent是private的,所以一旦初始化之后就不会再改变

    protected ClassLoader() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        //默认将父类加载器设置为系统类加载器,getSystemClassLoader()获取系统类加载器
        this.parent = getSystemClassLoader();
        initialized = true;
    }

    protected ClassLoader(ClassLoader parent) {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        //强制设置父类加载器
        this.parent = parent;
        initialized = true;
    }

由此可以推断出

  • AppClassLoader调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为标准扩展类加载器(ExtClassLoader)。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)
  • 扩展类加载器(ExtClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为null。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)

扩展类加载器(ExtClassLoader)的父类加载器被强制设置为null了,那么扩展类加载器为什么还能将加载任务委派给启动类加载器呢?
***因为根据loadclass的源码中显示如果父类加载器为空, 则委派给启动类加载加载 ***

3.2为什么使用双亲委托模型

这样做可以避免重复加载,当父加载器已经加载过该类的时候,就没必要子加载器再去加载一遍。
ClassLoader搜索类的算法

3.3在jvm搜索类的时候,如何判定两个class是否相同

jvm在判定两个class是否相同时,不仅需要判断雷鸣是否相同,还要判断两个class是否由同一个类加载器加载。
就算是同一份字节码,如果被两个不同的classloader加载的话,jvm也会认为他们是两个不同的class。

在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间。我们可以用两个自定义类加载器去加载某自定义类型(注意不要将自定义类型的字节码放置到系统路径或者扩展路径中,否则会被系统类加载器或扩展类加载器抢先加载),然后用获取到的两个Class实例进行java.lang.Object.equals(…)判断,将会得到不相等的结果

3.4ClassLoader的体系结构

从下往上检查类是否已经加载
从上往下尝试加载类
每个类加载器有自己的名字空间,对于同一个类加载器实例来说,名字相同的类只能存在一个,并且仅加载一次。不管该类有没有变化,下次再需要加载时,它只是从自己的缓存中直接返回已经加载过的类引用。

类加载过程

1、首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则转入步骤2; 
2、委派类加载请求给父类加载器(更准确的说应该是双亲类加载器,真实虚拟机中各种类加载器最终会呈现树状结构),如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则转入步骤3;
3、调用本类加载器的findClass(…)方法,试图获取对应的字节码,如果获取的到,则调用defineClass(…)导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(…), loadClass(…)转而抛异常,终止加载过程(注意:这里的异常种类不止一种)。

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

运行结果:
sum.misc.Launcher&AppClassloader
sum.misc.Launcher&ExtensionClassloader
null//其实是BootStrapClassLoader,因为它不是普通java类,所以打印null

将这个class打jar包放到ExtensionClassLoader的加载目录 /JAVA_HOME/jre/lib/ext目录下重新运行这个程序

运行结果:
sum.misc.Launcher&ExtensionClassloader
null//其实是BootStrapClassLoader,因为它不是普通java类,所以打印null
按照委托模型,从上往下加载,首先BootStrapClassLoader没有加载到,就交给ExtensionClassLoader去加载,ExtensionClassLoader加载这个类到内存,并生成这个类的对象实例,然后把这个类返回,那么这个类的类加载器就是ExtensionClassLoader

把类交给BootStrap有两种方式

  • 使用 -Xbootclasspath指定BootStrapClassLoader的加载路径
    -Xbootclasspath/a 表示追加到原有的路径
  • 把class文件放到BootStrapClassLoader原有的加载路径之下
  • 虚拟机出于安全等因素考虑,不会加载<Java_Runtime_Home>/lib存在的陌生类,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的

四 自定义类加载器

4.1为什么要使用自定义类加载器

虽然jvm已经提供了三种类加载器,但是它们只能加载指定目录下的class和jar,如果我们想加载其他位置的class或者jar,比如网络上的class文件,就需要使用自定义类加载器,才能将这个class加载到内存,然后调用这个类的方法实现自己的逻辑。

4.2自定义类加载器分为两步

  • 继承ClassLoader
  • 重写findClass方法

为什么要重写findClass方法
jdk已经在loadClass方法中实现了搜索类的算法,当在loadclass方法中搜索不到类的时候,loadclass会调用findclass方法来搜索类,所以我们只需要重写findclass即可。如果没有特殊的需求,一般不需要重写loadclass方法。

4.2.1本地类加载器

defineClass
方法接受一组字节,然后将其具体化为一个Class类型实例,它一般从磁盘上加载一个文件,然后将文件的字节传递给JVM,通过JVM(native 方法)对于Class的定义,将其具体化,实例化为一个Class类型实例。

package classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

// 文件系统类加载器
public class FileSystemClassLoader extends ClassLoader {

    private String rootDir;

    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    // 获取类的字节码
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);  // 获取类的字节数组
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
           
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
        // 读取类文件的字节
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            // 读取类文件的字节码
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        // 得到类文件的完全路径
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }
}

使用

package classloader;

import java.lang.reflect.Method;

public class ClassIdentity {

    public static void main(String[] args) {
        new ClassIdentity().testClassIdentity();
    }

    public void testClassIdentity() {
        String classDataRootPath = " ";
        FileSystemClassLoader fscl= new FileSystemClassLoader(classDataRootPath);
      
        String className = "com.example.Sample";
        try {
            Class<?> class1 = fscl.loadClass(className);  // 加载Sample类
            Object obj = class1.newInstance();  // 创建对象 
            Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);
            setSampleMethod.invoke( );
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4.2.2网络类加载器

在通过NetworkClassLoader加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用Java反射API。另外一种做法是使用接口。需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为客户端代码的类加载器找不到这些类。使用Java反射API可以直接调用Java类的方法。而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类

package classloader;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;

public class NetworkClassLoader extends ClassLoader {

    private String rootUrl;

    public NetworkClassLoader(String rootUrl) {
        // 指定URL
        this.rootUrl = rootUrl;
    }

    // 获取类的字节码
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
        // 从网络上读取的类的字节
        String path = classNameToPath(className);
        try {
            URL url = new URL(path);
            InputStream ins = url.openStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            // 读取类文件的字节
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        // 得到类文件的URL
        return rootUrl + "/"
                + className.replace('.', '/') + ".class";
    }
}

五 常见问题分析

5.1 在代码中直接调用Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?

Class.forName(String name)默认会使用调用类的类加载器来进行类加载

     //java.lang.Class.java
    publicstatic Class<?> forName(String className) throws ClassNotFoundException {
        return forName0(className, true, ClassLoader.getCallerClassLoader());
    }

    //java.lang.ClassLoader.java
    // Returns the invoker's class loader, or null if none.
    static ClassLoader getCallerClassLoader() {
        // 获取调用类(caller)的类型
        Class caller = Reflection.getCallerClass(3);
        // This can be null if the VM is requesting it
        if (caller == null) {
            return null;
        }
        // 调用java.lang.Class中本地方法获取加载该调用类(caller)的ClassLoader
        return caller.getClassLoader0();
    }

    //java.lang.Class.java
    //虚拟机本地实现,获取当前类的类加载器,前面介绍的Class的getClassLoader()也使用此方法
    native ClassLoader getClassLoader0();

5.2 在编写自定义类加载器时,如果没有设定父加载器,那么父加载器是谁?

AppClassLoader

    //摘自java.lang.ClassLoader.java
    protected ClassLoader() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        this.parent = getSystemClassLoader();
        initialized = true;
    }

5.3在编写自定义类加载器时,如果将父类加载器强制设置为null,那么会有什么影响

JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器(这个问题前面已经分析过了)。同时,我们可以得出如下结论:  
即使用户自定义类加载器不指定父类加载器,那么,同样可以加载到<Java_Runtime_Home>/lib下的类,但此时就不能够加载<Java_Runtime_Home>/lib/ext目录下的类了。

5.4自定义类加载器需要注意的地方

  • 尽量不复写loadclass方法
  • 指定父类加载器
  • 保证findclass的正确执行

六 classloader的延伸

6.1 自定义加载流程

每个类加载器有自己的名字空间,对于同一个类加载器实例来说,名字相同的类只能存在一个,并且仅加载一次。不管该类有没有变化,下次再需要加载时,它只是从自己的缓存中直接返回已经加载过的类引用。
我们编写的应用类默认情况下都是通过 AppClassLoader 进行加载的。当我们使用 new 关键字或者 Class.forName 来加载类时,所要加载的类都是由调用 new 或者 Class.forName 的类的类加载器(也是 AppClassLoader)进行加载的。要想实现 Java 类的热替换,首先必须要实现系统中同名类的不同版本实例的共存,通过上面的介绍我们知道,要想实现同一个类的不同版本的共存,我们必须要通过不同的类加载器来加载该类的不同版本。另外,为了能够绕过 Java 类的既定加载过程,我们需要实现自己的类加载器,并在其中对类的加载过程进行完全的控制和管理。

findLoadedClass:每个类加载器都维护有自己的一份已加载类名字空间,其中不能出现两个同名的类。凡是通过该类加载器加载的类,无论是直接的还是间接的,都保存在自己的名字空间中,该方法就是在该名字空间中寻找指定的类是否已存在,如果存在就返回给类的引用,否则就返回 null。这里的直接是指,存在于该类加载器的加载路径上并由该加载器完成加载,间接是指,由该类加载器把类的加载工作委托给其他类加载器完成类的实际加载。
getSystemClassLoader:Java2 中新增的方法。该方法返回系统使用的 ClassLoader。可以在自己定制的类加载器中通过该方法把一部分工作转交给系统类加载器去处理。
defineClass:该方法是 ClassLoader 中非常重要的一个方法,它接收以字节数组表示的类字节码,并把它转换成 Class 实例,该方法转换一个类的同时,会先要求装载该类的父类以及实现的接口类
loadClass:加载类的入口方法,调用该方法完成类的显式加载。通过对该方法的重新实现,我们可以完全控制和管理类的加载过程。
resolveClass:链接一个指定的类。这是一个在某些情况下确保类可用的必要方法

实现一个定制的类加载器来完成这样的加载流程:我们为该类加载器指定一些必须由该类加载器直接加载的类集合,在该类加载器进行类的加载时,如果要加载的类属于必须由该类加载器加载的集合,那么就由它直接来完成类的加载,否则就把类加载的工作委托给系统的类加载器完成。

class CustomCL extends ClassLoader { 

    private String basedir; // 需要该类加载器直接加载的类文件的基目录
    private HashSet dynaclazns; // 需要由该类加载器直接加载的类名

    public CustomCL(String basedir, String[] clazns) { 
        super(null); // 指定父类加载器为 null 
        this.basedir = basedir; 
        dynaclazns = new HashSet(); 
        loadClassByMe(clazns); 
    } 

    private void loadClassByMe(String[] clazns) { 
        for (int i = 0; i < clazns.length; i++) { 
            loadDirectly(clazns[i]); 
            dynaclazns.add(clazns[i]); 
        } 
    } 

    private Class loadDirectly(String name) { 
        Class cls = null; 
        StringBuffer sb = new StringBuffer(basedir); 
        String classname = name.replace('.', File.separatorChar) + ".class";
        sb.append(File.separator + classname); 
        File classF = new File(sb.toString()); 
        cls = instantiateClass(name,new FileInputStream(classF),
            classF.length()); 
        return cls; 
    }           

    private Class instantiateClass(String name,InputStream fin,long len){ 
        byte[] raw = new byte[(int) len]; 
        fin.read(raw); 
        fin.close(); 
        return defineClass(name,raw,0,raw.length); 
    } 
    
    protected Class loadClass(String name, boolean resolve) 
            throws ClassNotFoundException { 
        Class cls = null; 
        cls = findLoadedClass(name); 
        if(!this.dynaclazns.contains(name) && cls == null) 
            cls = getSystemClassLoader().loadClass(name); 
        if (cls == null) 
            throw new ClassNotFoundException(name); 
        if (resolve) 
            resolveClass(cls); 
        return cls; 
    } 

}

6.2自定义类加载器对类的加解密

七 ContextClassLoader上下文类加载器

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现,这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,但是SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。

ContextClassloader可以解决这个问题,在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。

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

使用线程上下文类加载器,可以在执行线程中抛弃双亲委派加载链模式,使用线程上下文里的类加载器加载类

defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)是java.lang.Classloader提供给开发人员,用来自定义加载class的接口。使用该接口,可以动态的加载class文件。例如在jdk中,URLClassLoader是配合findClass方法来使用defineClass,可以从网络或硬盘上加载class。而使用类加载接口,并加上自己的实现逻辑,还可以定制出更多的高级特性。

我们知道Java缺省的加载器对相同全名的类只会加载一次,以后直接从缓存中取这个Class object。因此要实现hot swap,必须在加载的那一刻进行拦截,先判断是否已经加载,若是则重新加载一次,否则直接首次加载它。

一般来说,上下文类加载器要比当前类加载器更适合于框架编程,而当前类加载器则更适合于业务逻辑编程。


java虚拟机的连接模型和java语言的动态性


参考
http://zy19982004.iteye.com/blog/search?query=Classloader
http://blog.csdn.net/qbg19881206/article/details/8890600
http://blog.csdn.net/v1v1wang/article/details/6864573
http://blog.csdn.net/zhoudaxia/article/details/35824249
http://blog.csdn.net/yaerfeng/article/details/24960121
http://www.yiibai.com/java/lang/read_setcontextclassloader.html
http://www.cnblogs.com/549294286/p/3714692.html
http://blog.jobbole.com/96145/
http://cjnetwork.iteye.com/blog/851544
http://ifeve.com/classloader/
http://www.ibm.com/developerworks/cn/java/j-lo-hotswapcls/

推荐阅读更多精彩内容