Class装载系统
Class类型通常以文件的形式存在(当然,任何二进制流都可以是Class类型),只有被Java虚拟机装载的Class类型才能在程序中使用。系统加载Class类型可以分为加载、链接和初始化3个步骤。其中,链接又可以分为验证、准备和解析3个步骤。
类装载的条件
Class只有在必须要使用的时候才会被装载,Java虚拟机不会无条件的装载Class类型。Java虚拟机规定:一个类或者接口在初次使用时,必须进行初始化。这里的使用指的是主动使用,主动使用有以下几种情况:
- 当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化。
- 当调用类的静态方法时,即当使用了字节码invokestatic指令
- 当使用类或者接口的静态字段时(final常量除外),即使用getstatic或者putstatic指令
- 当使用java.lang.reflect包中的方法反射类的方法时
- 当初始化子类时,必须先初始化父类
- 作为启动虚拟机、含有main方法的那个类 除了以上情况属于主动使用外,其他情况均属于被动使用,被动使用不会引起类的初始化。
加载
- 装载类的第一个阶段
- 通过类的全限定名取得类的二进制流
- 转为方法区数据结构
- 在Java堆中生成对应的java.lang.Class对象
链接 -> 验证
目的:保证Class流的格式是正确的
文件格式的验证
- 是否以0xCAFEBABE开头
- 版本号是否合理
元数据验证(语义检查)
- 是否有父类
- 继承了final类?
- 非抽象类实现了所有的抽象方法
字节码验证 (很复杂)
- 跳转指令是否指向正确的位置
- 操作数类型是否合理
符号引用验证
- 符号引用的直接引用是否存在
链接 -> 准备
当一个类验证通过后,虚拟机就会进入准备阶段,在这个阶段,虚拟机会为这个类分配相应的内存空间,并设置初始值。
类型 | 默认初始值 |
---|---|
nt | 0 |
long | 0L |
short | (short)0 |
char | \u0000 |
boolean | false |
reference | null |
float | 0f |
double | 0f |
注意:java并不支持boolean类型,对于boolean类型,内部实现是Int,由于int的默认值是0,故对应的,boolean的默认值是false
这里举个例子:
public static int v=1;
对于这句代码:
- 在准备阶段中,v会被设置为0
- 在初始化
<clinit>
方法中,v才会被设置为1 - 对于static final类型(常量),在准备阶段就会被赋上正确的值
- public static final int v=1;
链接 -> 解析
解析阶段的任务就是将类、接口、字段和方法的符号引用转为直接引用。
符号引用只是一种表示的方式,比如某个类继承java.lang.object,在符号引用阶段,只会记录该类是继承”java.lang.object”,以这种字符串的形式保存,但是不能保证该对象被记载。
直接引用就是真正能使用的引用,它是指针或者地址偏移量,引用对象一定在内存。最终知道在内存中到底放在哪里。
替换后,Class才能索引到它要用的那些内容。
初始化
初始化时类装载的最后一个阶段。如果前面的步骤没有出现问题,那么表示类可以顺利装载到系统中。此时,类才会开始执行java字节码。
初始化阶段的重要工作是执行类的初始化方法<clinit>
方法,<clinit>
是由编译器自动生成的,它是由类静态成员的赋值语句以及static语句块合并产生的。
- static变量的赋值语句
- static{}语句
子类的<clinit>
调用前保证父类的<clinit>
被调用,因此,在虚拟机中第一个被执行的<clinit>
方法的类肯定是java.lang.Object
实验:
public class Test extends A
{
static {
System.out.println("Test");
}
public static void main(String[] args)
{
A a = new A();
}
}
class A {
static {
System.out.println("A");
}
}
输出:
A
Test
<clinit>
是线程安全的
那么Java.lang.NoSuchFieldError错误可能在什么阶段抛出呢?
很显然是在链接的验证阶段的符号引用验证时会抛出这个异常,或者NoSuchMethodError等异常。
什么是类装载器ClassLoader
- ClassLoader是一个抽象类
- ClassLoader的实例将读入Java字节码将类装载到JVM中
- ClassLoader可以定制,满足不同的字节码流获取方式(譬如从网络中加载,从文件中加载)
- ClassLoader负责类装载过程中的加载阶段
ClassLoader的重要方法
系统中的ClassLoader
- BootStrap ClassLoader(启动ClassLoader)
- Extension ClassLoader(扩展ClassLoader)
- App ClassLoader (应用ClassLoader/系统ClassLoader)
- Custom ClassLoader(自定义ClassLoader)
每个ClassLoader都有一个Parent作为父亲( BootStrap除外)
JDK中ClassLoader默认设计模式 – 协同工作
自底向上检查类是否被加载,一般情况下,首先先从AppClassLoader中调用findLoadedClass查看是否已经加载,如果没有加载,则会交给父类,ExtensionClassLoader去查看是否加载,还没加载,则再调用其父类,BootstrapClassLoader查看是否已经加载,如果仍然没有,自顶向下尝试加载类,那么从 BootstrapClassLoader到 AppClassLoader依次尝试加载。
其中Bootstrap ClassLoader之中的参数 -Xbootclasspath可以指定加载的路径,这样该路径下的类也会存在于Bootstrap ClassLoader之中。
为了证明自顶向下尝试加载类,举个例子:
此时运行显示: I am in apploader
如果在建一个类,放到路劲D:/tmp/clz 之中
此时加上参数 -Xbootclasspath/a:D:/tmp/clz 运行的时候显示: I am in bootloader
证明ClassLoader自顶向下尝试加载类
加载的源码:
从代码上可以很容易看出来,首先先自己查看这个类是否被调用,如果没有找到,则调用父亲的loadClass,直到BootstrapClassLoader(没有父亲)。
我们把这个加载的过程叫做双亲模式。
双亲委托机制的作用是防止系统jar包被本地替换
双亲模式的问题
这种模式下会有一个问题:
顶层ClassLoader,无法加载底层ClassLoader的类
Java框架(rt.jar)如何加载应用的类?
比如:javax.xml.parsers包中定义了xml解析的类接口
Service Provider Interface SPI 位于rt.jar
即接口在启动ClassLoader中。
而SPI的实现类,在AppLoader。
这样就无法用BootstrapClassLoader去加载SPI的实现类。
解决
JDK中提供了一个方法:
Thread. setContextClassLoader()
- 上下文加载器
- 是一个角色
- 用以解决顶层ClassLoader无法访问底层ClassLoader的类的问题
- 基本思想是,在顶层ClassLoader中,传入底层ClassLoader的实例
上下文ClassLoader可以突破双亲模式的局限性
举个例子:
主要就是在 cl != null的情况下,返回cl.loadClass(className)
顺便说一下:
- 双亲模式是默认的模式,但不是必须这么做
- Tomcat的WebappClassLoader就会先加载自己的Class,找不到再委托parent
- OSGi的ClassLoader形成网状结构,根据需要自由加载Class