19、类加载器

讲解之前先引入几个问题:

1.jvm如何识别.class文件?

2.jvm如何加载.class文件里面的字节码?

3.jvm如何创建类、对象、方法、属性?

 

上一章中讲到,jvm是通过.class文件的二进制流转换成16进制,得到字符串cafebabe认为这是一个.class文件;若任意文件本身不是由javac生成的.class文件,即使更改文件名为.class后缀的文件,不能被认定为cafebabe(6f6b);


由上图可知类加载器是JVM的一部分,主要作用是将字节码加载进入执行引擎,以供执行。当调用ava.exe执行一个.class文件时,从而根据%JAVA_HOME%\jre\lib\i386\jvm.cfg配置来选择激活jvm,初始化工作完成之后便启动Bootstrap Loader(引导类)加载器,它由C++编写。JVM中另外两个内置类加载器是ExtClassLoader和AppClassLoader,它们定义在sun.misc.Launcher.class中,为内部类,且由Bootstrp Loader加载进入虚拟机。 

启动(Bootstrap)类加载器:引导类装入器是用本地代码实现的类装入器,它负责将 <Java_Runtime_Home>/lib 下面的类库加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

标准扩展(Extension)类加载器:扩展类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader) 实现的。它负责将 < Java_Runtime_Home >/lib/ext 或者由系统变量 java.ext.dir 指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

系统(System)类加载器:系统类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。

除了以上列举的三种类加载器,还有一种比较特殊的类型就是线程上下文类加载;

类加载器也是Java类,本身也要被类加载器加载,第一个类加载器不是java类,而是BootStrap。Java虚拟机中的所有类装载器采用具有父子关系的树形结构进行组织,在实例化每个类装载器对象时,需要为其指定一个父级类装载器对象或者默认采用系统类装载器为其父级类加载。

1、类加载器的委派模型:假设AppClassLoader需要加载一个类,它会首先委托其父加载器ExtClassLoader来加载此类,ExtClassLoader也会递归性的委托其父加载器Bootstrap Loader来加载此类,如果Bootstrap Loader在sun.boot.class.path下找到被加载类时即加载,如果无法找到时再依次由子类加载器去加载。委派模型是针对Java安全而设计的,这也印证了Java语言的设计初衷:面向网络的编程语言。 

2、由同一个类加载器所加载的类只能引用该加载器和其父加载器所加载的其他类。  

两种方法可以在运行时动态加载.class文件:1) Class clazz = Class.forName("类名称"); 2) 自定义类加载器,然后调用loadClass(“类名称”)方法;

一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 .class 文件,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。

基本上所有的类加载器都是 java.lang.ClassLoader类的一个实例。


&:类是Class的实例,类加载器是抽象类ClassLoader的实例(除了启动类加载器BootStrap);

java.lang.ClassLoader类的基本功能就是根据一个指定的类名称,找到或者生成其对应的字节码(.class文件),然后从这些字节代码中创建一个 java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。ClassLoader 中与加载类相关的方法:

//加载指定名称(包括包名)的二进制类型,供用户调用的接口   
public Class<?> loadClass(String name) throws ClassNotFoundException{ … }  
  
//加载指定名称(包括包名)的二进制类型,同时指定是否解析(但是这里的resolve参数不一定真正能达到解析的效果),供继承用   
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ … }  
 
//findClass方法一般被loadClass方法调用去加载指定名称类,供继承用   
protected Class<?> findClass(String name) throws ClassNotFoundException { … }  

//定义类型,一般在findClass方法中读取到对应字节码后调用,可以看出不可继承   
//(说明:JVM已经实现了对应的具体功能,解析对应的字节码,产生对应的内部数据结构放置到方法区,所以无需覆写,直接调用就可以了)   
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ … } 

不同的类加载器为相同名称的类创建了不同的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。

类加载器   加载类的时候,首先会让父类加载器来加载这个类。这就意味着:完成类的加载工作的类加载器和启动这个加载过程的类加载器(&:加载一个类是分为两个步骤完成的),可能不是同一个。真正完成类的加载工作是通过调用 defineClass来实现的;而启动类的加载过程是通过调用 loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,是哪个类加载器启动了的加载过程并不重要,重要的是最终是那个类加载器定义了这个类。

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

