代码编译的结果从本地机器码变为字节码,是储存格式发展的一小步,倒是编程语言发展的一大步——《深刻理解Java虚拟机》
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转化解析和初始化,最终造成了能够被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。java
类型的加载、链接和初始化都是在程序运行期间完成的,虽然说加大了运行时期的开销,可是大大增长了Java的灵活度,方便动态加载和链接。Java不只能够从Class文件获取属于,也能够从其余地方例如网络中直接获取二进制流数据,这极大提升了Java的延展性。编程
类从开始加载到卸载一共通过了七个过程,以下图。数组
其中验证、准备、解析统称为链接。另外,加载、验证、准备、初始化和卸载这5个过程只是开始要按照顺序,能够同时执行,不用等待上一个过程结束以后才执行。例如,我在9点开始准备,9点10分开始初始化,9点20准备结束。缓存
有且只有下面五种状况,才能够称为“初始化”:网络
除此以外,全部引用类的方式都不会触发初始化,仅被称为被动引用。数据结构
开个小差,在一个类的静态代码块中,若是某变量提早被被赋值,就能够被使用;若是某变量以后才赋值的,在静态代码块中使用就会报错。可是不管什么时候赋值,只要声明了,在静态代码块中再赋值是被容许的。看下这个例子:编程语言
public class Test{ static{ i=0;//给变量赋值能够正常编译经过 System.out.print(i);//这句编译器会提示"非法向前引用" } static int i=1; }
对于接口来讲,有且仅有前三种状况才会被称为初始化。另外,对于接口,不须要知足提早让父接口初始化,除非你有用到父接口的时候。函数
逐步看下加载、验证、准备、解析和初始化这5个过程。布局
加载过程须要完成如下三个事情:post
对于非数组的类,加载能够经过虚拟提供的类加载器,也能够经过一用户自定义的加载器。对于数组类,数组自己不是经过加载器加载的,而是经过Java虚拟机直接建立的,数组中的元素是经过加载器建立的。
加载过程结束后,内存中就会获得一个该类的java.lang.Class对象,为后续铺垫。
在加载开始的同时,验证择机开启。验证是为了确保Class文件的字节流种包含的信息符合上章讲的规格,不会危害虚拟机自己。这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击,从执行性能的角度讲,验证阶段的工做量在虚拟机类加载子系统中又占了至关大的一部分。
首先须要验证是否符合Class文件格式的规范,好比魔数(咖啡宝贝)是否存在,主次版本号是否能够被当前虚拟机运行、常量类型的tag标志等等。这个阶段的验证时基于二进制字节流进行的,只有经过了这个阶段的验证后,字节流才会进入内存的方法区中进行储存,后面三个验证阶段全是基于方法区的储存结构进行的,再也不直接进行字节流操做。
此过程包含验证是否有父类、父类是否容许被继承啊,各类修饰符是否冲突啊等等。
主要目的时经过数据流和控制流分析肯定程序语义是合法的、符合逻辑的。此过程保证任意时刻的操做数栈的数据类型与指令代码序列都能配合工做,保证跳转指令不会跳转到方法体之外的字节码指令上,保证类型转化是正常的,保证父类和子类之间的字段不冲突等等。
因为数据流验证很是复杂,为了减缓消耗的时间,自JDK1.6开始,方法体的Code属性的属性表中增长了一项为“StackMapTable”的属性,这项属性描述了方法体中全部的基本块。在字节码验证期间,就不须要根据程序推到这些状态的合法性,只须要检验StackMapTable属性中的记录是否合法便可。大大节省了字节码验证的时间。
此阶段发生在虚拟机将符号引用转化成直接引用的时候,这个转化动做将在链接的第三个阶段解析的时候发生。须要验证是否能够经过字符串的全限定名找到这个类,指定的类中是否符合方法的字段描述符以及简单名称所描述的方法和字段,类、方法、字段的访问性等等。
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。此时给静态变量设置初始值是零值,并非代码中设置的具体值,具体值还须要在putstatic指令执行时才会初始代码中设置的值。除非此static变量被final修饰了们就会在此时直接设置代码中的值。
解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程。
符号引用:符号引用以一组符号来描述所引用的目标,符号能够是任何形式的字面量,只要使用时能无歧义地定位到目标便可。符号引用与虚拟机实现的内存分布无关,引用的目标并不必定已经加载到内存中。
直接引用:直接引用能够是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不一样虚拟机实例上翻译出来的直接引用通常不会相同。若是有了直接引用,那引用的目标一定已经在内存中存在。
除了invokedynamic指令之外,虚拟机实现能够对第一次解析的结果进行缓存。invokedynamic指令是可动态语言支持相关的指令,因此没法作到缓存。
类初始化时类加载过程的最后一步。前面的操做除了自定义的类加载器以外,都是虚拟机主导的操做,初始化阶段,开始整整执行类中定义的Java代码了。
初始化阶段时执行类构造器<client>()方法的过程。<client>()方法是由编译器自动收集类中的全部类变量的赋值动做和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。<client>()方法不须要显示的构造父类的构造函数,已经本身构造好了,而且父类的静态代码块是先于子类的静态代码块的。而且<client>()方法执行时带锁的,不一样线程执行这个方法可能会出现线程阻塞的现象。
虚拟机设计团队把类加载阶段中“经过一个类的全限定名来获取此类的二进制字节流”这个动做放到Java虚拟机外部去实现了,实现这个动做的代码块叫作类加载器。
对于任意一个类,都须要由加载它的类加载器和这个类自己一同肯定其在Java虚拟机中的惟一性。若是说某个类相等,那么这两个类必定是在同一个类加载器下加载完成的。这里的相等可使用Class的equals方法、isAssignableFrom()方法、isInstance()方法验证,也可使用instanceof关键字作对象所属关系的判断。例如全限定名都是com.pjjlt.MyTest。一个用虚拟机本身的类加载器加载,一个用用户自定义的类加载器加载,那么这两个类就不相等,分别产生的对象实例用instanceof关键字只能做用域本身的类上才会是true。
那么问题来了,我要用自定义的类加载器加载一个Object放到内存中,那岂不是整个Java的基础功能全废了。其实否则,新建的Object类也会和原生的那个Object类是被同样对待的。这就涉及了双亲委派机制。
对于虚拟机的角度来讲,只有虚拟机的类加载器和用户自定义的类记载器。对于用户来讲有启动类加载器(Bootstrap ClassLoader)、拓展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader)这么几种,并且他们是一种组合关系来复用父加载器。
双亲委派机制工做原理:若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把这个请求委派给父加载器去完成,每一层次的类加载器都是如此,所以全部的加载请求最终都应该传送到顶层的启动类加载器中,只有它反馈本身没法加载的时候,才会交给子加载器加载。
这也解释了为何你写的Object加载器创造出来的类和原生的是同一款了,由于人家就没有被你本身写的类加载器所加载,而是某父层的加载器加载了。