ClassLoader 学习笔记

概述

在经过编译后.java文件会生成对应的.class文件,但需要执行的时候,虚拟机首先会从class文件中读取必要的信息,而这个过程则成为类加载。
类加载时类的生命周期的一部分,也是它的初始步骤。

对应于C语言等,这个过程其实就是装载的过程,但稍有不同的是,C语言装载的是经过编译连接后的文件,而类加载的时候,class文件仅是编译后的产物,连接的过程发生在类加载之后,这也是Java语言得以动态扩展的保证。

完成将类加载到虚拟机的工具,则是类加载器

类加载器

概述

作用:通过一个类的全限定名来获取描述此类的二进制字节流。

在加载阶段,虚拟机需要完成3件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class类对象,作为方法区这个类的各种数据的访问入口。

类加载器会去获取类的信息,但是类加载器并没有规定类的格式。具体的格式通常有一下几种:

  • 文件格式: class文件、jar文件、war文件等
  • 动态生成:程序在执行过程中,动态生成类的字节码,主要表现为动态代理
  • 有其他文件生成:比如说由JSP文件生成对应的Class文件
  • 网络获取,Applet

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中。然后在内存中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的这些类型数据的外部接口。

这个阶段,相比于C语言转载的过程,都是把可执行文件装入装入内存的过程。

设计理念

虚拟机团队在设计类加载过程时,将“通过一个类的全限定名来获取此类的二进制字节流”这个操作放到了虚拟机外部去实现,目的是让应用程序自己决定如何去获取所需要的类。而实现这个加载流程的代码就是类加载器。

对于任意一个类,都需要由它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

这个就是说在代码里面import某个类的时候,虚拟机去查找这个类的时候,需要同时确定类加载器和全限定名才能唯一确定一个类。这样做其实是很容易理解的。举个例子:如果有A、B两个应用程序同时部署在Tomcat上,而且A、B上都有相同命名的包,假设com.wpr,里面还存在同名类Clazz,对于B应用import com.wpr.Clazz对象,如果仅仅是限定名称,那么Tomcat将无法区别这两个类。在这种情况下,需要不同的类加载器去加载各自的com.wpr.Clazz类,通过类加载器+权限定名才能唯一的确定需要的类。

这就是说,比较两个类是否相同,只有在这两个类是在同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不同。(这里的相同包括代表类的Class对象的equals方法、isInstance()方法返回的结果,也包括使用instanceof关键字做对象属性关系判定等情况)

虚拟机自带的类加载器

不同的类需要不同的类加载器去加载,再看一下虚拟机自带的类加载器

从Java虚拟机的角度来讲,只存在两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader),这个类加载器是用C++语言实现的,是虚拟机的一部分
  • 另一种就是所有其他的类加载器,这些类加载器都是有Java语言实现,独立于虚拟机外部并且全部继承自抽象类java.lang.ClassLoader

从Java开发者角度看,将类加载器可以区分为三种:

  • 启动类加载器(Bootstrap ClassLoader):这个类负责将存放在<JAVA_HOME>\lib目录下,或者-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,rt.jar,名字不符合的类库及时放在lib目录下也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,可以直接使用null代替
  • 扩展类加载器(Extension ClassLoader):这个加载器由sum.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME/>/lib/ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的素有类库,开发者可以直接使用扩展类加载器
  • 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Laucher$AppClassLoader实现,称为系统类加载器,负责加载用户类路径(ClassPath)上指定的类库。如果应用程序中没有自定义过自己的类加载器,一般情况下使用的就是这个默认的类加载器

存在这些加载器,接下来就需要规定一个类应当由谁来加载的问题,这个就涉及到了Java的类加载机制---双亲委派模型

双亲委派模型


双亲委派模型的目的就是确定由那个类加载器来加载类。

具体要求:除了顶层的启动类加载器外,其余的类加载都应当由自己的父类加载器

工作流程:
如果一个类收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求都最终会传送到顶层的启动类加载器,只有当父加载器类反馈自己无法完成这个加载请求(搜索范围内没有找到所需的类),自加载器才会尝试去加载。

Java类随着它的类加载器一起具备了优先级的层次关系。例如java.lang.Object,它存放在rt.jar中,双亲委派模型会让最顶端的启动类来进行加载,这样Object类在程序的各种类加载环境中都是同一个类。

如果没有使用双亲委派,由各个类加载器去加载的话,用户自己编写了一个java.lang.Object,并放在Classpath
下,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无妨保证。

protected synchronized Class<?> loadClass(String name,boolean resolve) throw ClassNotFoundException{
	//检查请求的类是否已经被加载过了
	Class c = findLoadedClass(name);
	if(c==null){
		try{
			if(parent!=null){
				c = parent.loadClass(name,false);
			}else{
				c = findBootstrapClassOrNull(name);
			}
		}catch(ClassNotFoundException e){
			//如果父类加载器抛出ClassNotFoundException说明父类无法完成类加载
		}
		if(c == null){
			c = findClass(name);
		}
	}
	if(resolve){
		resolveClass(c);
	}
	return c;
}

编写自己的ClassLoader