public static void main(String[] args) {  
        try {  
            System.out.println(ClassLoader.getSystemClassLoader());  
            System.out.println(ClassLoader.getSystemClassLoader().getParent());  
           System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
}  
sun.misc.Launcher$AppClassLoader@6d06d69c  
sun.misc.Launcher$ExtClassLoader@70dea4e  
null  

通过以上的代码输出,我们可以判定系统类加载器的父加载器是标准扩展类加载器,但是我们试图获取标准扩展类加载器的父类加载器时确得到了null,由于jvm对于父类加载器是引导类加载器的情况,getParent()方法返回 null

我们首先看一下java.lang.ClassLoader抽象类中默认实现的两个构造函数(大楷意思,与源码不太一致):标准扩展类加载器和系统类加载器及其父类(java.net.URLClassLoader和java.security.SecureClassLoader)都没有覆写java.lang.ClassLoader中默认的加载委派规则---loadClass(…)方法。

public Class<?> loadClass(String name) throws ClassNotFoundException {  
    return loadClass(name, false);  
}   
protected synchronized Class<?> loadClass(String name, boolean resolve)  
        throws ClassNotFoundException {  
    // 首先判断该类型是否已经被加载  
    Class c = findLoadedClass(name);  
    if (c == null) {  
        //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载  
        try {  
            if (parent != null) {  
                //如果存在父类加载器,就委派给父类加载器加载  
                c = parent.loadClass(name, false);  
            } else {  
                //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,  
                //通过调用本地方法native findBootstrapClass0(String name)  
                c = findBootstrapClass0(name);  
            }  
        } catch (ClassNotFoundException e) {  
            // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能  
            c = findClass(name);  
        }  
    }  
    if (resolve) {  
        resolveClass(c);  
    }  
    return c;  
} 

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

动态加载类:

1.调用class.forName(String);

 public static Class<?> forName(String className)
                throws ClassNotFoundException {
        return forName0(className, true, ClassLoader.getCallerClassLoader());     //ClassLoader
    }
设置为true表示强制加载同时完成初始化。例如典型的就是利用DriverManager进行JDBC驱动程序类注册的问题。因为每一个JDBC驱动程序类的静态初始化方法都用DriverManager注册驱动程序,这样才能被应用程序使用。这就要求驱动程序类必须被初始化,而不单单被加载。Class.forName的一个很常见的用法就是在加载数据库驱动的时候。如 Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()用来加载 Apache Derby 数据库的驱动。

2.自定义类加载器

通过前面的分析,除了启动类加载器之外,标准扩展类加载器和系统类加载器都可以当做自定义类加载器,唯一区别是是否被虚拟机默认使用。自定义的类加载器必须符合以下要求:

     继承ClassLoader类;

     覆写findClass方法

package org.Smart;

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; 
    } 
    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);   //ClassLoader里的方法:创建对象
        } 
    } 
    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; 
    } 
    String classNameToPath(String className) {
    	StringBuffer s=new  StringBuffer(rootDir+"/"+className.replace('.', '/')+".class");
    	return s.toString();
    } 
 }
测试:
 public static void main(String...s) throws ClassNotFoundException {
    	@SuppressWarnings("rawtypes")
		Class app=new FileSystemClassLoader("E:/side1/Smart/target/classes").findClass("org.Smart.App");
    	System.out.println(app.getClassLoader());          //org.Smart.FileSystemClassLoader@3020ad
    	System.out.println(App.class.getClassLoader());    //sun.misc.Launcher$AppClassLoader@15253d5
    }
 自定义的类加载器测试成功!!

    

线程上下文类加载器(context class loader)

是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers包中。这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。

     Java默认的线程上下文类加载器是系统类加载器(AppClassLoader)。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。

后续:

http://blog.csdn.net/zhoudaxia/article/details/35897057

猜你喜欢

转载自nickfover.iteye.com/blog/2120327