JVM 类加载器详解

类加载器介绍:
类加载器负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。
每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。

 

java.lang.ClassLoader 类介绍:
    基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,
    然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class 类的一个实例。
    除此之外,ClassLoader 还负责加载 Java 应用所需的资源,如图像文件和配置文件等.

 

ClassLoader 中与加载类相关的方法:
getParent() 返回该类加载器的父类加载器。
loadClass(String name) 加载名称为 name 的类,返回的结果是 java.lang.Class 类的实例。
findClass(String name) 查找名称为 name 的类,返回的结果是 java.lang.Class 类的实例。
findLoadedClass(String name)  查找名称为 name 的已经被加载过的类,返回的结果是 java.lang.Class 类的实例。
defineClass(String name, byte[] b, int off, int len) 把字节数组 b 中的内容转换成 Java 类,返回的结果是 java.lang.Class 类的实例。这个方法被声明为 final 的。
resolveClass(Class<?> c)  链接指定的 Java 类。
   

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Java 中的类加载器大致可以分成两类:
    一类是系统提供的
   一类则是由 Java 应用开发人员编写的

 

系统提供的类加载器主要有下面三个:

  1. 引导类加载器(bootstrap classloader)
  2. 扩展类加载器(extensions classloader)
  3. 应用程序类加载器(application classloader)

1, 引导类加载器(bootstrap classloader)
 负责加载核心的Java库,比如位于<JAVA_HOME>/jre/lib 目录下的vm.jar,core.jar。
 这个类加载器,是JVM核心部分,是用native代码写成的。
 并不继承自 java.lang.ClassLoader。

 

2, 扩展类加载器(extensions classloader)
 负责加载扩展路径下的代码,一般位于<JAVA_HOME>/jre/lib/ext  或者通过java.ext.dirs 这个系统属性指定的路径下的代码。
 这个类加载器是由sun.misc.Launcher$ExtClassLoader 实现的。 
 
3, 应用程序类加载器(application classloader)
 负责加载java.class.path(映射系统参数 CLASSPATH的值) 路径下面的代码,这个类加载器是由 sun.misc.Launcher$AppClassLoader 实现的。 
 可以通过 ClassLoader.getSystemClassLoader() 来获取它

 

java.ext.dirs属性指的是系统属性下的一个key,可以通过System.getProperties()方法获得。
java.class.path属性指的是系统属性下的一个key,可以通过System.getProperties()方法获得。

 

类加载器之间的关系:父子
引导类加载器 
      |
扩展类加载器 
      |
应用程序类加载器

自定义的类加载器的父亲是应用程序类加载器。

 

类的加载模式:父委托模式
 类加载器在加载自己的类之前,先委托加载父类。父类加载器可以是客户化的类加载器或者引导类加载器。
 但是有一点很重要,类加载器只能委托自己的父类加载器,而不能是子类加载器(只能向上不能向下)
 如果应用程序类加载器需要加载一个类,它首先委托扩展类加载器,扩展类加载器再委托引导类加载器。
 如果父类加载器不能加载类,子类加载器就回在自己的库中查找这个类。基于这个特性,类加载器只负责它的祖先无法加载的类。

 

重点注意:当一个类已经被类加载器加载后,这个类需要的任何其他的新类都必须用同一个类加载器加载他们(或者遵循父委托模式,由父类加载器加载)。

 

例如:WhichClassLoader2 引用 WhichClassLoader3,当类加载器加载WhichClassLoader2时,由于WhichClassLoader3被引用了,
 该类加载器会试着加载WhichClassLoader3。

 

注意:开发者通常会使用如下语法通过类加载器机制加载属性文件:
Properties p = new Properties();
p.load(MyClass.class.getClassLoader().getResourceAsStream("myApp.properties"));
这个意思是:如果MyClass 由扩展类加载器加载,而 myApp.properties 文件只能应用程序类加载器加载,
 否则装入属性文件就会失败。

 

每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 getClassLoader() 方法就可以获取到此引用。
例如:targetObject.class.getClassLoader();

 

Java 虚拟机是如何判定两个 Java 类是相同的:
 一个类的全名和一个加载类ClassLoader的实例作为唯一标识,
 也就是:Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。
 只有两者都相同的情况,才认为两个类是相同的。

 

不同两个类的实例之间是不能相互赋值的。

 

父委托模式是为了保证 Java 核心库的类型安全,所有 Java 应用都至少需要引用 java.lang.Object 类,
也就是说在运行的时候,java.lang.Object 这个类需要被加载到 Java 虚拟机中。
如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object 类,而且这些类之间是不兼容的。

 

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

 

线程上下文类加载器:
 类 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 应用的线程的上下文类加载器默认就是应用程序类加载器。
 在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。

 

Class.forName:
 Class.forName 是一个静态方法,同样可以用来加载类。该方法有两种形式:
 Class.forName(String name, boolean initialize, ClassLoader loader)
 Class.forName(String className)
 第一种形式的参数 name 表示的是类的全名;initialize 表示是否初始化类;loader 表示加载时使用的类加载器。
 第二种形式则相当于设置了参数 initialize 的值为 true,loader 的值为当前类的类加载器。
 Class.forName 的一个很常见的用法是在加载数据库驱动的时候。
 如 Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance() 用来加载 Apache Derby 数据库的驱动。
开发自己的类加载器:

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

 

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

 

类加载器与 Web 容器:
 对于运行在 Java EE™ 容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。
 以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。
 该类加载器也使用父委托模式,所不同的是它是首先尝试去加载某个类,
 如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。
 这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。
 这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。

 

我们在加载类时通常会遇到两种异常:
 方法 loadClass() 抛出的是 java.lang.ClassNotFoundException 异常:没有找到这个类会抛出这个异常
 方法 defineClass() 抛出的是 java.lang.NoClassDefFoundError 异常:通常是加载SPI类时,由于SPI的实现类没有定义,导致加载该类的定义失败。

你可能感兴趣的:(java,jvm,应用服务器,ext,Derby)