讲类的加载之前,先了解一下 JVM 和字节码文件,有助于理解后面的内容。
从学习 Java 语言开始,就了解到我们先编写 .java
文件,然后 Java 编译器会将 .java
文件编译成 .class
文件,然后交由 JVM 去解释执行。
我们经常说的一次编写,多处执行
是指不同平台下的 JVM 可以对同一个字节码文件作出同样的解释,执行得到同样的结果。这个字节码文件就是一个关键所在,《Java 虚拟机规范》为字节码文件的格式做了详尽的规范,Java 语言中的各种变量、关键字和运算符的语义最终都是由多条字节码命令组合而成的。
在 Java 语言中,最基本的单位是类,一个 class 文件通常意义上就是一个类或者接口,将 class 文件加载到虚拟机内存中,并解释成 Java 语言中相应的对象的过程,被称之为 类加载过程.
类什么时候被加载/类加载时机:
class.forName("类名")
;类加载过程可以大致分为三步:
其中链接
部分又可以细分为三部分:
初始化之后 JVM 就开始对我们的字节码文件做解释执行了,进入到 使用(Using)
阶段;当该类的对象没有任何引用的时候,该类就会被卸载(Unloading)
。
从加载到卸载,贯穿了类的整个生命周期,今天重点讲加载部分,也就是从加载到初始化。
类的加载简单意义上来说就是将 class 文件从各个来源通过类加载器载入到 JVM 内存中。
为什么说是各个来源呢?因为 JVM 并没有限定我们从什么地方读取字节码内容,所以也就有了多种多样的读取方式:
.class
文件读取这里,字节码文件被载入到 JVM 内存当中,这仅仅是第一步。
之前说过了,JVM 虚拟规范对字节码文件的格式做了详尽的规范,这一步就是验证载入到内存中的字节码是否符合规范:
载入内存的字节码通过验证,则开始下一步:
这一步开始为 类变量(static 修饰)
分配内存,并且为它们 赋初值
;注意,这里的赋初值并不是赋予代码中的值,而是根据变量类型赋予该类型的默认值。譬如下面的代码:
public class Demo {
private static Demo demo = new Demo();
public static int a;
public static int b = 0;
private Demo() {
a++;
b++;
}
public static Demo getInstance() {
return demo;
}
public static void main(String[] args) {
Demo demo = Demo.getInstance();
System.out.println("a=" + a);
System.out.println("b=" + b);
}
}
在上面的例子中,类变量就是 demo、a 和 b,在准备阶段,这三者的值分别就是 null、0 和 0。
需要注意的是,在该阶段,常量值就是代码中设置的值,而非类型默认值。
将常量池中的符号引用替换为直接引用的过程。
两个需要注意的地方:
具体有关 符号引用
和 直接引用
的内容,请看这里 JVM里的符号引用如何存储?
类初始化是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段程序可以通过自定义类加载参数参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的 Java 代码(或者说字节码)。
初始化类主要有以下几个步骤:
至此,类加载完成。
需要注意的是,在加载过程中也可以验证,也就是说,类的加载过程并不是完全一致的,字节码文件的数据结构从前到后包括
魔数/版本/常量池/访问标志/类索引/父类索引/接口索引/字段表/方法表/属性表
,在加载了魔数之后,系统会直接判断魔数对不对,如果不对,后面的内容就不加载了,如果对,就继续加载。
在加载阶段,将外部内容转为加载到 JVM 中,实现这个动作的模块就是 类加载器
,JVM 规定,对于任意一个类,加载它的加载器和它本身一同确定其在 JVM 中的唯一性,也就是说,同一个类,由不同的加载器加载到虚拟机中,虚拟机会认为它们不是同一个类。
从 Java 虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器,这个类加载器使用 C++ 语言来实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由 Java 语言实现,独立于虚拟机外部,并且全都集成子抽象类 java.lang.ClassLoader
。
从 Java 开发者角度来看,类加载器还可以划分的更细致一些,绝大部分 Java 程序都会使用到以下 3 种系统提供的类加载器:
在日常开发中,类的加载几乎都是由上述三种类加载器相互配合执行的,当然,我们也可以自定义自己的类加载器。
Java 虚拟机对字节码文件采用的是按需加载的方式,也就是说,当需要使用这个类的是,才会将它加载到内存当中生成类的对象,并且在加载类的时候,使用的是双亲委派模式
,即把请求交由父类处理。
这里类加载器之间的父子关闭一般不会一继承的关系来实现,而是都使用组合关系来复用父类加载器的代码。
它的工作模式是:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个二垒,而是把这个请求委派给父类加载这个类,父类同样也会执行这个操作,继续向上传递,直到最顶层的类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
上面说了,JVM 使用加载器和类一同确定类的唯一性,所以双亲委派模式就保证了同一个类是由同一个加载器加载的,确保了唯一性。
双亲委派模式并不是一个强制性的约束,而是 Java 设计者推荐给开发者的一种类加载实现方式。