类加载子系统

类加载子系统负责从文件系统或网络中加载.class字节码文件。ClassLoader(类加载器)只负责字节码文件的加载,至于它是否可以运行则由Execution Engine(执行引擎)决定。加载的类信息存放在元数据区(Java7及以前称为方法区)中,除了类信息外,元数据区还存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是字节码文件中常量池部分的内存映射)。

类的加载过程

字节码文件存放在本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中,根据它实例化出n个一模一样的实例。字节码文件加载到JVM中,被称为DNA元数据模板,放在元数据区。而字节码文件从被加载为元数据模板的过程就需要一个运输工具,即ClassLoader(类加载器)。

类加载的具体过程如图所示:

类加载的步骤

  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

类链接的步骤

验证:目的在于确保字节码文件的字节流中包含的信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害虚拟机自身的安全。验证主要包括4种:文件格式验证,元数据验证,字节码验证,符号引用验证。

准备:为类变量分配内存并设置该类变量的默认初始值,即零值。这里不包含final修饰的static类变量,因为final在编译的时候就会分配了,准备阶段会显式初始化。此外这里不会为实例变量分配初始值,类变量分配在方法区中,而实例变量会随着对象一起分配到Java堆中。

解析:解析就是将常量池内的符号引用转换为直接引用的过程。符号引用就是一组符号来描述所引用的目标,符号引用的字面量形式明确定义在Java虚拟机规范的字节码文件格式中。直接引用就是直接指向目标的指针、相对偏移或一个间接定位到目标的句柄。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等,对应常量池中的CONSTANT_Class_infoCONSTANT_Fieldref_infoCONSTANT_Methodref_info等。

类初始化的步骤

如果加载到类中有类变量,编译器会为我们生成一个<clinit>()方法(其中cl代表class,即class init),初始化阶段就是执行<clinit>()方法的过程,此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来,该方法中的指令按语句在源文件中出现的顺序执行。若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕。虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁。

注:<clinit>()不用于我们代码中定义的构造器方法,我们定义的构造器是虚拟机视角下的<init>()

有关<clinit>()方法,我们可以编写如下代码并编译,在jclasslib软件中查看到:

package com.gacfox.demo;

public class Demo {
    public static int i = 1;
    public Demo() {}
}

类加载器

JVM支持两种类型的类加载器:BootStrap ClassLoader(引导类加载器)和User-Defined ClassLoader(自定义类加载器)。虽然后者名字叫“自定义”类加载器,但实际上Java虚拟机规范将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器,因此JDK自带的ExtClassLoader和AppClassLoader也都属于此类。

除此之外我们还应该知道,在JVM中,判定两个类Class对象是否为同一个类存在两个必要条件:1)类的完整类名必须一致 2)类加载器必须相同

BootStrapClassLoader 引导类加载器

引导类加载器是由C/C++语言实现的,在JVM内部,我们是无法直接查看其代码的。引导类加载器用于加载Java的核心库来提供JVM自身需要的类,以及加载其他应用程序类加载器,因此引导类加载器是其它类加载器的父加载器。出于安全考虑,引导类加载器只加载包名为javajavaxsun等开头的类。

引导类加载器并不继承java.lang.ClassLoader,没有父加载器。

ExtClassLoader 扩展类加载器

扩展类加载器是Java语言编写的,由sun.misc.Launcher$ExtClassLoader实现。扩展类加载器派生自ClassLoader类,其父加载器为引导类加载器。

扩展类加载器从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的Jar放在此目录下,也会自动由扩展类加载器加载。

AppClassLoader 应用类加载器

应用类加载器是Java语言编写的,由sun.misc.Launcher$AppClassLoader实现。应用类加载器派生自ClassLoader类,其父加载器为扩展类加载器。

应用类加载器负责加载环境变量CLASSPATH或系统属性java.class.path指定路径下的类库,该类加载器是程序中默认的类加载器,一般来说Java应用的类都是由它来完成加载的。我们可以通过ClassLoader.getSystemClassLoader()方法获取该类加载器。

自定义类加载器

我们日常开发中,类加载基本上都是由上述3个类加载器相互配合实现的,但我们其实还可以自定义类加载器,来定制类的加载方式。自定义类加载器主要有以下几个用途:

  1. 隔离加载类
  2. 修改类加载的方式
  3. 扩展加载源
  4. 防止源码泄露

具体这里就不展开介绍了。

获取ClassLoader的途径

获取类加载器有4个途径:

  1. 获取当前类的加载器:clazz.getClassLoader()
  2. 获取当前线程上下文的加载器:Thread.currentThread().getContextClassLoader()
  3. 获取当前系统的加载器:ClassLoader.getSystemClassLoader()
  4. 获取调用者的加载器:DriverManager.getCallerClassLoader()

双亲委派机制

Java虚拟机对字节码文件采取的是按需加载的方式,也就是说当需要使用该类时才会将其加载到内存生成Class对象。加载时Java虚拟机会采用双亲委派模式,把请求交给父加载器处理,父加载器无法处理再交给子级处理,这是一种任务委派模式。

双亲委派模式的工作原理:

  1. 类加载器收到类加载请求,它并不会自己先去加载,而是把这个请求委托给父加载器去执行
  2. 如果父加载器还存在其他父加载器,则进一步向上委托,请求最终将到达顶层的引导类加载器
  3. 如果父加载器可以完成类加载任务,就成功返回;如果无法完成,子加载器才会尝试自己加载

双亲委派机制的作用:

  1. 避免类的重复加载
  2. 保护程序安全,防止核心API被篡改
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。