JVM加载类的过程分为如下几个部分(下文中★为重重重点)
加载(Loading)
类的Loading指的是将类的.class文件中的二进制数据读到内存中,将其放在运行时数据区的方法区内。然后在堆区创建一个java.lang.Class对象,用来封装在方法区内的数据结构。类的加载的最终产品是位于堆区的Class对象,Class对象封装了类在方法区内的数据接口,并且向Java程序员提供了访问方法区内数据接口接口(就是反射API)。所以JVM干了三件事:
1、通过一个类的全限定名获取该类的二进制字节流
2、将这个JVM外部的字节流所代表的的静态存储结构按照JVM需求格式转化为方法区的运行时数据结构。
3、在JVM内存 堆中生成了代表这个类的java .lang.Class对象,作为方法区这些数据的访问入口(反射API)
注意:加载.class文件的有很多种,不仅仅是从本地加载,还可以通过网络下载.class文件;从zip,jar等归档中加载;从专有数据库中提取;反射动态编译。
总结:Loading这个阶段相对于其他阶段来说,他的可控性是最强的阶段因为获取类的二进制字节流的动作,开发人员是可以自定义类加载器完成加载的
验证(Verification)
验证的目的就是为了确保Class文件中国的字节流包含的信息符合当前JVM的要求,并且不会危害到虚拟机自身的安全。不同的虚拟机对于验证的实现可能会不同,但是大致可以分为下面四个验证阶段:
·文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
·元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
·字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
·符号引用验证:确保解析动作能正确执行
★准备(Preparation)
这一阶段就是正式为类变量(静态变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存会将在方法区中进行分配。
在这个阶段进行内存分配的变量仅仅只是类变量(被static修饰的变量),并没有包括实例变量。
设置的初始值“一般情况”下是数据变量的零值(0,null,false):如果我这样定义 public static int i=222;
那这个变量在准备过后的初始值是0 而不是222.因为这个时候还没有执行任何的java方法,而把i赋值为222的putstatic指令是在程序被编译后,存放在类构造器方法中,所以i的赋值操作是在初始化阶段才会执行的。
但是这里有一个特殊情况就是:public static final int i=222;就是当这个类变量字段属性是ConstantValue(常量),会在准备阶段(就是本阶段)初始化为指定的值。
解析(Resolution)
解析阶段就是JVM把常量池中的符号引用转化为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行
符号引用就是.class文件中的
·CONSTANT_Class_info ·CONSTANT_Field_info ·CONSTANT_Method_info 等类型的常量
·符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是他们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确了在JVM规范的.Class文件格式中。
·直接引用可以指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在
★初始化(Initialization)
前面在准备阶段JVM为类变量分配了内存,并初始化为零值。而在这一阶段,是JVM真正按照程序员的指定去初始化类变量和其他资源,或者说:初始化阶段是执行类构造器<clinit>()方法的过程。
<client>方法是由编译器自动收集类的类变量的赋值操作和静态语句块的语句合并而成的。JVM会保证<client>方法执行之前,父类的<client>方法已经执行完毕。PS:如果一个类没有对静态成员赋值也没有静态语句块,那么JVM就不会为这个类生成<client>()方法;编译器的收集顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义于静态语句块之前的变量,定义在他之后的变量,在前面的静态语句块可以赋值但是不能访问。
java中对类变量进行初始值设定有两种方式:
① 声明类变量为指定值 public static int i=123;
②使用静态代码块为类变量指定初始值 static { i=123};
类的生命周期是:加载->验证->准备->解析->初始化->使用->卸载,只有在准备阶段和初始化阶段才会涉及类变量的初始化和赋值,因此只针对这两个阶段进行分析;
类的准备阶段需要做是为类变量分配内存并设置默认值,因此类变量st为null、b为0;(需要注意的是如果类变量是final,编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将变量设置为指定的值,如果这里这么定义:static final int b=112,那么在准备阶段b的值就是112,而不再是0了。)类的初始化阶段需要做是执行类构造器(类构造器是编译器收集所有静态语句块和类变量的赋值语句按语句在源码中的顺序合并生成类构造器,对象的构造方法是<init>(),类的构造方法是<clinit>(),可以在堆栈信息中看到),因此先执行第一条静态变量的赋值语句即st = new StaticTest (),此时会进行对象的初始化,对象的初始化是先初始化成员变量再执行构造方法,因此设置a为110->打印2->执行构造方法(打印3,此时a已经赋值为110,但是b只是设置了默认值0,并未完成赋值动作),等对象的初始化完成后继续执行之前的类构造器的语句,接下来就不详细说了,按照语句在源码中的顺序执行即可。
★类初始化时机(类什么时候会初始化):只有当对类的主动使用的时候才会导致类的初始化
★new实例的时候,读取或设置一个类的静态字段(被final修饰、已被编译器讲结果放入常量池的静态字段除外)
★ 访问某个类或接口的静态变量,或者对该静态变量赋值
★ 调用类的静态方法
★ 反射(如Class.forName(“com.shengsiyuan.Test”)),如果类没有初始化,就会先初始化
★初始化某个类的子类,而父类还没有进行初始化,则先触发其父类也会被初始化
★Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
结束生命周期
在如下几种情况下,Java虚拟机将结束生命周期
– 执行了System.exit()方法
– 程序正常执行结束
– 程序在执行过程中遇到了异常或错误而异常终止
– 由于操作系统出现错误而导致Java虚拟机进程终止
重点
类加载机制最重要的两个阶段就是准备阶段与初始化阶段。我们一定要好好理解类加载在准备阶段、初始化阶段干了什么、还有类初始化的时机。
下面有两个例子
public class SingleTon {
private static SingleTon singleTon = new SingleTon();
public static int count1;
public static int count2 = 0;
public static String count3="你好啊";//如果是String count3=null; 就会赋值为null;静态变量只需定义
static {
count3="哈哈哈";
}
private SingleTon() {
count1++;
count2++;
System.out.println("count1:"+count1+" count2:"+count2);
}
public static SingleTon getInstance() {
return singleTon;
}
public static void main(String[] args) {
System.out.println("count1=" + count1);
System.out.println("count2=" + count2);
System.out.println("count3=" + count3);
}
}
count1:1 count2:1
count1=1
count2=0
count3=哈哈哈
我们分析一下:我们先看main方法,main方法在当前类中,所以他主动访问静态成员count1,于是触发了SingleTon类的初始化
首先在类加载准备阶段,JVM为静态变量分配内存,并赋零值。所以静态变量singleton为null;count1为0;count2为0;count3为null;
接着在初始化阶段,JVM按程序员要求赋值,按照源文件顺序先为singleTon调用了SingleTon()方法,此时count1为1;count2为1,并输出第一行答案(count1:1 count2:1)
接着count1只有定义并没有赋值,所以count1任为1,count2被赋值为0,count3为“你好啊”,接着按顺序执行了静态代码块所以count3被赋值为了“哈哈哈”,最后得出了我们的答案。
class Parent {
/* 静态变量 */
public static String p_StaticField = "父类--静态变量";
/* 变量 */
public String p_Field = "父类--变量";
protected int i = 9;
protected int j = 0;
/* 静态初始化块 */
static {
System.out.println( p_StaticField );
System.out.println( "父类--静态初始化块" );
}
/* 初始化块 */
{
System.out.println( p_Field );
System.out.println( "父类--初始化块" );
}
/* 构造器 */
public Parent()
{
System.out.println( "父类--构造器" );
System.out.println( "i=" + i + ", j=" + j );
j = 20;
}
}
public class SubClass extends Parent {
/* 静态变量 */
public static String s_StaticField = "子类--静态变量";
/* 变量 */
public String s_Field = "子类--变量";
/* 静态初始化块 */
static {
System.out.println( s_StaticField );
System.out.println( "子类--静态初始化块" );
}
/* 初始化块 */
{
System.out.println( s_Field );
System.out.println( "子类--初始化块" );
}
/* 构造器 */
public SubClass()
{
System.out.println( "子类--构造器" );
System.out.println( "i=" + i + ",j=" + j );
}
/* 程序入口 */
public static void main( String[] args )
{
System.out.println( "子类main方法" );
new SubClass();
}
}
父类--静态变量
父类--静态初始化块
子类--静态变量
子类--静态初始化块
子类main方法
父类--变量
父类--初始化块
父类--构造器
i=9, j=0
子类--变量
子类--初始化块
子类--构造器
i=9,j=20
(1)静态内容:静态的成员变量、静态代码块、静态的成员方法按顺序加载。
(2)非静态内容:成员变量、代码块、成员方法按顺序加载。
总顺序:静态内容--》非静态内容--》类构造方法