1. 加载
加载是类加载过程中的一个阶段,不要将这2个概念混淆了。
在加载阶段,虚拟机需要完成以下3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
2. 链接阶段
验证:确保被加载的类的正确性
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 文件格式验证:验证字节流是否符合Class文件格式的规范,如:是否以模数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围内等等。
- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求;如:这个类是否有父类,是否实现了父类的抽象方法,是否重写了父类的final方法,是否继承了被final修饰的类等等。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,如:操作数栈的数据类型与指令代码序列能配合工作,保证方法中的类型转换有效等等。
符号引用验证:确保解析动作能正确执行;如:通过符合引用能找到对应的类和方法,符号引用中类、属性、方法的访问性是否能被当前类访问等等。
验证阶段是非常重要的,但不是必须的。可以采用-Xverify:none参数来关闭大部分的类验证措施。
准备: 为类的静态变量分配内存,并将其赋默认值
- 只对static修饰的静态变量进行内存分配、赋默认值(如0、0L、null、false等)。
- 对final的静态字面值常量直接赋初值(赋初值不是赋默认值,如果不是字面值静态常量,那么会和静态变量一样赋默认值)。
解析:将常量池中的符号引用替换为直接引用(内存地址)的过程
- 符号引用就是一组符号来描述目标,可以是任何字面量。属于编译原理方面的概念如:包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
- 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。如指向方法区某个类的一个指针。
- 假设:一个类有一个静态变量,该静态变量是一个自定义的类型,那么经过解析后,该静态变量将是一个指针,指向该类在方法区的内存地址。
初始化:为类的静态变量赋初值
赋初值两种方式:
- 定义静态变量时指定初始值。如 private static String x=“123”;
- 在静态代码块里为静态变量赋值。如 static{ x=“123”; }
注意:只有对类的主动使用才会导致类的初始化
clinit 与 init
clinit
在编译生成class文件时,编译器会产生两个方法加于class文件中,一个是类的初始化方法clinit, 另一个是实例的初始化方法init。
clinit指的是类构造器,主要作用是在类加载过程中的初始化阶段进行执行,执行内容包括静态变量初始化和静态块的执行。
注意事项:
1. 如果类中没有静态变量或静态代码块,那么clinit方法将不会被生成。
2. 在执行clinit方法时,必须先执行父类的clinit方法。
3. clinit方法只执行一次。
3. static变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定。
public class TestClass {
public static void main(String[] args) {
ClassInit init=ClassInit.newInstance();
System.out.println(init.x);
System.out.println(init.y);
}
}
class ClassInit{
private static ClassInit init=new ClassInit();
public static int x;
public static int y=0;
static{
x=10;
y=10;
}
private ClassInit(){
x++;
y++;
}
public static ClassInit newInstance(){
return init;
}
}
//在类加载到连接完成阶段,ClassInit类在内存中的状态为:init=null,x=0,y=0
//初始化阶段时,需要执行clinit方法,该方法类似如下伪代码:
clinit(){
//init=new ClassInit();调用构造方法
x++;//x=1 因为此时x的值为连接的准备阶段赋的默认值0,然后++变成1
y++;//y=1 因为此时y的值为连接的准备阶段赋的默认值0,然后++变成1
//x=0;//为什么这里没有执行x=0,因为程序没有给x赋初值,因此在初始化阶段时,不会执行赋初值操作
y=0;//因为类变量y在定义时,指定了初值,尽管初值为0,因此在初始化阶段的时候,需要执行赋初值操作
x++;//第一个静态块的自增操作,结果为x=2;
y++;//第一个静态块的自增操作,结果为y=1;
}
//所以最终结果为x=2,y=1
//如果private static ClassInit init=new ClassInit(); 代码在public static int y=0;后面,那么clinit方法的伪代码如下:
clinit(){
//x=0;//这里虽然没有执行,但此时x的值为连接的准备阶段赋的默认值0
y=0;//因为类变量y在定义时,指定了初值,尽管初值为0,因此在初始化阶段的时候,需要执行赋初值操作
//init=new ClassInit();调用构造方法
x++;//x=1 因为此时x的值为连接的准备阶段赋的默认值0,然后++变成1
y++;//y=1 因为此时y的值为初始化阶段赋的初值,只是这个初值刚好等于默认值0而已,然后++变成1
x++;//第一个静态块的自增操作,结果为x=2;
y++;//第一个静态块的自增操作,结果为y=2;
}
//最终结果为x=2,y=2
init
init指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行。
注意事项:
1. 如果类中没有成员变量和代码块,那么clinit方法将不会被生成。
2. 在执行init方法时,必须先执行父类的init方法。
3. init方法每实例化一次就会执行一次。
3. init方法先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块。如下代码所示:
public class TestClass {
public static void main(String[] args) {
ClassInit init=new ClassInit();
}
}
class ClassInit{
public int x;
public int y=111;
public ClassInit(){
x=1;
y=1;
}
{
x=2;
y=2;
}
{
x=3;
y=3;
}
}
//实例化步骤为:先为属性分配空间,再执行赋默认值,然后按照顺序执行代码块或赋初始值,最后执行构造方法
//根据上述代码,init方法的伪代码如下:
init(){
x=0;//赋默认值
y=0;//赋默认值
y=111;//赋初值
x=2;//从上到下执行第一个代码块
y=2;//从上到下执行第一个代码块
x=3;//从上到下执行第二个代码块
y=3;//从上到下执行第二个代码块
//ClassInit();执行构造方法
x=1;//最后执行构造方法
y=1;//最后执行构造方法
}
//如果上述代码的成员变量x,y的定义在类最后时,那么init方法的伪代码如下:
init(){
x=0;//赋默认值
y=0;//赋默认值
x=2;//从上到下执行第一个代码块
y=2;//从上到下执行第一个代码块
x=3;//从上到下执行第二个代码块
y=3;//从上到下执行第二个代码块
y=111;//赋初值
//ClassInit();执行构造方法
x=1;//最后执行构造方法
y=1;//最后执行构造方法
}
卸载阶段
执行了System.exit()方法
程序正常执行结束
程序在执行过程中遇到了异常或错误而异常终止
由于操作系统出现错误而导致Java虚拟机进程终止