java核心学习(三十六) 类加载和初始化
在我们运行了java程序之后,我们所运行的.class文件会加载、连接、初始化三个步骤,这三个步骤中,加载与连接都由JVM来实现,程序员对此无感知,初始化过程则根据我们写的类定义来实现。
一、JVM和类
当使用java命令运行某个java程序时,该命令会启动一个运行在JVM上的进程,该JVM进程被中止的情形为:
- 程序运行到最后正常结束;
- 程序调用System.exit()或Runtime.getRuntime().exit(),结束;
- 程序在执行过程中遇到未捕获的异常或错误而结束;
- 程序所在平台强制结束了JVM进程;
二、类的加载
类加载是指:根据类的全限定名称将类的class文件读入内存,之后JVM便获取到该类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,之后在java堆内存中生成一个代表 这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
加载阶段由java提供的类加载器完成,也可以由程序员自定义的加载器完成,加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下四种来源:
- 通过本地文件系统加载class文件;
- 从jar包中加载class文件;
- 通过网络加载class文件;
- 把一个java源文件动态编译,并执行加载;
三、类的连接
类的连接负责把类的二进制数据合并到jre中,具体分为三个过程:验证、准备、解析。
1、验证:
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
Java语言本身是相对安全的语言,使用Java编码是无法做到如访问数组边界以外的数据、将一个对象转型为它并未实现的类型等,如果这样做了,编译 器将拒绝编译。但是,Class文件并不一定是由Java源码编译而来,可以使用任何途径,包括用十六进制编辑器(如UltraEdit)直接编写。如果 直接编写了有害的“代码”(字节流),而虚拟机在加载该Class时不进行检查的话,就有可能危害到虚拟机或程序的安全。
不同的虚拟机,对类验证的实现可能有所不同,但大致都会完成下面四个阶段的验证:文件格式验证、元数据验证、字节码验证和符号引用验证。
- 文件格式验证,是要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。如验证魔数是否0xCAFEBABE;主、次版本 号是否正在当前虚拟机处理范围之内;常量池的常量中是否有不被支持的常量类型……该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区中, 经过这个阶段的验证后,字节流才会进入内存的方法区中存储,所以后面的三个验证阶段都是基于方法区的存储结构进行的。
- 元数据验证,是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。可能包括的验证如:这个类是否有父类;这个类的父类是否继承了不允许被继承的类;如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法……
- 字节码验证,主要工作是进行数据流和控制流分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。如果一个类方法体的字节码没有通过字节码验证,那肯定是有问题的;但如果一个方法体通过了字节码验证,也不能说明其一定就是安全的。
- 符号引用验证,发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在“解析阶段”中发生。验证符号引用中通过字符串描述的权限定名是否能 找到对应的类;在指定类中是否存在符合方法字段的描述符及简单名称所描述的方法和字段;符号引用中的类、字段和方法的访问性(private、 protected、public、default)是否可被当前类访问。
验证阶段对于虚拟机的类加载机制来说,不一定是必要的阶段。如果所运行的全部代码确认是安全的,可以使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载时间。
2、准备: 准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。比如 int型的静态变量默认值为0。
3、解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
- 符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
- 直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,如果有了直接引用,那么引用的目标必定已经在内存中存在。
四、类的初始化
类初始化是类加载过程的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。
初始化阶段是执行类构造器<clinit>()方法(注意!不是对象构造器,但是对象构造器的执行过程与之类似,也是先顺序执行实例变量初始化语句与代码块中的实例变量赋值语句,然后执行构造器里面的代码)的过程。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,顺序收集顺序执行。
如果该类还没有被加载和连接,则先加载和解析。
如果该类的父类还没有被初始化,则先初始化父类,以此类推,如果Object类没有被初始化,则先初始化Object。
五、类初始化时机
对一个类进行“主动引用”会初始化类,主动引用有以下6种情况:
- 创建类的实例。
- 访问类的静态变量(除常量【被final修辞的静态变量】原 因:常量一种特殊的变量,因为编译器把他们当作值(value)而不是域(field)来对待。如果你的代码中用到了常变量(constant variable),编译器并不会生成字节码来从对象中载入域的值,而是直接把这个值插入到字节码中。这是一种很有用的优化,但是如果你需要改变 final域的值那么每一块用到那个域的代码都需要重新编译。
- 访问类的静态方法。
- 反射如(Class.forName("my.xyz.Test"))。
- 当初始化一个类时,发现其父类还未初始化,则先出发父类的初始化。
- 虚拟机启动时,定义了main()方法的那个类先初始化。
- 子类调用父类的静态变量,子类不会被初始化。只有父类被初始化。。对于静态字段,只有直接定义这个字段的类才会被初始化.
- 通过数组定义来引用类,不会触发类的初始化
- 访问类的常量,不会初始化类
使用ClassLoader类的loadClass()方法加载某个类时,并不会初始化该类。