目录
一 Java 虚拟机具体是怎样运行Java 字节码的
以标准JDK 中的HotSpot 虚拟机为例
虚拟机视角
从虚拟机视角来看,执行Java 代码首先需要将它编译而成的class 文件加载到Java 虚拟机中。加载后的Java 类会被存放于方法区
(Method Area)中。实际运行时,虚拟机会执行方法区内的代码
Java 虚拟机同样也在内存中划分出堆和栈来存储运行时数据
Java 虚拟机会将栈细分为面向Java 方法的Java 方法栈
,面向本地方法(用C++写的native 方法)的本地方法栈
,以及存放各个线程执行位置
的PC 寄存器。
在运行过程中,每当调用进入一个Java 方法,Java 虚拟机会在当前线程的Java 方法栈中生成一个栈帧,用以存放局部变量以及字节码的操作数。这个栈帧的大小是提前计算好
的,而且Java 虚拟机不要求
栈帧在内存空间里连续分布
。
当退出当前执行的方法时,不管是正常返回还是异常返回,Java 虚拟机均会弹出当前线程的当前栈帧,并将之舍弃。
硬件视角
Java 字节码无法直接执行。因此,Java 虚拟机需要将字节码翻译
(前面是编译)成机器码。
在HotSpot 里面,上述翻译过程有两种形式:
- 第一种是解释执行,即
逐条
将字节码翻译成机器码并执行; - 第二种是即时编译(Just-In-Time compilation,
JIT
),即将一个方法中包含的所有字节码
编译成机器码后再执行。
硬件只管执行机器码
编译/解释混合模式
前者的优势在于无需等待编译(编译快
),而后者的优势在于实际运行速度更快
。HotSpot 默认采用混合模式,综合了解释执行和即时编译两者的优点。它会先解释执行字节码,而后将其中反复执行的热点代码
,以方法为单位
进行即时编译。
运行效率
HotSpot 采用了多种技术来提升启动性能
以及峰值性能
,刚刚提到的即时编译便是其中最重要的技术之一。
即时编译建立在程序符合二八定律
的假设上,也就是百分之二十的代码占据了百分之八十的计算资源。
对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。
分层编译
从Java 7 开始,HotSpot 默认采用分层编译的方式:热点方法首先会被C1 编译,而后热点方法中的热点会进一步被C2 编译
- C1 又叫做Client 编译器,面向的是对启动性能有要求的客户端GUI 程序,采用的优化手段相对简单,因此编译时间较短。
- C2 又叫做Server 编译器,面向的是对峰值性能有要求的服务器端程序,采用的优化手段相对复杂,因此编译时间较长,但同时生成代码的
执行效率较高
。
先快速启动,后慢慢提高峰值性能
二 类加载
从class 文件
到内存中的类
,按先后顺序需要经过加载、链接以及初始化三大
步骤
2.1引用类型分类
Java 将其细分为四种:类、接口、数组类和泛型参数。由于泛型参数
会在编译过程中被擦除
,因此Java 虚拟机实际上只有前三种
。在类、接口和数组类中,数组类是由Java 虚拟机直接生成
的,其他两种则有对应的字节
流。
说到字节流,最常见的形式要属由Java 编译器生成的class 文件。除此之外,我们也可以在程序内部直接生成,或者从网络中获取(例如网页中内嵌的小程序Java applet)字节流。这些不同形式的字节流,都会被加载到Java 虚拟机
中,成为类或接口.
无论是直接生成的数组类,还是加载的类,Java 虚拟机都需要
对其进行链接和初始化。接下来,我会详细给你介绍一下每个步骤具体都在干些什么。
2.2加载
加载,是指查找字节流,并且据此创建类
的过程。数组类是直接生成的,没有这一步!
启动类加载器
启动类加载器(boot classloader)。启动类加载器是由C++ 实现的,没有对应的Java 对象,因此在Java 中只能用null
来指代。(所以我们找不到该对象)
在Java 9
之前,启动类加载器负责加载最为基础、最为重要的类,比如存放在JRE 的lib 目录
下jar 包中的类(以及由虚拟机参数-Xbootclasspath 指定的类
(人工指定加载))。除了启动类加载器之外,另外两个重要的类加载器是扩展类加载器(extension class loader)和应用类加载器(application class loader),均由Java 核心类库提供。
ClassLoader的各种子类
除了
启动类加载器之外,其他的类加载器都是
java.lang.ClassLoader 的子类,因此有对应的Java 对象。这些类加载器需要先由另一个类加载器
,比如说启动类加载器,加载至Java 虚拟机中,方能
执行类加载。(他们也要先被类加载器加载进来,才能去加载别的类)
- 扩展类加载器的父类加载器是启动类加载器。它负责加载相对次要、但又通用的类,比如存放在JRE 的lib/ext 目录下jar 包中的类(以及由系统变量java.ext.dirs 指定的类)。
- 应用类加载器的父类加载器则是扩展类加载器。它负责加载应用程序路径下的类。(这里的应用程序路径,便是指虚拟机参数-cp/-classpath、系统变量java.class.path 或环境变量CLASSPATH 所指定的路径。)
默认
情况下,应用程序中包含的类
便是由应用类加载器加载的。
双亲委派模型
每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器
。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试
去加载。
自定义类加载器
除了由Java 核心类库提供的类加载器外,我们还可以加入自定义的类加载器,来实现特殊的加载方式。举例来说,我们可以对class 文件进行加密,加载时再利用自定义的类加载器对其解密。
命名空间
除了加载功能之外,类加载器还提供了命名空间
的作用。
在Java 虚拟机中,类的唯一性
是由类加载器实例以及类的全名一同
确定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。
2.3 链接
链接,是指将创建成的类合并至Java 虚拟机中,使之能够执行的过程。它可分为验证、准备以及解析三个阶段
验证阶段
在于确保被加载类能够满足Java 虚拟机的约束条件。
通常而言,Java 编译器生成的类文件必然满足Java 虚拟机的约束条件。
准备阶段
目的是为被加载类的静态字段分配内存
(因为静态字段是属于类的)。Java 代码中对静态字段的具体初始化(不在本阶段!!!
),则会在稍后的初始化阶段中进行。
除了分配内存外,部分Java 虚拟机还会在此阶段构造其他跟类层次相关的数据结构,比如说用来实现虚方法的动态绑定的方法表。
解析阶段
目的,正是将这些符号引用解析成为实际引用
。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载
(但未必触发这个类的链接以及初始化。)
编译期是符号引用
在class 文件被加载至Java 虚拟机之前,这个类无法知道
其他类及其方法、字段所对应的具体地址
,甚至不知道自己方法、字段的地址。因此,每当需要引用这些成员时,Java 编译器会生成一个符号引用。在运行阶段,这个符号引用一般都能够无歧义地定位到具体目标上。
举例来说,对于一个方法调用,编译器会生成一个包含目标方法所在类的名字、目标方法的名字、接收参数类型以及返回值类型的符号引用,来指代所要调用的方法。
Java 虚拟机规范并没有要求在链接过程中完成解析。它仅规定了:如果某些字节码使用了符号引用,那么在执行
这些字节码之
前,需要完成
对这些符号引用的解析
。
在执行调用指令前,它所附带的符号引用需要被解析成实际引用。对于可以静态绑定的方法调用而言,实际引用为目标方法的指针。对于需要动态绑定的方法调用而言,实际引用为辅助动态绑定的信息(方法表的索引)。
2.4 初始化
在Java 代码中,如果要初始化一个静态字段,我们可以在声明
时直接赋值,也可以在静态代码块
中对其赋值。
如果直接赋值的静态字段被final 所修饰,并且它的类型是基本类型或字符串时,那么该字段便会被Java 编译器
标记成常量值(ConstantValue),其初始化直接由Java 虚拟机完成。除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被Java 编译器置于同一方法中
.换句话说该阶段是执行类构造器<clinit>
()方法的过程
就是静态变量初始化(非final修饰的)
Java 虚拟机会通过加锁
来确保类的方法仅被执行一次。只有当初始化完成之后,类才正式成为可执行的状态。
2.5 类的初始化何时会被触发
- 当虚拟机启动时,初始化用户指定的主类;
- 当遇到用以新建目标类实例的new 指令时,初始化new 指令的目标类;
- 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
- 当遇到访问静态字段的指令时,初始化该静态字段所在的类;(
静态内部类实现单例原理
) - 子类的初始化会触发父类的初始化
- 如果一个接口定义了default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
- 使用反射API 对某个类进行反射调用时,初始化这个类;
- 当初次调用MethodHandle 实例时,初始化该MethodHandle 指向的方法所在的类。
2.6 按顺序开始(交叉混合)
其中加载、检验、准备、初始化和卸载这个五个阶段的顺序是固定的,而解析则未必
。为了支持动态绑定,解析这个过程可以发生在初始化阶段之后。另外,这个过程表示的是按顺序开始,不是所谓的第一步、第二步、第三步的关系,而往往是交叉混合进行,在一个阶段中可能调用或者激活另一个过程。