一个java文件的整个生命周期,总共要经历加载-验证-准备-解析-初始化-使用-卸载这几个阶段,有的人把验证准备解析概括为一个阶段称为连接,全部有的说5个阶段的,也有说7个阶段的,两种说法。java
1.用new实例化对象的时候。程序员
2.读取或者设置一个类的静态字段的时候。数据库
3.调用一个类的静态方法的时候。缓存
4.使用java.lang.reflect包的方法对类进行反射的时候,若是类没有进行过初始化,则须要先触发其初始化。安全
5.当初始化一个类的时候,若是发现这个类的父类尚未进行过初始化,则须要先触发其父类的初始化。数据结构
6.当虚拟机启动的时候,若是java程序中包含main()主函数的类,则该类的加载由JVM自动触发。框架
所谓加载,就是将java类的字节码文件加载到机器内存中,并在内存中构建出java类的原型-类模板对象。ide
所谓类模板对象,其实就是java类在JVM内存中的一个快照,JVM将从字节码文件中解析出常量池、类字段、类方法等信息存储到类模板中,这样JVM在运行期便能经过类模板而获取Java类中的任意信息,可以对java类的成员变量进行遍历,也能进行java方法的调用,这就是反射机制背后的原理,若是JVM没有将java类的声明信息保存起来,则JVM在运行期也没法对类进行反射。函数
在这个加载阶段,虚拟机须要完成如下三件事情:spa
1.经过一个类的全限定名(完整包名、URL地址、数据库生成、等等)来获取定义此类的二进制字节流。
2.将整个字节流所表明的静态存储结构转化为方法区的运行时数据结构。
3.在内存中生成一个表明整个类的java.lang.Class对象,做为方法区整个类的各类数据的访问入口。
加载阶段完成以后,虚拟机外部的二进制字节流就按照虚拟机所须要的格式存储在方法区之中,而后在内存中实例化一个java.lang.Class类的对象。
须要注意的是,加载阶段与后面的验证准备解析阶段并不是是阻塞式进行,可能加载阶段还没有完成,后面的阶段就已经开始了。
验证这一阶段的目的是为了确保class文件的字节流中包含的信息是否符合虚拟机的要求。这个很好理解,随便一个程序,你少写一个标点符号看看还能不能进行编译。
虚拟机若是不检查输入的字节流,对其彻底信任的话,极可能会由于载入了有害的字节流而致使系统崩溃,因此验证是虚拟机对自身保护的一项重要操做。
首先须要验证字节流是否符合class文件格式的规范,而且能够被当前虚拟机处理。
验证内容列举几项:
1.版本号是否在当前虚拟机处理范围以内。
2.常量池的唱两种是否有不被支持的常量类型。
3.指向常量的各类索引值中是否有指向不存在的常量或不符合类型的常量。
4.class文件中各个部分以及文件自己是否有被删除的或附加的其余信息。
接下来是对字节码描述的信息进行语义分析,以保证符合java语言规范的要求。
验证内容列举几项:
1.这个类有没有父类,由于除了java.lang.Object以外,全部的类都应该有父类 。
2.这个类的父类是否继承了不容许被继承的类(好比被final修饰的类)。
3.若是这个类不是抽象类,是否实现了其父类或接口之中要求实现的全部方法。
4.类中的字段、方法是否与父类产生矛盾。
这个验证将对类的方法体进行校验分析,保证被校验的方法在运行时不会作出危害虚拟机安全的事件。
验证内容举例几项:
1.保证任意时刻操做数栈的数据类型与指令代码序列都能配合工做,例如不会出现相似这样的状况:在操做栈放置了一个Int类型的数据,使用时却按照long类型来加载入本地变量表中。
2.保证跳转指令不会跳转到方法体之外的字节码指令上。
3.保证方法体中的类型转换是有效合法的。
符号引用验证能够看作是对类自身之外(常量池中的各类符号引用)的信息进行匹配性校验。
验证内容举例几项:
1.符号引用中经过字符串描述的全限定名是否能找到对应的类。
2.在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
3.符号引用中类、字段、方法的访问性(private/protected/public/default)是否可被当前类访问。
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
这个阶段有两个概念容易产生混淆。首先,准备阶段进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一块儿分配在java堆中。
其次,这里所说的初始值一般状况下是数据类型的零值,假设一个类变量定义为:
public static int value = 123;
那变量value在准备阶段事后的初始值为0,而不是123,由于这时候尚未开始执行任何java方法。
可是,若是是final修饰的变量,如:
public static final int value = 123;
那么在准备阶段变量value就会被初始化为123。
解析的过程就是JVM将常量池中的符号引用替换为直接引用的过程。
好比说一个变量的类型是某个对象,那么解析的时候须要把这个变量类型替换成直接指向该对象的指针。
对同一个符号引用进行屡次解析是很常见的事情,虚拟机会对第一次解析的结果进行缓存,从而避免解析动做重复执行。
完成上面几个阶段后,便会进入类的初始化阶段。
所谓初始化,说白了就是调用java类的<clinit>()方法,该方法是编译器在编译期间自动生成的,当java类中出现静态字段或者包含static{}块时,编译出来的java字节码文件中就会自动包含一个名为<clinit>的方法,该方法不能由程序员在java程序中调用,只能由JVM在运行期调用,这个调用的过程就是java类的初始化。
注意:<clinit>()方法并不是类的构造函数。
要想再JVM内部建立一个与java类彻底对等的结构模型,必须通过类加载器。
java体系中定义了3种类加载器,分别以下:
1.Bootstrap ClassLoader,引导类加载器,也被称做启动类加载器,加载指定的JDK核心类库,该加载器是由C++语言定义的,是虚拟机自身的一部分,没法由java应用程序直接引用,负责加载下列三种状况下所指定的核心类库:
①、%JAVA_HOME%/jre/lib目录
②、-Xbootclasspath参数所指定的目录
③、系统属性sun.boot.class.path指定的目录中特定名称的jar包
2.Extension ClassLoader,扩展类加载器,加载扩展类,扩展JVM的类库,该加载器加载下列两种状况下所指定的类库:
①、%JAVA_HOME%/jre/lib/ext目录
②、系统属性java.ext.dirs所指定的目录中的全部类库
3.App ClassLoader,系统类加载器,也被称做应用程序类加载器,加载java应用程序类库,开发者能够直接使用这个类加载器,若是应用程序中没有自定义过本身的类加载器,通常状况下这个就是程序中默认的类加载器。
对于任意一个类,都须要由加载它的类加载器和这个类自己一同确立在java虚拟机中的惟一性,每个类加载器,都拥有一个独立的类名称空间。
这句话反过来讲就是,比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下,比较才有意义,不然,就算这两个类都来源于同一个class文件,被同一个虚拟机加载,只要加载他们的类加载器不一样,那么这两个类就一定不相等。
这里所指的“相等”,包括equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字对所属关系断定状况。
如下演示不一样类加载器加载出的类比较结果:
public class Test { public static void main(String[] args) throws Exception{ ClassLoader loader = new ClassLoader() { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try{ String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; InputStream is = getClass().getResourceAsStream(fileName); if(is == null){ return super.loadClass(name); } byte[] b = new byte[is.available()]; is.read(b); return defineClass(name,b,0,b.length); }catch (IOException e){ throw new ClassNotFoundException(name); } } }; Object obj1 = new Test(); System.out.println("obj1:" + obj1.toString()); Object obj2 = loader.loadClass("Test").newInstance(); System.out.println("obj2:" + obj2.toString()); System.out.println(obj1.equals(obj2)); } }
输出结果:
obj1:Test@88ff2c
obj2:Test@c0663d
false
该示例中,obj1对象是由系统类加载器加载的,obj2对象是由咱们自定义的类加载器加载的,虽然都来自于同一个class文件,但依然是两个独立的类。
这里有的人会遇到一个问题,把obj2转成Test类型,以下:
Test obj2 = (Test)loader.loadClass("Test").newInstance();
一运行会发现抛出这样一个错:
Exception in thread "main" java.lang.ClassCastException: Test cannot be cast to Test
缘由在于等号左边所声明的Test类型并无明确为其指定类加载器,因此JVM会使用系统类加载器加载Test类,而等号右边则明确使用了自定义的类加载器加载Test类,因此等号左右两边的两个Test类型的加载器并非同一个。
这种异常在使用第三方框架好比Spring的时候会比较常见,究其缘由,是由于不少中间件内部都有自定义的类加载器,所以被内存加载器所加载的类型,是没有办法直接转换为使用默认加载器加载的类型。
JVM加载一个类的逻辑为如下三步:
第一步:在当前加载器的缓存中查找有没有这个类,若是有,直接返回,不然走下一步。
第二步:跳到父加载器,重复第一步内容,直到跳到最顶级的引导类加载器为止,若是缓存中尚未这个类,则继续下一步。
第三步:引导类加载器进行加载,若是加载不到,则让子加载器一级一级进行加载,直到加载成功。
假设当前加载的是java.lang.Object这个类,当JVM准备加载时,JVM默认会使用系统类加载器去加载,按照上面三步的逻辑,第一步走过,由于系统类加载器和扩展类加载器的缓存中都不会有该类,走到第二步 到了引导类加载器,若是加载过,则取缓存,若是没加载过,则由引导类加载器进行加载,若是引导类加载器的搜索范围内找不到该类,那么会下发到扩展类加载器进行加载。
这就是双亲委派机制。
这种机制保证核心类库必定是由引导类加载器进行加载,而不会被多种加载器加载,不然每一个加载器都会加载一遍核心类库,这个世界就乱了,同时也会存在安全隐患。
双亲委派模型的工做过程是:若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每个层次的类加载器都是如此,所以全部的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈本身没法完成这个加载请求(它的搜索范围中没有找到该类)时,子加载器才会尝试本身去加载。
如图:
简而言之,双亲委派从本质上而言,其实规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载。
类的生命周期分为7个阶段,加载完成以后须要进行连接(验证、准备、解析)和初始化,在连接阶段,字节码指令会被重写,将其所引用的常量池的索引号转换为直接引用。
好比说,在实例化一个类的时候,编译后生成的字节码指令为:new #2。后面这个#2表示常量池中索引为2的元素,该元素指向某个java类的全限定名。
若是是实例化Long,常量池中2号索引里存在的是字符串:java.lang.Long。重写后的new字节码指令,后面跟着的就不是#2了,就是指向"java.lang.Long"这个字符串的内存地址。
当JVM真正运行到new这条指令的时候,它要根据java类的全限定名称,在内存metaspace区定位到这个java类在内存中的类模板对象-instanceKlass。类模板对象包含了原始java类中的一切信息,JVM会根据这个模板建立出java类的实例对象。
注意:为了不每次new都要进行一次定位,JVM会在第一次执行new指令时,就会将定位到的类模板对象缓存起来,这样子后续须要再次实例化一样的java类对象时,便会直接从缓存中读取模板。