集成ClassLoader后,由于loadClass()方法是双亲认证的流程,因此,只有父类找不到类型信息,需要自定义的类加载器来加载,因此无需覆盖原来的loadClass方法。在编写自己的类加载器时,只需要重新编写findClass方法即可。

public class FileSystemClassLoader extends ClassLoader { 

    private String rootDir; 

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

    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"; 
    } 
 }

如何加载

这个步骤也是很清楚的,类加载过程首先要找到对应的类,然后进行装载的操作。对于ClassLoader而言,真正完成类的加载工作是通过调用 defineClass来实现的;而启动类的加载过程是通过调用 loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。

方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。

类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。

与程序员相关的

在类加载的过程中,能够通过程序进行控制的,主要是字节码生成以及类加载器这两部分的功能。
关于字节码的生成,可以由多种形式,上面已经提到。而类加载器则是我们关注的重点。

以Tomcat为例,列举需求如下:

  • 部署在同一台服务器的两个WEB应用程序所使用的Java类库应当隔离(两个应用程序依赖了同一个第三方类库的不同版本,应当保证两个应用程序的类库可以独立使用)
  • 部署在同一个服务器上的两个WEB应用可以共享同一个Java类库。(多个应用同时使用同一个版本的第三方依赖,如果各自存放在应用程序的隔离目录中,内存浪费)
  • 服务器所使用的类库应当与应用程序的类库相互隔离。这个也容易理解,处于安全的考虑,服务器不应当受部署程序的影响
  • 支持JSP应用的热部署HotSwap功能。在Tomcat中,修改了JSP文件后不需要重新启动服务器,该功能依赖于HotSwap。

由于存在以上需求,单独的一个ClassPth就无法满足需求,所以各种Web服务器都使用了多个ClassPath。

在Tomcat目录结构中,有3组目录"/common/" "/server/" "/shared/" 可以存放java类库,另外在Web应用程序自身的 /WEB-INF/,一共4组,其具体含义是:

  • /common目录中:类库可以被Tomcat和所有的Web应用程序共同使用,由CommonClassLoader加载
  • /server目录中:类库可以被Tomcat使用、对所有的Web应用程序都不可见,由CatalinaClassLoader加载
  • /shared目录下:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见,SharedClassLoader加载
  • /WEBAPP/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web程序都不可见,由WebAppClassLoader加载

Tomcat采用了正统的双亲委派模型,具体如下图所示:

Tomcat类加载的实现

注意: 对于Tomcat的6.x版本,只有指定了tomcat/conf/catalina.properties配置文件的server.loader和share.loader项后才会真正建立CatalinaClassLoader和SharedClassLoader的实例,否则用到这两个类加载器的地方会被CommonLoader的实例替代,默认是没有这个配置的,因此在omcat6.x把/common、/server、/shared三个目录默认合并到了一个/lib目录下,这个目录库相当于/common目录

在Tomcat的初始化中,Bootstrap的init方法中,会调用initClassLoaders方法,完成CommonClassLoader、CatalinaClassLoade、SharedClassLoader方法

    private void initClassLoaders() {  
        try {  
            //初始化CommonClassLoader  
            commonLoader = createClassLoader("common", null);  
            if( commonLoader == null ) {  
                commonLoader=this.getClass().getClassLoader();  
            }  
//初始化其它两个类加载器  
            catalinaLoader = createClassLoader("server", commonLoader);  
            sharedLoader = createClassLoader("shared", commonLoader);  
        } catch (Throwable t) {  
            log.error("Class loader creation threw exception", t);  
            System.exit(1);  
        }  
    }

可以看到核心的业务交给了createClassLoader方法来实现:
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
//读取配置属性,相关的配置属性在catalina.properties文件中
String value = CatalinaProperties.getProperty(name + ".loader");
//如果没有对应的配置,将不会创建新的类加载器,而是返回传入的父类加载器
if ((value == null) || (value.equals("")))
return parent;

       //解析得到的配置文件,确定本ClassLoader要加载那些目录下的资源和JAR包等  
        StringTokenizer tokenizer = new StringTokenizer(value, ",");  
        while (tokenizer.hasMoreElements()) {  
            String repository = tokenizer.nextToken();  
  
          //此处省略的代码为将配置文件中的${catalina.base}、${catalina.home}等变量转  
//换为绝对路径  
  
        //格式化得到的位置路径和类型  
        String[] locations = (String[]) repositoryLocations.toArray(new String[0]);  
        Integer[] types = (Integer[]) repositoryTypes.toArray(new Integer[0]);  
 //生成真正的类加载器   
        ClassLoader classLoader = ClassLoaderFactory.createClassLoader  
            (locations, types, parent);  
  
       //以下的代码为将生成的类加载器注册为MBean  
  
        return classLoader;  
  
    }  

更详细的后面的参考文章

参考文章

  1. 深入理解Java虚拟机 JVM高级特性与最佳实践
  2. IBM深入探讨 Java 类加载器
  3. Tomcat的类加载机制
posted @ 2016-07-31 16:16  卡卡西sir  阅读(1514)  评论(0编辑  收藏  举报