xiaozhang2014

JVM-类加载过程

一般来说,Java源代码(.java)经过编译器编译成字节码(.class)后,类加载器读取字节码文件,最终加载并转换成 java.lang.Class类的一个实例。

Java中的类加载器大致分为两种,一种是系统提供的,另外一种是Java开发者开发的。而系统提供的类加载器主要有三个:

  • 引导类加载器(Bootstrap Class Loader):最顶层的类加载器,主要加载核心的类库,JRE目录下的rt.jar,resources.jar,charsets.jar等jar包和class,一般是用原生C或C++来实现的。
  • 扩展类加载器(Extension Class Loader):用来加载JRE\lib\ext目录下的jar包和class等,由ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现;
  • 系统类加载器(System Class Loader),也可以成为应用类加载器:加载Java当前应用CLASSPATH下的所有类,由AppClassLoader(sun.misc.Launcher$AppClassLoader)实现,一般情况下,它是Java应用程序默认的类加载器。

除了系统提供的类加载器外,开发者可以通过继承java.lang.ClassLoader类或组合的形式来实现自己的类加载器,以满足一些特殊的需求。

JVM要求除了最上层的引导类加载器之外,所有的类加载器都应当由一个父类加载器,而这个父加载器可以通过下表给出的getParent方法得到。这种加载模式就是所谓的双亲委派模式,如下图所示:

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

ClassLoader类大致说明:

  java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。不过本文只讨论其加载类的功能。为了完成加载类的这个职责,ClassLoader提供了一系列的方法,比较重要的方法如下:

表示类名称的 name参数的值是类的二进制名称。需要注意的是内部类的表示,如 com.example.Sample$1和 com.example.Sample$Inner等表示方式。

每一个Java类都维护着一个指向定义它的类加载器的引用,通过 getClassLoader()方法就可以获取到此引用。所以说这种类加载器间的父子关系一般都是通过组合,而不是继承来实现的。

通过一个小程序来看看:

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        while (classLoader != null) {
            System.out.println(classLoader.toString());
            classLoader = classLoader.getParent();
        }
    }
}

输出:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d

可以看到,第一个输出的是ClassLoaderTest类的类加载器,即系统类加载器,它是 sun.misc.Launcher$AppClassLoader@18b4aac2类的实例;第二个是扩展类加载器,是 sun.misc.Launcher$ExtClassLoader@1540e19d 类的实例。需要注意的是,没有输出引导类的加载器,因为由于bootstrap的实现不是Java,所以JDK源码中,对于父加载器是bootstrap的情况下,getParent方法返回的是null。

  让我们来看一下双亲委派模式的简易流程:

“双亲委派模型”简单来说就是:

1.先检查需要加载的类是否已经被加载,如果没有被加载,则委托父加载器加载,父类继续检查,尝试请父类加载,这个过程是从下-------> 上;

2.如果走到顶层发现类没有被加载过,那么会从顶层开始往下逐层尝试加载,这个过程是从上 ------> 下;

3.如果最终都加载不了,那就会抛出异常;

 

这里必须要提一提JVM如何判定两个类是否相等:

  Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。比如一个 Java 类 com.example.Sample,编译之后生成了字节代码文件 Sample.class。两个不同的类加载器 ClassLoaderA和 ClassLoaderB分别读取了这个 Sample.class文件,并定义出两个 java.lang.Class类的实例来表示这个类。这两个实例是不相同的。对于 Java 虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常 ClassCastException。

  对于两个相同名称的类而言,不同的类加载器为相同名称的类创建了额外的命名空间,不同类加载器间加载的类之间是不兼容的。命名空间的作用抽象理解就是:

  • 竖直方向上,父加载加载的类对所有子加载器可见;
  • 水平方向上,子类之间各自加载的类对于各自是不可见的,达到了隔离的效果;

这就解释了另外一个问题,为什么JVM使用双亲委派模式,主要为了保证 Java 核心库的类型安全,防止重复与恶意加载;
  比如Java应用都至少要引用java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。

  并且如果要加载一个System类,使用委托机制,会递归的向父类查找,最终都是委托给最顶层的启动类加载器进行加载也就是用Bootstrap尝试加载,如果加载不了再向下查找。如果能在Bootstrap中找到然后加载,然后缓存下来,如果此时另一个类也要加载System,也从Bootstrap开始,此时Bootstrap发现已经加载过了System那么直接返回缓存中的System即可而不需要重新加载,从而避免了恶意加载。

 

线程上下文类加载器(Context Class Loader)

 还有一种类加载器,被称为Context  Class Loader,线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。ContextClassLoader并不是一种新的类加载器,而是一种抽象的说法,它的获取和设置可以通过Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl) 来实现。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。

