从0到1理解JVM_类的加载机制

JAVA 的跨平台性

JAVA是跨平台性的语言,之所以会这样,是 JAVA 源码被编译的时候,并没有直接编译成机器能运行的二进制流,而是编译成 class 文件,在实际运行时,再将 class 文件中的类加载进机器中运行。
JAVA 中的类直到运行时才会被加载和链接,这使得 JAVA 成为一门动态语言(你甚至可以在运行时才从网络上下载一个全新的类到本地运行…)

JVM 加载类

当JAVA 需要使用一个类时,JAVA 得确保类是可用的,什么叫可用呢?可用就是:类已经被加载(确切地说,是加载进 JVM 内存模型的方法区)、连接(验证、准备、解析)、初始化。

加载

类的加载是指从某个位置获得类的二进制流,通常是.class 文件,将.class 文件保存在方法区中,并在方法区中创建一个 Class 对象来表示该类。

连接

在类被加载进方法区后,类通常不完整,会进入连接阶段,这一阶段包括验证(格式、元数据、字节码数据流、符号引用验证以确保解析阶段能进行)、准备(在方法区中为静态变量开辟空间并设置静态变量为零值)、解析(将符号引用转换为直接引用,解析时优先解析父类,并确保父类已经加载)。

初始化

既然类已经被加载并且连接,是不是可以用了呢?答案是不可以,因为类的静态变量还没有被赋值,比如说定义了一个静态变量:

1
static int a = 5;

加载并连接完类后,a 的值是0而不是5,必须要经过初始化,a 的值才会变成5。

初始化是指:将所有的静态变量赋值语句和静态语句块合在一起生成 cinit()方法,并执行该方法,比如下面的一段代码:

1
2
3
4
static int a = 6;
static {
a = 8;
}

所谓初始化,就是执行类似上面的代码。值得注意的是初始化也会优先初始化父类。

类加载结果

一个类被加载之前和被加载后,机器内部都发生了那些变化呢?
简单的说就是:内存的方法区中多了一个保存类的结构,该类的符号引用被转换为直接引用,静态变量被赋值,静态语句被执行(如果该类有父类且父类不可用,那么也会加载父类)。

加载器

加载类有三个阶段:加载、连接、初始化。第一个阶段,加载需要加载器,在 JVM 中,加载器分几种:根加载器、扩展加载器、系统加载器、用户加载器。
从 JDK1.2开始,JAVA 引入了双亲加载机制,当一个类加载器要加载类时,需要将类上缴给更高级的类加载器去加载,只有当更高级的的类加载器无能为力时才轮到该类加载器去加载。
根加载器是最顶层的加载器(加载 rt.jar 类库),接着是扩展加载器(加载 java.ext.dir 指定目录下的类),然后是系统加载器(加载 classPatch 和 java.class.path),最后才是用户加载器(也就是程序员可以定义的加载器)