Biaobiaoqi的博客

Java 类加载器

| Comments

背景知识

Java 平台无关的特性是由 JVM(Java 虚拟机)支撑的。不同平台有不同的 JVM 支持。

计算机领域有这么一句话:

计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。

JVM 其实也可以看做是运行的 Java 程序和实际的硬件架构之间的一个新抽象层。

一个 Java 程序从编写到执行的流程一句话概括如下:首先将 Java 源代码(*.java 文件)编译成字节码(*.class 文件,字节码之于 Java 源码,类比于汇编代码之于 C 源码),然后由 JVM 运行那些字节码文件。

JVM 工作原理如下图

JVM framework

Java 中所有的类文件都需要由类加载器(Class Loader)装载到 JVM 中。可以简单的将 JVM 理解为一个工厂,类文件就是等待加工的原料,加载器则是装载货物的工人。Java 类被装载之后,才能进入到 JVM 的运行时机制中,开始运行。

类加载器的作用

顾名思义,Java 类加载器的作用是向 JVM 中装载类。

这种动态装载的技术是 Java 的一种创新,让类能够动态加载到 JVM 中执行(更详细的介绍参见 Java programming dynamics, Part 1: Java classes and class loading)。

而它的意义远非仅仅是加载类这么简单。实际上,类加载器对 Java 的沙箱模型具有重大意义。他和安全管理模块(负责对类文件中的字节码进行校验,防止恶意代码的攻击)一起保证了 JVM 运行的安全性。

类加载机制

大体上,每个 Java 应用使用了如下几种类型的类加载器:

  • 1.引导类加载器(bootstrap class loader)

    它由 C++编写(注意,它非常特殊,并非 Java 中的 ClassLoader 类的子类)。当 JVM 启动时,引导类加载器也随之启动,它负责加载 Java 核心类,如 JRE 的 rt.jar、charsets.jar 等。从 Java1.2 开始,它只加载 Java 核心 API 部分。

    因为这些类是系统信任的类,所以这里的装载,跳过了很多对字节码的验证过程。

  • 2.扩展类加载器(extension class loader)

    它负责加载/lib/ext 中的 java 扩展类。

  • 3.系统类加载器(System Class Loader)

    这是很重要的一个加载器,加载 Java 的路径 classpath 下的类。应用程序的装载默认由它负责。

  • 4.自定义类加载器

    由系统类加载器继承得到。它的存在让我们能定制出各种不同功能的加载器,增加了 Java 的可扩展性。自定义的类加载器如果没有显示的继承关系,则其父类默认为系统类加载器。

一个 JVM,只拥有一个引导类加载器,同时可以拥有多个自定义类加载器,方便不同应用环境的用户定制。比如,自定义类加载器能够动态的修改字节码,让它能接收并加载从网上传来的类文件或 Jar 包,甚至是任何编码方式的压缩包。只要自定义类加载器能够正确识别并调用相应方法来实现类的加载和解析,一切都有可能。

四种加载器不是四个独立的部分,他们之间具有一种特殊的父子关系,每个类加载器都保持着他们的父加载器的应用,共同组成了一条父子关系链,被称作 parent-delegation 模式。如下图

class loader hirerachy

类加载器按照如此树形排列。类加载的查找顺序是:

cache –> parent –> self

子类加载器需要加载某个类时,并不是直接加载,而是首先查看 cache(cache 可以理解为加载器已经加载过的类的记录)。如果没有,则向父加载器提出请求,查看是否存在于父加载器的 cache 中。如此往上,直到根部的引导类加载器。如果引导类加载器的 cache 也没有这个类,则它尝试直接加载这个类,如果无法成功,则请求儿子加载器加载,依次往下。

直接接受程序请求加载某类的加载器被称作初始类加载器(Initiating class loader),而最终加载了该类的加载器则成为定义类加载器(defining class loader)。

类加载器的getParent()方法可以获得加载器的父亲。下面的代码用于输出各个层级的类加载器。

1
2
3
4
5
6
7
8
public class ClassLoaderTest {
    public static void main(String[] args) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        System.out.println("current loader:"+loader);
        System.out.println("parent loader:"+loader.getParent());
        System.out.println("grandparent loader:"+loader.getParent().getParent());
    }
}

显示的结果是

1
2
3
current loader:sun.misc.Launcher$AppClassLoader@1c78e57
parent loader:sun.misc.Launcher$ExtClassLoader@5224ee
grandparent loader:null

grandparent 显示的值是 null,并不意味着他没有 parent,而是因为它是由 C++编写的引导类加载器。他并不是 ClassLoader 类的子类,也就无法使用 getParent()方法获得返回了。

如此,parent class loader 总是拥有更高的加载优先级,这让想利用自定义加载器伪装加载某些重要类的恶意代码无法得逞。如果好奇,你可以尝试自己写 package java.lang 里的 String 类,加载执行试试~ 另外,当类 A 调用另类 B 时,B 会由加载 A 的 class loader 加载,从而实现。

加载类的流程

类的装载大致可以分为三个步骤(如下图):

  • 1.装载(loading)
  • 2.链接(linking)
  • 3.初始化(initialising)

class loader process

跟 C++或者 C 程序有很大的不同,编译过后的类文件中的字节码并没有设计好内存布局,这些需要等到加载之后的链接阶段,才会完成。这也是 java 可移植性中精彩的一笔!

关于类的加载、链接和初始化,请参见另一篇博文:《Java 类的装载、链接和初始化》

关于类加载器的编程实践,请参见另一篇博文:《Java 类加载器编程实践》