ContextClassLoader的存在是为了解决双亲委派机制无法解决的问题。双亲委派机制委托链上面的图已经说过了,一般说来,处于委托链下层的classLoader可以很容易的使用上层classLoader所加载的类;而反过来,如果上层的classLoader要使用下层的classLoader所加载的类的话,由于双亲委派机制是单向的,所以无法通过双亲委派来实现,所以就有了ContextClassLoader,这种情况下就可以把某个位于委派链下层的ClassLoader设置为线程的contextClassLoader,这种情况及突破了双亲委派的限制了。

 

  其实,BootstrapClassLoader、ExtClassLoader、AppClassLoader实际是查阅相应的环境属性sun.boot.class.path、java.ext.dirs和java.class.path来加载资源文件的。他们的加载可以通过一个关键字来说明:路径;

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        System.out.println(System.getProperty("sun.boot.class.path"));
        System.out.println("=======================");
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println("==========================");
        System.out.println(System.getProperty("java.class.path"));
    }
}

结果:

E:\software\JDK8.0\jre\lib\resources.jar;
E:\software\JDK8.0\jre\lib\rt.jar;
E:\software\JDK8.0\jre\lib\sunrsasign.jar;
E:\software\JDK8.0\jre\lib\jsse.jar;
E:\software\JDK8.0\jre\lib\jce.jar;
E:\software\JDK8.0\jre\lib\charsets.jar;
E:\software\JDK8.0\jre\lib\jfr.jar;
E:\software\JDK8.0\jre\classes
=======================
E:\software\JDK8.0\jre\lib\ext;
C:\Windows\Sun\Java\lib\ext
==========================
E:\software\JDK8.0\jre\lib\charsets.jar;
E:\software\JDK8.0\jre\lib\deploy.jar;
E:\software\JDK8.0\jre\lib\ext\access-bridge-64.jar;
E:\software\JDK8.0\jre\lib\ext\sunpkcs11.jar;
......
E:\software\JDK8.0\jre\lib\ext\zipfs.jar;
E:\software\JDK8.0\jre\lib\javaws.jar;
E:\software\JDK8.0\jre\lib\jce.jar;
E:\software\JDK8.0\jre\lib\jfr.jar;
E:\software\JDK8.0\jre\lib\jfxswt.jar;
E:\software\JDK8.0\jre\lib\jsse.jar;
E:\software\JDK8.0\jre\lib\management-agent.jar;
E:\software\JDK8.0\jre\lib\plugin.jar;
E:\software\JDK8.0\jre\lib\resources.jar;
E:\software\JDK8.0\jre\lib\rt.jar;
C:\Users\IdeaProjects\untitled\out\production\JavaTest;
E:\software\idea\IntelliJ IDEA 2017.1.3\lib\idea_rt.jar

由于CLASSPATH下jar包太多,省略了一部分;不过,从打印的内容我们也可以看到他们加载的资源情况。

 

继承体系如下,classLoader的入口是:sun.misc.Launcher,其中ExtClassLoader和AppClassLoader是Launcher的内部静态类;

主要看下ClassLoader的loadClass方法,其他的源码等下篇文章再写:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查这个类是否已经被加载了
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                // 如果class没有被加载且已经设置parent,那么请求其父加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                    //如果没有设定parent类加载器,则寻找BootstrapClss并尝试使用Boot loader加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 如果父加载器找不到class时会抛出ClassNotFoundException异常
                    
                }

                if (c == null) {
                    // 如果当前这个loader所有的父加载器以及顶层的Bootstrap ClassLoader都不能加载待加载的类
                    // 那么则调用自己的findClass()方法来加载
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

 

注意:Class.forName使用的是哪个类加载器?

首先,class.forName是带参数的,可以指定类加载器,如果没有指定,那默认使用的是当前类的类加载器,也就是默认的AppClassLoader;

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        ClassLoader test = ClassLoaderTest.class.getClassLoader();
        System.out.println(test);

        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        System.out.println(contextClassLoader);

        ClassLoader forNameClassLoader = Class.forName("com.test.ForNameTest").getClassLoader();
        System.out.println(forNameClassLoader);
    }
}
class ForNameTest{}
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2

 

 

参考自:https://www.ibm.com/developerworks/cn/java/j-lo-classloader/

http://www.blogjava.net/zhuxing/archive/2008/08/08/220841.html

http://blog.csdn.net/briblue/article/details/54973413

感谢RednaxelaFX,R神。

 

分类:

技术点:

相关文章: