JVM类加载机制——类的生命周期

JVM类加载机制——类的生命周期

什么是类加载机制?

虚拟机把描述类的数据文件(字节码)加载到内存,并对数据进行验证、准备、解析以及类初始化,最终造成能够被虚拟机直接使用的java类型(java.lang.Class对象),这就是java虚拟机的类加载机制。——《 深刻理解java虚拟机》java

类的生命周期

从类被加载进内存开始直到卸载出内存为止,类的生命周期包括装载、验证、准备、解析、初始化、使用和卸载 7个过程,其中验证、准备、解析三个过程统称为连接。程序员

其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是肯定的,而解析阶段则不必定,它在某些状况下能够在初始化阶段以后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,由于这些阶段一般都是互相交叉地混合进行的,一般在一个阶段执行的过程当中调用或激活另外一个阶段(如:主动使用类触发类的初始化)。数据库

在Java中,类的加载和连接过程都是在程序运行期间完成的。另外,Java能够动态扩展的语言特性就是依赖运行期间动态加载、动态连接这个特色实现的。数组

接下来咱们详细了解下类的整个生命周期安全

1、装载(加载)

在加载阶段,虚拟机完成3件事:bash

  1. 经过一个类的全限定名来获取定义此类的二进制字节流。网络

    二进制字节流除了从本地classpath获取,还能够从哪些地方获取?

    从ZIP或jar包中读取
    从网络中获取
    运行时计算生成(Java动态代理技术)
    由其余文件生成(由JSP文件生成对应的Class类)
    从数据库中读取
    复制代码
  2. 加二进制字节流存储在方法区中(按照虚拟机所需的格式存储)。数据结构

  3. 在内存(堆区)中生成一个表明这个类的java.lang.Class对象,Class对象封装了类在方法区内的数据结构,而且向Java程序员提供了访问方法区内的数据结构的接口。多线程

2、验证

验证的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。 通常包括两个方面:jvm

  1. 格式语义校验:
    例如:
    1.是否以0xCAFEBASE开头
    2.主、次版本号是否在当前虚拟机处理范围内
    复制代码
  2. 代码逻辑校验

3、准备

准备阶段正式为静态变量分配内存并设置初始值,这些静态变量在方法区中分配内存。

注意:

  1. 准备阶段,JVM只会为静态变量(static修饰)分配内存,不包括实例变量,实例变量将会在对象实例化时随对象一块儿分配在Java堆中。

    准备阶段,只会为value分配内存,不会为name分配内存
    public static int value = 123;
    private String name = "Tom";
    复制代码
  2. 设置初始值:

  • static修饰的变量(无final):零值或null

准备阶段,未执行任何Java方法,而value赋值为123指令是程序编译后,存放于类构造器方法中,在初始化阶段才会执行,所以准备阶段,会设置零值。

准备阶段,会设置零值
public static int value = 123;
复制代码
  • static final修饰的常量:实际值
常量,准备阶段会设置实际值123
public static final int value = 123;
复制代码

注意:static final修饰的基本数据类型或者String会在javac编译时生成ConstantValue属性,在类加载的准备阶段直接把ConstantValue的值赋给该字段。能够理解为在编译期即把结果放入了常量池中。因此当A类调用B类的static final字段(基本数据类型或者String),不会触发B类的加载。

4、解析

解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程 (在某些状况下能够在初始化阶段以后开始) 解析动做主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info四种常量类型。

一、类或接口的解析 :判断所要转化成的直接引用是对数组类型,仍是普通的对象类型的引用,从而进行不一样的解析。

二、字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,若是有,则查找结束;若是没有,则会按照继承关系递归搜索该类所实现的各个接口和它们的父接口,尚未,则按照继承关系递归搜索其父类,直至查找结束.

三、类方法解析:对类方法的解析与对字段解析的搜索步骤差很少,只是多了判断该方法所处的是类仍是接口的步骤,并且对类方法的匹配搜索,是先搜索父类,再搜索接口。

四、接口方法解析:与类方法解析步骤相似,只是接口不会有父类,所以,只递归向上搜索父接口就好了。

看以下例子:

class Super{
	public static int m = 11;
	static{
		System.out.println("执行了super类静态语句块");
	}
}
 
class Father extends Super{
	public static int m = 33;
	static{
		System.out.println("执行了父类静态语句块");
	}
}
 
class Child extends Father{
	static{
		System.out.println("执行了子类静态语句块");
	}
}
 
public class StaticTest{
	public static void main(String[] args){
		System.out.println(Child.m);
	}
}
复制代码

输出结果:

执行了super类静态语句块
执行了父类静态语句块
33
复制代码

为什子类的static块不会执行?

static块是在初始化阶段执行的,而static变量发生在静态解析阶段,也便是初始化以前,此时已经将字段的符号引用转化为了内存引用,也便将它与对应的类关联在了一块儿,因为在子类中没有查找到与m相匹配的字段,那么m便不会与子类关联在一块儿,所以并不会触发子类的初始化。
复制代码

同理,若是注释掉Father类中对m定义的那一行,则输出结果以下:

执行了super类静态语句块
11
复制代码

5、初始化

初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员经过程序指定的主观计划去初始化类变量和其余资源,或者能够从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。

