概述
Java 的指令都是根据**栈**来设计的。
基于栈的 JVM 优点
- 跨平台
- 指令集小
- 编译器容易实现
缺点
- 实现同样的功能需要更多的指令(性能差)
Java 虚拟机栈(Java Virtual Machine Stack)的特点
- 每个线程再创建时都会创建一个虚拟机栈,以栈帧(Stack Frame)为数据单元,每个栈帧一一对应一个调用方法。栈帧是线程私有的,不同线程所包含的栈帧是不允许存在相互引用的。
- Java 栈的生命周期和线程一致。
- 主观 Java 程序的运行,它保存方法的局部变量表,部分结果,参与方法的调用和返回。
- 栈是一种快速有效的分配存储方式,访问速度仅次于 PC 寄存器(栈只有入栈,出栈,时间复杂度:O(1))
- 每个方法执行,伴随着入栈,执行结束后出栈。
- 对于栈来说不存在垃圾回收问题(对于没有主动的退出机制的数据,才使用垃圾回收。栈有主动出栈,所以不需要垃圾回收)。
栈中可能出现的异常
Java 栈的大小可以是动态的,也可以是固定不变的- 如果固定大小的 Java 虚拟机栈,在线程创建时独立选定。如果线程请求分配的栈容量超过了 Java 虚拟机栈允许的最大容量(不断压栈,超过栈的大小),会抛出一个 StackOverflowError 异常。
- 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存创建对应虚拟机栈,会抛出一个 OutOfMemoryError 异常
设置栈的大小: -Xss(stack size)
-Xss1m
-Xss1024k
-Xss1048576
栈的存储单位
栈内的数据都是以栈帧(Stack Frame)的格式存储的。
在线程上执行的每个方法都有各自对应一个栈帧(Stack Frame)
栈帧是一个内存区块,维系着方法执行过程中的各种数据信息。
栈的只有”压栈“ 和 ”出栈“ 两个操作,遵循 ”先进后出“ 的原则。
在一条活动线程中,一个时间点上,只有一个活动的栈帧。只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称作当前栈帧(Current Frame),与这个栈帧对应的方法就是当前方法(Current Method),这个方法所属的类就是当前类(Current Class)。
如果当前方法调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
Java 方法有两种返回函数的方式:
- 正常的函数返回,使用 return 指令
- 抛出异常。
这两种方式都会导致栈帧被弹出。
栈帧的内部结构
每个栈帧中存储着
- **局部变量表(Local Variables)**
- **操作数栈(Operand Stack)(或表达式栈)**
- 动态链接(Dynamic Linking)(或指向运行常量池的方法引用)
- 方法返回地址(Return Address)
- 一些附加信息
栈帧的大小主要受局部变量表的影响,局部变量表是存放的是方法运行的业务数据。
操作数栈的大小主要受代码行数的影响。
局部变量表
**局部变量表定义为一个数字数组,主要用于存储方法参数和定义在方法体内部的局部变量。**这些数据类型包括:
- 基本数据类型( 8 种)
- 对象引用
- returnAddress 类型
在局部变量表中,32 位以内的类型只占用一个 slot (包括 returnAddress 类型),64 位的类型(long 和 double )占用两个 slot
- byte、short、char 在存储前被转换为 int,boolean 也被转换为 int :0 表示 false ,非 0 表示 true
- long 和 doube 占据两个 slot ,如果需要访问局部变量表中一个 64bit 时,只需要使用前一个索引即可。
局部变量表所需的容量大小(数组长度)是在编译期确定下来的,保存在方法的 Code 属性:maximum local variables 数据项中。
下图中就是 main 方法的局部变量表
- startPC 是字节码的行号
- Lenght 和 startPC 配合表示当前变量的作用域。
- Index 每个参数在局部变量表中的下标
public static void main(String[] args) {
LocalVariblesTest test = new LocalVariblesTest();
int num = 10;
test.test();
}
局部变量表是线程私有数据,不存在数据安全问题。
如果当前帧是由构造方法或者实例方法创建的,那么该对象引用 this 将会存放在 index 为 0 的 slot 处,其余的参数按照参数顺序继续排列。
如果下图:局部变量表 0 位置是 this。doube weight 变量在位置 3,占两个 slot 。方法参数 dateP 和 name2 也在局部变量表中。
public String test1(Date dateP, String name2) {
dateP = null;
name2 = "dongyifeng";
double weight = 0.6;
char gender = '男';
return dateP + name2;
}
静态方法的局部变量中没有 this。
public static void testStatic() {
// 这里编译错误:因为 this 变量不存在于当前方法的局部变量表中。
System.out.println(this.count);
}
slot 重复利用
**栈帧中局部变量表中的槽位是可以重复利用的。**如果一个局部变量过了其作用域,那么在其作用以之后申明的新的局部变量就很可能会重复使用过期局部变量的槽位,从而**达到节省资源的目的。**
public void test4() {
int a = 0;
{
int b = 0;
b = a + 1;
}
// 变量 c 使用之前已经销毁的变量 b 的 slot 位置
int c = a + 1;
}
补充说明
变量根据在类中声明的位置分类:
- 成员变量:在使用前,都经历过默认初始化赋值
- 类变量(静态变量):在 linking 的 prepare 阶段:给类变量默认赋值 —> initial 阶段:给类变量显式赋值。
- 实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值
- 局部变量:在使用前,必须进行显式赋值!否则,编译不通过。
操作数栈(Operand Stack)
操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据或者提取数据,即压栈(push)/ 出栈(pop)。
如下图:
- 红色框 bipush 指令将变量 i 和 j 的数据压入操作数栈。
- 绿色框 iload 指令从操作数栈中弹出两个数据
- 绿色框 iadd 指令将上边两个数求和
- 将求和结果放入局部变量表 3 的位置。
操作数栈的作用:**主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。**
操作数栈就是 JVM 执行引擎的一个工作区。当一个方法开始执行时,会创建一个新的栈帧,此时的操作数栈是空的。
操作数栈与局部变量表一样,在编译期就确定下来了栈的最大深度(底层是数组长度),保存在方法的 Code 属性中:max_stack。
栈中可以存储任意 Java 类型数据
- 32 bit 的类型占用一个栈单位深度
- 64 bit 的类型占用两个栈单位深度
Java 虚拟机的**解释引擎是基于栈的执行引擎**,其中的栈指的就是**操作数栈**。
如下图:
- stack:表示操作数栈的最大深度。
- locals:表示局部变量表的最大容量。
如下图是字节码执行过程:
- bipush:将 int 数据压栈(操作数栈)
- istore_1:将操作数栈栈顶数据弹出,存储到局部变量表中 1 的位置
- iload_1:将局部变量表中 1 位置的数据压栈(操作数栈)
- iadd:从操作数栈弹出两个数据,求和后,再压栈(操作数栈)
栈顶缓存技术
JVM 是基于栈式架构的虚拟机的缺点是:需要更多入栈与出栈,这样更多的指令分派(instruction dispatch)次数和内存读/写次数。
由于操作数栈是存储在内存中,因此频繁读写必然会影响执行速度。为了解决这个问题,HotSpot JVM 的设计师提出了栈顶缓存(ToS,Top-of-Stack Cashing)技术,**将栈顶元素全部缓存在物理 CPU 的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率。**
动态链接(Dynamic Linking)
动态链接也叫:指向运行时常量池的方法引用。
每一个栈帧内部都包含一个指向**运行时常量池**中**该栈帧所属方法的引用**。包含这个引用的目的就是为了支持当前方法的代码能够实现**动态链接(Dynamic Linking)**。比如:invokedynamic 指令。
方法的调用:解析与分派
- 静态链接:如果被调用的**目标方法在编译期可以确定**,那么在字节码文件被装载 JVM 内部时,调用方法的符号引用转换为直接引用的过程称之为静态链接。
- 动态链接:如果被调用的**目标方法在编译期无法确定下来**,那么只能够在程序运行期间调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此被称之为动态链接。
静态链接和动态链接是针对方法,推广到字段、方法、类就是绑定了。
绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。- 早期绑定(Early Binding):目标方法在编译期可以确定
- 晚期绑定(Late Binding):目标方法在编译期无法确定下来
方法的参数是父类或者接口,在方法内部调用时,不能确定具体的实现类,因此是动态链接。
指定调用父类的方法或者自己的方法,在编译期间能够确定。能在编译器决定的事情,就不要放在运行时去做,这样性能更好。
class Animal {
public void eat() {
System.out.println("动物进食");
}
}
interface Huntable {
void hunt();
}
class Dog extends Animal implements Huntable {
@Override
public void eat() {
System.out.println("够吃骨头");
}
@Override
public void hunt() {
System.out.println("捕食耗子,多管闲事");
}
}
class Cat extends Animal implements Huntable {
public Cat() {
// 指定调用父类构造器
// 表现为:早期绑定
super();
}
public Cat(String name) {
// 指定调用自己的构造器
// 表现为:早期绑定
this();
}
@Override
public void eat() {
System.out.println("猫吃鱼");
}
@Override
public void hunt() {
System.out.println("捕食耗子,天经地义");
}
}
public class AnimalTest {
public void showAnimal(Animal animal) {
// 表现为:晚期绑定
animal.eat();
}
public void showHunt(Huntable h) {
// 表现为:晚期绑定
h.hunt();
}
}
Java 中任何一个普通方法都具有虚函数(多态性)的特征,如果不希望方法有虚函数的特征,可以使用关键词 final 来修饰。
虚方法与非虚方法
非虚方法:在编译器就确定了具体的调用版本的方法。
-
静态方法、私有方法、final 方法、实例构造器( this.xxx() )、父类方法(super.xxx())都是非虚方法。
-
其他方法都是虚方法。
多态性的前提:
- 类的继承关系
- 方法的重写
虚拟机调用方法的指令
- invokestatic: 调用静态方法(非虚方法)
- invokespecial:调用 \
方法,私有方法及父类方法(非虚方法)</font> - invokevirtual:调用所有虚方法(final 修饰方法除外,final 修饰方法 也是使用 invokevirtual 指令调用 )
- invokeinterface:调用接口方法
- invokedynamic:动态解析出需要调用的方法,然后执行(Java 7 新增的指令)
class Father {
public Father() {
System.out.println("father 的构造器");
}
public static void showStatic(String str) {
System.out.println("father " + str);
}
public final void showFinal() {
System.out.println("father show final");
}
public void showCommon() {
System.out.println("father 普通方法");
}
}
public class Son extends Father {
public Son() {
super();
}
public Son(int age) {
this();
}
// 不是重写父类的静态方法,因为静态方法不能被重写
public static void showStatic(String str) {
System.out.println("son " + str);
}
private final void showPrivate(String str) {
System.out.println("son private " + str);
}
public void show() {
// invokestatic 非虚方法:调用子类
showStatic("dyf");
// invokestatic 非虚方法:调用父类
super.showStatic("good");
// invokespecial 非虚方法:调用子类
showPrivate("hello");
// invokespecial 非虚方法:调用父类
super.showCommon();
// invokevirtual 非虚方法:调用父类
// 因为此方法声明有 final,不能被子类重写,
// 所以也认为此方法是非虚方法。
showFinal();
// invokevirtual 虚方法,调用父类
showCommon();
// invokevirtual 虚方法,调用父类
info();
MethodInterface in = null;
// invokeinterface 虚方法,借口调用
in.methodA();
}
public void info() {
}
public static void main(String[] args) {
Son son = new Son();
son.show();
}
}
interface MethodInterface {
void methodA();
}
方法重写的本质
重写方法查找步骤
- 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作 C。
- 在常量池中根据 C 名称相符的方法
- 如果找到了,则进行访问权限校验。
- 校验通过:返回方法的直接引用
- 校验不通过:返回 java.lang.IllegalAccessError
- 如果没找到:按照继承关系从下往上依次查找,并进行第 2 步查找并校验。
- 如果找到了,则进行访问权限校验。
- 如果最终没有找到合适的方法:则抛出 java.lang.AbstractMethodError 异常。
虚方法表
在面向对象编程中,会很频繁的使用到动态分派,如果每次动态分派的过程都要在类的方法元数据中搜索合适的方法,那么可能会影响到执行效率。因此,**为了提高性能**,JVM 采用在类的方法区建立一个**虚方法表(virtual method table)(非虚方法不会出现在表中)来实现。使用索引表来代替查找(空间换时间)。**
每个类中都有一个虚方法表,表中存放着各个虚方法的实际入口。
虚方法表会在类加载的链接阶段(解析)被创建并开始初始化,类的变量初始值准备完毕后,JVM 会把该类的方法表也初始化完毕。
类加载的链接阶段,解析这一步就是:将常量池内的符合引用转换为直接引用。
在 CockerSpaniel 类中
-
从 Dog 类继承类了 toString 方法
- 实现接口的方法:sayHello、sayGoodbye
- 从 Object 类中继承类其他方法
public interface Friendly {
void sayHello();
void sayGoodbye();
}
class Dog {
public void sayHello() {
}
@Override
public String toString() {
return "Dog";
}
}
class CockerSpaniel extends Dog implements Friendly {
@Override
public void sayHello() {
}
@Override
public void sayGoodbye() {
}
}
在 Cat 类中
-
自己方法是 eat,
-
重写Object 类的方法:finalize、toString。
-
实现接口的方法:sayHello、sayGoodbye
class Cat implements Friendly {
public void eat() {
}
@Override
public void sayHello() {
}
@Override
public void sayGoodbye() {
}
@Override
protected void finalize() {
}
@Override
public String toString() {
return "Cat";
}
}
方法返回地址(return Address)
方法返回地址:存储得是该方法的 PC 寄存器的值。
方法结束的方法,无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。
- 正常执行完成。调用者的 PC 计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。
- 出现未处理的异常,非正常退出,放回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
方法正常退出后,需要的返回指令
- ireturn:当返回值是 boolean、byte、char、short、int 类型时使用。
- lreturn:当返回值是 long 类型时使用。
- freturn:当返回值是 float 类型时使用。
- dreturn:当返回值是 double 类型时使用。
- areturn:当返回值是引用类型时使用。
- return:当返回值是 void 时使用,实例初始化方法、类和接口的初始化方法。
public class ReturnAddressTest {
public boolean methodBoolean() {
return false;
}
public byte methodByte() {
return 0;
}
public char methodChar() {
return 'a';
}
public short methodShort() {
return 0;
}
public int methodInt() {
return 0;
}
public long methodLong() {
return 0L;
}
public float methodFloat() {
return 0.0f;
}
public double methodDouble() {
return 0.0d;
}
public void methodVoid() {}
static {
int a = 1;
}
public static void main(String[] args) {}
public ReturnAddressTest() {}
}
下边是异常表:
- Start PC:需要异常处理的代码区域,起始行的行号
- End PC:需要异常处理的代码区域,终止行的行号
- Handler PC:发生异常后,需要处理的行号。
其他
栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。