前言
一个.class文件是如何被Java虚拟机加载并使用的呢,本文我们来研究下JVM中类加载的过程。
类加载的过程
类从被加载到虚拟机内存中开始,到卸载出内存,它的整个生命周期包括:加载(Loading)、验证
(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initiallization)、使用(Using)和卸载(Unloading)这7个阶段。其中验证、准备、解析3个部分统称为连接(Linking),这七个阶段的发生顺序如下图:
图中,加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地
开始,而解析阶段不一定:它在某些情况下可以初始化阶段之后在开始,这是为了支持Java语言的运行时绑定(也
称为动态绑定)。接下来讲解加载、验证、准备、解析、初始化五个步骤,这五个步骤组成了一个完整的类加载过
程。使用没什么好说的,卸载属于GC的工作 。
加载
加载是类加载的第一个阶段。有两种时机会触发类加载:
预加载
虚拟机启动时加载,加载的是JAVA_HOME/lib/下的rt.jar下的.class文件,这个jar包里面的内容是程序运行时非常常常用到的,像java.lang.*、java.util.、java.io. 等等,因此随着虚拟机一起加载。要证明这一点很简单,写一个空的main函数,设置虚拟机参数为"-XX:+TraceClassLoading"来获取类加载信息,运行一下:
运行时加载
虚拟机在用到一个.class文件的时候,会先去内存中查看一下这个.class文件有没有被加载,如果没有就会按照类的全限定名来加载这个类。
那么,加载阶段做了什么,其实加载阶段做了有三件事情:
- 获取.class文件的二进制流
- 将类信息、静态变量、字节码、常量这些.class文件中的内容放入方法区中
- 在内存中生成一个代表这个.class文件的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。一般这个Class是在堆里的,不过HotSpot虚拟机比较特殊,这个Class对象是放在方法区中的
虚拟机规范对这三点的要求并不具体,因此虚拟机实现与具体应用的灵活度都是相当大的。例如第一条,根本没有指明二进制字节流要从哪里来、怎么来,因此单单就这一条,就能变出许多花样来:
- 从zip包中获取,这就是以后jar、ear、war格式的基础
- 从网络中获取,典型应用就是Applet
- 运行时计算生成,典型应用就是动态代理技术
- 由其他文件生成,典型应用就是JSP,即由JSP生成对应的.class文件
- 从数据库中读取,这种场景比较少见
总而言之,在类加载整个过程中,这部分是对于开发者来说可控性最强的一个阶段。
链接
链接包含三个步骤: 分别是验证Verification , 准备Preparation , 解析Resolution 三个过程
1)验证Verification
连接阶段的第一步,这一阶段的目的是为了确保.class文件的字节流中包含的信息符合当前虚拟机的要求,并且不
会危害虚拟机自身的安全。
Java语言本身是相对安全的语言(相对C/C++来说),但是前面说过,.class文件未必要从Java源码编译而来,可以使用任何途径产生,甚至包括用十六进制编辑器直接编写来产生.class文件。在字节码语言层面上,Java代码至少从语义上是可以表达出来的。虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。
验证阶段将做一下几个工作,具体就不细讲了,这是虚拟机实现层面的问题:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
2)准备Preparation
准备阶段是正式为类变量分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配。关于这
点,有两个地方注意一下:
这时候进行内存分配的仅仅是类变量(被static修饰的变量),而不是实例变量,实例变量将会在对象实例化
的时候随着对象一起分配在Java堆中
这个阶段赋初始值的变量指的是那些不被final修饰的static变量,比如"public static int value = 123",value在准
备阶段过后是0而不是123,给value赋值为123的动作将在初始化阶段才进行;比如"public static final int value =123;"就不一样了,在准备阶段,虚拟机就会给value赋值为123。
各个数据类型的零值如下表:
来看两个代码案例:
案例一:
public class A {
static int a;
public static void main(String[] args) {
System.out.println(a);
}
}
输出:0
案例二:
public class A {
public static void main(String[] args) {
int a;
System.out.println(a);
}
}
<