这里简单说明下()方法的执行规则:

  1. <clinit>()方法是由编译器自动收集类中的全部类变量的赋值动做和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块以前的变量,定义在它以后的变量,在前面的静态语句中能够赋值,可是不能访问

  2. <clinit>()方法与实例构造器<init>()方法(类的构造函数)不一样,它不须要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行以前,父类的<clinit>()方法已经执行完毕。所以,在虚拟机中第一个被执行的<clinit>()方法的类确定是java.lang.Object

  3. <clinit>()方法对于类或接口来讲并非必须的,若是一个类中没有静态语句块,也没有对类变量的赋值操做,那么编译器能够不为这个类生成<clinit>()方法。

  4. 接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操做,所以接口与类同样会生成<clinit>()方法。可是接口与类不一样的是:执行接口的<clinit>()方法不须要先执行父接口的<clinit>()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也同样不会执行接口的<clinit>()方法。

  5. 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁和同步,若是多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其余线程都须要阻塞等待,直到活动线程执行<clinit>()方法完毕。若是在一个类的<clinit>()方法中有耗时很长的操做,那就可能形成多个线程阻塞,在实际应用中这种阻塞每每是很隐蔽的。

下面给出一个简单的例子,以便更清晰地说明如上规则:

class Father{
	public static int a = 1;
	static{
		a = 2;
	}
}
 
class Child extends Father{
	public static int b = a;
}
 
public class ClinitTest{
	public static void main(String[] args){
		System.out.println(Child.b);
	}
}
复制代码

输出结果:

2
复制代码

看下运行代码后的步骤:

  1. 准备阶段:为类变量分配内存并设置类变量初始值,这样a和b均被赋值为默认值0
  2. 初始化阶段:然后再在调用<clinit>()方法时给他们赋予程序中指定的值
    咱们调用Child.b时,触发Child的<clinit>()方法,根据规则2,在此以前,要先执行完其父类Father的<clinit>()方法,
    又根据规则1,在执行<clinit>()方法时,须要按static语句或static变量赋值操做等在代码中出现的顺序来执行相关的
    static语句,所以当触发执行Father的<clinit>()方法时,会先将a赋值为1,再执行static语句块中语句,将a赋值为2,
    然后再执行Child类的<clinit>()方法,这样便会将b的赋值为2.
    
    若是咱们颠倒一下Father类中“public static int a = 1;”语句和“static语句块”的顺序,程序执行后,则会打印出1。
    很明显是根据规则1,执行Father的<clinit>()方法时,根据顺序先执行了static语句块中的内容,
    后执行了“public static int a = 1;”语句。
    另外,在颠倒两者的顺序以后,若是在static语句块中对a进行访问(好比将a赋给某个变量),
    在编译时将会报错,由于根据规则1,它只能对a进行赋值,而不能访问。
    
    复制代码

6、使用

使用阶段包括主动引用和被动引用,主动引用会引发类的初始化,而被动引用不会引发类的初始化。

主动引用

jvm有严格的规定,当且仅当如下五种状况,也就是主动引用,才会触发类的初始化,除此以外,全部引用类的方法都不会触发初始化!

  1. 遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,假如类还没进行初始化, 则立刻对其进行初始化工做。 其实就是3种状况:用new实例化一个类时、读取或者设置类的静态字段时(不包括被final修饰的静态字段, 由于他们已经被塞进常量池了)、以及执行静态方法的时候。

  2. 使用java.lang.reflect.*的方法对类进行反射调用的时候, 若是类尚未进行过初始化,立刻对其进行。

  3. 初始化一个类的时候,若是他的父亲尚未被初始化,则先去初始化其父亲。

  4. 当jvm启动时,用户须要指定一个要执行的主类(包含static void main(String[] args)的那个类), 则jvm会先去初始化这个类。

  5. 用Class.forName(String className);来加载类的时候,也会执行初始化动做。

    • 注意:ClassLoader的loadClass(String className);方法只会加载并编译某类,并不会对其执行初始化。

被动引用

  • 引用父类的静态字段,只会引发父类的初始化,而不会引发子类的初始化。
  • 定义类数组,不会引发类的初始化。
  • 引用类的static final常量,不会引发类的初始化(若是只有static修饰,仍是会引发该类初始化的)。

被动引用的示例代码:

class InitClass{
	static {
		System.out.println("初始化InitClass");
	}
	public static String a = null;
	public final static String b = "b";
	public static void method(){}
}
 
class SubInitClass extends InitClass{
	static {
		System.out.println("初始化SubInitClass");
	}
}
 
public class Test4 {
 
	public static void main(String[] args) throws Exception{
	// String a = SubInitClass.a;// 引用父类的静态字段,只会引发父类初始化,而不会引发子类的初始化
	// String b = InitClass.b;// 使用类的final常量不会引发类的初始化
		SubInitClass[] sc = new SubInitClass[10];// 定义类数组不会引发类的初始化
	}
}
复制代码

当使用阶段完成以后,java类就进入了卸载阶段。

7、卸载

在类使用完以后,若是知足下面的状况,类就会被卸载:

  • 该类全部的实例都已经被回收,也就是java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象没有任何地方被引用,没法在任何地方经过反射访问该类的方法。

若是以上三个条件所有知足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。

总结

整个类加载过程当中,除了在加载阶段用户应用程序能够自定义类加载器参与以外,其他全部的动做彻底由虚拟机主导和控制。到了初始化才开始执行类中定义的Java程序代码(亦及字节码),但这里的执行代码只是个开端,它仅限于<clinit>()方法。类加载过程当中主要是将Class文件(准确地讲,应该是类的二进制字节流)加载到虚拟机内存中,真正执行字节码的操做,在加载完成后才真正开始。

参考资料

【深刻Java虚拟机】之四:类加载机制

JVM之类加载机制

相关文章
相关标签/搜索