Class装载系统
1 装载流程
系统装载Class类型可以分为加载、连接和初始化3个步骤,其中连接又可分为验证、准备和解析。
1.1 类装载的条件
Class只有在必须要使用的时候才会被加载,Java 虚拟机不会无条件地装载Class类型。Class虚拟机规定,一个类或者接口在初次使用前,必须要进行初始化。使用有以下几种情况:
- 当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化。
- 当调用类的静态方法时,即使用了字节码invokestatic指令。
- 当使用类或接口的静态字段时(final常量除外),比如使用getstatic或者putstatic。
- 当使用java.lang.reflect包中的方法反射类的方法时。
- 当初始化子类时,要求先初始化父类。
- 作为启动虚拟机,含有main() 方法的那个类。
注:在引用一个字段时,只用直接定义该字段的类才会被初始化。如通过子类调用父类中的静态变量,则子类不会初始化。虽然子类没有初始化,但是子类已经被系统加载,只是没有进入到初始化阶段。
1.2 加载类
加载类处于类装载的第一个阶段。在加载类时,Java虚拟机必须完成以下工作:
- 通过类的全名,获取类的二进制数据流。
- 解析类的二进制数据流为方法区内的数据结构。
- 创建java.lang.Class类的实例,表示该类型。
1.3 验证类
验证主要包括:格式检查、语义检查、字节码验证、符号引用验证。
1.4 准备
当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机就会为这个类分配相应的内存空间,并设置初始值。
1.5 解析类
解析阶段的工作就是将类、接口、字段和方法的符号引用转为直接引用。
1.6 初始化
初始化阶段的重要工作时执行类的初始化方法<clinit>,方法<clinit>是由编译器自动生成的,它是由类静态成员的赋值语句以及static语句块合并产生的。但Java编译器并不会为所有的类都产生<clinit>初始化函数。如果一个类既没有赋值语句,也没有static语句块,那么生成的<clinit>函数就应该为空,因此编译器就不会为该类插入<clinit>函数。
注:final常量在准备阶段初始化,并不在初始化阶段处理。
对于<clinit>函数的调用,也就是类的初始化,虚拟机会在内部确保其多线程环境中的安全性。也就是说,当多个线程试图初始化同一个类时,只有一个线程可以进入<clinit>函数。
2 ClassLoader
ClassLoader的主要工作是在Class装载的加载阶段,其主要作用是从系统外部获得Class二进制流数据。
2.1 什么是ClassLoader?
ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入系统,然后交给Java虚拟机进行连接、初始化等操作。因此,ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的连接和初始化行为。
从代码层面看,ClassLoader是一个抽象类,它提供了一些重要的接口,用于自定义Class的加载流程和加载方式。
ClassLoader主要方法:
- public Class<?> loadClass(String name) throws ClassNotFoundException
给定一个类名,加载一个类,返回代表这个类的Class实例,如果找不到这个类,则返回ClassNotFoundException异常。 - protected final Class<?> defineClass(byte[] b, int off, int len)
根据给定的字节码流b定义一个类。这是受保护的方法,只有在自定义的ClassLoader子类中可以使用。 - protected Class<?> findClass(String name) throws ClassNotFoundException
查找一个类,这是一个受保护的方法,也是重载ClassLoader时,重要的系统扩展点。这个方法会在loadClass() 时被调用,用于自定义查找类的逻辑。如果不需要修改类加载默认机制,指向改变类加载的形式,可以重载该方法。 - protected final Class<?> findLoadedClass(String name)
这是一个受保护的方法,它会去寻找已经加载的类。这个方法时final方法,无法被修改。
在ClassLoader的结构中,还有一个重要的字段parent,它也是一个ClassLoader实例,这个字段所表示的ClassLoader也称为这个ClassLoader的双亲。在类加载的过程中,ClassLoader可能会将某些请求交予自己的双亲处理。
2.2 ClassLoader分类
在标准的Java程序中,Java虚拟机会创建3类ClassLoader为整个应用程序服务。它们分别是:BootStrap ClassLoader(启动类加载器)、Extension ClassLoader(扩展类加载器)、App ClassLoader(应用类加载器,也称为系统类加载器)。此外,每一个应用程序还可以拥有自定义的ClassLoder,扩展Java虚拟机获取Class数据的能力。当系统需要使用一个类时,在判断类是否已经被加载时,会先从当前底层类加载器进行判断。当系统需要加载一个类时,会从顶层类开始加载,依次向下尝试,直到成功。
在这些Class Loader中,启动类加载器最为特别,它是完全由C代码实现的,并且在Java中没有对象与之对应。系统的核心类就是由启动类加载器进行的,它也是虚拟机的核心组件。扩展类加载器和应用类加载器都有对应的Java对象可供使用。
String.class.getClassLoader()
上述方法返回的结果是null。因为String属于Java核心类,因此会被启动类加载器加载。
2.3 ClassLoader的双亲委托机制
双亲委托机制是指在类加载时,系统会判断当前类是否已经被加载,如果已经被加载,就会直接返回可用的类,否则就会尝试加载,在尝试加载时,会先请求双亲处理,如果双亲请求失败,才会自己加载。
注:如果双亲为null,则使用启动类加载器。因为双亲为null有两种情况:1. 双亲为启动类加载器,2. 当前加载器就是启动类加载器。
2.4 双亲委托机制的弊端
双亲委托机制存在一种模型缺陷:检查类是否已经加载的委托过程是单向的,这种方式虽然从结构上说比较清晰,使得各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类。
通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。按照这种模式当系统类访问应用类会出现问题。比如,在系统类中提供了一个接口,该接口需要在应用中得以实现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这时,就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。
例如加载JNDI、JDBC、JCE、JAXB和JBI等组件时,就必须破坏双亲委托机制。
2.5 双亲委托机制的补充
在Java平台中,把核心类(rt.jar)中提供外部服务,可由应用层自行实现的接口,通常可以成为Service Provider InterFace,即SPI。
Thread有两个方法:public ClassLoader getContextClassLoader()
和public void setContextClassLoader(ClassLoader cl)
。这两个方法分别是取得在线程中的上下文加载器和设置在线程中的上下文加载器。默认情况下,上下文加载器就是应用类加载器,这样即使是在启动类加载器中的代码也可以通过这种方式访问应用类加载器中的类。
2.6 突破双亲模式
双亲模式的类加载方式是虚拟机默认的行为,但并非必须这么做,通过重载ClassLoader可以修改该行为。Tomcat和OSGI框架都有各自独特的类加载顺序。
2.7 热替换
热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为。热替换的关键需求在于服务不能中断,修改必须立即表现在正在运行的系统之中。基本上,大部分脚本语言都支持。但对Java来说,热替换并非天生支持,如果一个类已经加载到系统中,通过修改类文件,无法让系统再来加载并重定义这个类,因此在Java中实现这一功能的一个可行的方法就是灵活运用ClassLoader。