深刻理解java虚拟机【Java虚拟机类生命周期】

C/C++等纯编译语言从源码到最终执行通常要经历:编译、链接和运行三个阶段,链接是在编译期间完成,而java在编译期间仅仅是将源码编译为Java虚拟机能够识别的字节码Class类文件,Java虚拟机对中Class类文件的加载、链接都在运行时执行,虽然类加载和链接会占用程序的执行时间增长性能开销,可是却能够为java语言带来高度灵活性和扩展性,java的针对接口编程和类加载器机制实现的OSGi以及热部署等就是利用了运行时类加载和链接的特性,java的Class类在虚拟机中的生命周期以下:
java

上图中加载、验证、准备、初始化和卸载这个五个阶段的顺序是肯定的,而解析阶段则不必定,在某些状况下为了支持java语言的运行时动态绑定,也能够在初始化阶段以后再开始。编程

(1).加载:缓存

Java虚拟机把Class类文件加载到内存中,并对Class文件中的数据进行校验、转换解析和初始化,最终造成能够被虚拟机直接使用的java类型的过程。安全

在加载阶段,java虚拟机须要完成如下3件事:性能优化

a.经过一个类的全限定名来获取定义此类的二进制字节流。数据结构

b.将定义类的二进制字节流所表明的静态存储结构转换为方法区的运行时数据结构。多线程

c.在java堆中生成一个表明该类的java.lang.Class对象,做为方法区数据的访问入口。jvm

加载阶段与链接阶段是交叉进行的,加载阶段还没有完成,链接阶段可能已经开始,这些夹在加载阶段之中进行的动做仍然属于链接阶段,加载和链接阶段仍然保持着固定的前后顺序。布局

(2).验证:性能

验证是链接阶段的第一步,其目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机的安全,若是验证失败,会抛出java.lang.VerifyError异常。

验证阶段的主要工做有:

a.文件格式验证:验证Class文件魔数、主次版本、常量池、类文件自己等等。

b.元数据验证:主要是对字节码描述的信息进行语义分析,包括是否有父类、是不是抽象类、是不是接口、是否继承了不容许被继承的类(final类)、是否实现了父类或者接口的方法等等。

c.字节码验证:是整个验证过程当中最复杂的,主要进行数据流和控制流分析,如保证跳转指令不会跳转到方法体以外的字节码指令、数据类型转换安全有效等。

d.符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候(链接第三阶段-解析阶段进行符号引用转换为直接引用),符号引用验证的目的是确保解析动做能正常执行,若是没法经过符号引用验证,则会抛出java.lang.IncompatibleClassChangeError异常的子类异常,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。

验证阶段对于虚拟机来讲很是重要,可是不是一个必需的阶段,若是所运行的代码已经反复被使用和验证过了,能够经过-Xverify:none参数关闭大部分的验证措施,以提升虚拟机时间时间。

(3).准备:

准备阶段是正式为类变量(静态变量,注意不是实例变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。

对于普通非final的类变量,如public static int value = 123;在准备阶段事后的初始值是0(数据类型的零值),而不是123,而把123赋值给value是在初始化阶段才进行的动做。

对于final的类变量,即常量,如public staticfinal int value =123;在准备阶段过程的初始值直接就是123了,不须要准备为零值。

(4).解析:

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用(SymbolicReference):以一组符号来描述所引用的目标,与虚拟机内存布局无关,引用的目标不必定已经被加载到虚拟机内存中。

直接引用(DirectReference):能够直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局相关,同一个符号引用在不一样虚拟机上翻译处理的直接引用不必定相同,若是有了直接引用,则引用的目标对象必须已经被加载到虚拟机内存中。

解析的动做主要针对类或接口、字段、类方法、接口方法四类符号引用进行解析。

(5).初始化:

初始化是类使用前的最后一个阶段,在初始化阶段java虚拟机真正开始执行类中定义的java程序代码。

Java虚拟机规范严格规定了有且只有如下四种状况必须当即对类进行初始化:

a.遇到new、获取静态变量(final常量除外)、为静态变量赋值以及调用静态方法时,若是类没有进行过初始化,则须要先触发其初始化。

b.使用java.lang.reflect包的方法对类进行反射调用的时候(Class.forName(…)),若是类尚未初始化,须要先触发对其的初始化。

c.当初始化一个类的时候,若是发现其父类尚未初始化,则须要先触发对其父类的初始化。

d.当虚拟机启动时,用户须要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。

上述四种状况称为对一个类的主动引用,除此以外的引用方式都不会触发初始化,称为被动引用。

初始化的过程其实就是一个执行类构造器<clint>方法的过程,类构造器执行的特色和注意事项:

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

2).类构造器<clint>方法与实例构造器<init>方法不一样,它不须要显式地调用父类构造器方法,虚拟机会保证在调用子类构造器方法以前,父类的构造器<clinit>方法已经执行完毕。

3).因为父类构造器<clint>方法先与子类构造器执行,所以父类中定义的静态初始化块要先于子类的类变量赋值操做。

4). 类构造器<clint>方法对于类和接口并非必须的,若是一个类中没有静态初始化块,也没有类变量赋值操做,则编译器能够不为该类生成类构造器<clint>方法。

5).接口中不能使用静态初始化块,但能够有类变量赋值操做,所以接口与类同样均可以生成类构造器<clint>方法。

接口与类不一样的是:

首先,执行接口的类构造器<clint>方法时不须要先执行父接口的类构造器<clint>方法,只有当父接口中定义的静态变量被使用时,父接口才会被初始化。

其次,接口的实现类在初始化时一样不会执行接口的类构造器<clint>方法。

6).java虚拟机会保证一个类的<clint>方法在多线程环境中被正确地加锁和同步,若是多个线程同时去初始化一个类,只会有一个线程去执行这个类的<clint>方法,其余线程都须要阻塞等待,直到活动线程执行<clint>方法完毕。

初始化阶段,当执行完类构造器<clint>方法以后,才会执行实例构造器的<init>方法,实例构造方法一样是按照先父类,后子类,先成员变量,后实例构造方法的顺序执行。
(6).使用:

当初始化完成以后,java虚拟机就能够执行Class的业务逻辑指令,经过堆中java.lang.Class对象的入口地址,调用方法区的方法逻辑,最后将方法的运算结果经过方法返回地址存放到方法区或堆中。

(7).卸载:

当对象再也不被使用时,java虚拟机的垃圾收集器将会回收堆中的对象,方法区中再也不被使用的Class也要被卸载,不然方法区(Sun HotSpot永久代)会内存溢出。

Java虚拟机规定只有当加载该类型的类加载器实例为unreachable状态时,当前被加载的类型才被卸载.启动类加载器实例永远为reachable状态,由启动类加载器加载的类型可能永远不会被卸载,类型卸载仅仅是做为一种减小内存使用的性能优化措施存在的,具体和虚拟机实现有关,对开发者来讲是透明的.

卸载自定义来加载器加载的类的可靠作法为:

a.每次建立特定类加载器的新实例来加载指定类型的不一样版本,这种使用场景下,通常就要牺牲缓存特定类型的类加载器实例以带来性能优化的策略了.

b.对于指定类型已经被加载的版本, 会在适当时机达到unreachable状态,被unload并垃圾回收.每次使用完类加载器特定实例后(肯定不须要再使用时), 将其显示赋为null, 这样可能会比较快的达到jvm 规范中所说的类加载器实例unreachable状态, 增大已经再也不使用的类型版本被尽快卸载的机会.

相关文章
相关标签/搜索