一、类的生命周期
类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期包括7个阶段:
加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。
二、类的加载过程
1. 加载
加载过程要完成以下三件事情:
* 通过类的完全限定名称获取定义该类的二进制字节流
* 将该字节流表示的静态存储结构转化为方法区的运行时存储结构
* 在内存中生成一个代表该类的java.lang.Class对象,作为方法区中该类各种数据的访问路口
2. 验证
作用是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全。
包括 文件格式验证、元数据验证、字节码验证以及符号引用验证
3. 准备
该阶段为类变量分配内存并设置初始值,其中类变量是被static修饰的变量,使用的是方法区内存。
而实例变量则会随着对象实例化的时候随对象一起在堆上分配内存。实例化也并不是类加载的一个过程,类加载发生在所有实例化操作之前。类只加载一次,而实例化可以多次进行。
类变量的初始值设置一般为0。 但是如果类变量是常量,入final修饰的变量,则会被初始化为其本身。
4.解析
将常量池的符号引用替换为直接引用的过程。
符号引用:
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用:
(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
(3)一个能间接定位到目标的句柄
直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。
5. 初始化
初始化阶段才是正真开始执行类中定义的Java程序代码。初始化阶段是执行类构造器<clinit>()方法的过程.。在准备阶段,变量已经付过一次系统要求的初始值,而在初始化阶段,则根据程序制定的主管计划去初始化类变量和其他资源。
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
父类的<clinit>()方法先执行,也就是父类中定义的静态语句块的执行优先于子类。
static class parent {
public static int A = 1;
static {
A = 2; // 父类的static静态语句块优先于子类执行
}
}
static class Sub extends parent {
public int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B); // 输出2
}
<clinit>()方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生产<clinit>()方法。
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有好事很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的。
有关类加载的代码示例:
public class SSClass {
static {
System.out.println("SSClass init");
}
}
public class SuperClass extends SSClass {
static {
System.out.println("SuperClass init");
}
public static int value = 1;
public SuperClass() {
System.out.println("init SuperClass");
}
}
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init");
}
static int a;
public SubClass() {
System.out.println("init SubClass");
}
}
public class NotInitialization
{
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
结果输出:
SSClass init
SuperClass init
1
结果中还有一点要说明的是,通过子类引用父类的静态字段,不会导致子类初始化。所以上述代码中SubClass并没有初始化。
参考博客:
http://www.importnew.com/18548.html
https://www.cnblogs.com/shinubi/articles/6116993.html