理解jvm的ClassLoader分析-基础篇

什么是ClassLoader

我们都知道,java源码编译后,生成的是一个个.class文件,这些类必须要被装载到jvm才能被运行。ClassLoader就是用来完成这些类的装载工作。

ClasssLoader加载机制

jvm类加载器分三个层次:

1.引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等,下面通过程序输出bootstrap加载器加载的核心类。

//打印bootstrap classloader加载了哪些文件
public static void main(String[] args) {
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (int i = 0; i < urls.length; i++) {
            System.out.println(urls[i].toExternalForm());
        }
    }

运行结果:

file:/home/wzz/bin/jdk1.8.0_101/jre/lib/resources.jar
file:/home/wzz/bin/jdk1.8.0_101/jre/lib/rt.jar
file:/home/wzz/bin/jdk1.8.0_101/jre/lib/sunrsasign.jar
file:/home/wzz/bin/jdk1.8.0_101/jre/lib/jsse.jar
file:/home/wzz/bin/jdk1.8.0_101/jre/lib/jce.jar
file:/home/wzz/bin/jdk1.8.0_101/jre/lib/charsets.jar
file:/home/wzz/bin/jdk1.8.0_101/jre/lib/jfr.jar
file:/home/wzz/bin/jdk1.8.0_101/jre/classes

2.扩展类加载器(extensions class loader):负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。该加载器是由sun.misc.Launcher ExtClassLoader3.systemclassloaderJavaCLASSPATHJavasun.misc.Launcher AppClassLoader实现。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

双亲委托模型

  • 前面说了,java中有三个类加载器,问题就来了,碰到一个类需要加载时,它们之间是如何协调工作的,即java是如何区分一个类该由哪个类加载器来完成呢。 在这里java采用了委托模型机制,这个机制简单来讲,就是“类装载器有载入类的需求时,会先请示其Parent使用其搜索路径帮忙载入,如果Parent 找不到,那么才由自己依照自己的搜索路径搜索类”。
    理解jvm的ClassLoader分析-基础篇_第1张图片
  • Java 虚拟机是如何判定两个 Java 类是相同的的呢?Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。比如一个 Java 类 com.example.Sample,编译之后生成了字节代码文件 Sample.class。两个不同的类加载器 ClassLoaderA和 ClassLoaderB分别读取了这个 Sample.class文件,并定义出两个 java.lang.Class类的实例来表示这个类。这两个实例是不相同的。对于 Java 虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常 ClassCastException。这样的好处是不同层次的类加载器具有不同优先级,比如所有Java对象的超级父类java.lang.Object,位于rt.jar,无论哪个类加载器加载该类,最终都是由启动类加载器进行加载,保证安全。

自定义ClassLoader

虽然在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输 Java 类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在 Java 虚拟机中运行的类来。下面将通过具体的实例来说明类加载器的开发。

package cn.wuzhizhan.study.classloader;

import java.io.IOException;
import java.io.InputStream;

/**
 * 作者: wzz
 * 日期: 16-10-15
 */
public class ClassLoaderDemo {
    public static void main(String[] args) throws Exception {

        ClassLoader clazzLoader = new ClassLoader() {
            @Override
            public Class loadClass(String name) throws ClassNotFoundException {
                try {
                    String clazzName = name.substring(name.lastIndexOf(".") + 1) + ".class";

                    InputStream is = getClass().getResourceAsStream(clazzName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };

        String currentClass = "cn.wuzhizhan.study.classloader.ClassLoaderDemo";
        Class clazz = clazzLoader.loadClass(currentClass);
        Object obj = clazz.newInstance();

        System.out.println(obj.getClass());
        System.out.println(obj instanceof cn.wuzhizhan.study.classloader.ClassLoaderDemo);
    }
}

运行结果:

class cn.wuzhizhan.study.classloader.ClassLoaderDemo
false

虽然两个类名字相同,但是由于加载器不同,所以,它们在jvm看来,不是同一个类。这也就是有些情况,明明是看到依赖有这个类,但运行时就是提示找不到类的原因。

线程上下文类加载器

线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。
前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers包中。这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory类中的 newInstance()方法用来生成一个新的 DocumentBuilderFactory的实例。这里的实例的真正的类是继承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而问题在于,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 数据库的驱动。

回顾

本文主要介绍了jvm类加载器的三个层次、双亲委托代理、以及实现自定义加载器。
参考:
http://blog.csdn.net/xyang81/article/details/7292380
http://blog.jobbole.com/96145/
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/
http://gityuan.com/2016/01/24/java-classloader/

你可能感兴趣的:(JAVA)