引言
我们写的代码是放在.java文件中,经过编译器编译后,转成.class文件。Class文件是一串二进制流,它可以被各平台的虚拟机所接受,实现跨平台。
虚拟机将描述类的数据从class文件加载到内存,并对数据进行校验、解析、初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。Java中,类型的加载、连接和初始化过程都是在程序运行期间完成的,比如要编写一个面向接口的程序,就可以等到运行时再指定具体实现类。
类的生命周期
类的生命周期包括七个阶段:加载、验证、准备、解析、初始化、使用、卸载。其中,验证、准备、解析三个阶段合称为连接阶段。如图:
其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,而有些情况下解析过程可以在初始化之后进行。这些阶段通常都是相互交叉的混合式进行的,通常会在一个阶段执行的过程中调用、激活另一个阶段。
什么时候进行类加载的第一个阶段呢?Java虚拟机没有约束,而是交给虚拟机自己把握。但是,对于初始化阶段,虚拟机规范严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然在这之前进行)。(下面是5个必须进行初始化类的地方,也就是什么时候开始初始化,对于初始化的内容,在后面具体过程中在介绍。)
1)遇到new、getstatic、putstatic、invokestatic四条字节码指令时,如果没有对类进行初始化,就要立马对其进行初始化。生成这四条指令最常见的Java场景:new 关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译时把结果放入常量池的静态字段除外)、调用一个静态方法。
2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化则需要先触发其初始化。
3)当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机先初始化这个主类。
5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。
注意:有且只有这五种情况才会触发类的初始化,这5个情况中的行为称为对一个类进行主动引用。除此之外的所有其他引用类的方式都不会初始化类,称为被动引用,下面列举三个不会触发初始化的情况。
1)子类继承父类,通过子类调用父类的静态字段,只会导致父类初始化,不会导致子类初始化。
2)通过数组定义来引用类,不会触发此类的初始化。SuperClass[] a=new SuperClass[10];
3)调用静态常量不会初始化类。public static final String s="hello world";常量在编译阶段就会存入调用类的常量池,本质上并没有直接引用到定义常量的类。
类加载的过程
1.加载
在加载阶段,虚拟机完成三件事:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的类的静态存储结构(如静态属性、静态字段)转化为方法区的运行时数据结构(也就是按照方法区的格式存进方法区)。
3)在内存中生成一个代表这个类的java.lang.Class对象,用来作为访问方法区中这些类型数据的入口。
对于上面的第一条,虚拟机没有指明二进制流从哪获取、怎么获取。所以,这里Java开发人员就玩出许多花样来:
a)从zip包中读取,最终成为JAR、EAR、WAR的基础。
b)从网络获取,如Applet。
c)运行时计算生成,动态代理。
d)从其他文件生成,jsp应用,从jsp生成对应的Class类。
。。。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中了,然后在内存中实例化一个java.lang.Class类的对象(没有明确要放在堆中,hotspot放在方法区中),作为程序访问方法区中这些类型数据的外部接口。
2.验证
验证阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证这一阶段很复杂,但从整体上看,验证阶段大致会完成下面四个阶段的检验:文件格式验证、元数据验证、字节码验证、符号引用验证。
1)文件格式验证
第一个阶段就是要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。这里验证的内容也挺多的,此处省略好多字。
这个阶段的验证是基于二进制字节流进行的,只有通过这个阶段后,字节流才会进入到内存的方法区中,后面的验证都是基于方法区的存储结构进行的,不在直接操作字节流。
2)元数据验证
这一阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java虚拟机语言规范的要求,比如:这个类是否含有父类,父类是否继承了不允许被继承的类(final),如果该类不是抽象类,是否实现了其父类或接口中的要求实现的所有方法。。。等等。
3)字节码验证
主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段是对类的方法体进行校验,保证这些方法在运行时不会危害虚拟机。
4)符号引用验证
符号引用验证发生在将符号引用转换为直接引用的时候(解析阶段发生)。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
3.准备
准备阶段正式为类变量(static变量)分配内存并设置类变量初始值。这些类变量所使用的内存都在方法区中分配。这里只是static变量,实例变量将会在对象实例化的时候随着对象一起进入到Java堆中。通常情况下,将类变量初始化为零值(根据不同基本数据类型会不一样),但是对于特殊情况:如果该类字段被final修饰,这时就会将其设置为自身的数据。
4.解析
解析阶段虚拟机将常量池中的符号引用替换为直接引用。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载在内存中了。
直接引用:直接引用可以是直接指向目标的指针、相对偏移或一个能间接定位到目标的句柄。直接引用和虚拟机的内存布局有关,如果有了直接引用,那引用的目标必定已经在内存中存在了 。
对于同一个符号引用进行多次解析请求是很常见的,除了invokedynamic指令外,虚拟机实现可以对第一次解析的结果进行缓存,从而避免解析动作重复进行。如果一个符号引用之前被成功解析过,那么后续的引用解析请求应当一直成功;反之收到相同异常。
5.初始化
在准备阶段已经对类变量进行过一次系统要求的初始化,而在初始化阶段,则根据程序员的意愿去初始化类变量和其它资源,或者换一个角度表达:初始化阶段是执行类构造器<clinit>()方法的过程。下面是该放的一些特点:
1)<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并而成的,编译器的收集顺序有语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的静态变量,定义在它之后的变量,在前面的语句块可以赋值,但是不能访问。
2)<clinit>)方法与类的构造函数不同,它不需要显示的调用父类构造器,虚拟机会保证在子类的<clinit>()调用之前,父类的<clinit>已经被调用了,所以,虚拟机中第一个被执行的<clinit>肯定是Object的
3)父类中定义的静态代码块要优先于子类的变量赋值。
4)<clinit>()方法对于一个类或接口来说不是必须的,如果没有静态代码块,可以不为此类生成该方法。
5)虚拟机会保证每个类的《clinit》()方法在多线程下被正确的加锁、同步,如果多线程同时去初始化一个类,那么只有一个线程会执行这个<clinit>()方法,其他线程阻塞等待。
6)接口中不能使用静态代码块,但是接口中也有变量的赋值操作(只能常量)。
类加载器
虚拟机设计团队将类加载阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。
比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,然后再看代表类的Class对象的equals()方法,。。等方法。
双亲委派模型
从Java开发人员的角度来看,类加载器可以划分为以下三种系统提供的类加载器。
1)启动类加载器:这个类加载器负责将放在<JAVA_HOME>lib目录下的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。开发者不能直接使用启动类加载器
2)扩展类加载器:负责加载<JAVA_HOME>libext下,或者被java.ext.dirs系统变量所指定的路径中的类库,开发者可以直接使用扩展类加载器。
3)应用程序类加载器(也称为系统类加载器):它负责加载用户路径(ClassPath)上所指的类库,开发者可以直接使用这个加载器。如果应用程序没有自己定义类加载器,一般默认使用这个类加载器。
双亲委派模型(parents delegation Model)的层次关系r如图:
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承实现,而都是使用组合。
双亲委派模型的工作过程:如果一个类加载器收到了一个类加载请求,它首先不会自己去尝试加载这个类,而是把请求委派给父类加载器,每一层的类加载器都是如此,因此所有的类加载器最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时(它的搜索范围内没有找到所需要的类),子类加载器才会尝试去加载这个类。
使用双亲委派模型来组织类加载器之间的关系,一个好处就是Java类随着加载器一起具备了优先级的层级关系。例如java.lang.Object类,无论哪个类加载器需要加载这个类,最终都是为派给启动类加载器进行加载,所以Object类在程序的任意一个类加载器环境中都是同一个类。