18-类加载机制与反射

当程序主动使用某个类时,如果该类还没有被加载到 JVM 内存中,则系统会通过 加载、连接、初始化 三个连续步骤对该类进行初始化。

类的加载

类加载指的是将类的 class 文件读入 JVM 内存,并为之创建一个 java.lang.Class 对象。

类的加载由类加载器完成,类加载器由 JVM 提供,称为 系统类加载器。除此之外,还可以通过继承 ClassLoader 基类来创建自己的类加载器。

通过使用不同的类加载器,可以从不同来源加载类的二进制数据:

  • 从本地文件系统加载 class 文件,这是绝大部分的类加载方式。
  • 从 JAR 包加载 class 文件,例如在 JDBC 编程时所使用到的数据库驱动程序就放在 JAR 文件中,JVM 可以从 JAR 文件中直接加载该 class 文件。
  • 通过网络加载 class 文件。
  • 把一个 Java 源文件动态编译,并执行加载。

类加载器简介

类加载器负责加载所有的类,系统为所有被载入 JVM 内存的类生成一个 java.lang.Class 实例。一旦一个类被载入 JVM 中,同一个类就不会再次被加载了。

在 Java 程序中,一个类用其全限定类名作为标识;在 JVM 中,一个类用其全限定类名和其类加载器作为其唯一标识。

当 JVM 启动时,会形成由三个类加载器组成的初始类加载器层次结构:

  • Bootstrap ClassLoader:根类加载器。它负责加载 Java 的核心类。根类加载器比较特殊,它不是用 Java 语言实现的,而是用 C++ 实现。执行 java.exe 命令时,可以使用 -Xbootclasspath 选的或 -D 选项来指定 sun.boot.class.path 系统属性加载附加类。
  • Extension ClassLoader:扩展类加载器。它负责加载 JRE 的扩展目录 (jre/lib/ext/*.jar) 中的 JAR 包的类。通过这种方式,就可以为 Java 扩展核心类以外的新功能,只要把自己开发的类打包成 JAR 文件,放入扩展目录路径即可。
  • App ClassLoader:自定义类加载器。应用类加载器负责在 JVM 启动时加载来自 classpath 指定的目录中的类。可以通过在程序中调用 ClassLoader.getSystemClassLoader() 来获取应用类加载器。
1
2
3
4
5
6
//获取根类加载器所加载的URL数组
URL[] urls=sun.misc.Launcher.getBootstrapClassPath().getURL();
//获取系统类加载器
ClassLoader.getSystemClassLoader();
//获取根类加载器
ClassLoader.getSystemClassLoader().getParent();

类加载机制:

  • 全盘负责:就是当一个类加载器负责加载某个 Class 时,该 Class 所依赖的和引用的其他 Class 也将由该类加载器负责载入,除非显式使用另一个类加载器来载入。
  • 父类委托:就是先让父类加载器试图加载该 Class,只要在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
  • 缓存机制:缓存机制将保证所有加载过的 Class 都会被缓存,当程序中需要某个 Class 时,类加载器先从缓存区中搜寻该 Class,只要当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象,存入缓存区中。这就是为什么修改了 Class 后,必须重新启动 JVM,程序所作的修改才会生效的原因。

类加载器之间的父子关系并不是类继承上的父子关系。

实现自己的类加载器

自定义的类加载器通过继承 ClassLoader 来实现。ClassLoader 类有两个关键方法:

  • loadClass(String name,boolean resolve):该方法为 ClassLoader 的入口点,根据指定名称来加载类。
  • findClass(String name):根据指定名称来查找类。

类的连接

当类被加载之后,系统会为之生成一个对应的 Class 对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到 JRE 中,类的连接又可以分为如下三个阶段:

  • 验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。
  • 准备:类准备阶段负责为 类的类变量分配内存,并设置默认初始值。
  • 解析:将类的二进制数据中的符号引用替换成直接引用。

类的初始化

类的初始化阶段,虚拟机负责对类进行初始化,主要就是对类变量进行初始化。在 Java 类中对类变量指定初始值有两种方式:

  • 声明类变量时指定初始值。
  • 使用静态初始化块为类变量指定初始值。

JVM 初始化一个类包含如下几个步骤:

  • 假如这个类还没有被加载和连接,则程序先加载并连接该类。
  • 假如该类的直接父类还没有被初始化,则先初始化其直接父类。注意这里对直接父类的初始化也遵循这 3 个步骤。
  • 假如该类中有初始化语句,则系统依次执行这些初始化语句。

类初始化的时机

当程序首次通过下面 6 种方式来使用某个类或接口时,系统就会初始化该类或接口:

  • 创建类的实例。创建类的实例有 4 种方式:使用 new 关键字;调用 Object 类的 clone() 方法;通过反序列化创建实例;通过反射创建实例。
  • 调用某个类的静态方法。
  • 访问某个类或接口的类变量,或为该类变量赋值。
  • 使用反射方式来强制创建某个类或接口对应的 java.lang.Class 对象。
    Class.forName("Person");,如果系统还没有初始化 Person 类,则这段代码将会导致 Person 类被初始化,并返回 Person 类对应的 java.lang.Class 对象。
  • 初始化某个类的子类。当初始化某个类的子类时,该子类的所有父类都会被初始化。
  • 直接使用 java.exe 命令来运行某个主类时,程序会先初始化该主类。

当某个类的静态变量使用了 final 修饰,而且它的值可以在编译时就确定下来,那么程序其他地方使用该类变量时,实际上并没有使用该类变量,而是相当于使用常量,也就不会导致该类的初始化。