日常开发中,我们写的 Java
代码是一个个 .java
文件,每个 .java
文件就是一个类( class
)。这些文件都需要经过编译,才能成为计算机可运行的 .class
文件。这些 class
文件在程序运行时是怎么运行的呢?我们就来一探究竟!
Java语言中,类的加载、连接和初始化均在程序运行时完成,这样虽然比较耗性能,但是提高了程序的灵活性。比如,一个接口的实现可以等到运行时才指定;用户可以自定义类加载器来加载二进制流作为程序的一部分。
class文件
全名称为 Java class
文件,主要在平台无关性和网络移动性方面使 Java
更适合网络。它在平台无关性方面的任务是:为 Java
程序提供独立于底层主机平台的二进制形式的服务。
这些编译后的 .class
文件为程序的提供服务,什么时候提供服务?需要经历那些过程呢?
Loading Verification Preparation Resolution Initialization Using Unloading
虚拟机规范严格规定了以下 5
中情况必须对类初始化:
1)遇到 new
、 getstatic
、 putstatic
、 invokestatic
这 4
条字节码指令时,触发初始化(加载、验证、准备均在此之前)。 new
- 使用 new
关键字实例化对象时
getstatic
- 读取一个类的静态字段
putstatic
- 设置一个类的静态字段
invokestatic
- 调用一个静态方法
2)使用反射API对类进行反射调用
3)子类初始化需先初始化父类
4)虚拟机启动需要指定一个主类(包含main方法)
5)当使用 JDK1.7
的动态语言支持时,如果一个 java.lang.invoke.MethodHandle
实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic
的方法句柄,并且这个方法句柄对应的类未初始化,则先初始化。
以上 5
中行为称为对类进行 主动引用 ;其他情况都为 被动引用 。
虚拟机主要完成以下3方面:
java.lang.Class
目的:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证字节流是否符合Class文件的格式规范,并且能够被当前版本的虚拟机处理
对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息 (元数据(其实就是类)数据类型校验)
通过数据流和控制流分析,确定语义是合法的、符合逻辑的 (类的方法体进行校验分析)
对类自身以外(常量池中各种符号引用)的信息进行匹配性校验
正式为类变量分配内存并设置类变量初始值的阶段,所有的变量使用的内存都在方法区进行分配。
static
修饰的变量),而不包括实例变量,实例变量会在对象实例化时随着对象一起分配在Java堆中。 ConstantValue
属性(类属性被 final
修饰),那么在准备阶段就会被初始化为 ConstantValue
属性所指定的值。 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用( Symbolic Rederences
)
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
直接引用( Direct References
)
直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
类初始化阶段是类加载过程的最后一步。类加载过程中,用户程序可以通过自定义类加载器参与之外,基本都是虚拟机主导和控制的。初始化阶段才开始执行 .class
文件。 准备阶段,变量已经初始化赋值一次(仅仅是static修饰的变量),初始化阶段则按照程序员的主观计划( .java
生成的 .class
文件)进行变量及其他资源的初始化。 另外一个角度阐述:初始化阶段是执行类构造器 <clinit>()
方法的过程。
<clinit>()
方法是虚编译器自动收集类中 所有类变量的赋值动作 、 静态代码块中的语句 合并产生的。编译器的收集顺序是按照源文件的代码顺序进行收集的。静态语句块中的只能访问到静态语句块之前的变量,不能访问到之后的变量;但是可以给定义在之后的变量赋值。 <clinit>()
方法与 <init>()
方法不同,虚拟机会保证父类的 <clinit>()
会在子类之前进行,故不需要像 <init>()
方法一样显示调用父类的 <clinit>()
方法 <clinit>()
方法不是必须的,如果类或接口没有静态语句块或者变量赋值操作,则不生成此方法。 "通过一个类的全限定名来获取描述此类的二进制字节流",让程序自己决定如何获取所需要的类。完成这个功能的代码块称为"类加载器"。
从 Java
虚拟机的角度
Java
开发人员角度
应用程序都是由启动类加载器、扩展类加载器、应用程序类加载器,也有可能有自定义类加载器相互配合完成类的加载。
双亲委派模型( Parents Delegation Model
)工作过程:
每个类加载器都是"啃老族",收到类加载的请求后,自己并不获取尝试加载类,而是将这个请求委派给自己的父类加载器(这里的父类不是继承的关系,而是以组合的关系复用父加载器的代码)去完成,每一个层次的类加载器均如此,直至传递至启动类加载器。如果父类加载器实在无法完成请求,子类才会自己尝试去加载。
双亲委派模型并不是强制性的约束模型,而是推荐的类加载器实现方式。
以上内容来自《深入理解Java虚拟机》第2版 周志明(著)学习笔记。如有不完善之处,请不吝赐教!