运行时栈帧结构
栈帧是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态链接、和方法返回地址等信息。
局部变量表
局部变量表的容量以变量槽为最小单位。每个变量槽应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress(可忽略,现在已经很少见了)。reference类型表示对一个对象实例的引用,即根据引用直接或间接的查到对象在java堆中的数据存放的起始地址、索引或对象所属数据类型在方法区中的存储的类型信息。上述类型均占用一个变量槽。long和double占用两个连续的变量槽。
示例1
实例方法(没有被static修饰的方法)局部变量表第0位是this。
public void soltTest() {
byte i = 15;
}

public void soltTest() {
long i = 15;
}

为了尽可能节省栈帧耗用的内存空间,局部变量表中的变量槽是可以重复使用的,方法体中定义的变量作用域没有全部覆盖整个方法,此变量占用的变量槽是可以被重复利用的。
注意:示例需设置虚拟机参数“-verbose:gc”
示例2
public static void main(String[] args) {
byte[] bytes = new byte[64 * 1024 * 1024];
System.gc();
}
控制台输出:
[GC (System.gc()) 72123K->66690K(251392K), 0.0177919 secs]
[Full GC (System.gc()) 66690K->66523K(251392K), 0.0042184 secs]
示例3
public static void main(String[] args) {
{
byte[] bytes = new byte[64 * 1024 * 1024];
}
System.gc();
}
控制台输出:
[GC (System.gc()) 72123K->66674K(251392K), 0.0007715 secs]
[Full GC (System.gc()) 66674K->66523K(251392K), 0.0041207 secs]
示例4
public static void main(String[] args) {
{
byte[] bytes = new byte[64 * 1024 * 1024];
}
int a = 0;
System.gc();
}
控制台输出:
[GC (System.gc()) 72123K->66690K(251392K), 0.0009232 secs]
[Full GC (System.gc()) 66690K->987K(251392K), 0.0042235 secs]
结论:变量槽在没有复用时,不GC
操作数栈
操作数栈是后进先出栈。个人感觉操作数栈是局部变量表与方法区中间的数据中转站。
方法调用
方法调用不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定调用哪个方法,暂时还未涉及方法内部的具体运行过程 。
解析
调用方法在程序代码写好、编译器进行编译那一刻就确定下来了,这类方法的调用被称为解析。在Java中符合“编译期可知,运行期不可变”要求的方法主要有静态方法和私有方法两大类。
调用不同类型的方法,字节码指令集里面设计了不同的指令。分别是:
- invokestatic:用于调用静态方法。
- invokespecial:用于调用实例构造器
()方法,私有方法和父类中的方法。 - invokevirtual:用于调用所有的虚方法。
- invokeinterface:用于调用接口方法,在运行时再确定一个实现该接口的对象。
- invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。
静态方法、私有方法、实例构造器、父类方法及final修饰的方法会在类加载的时候就可以把符号引用解析为该方法的直接引用。这些方法统称为”非虚方法“。
方法静态解析演示
/**
* @author Wang Chinda
* @date 2020/3/31
* @see
* @since 1.0
*/
public class StaticResolution {
public static void sayHello() {
System.out.println("Hello world");
}
public static void main(String[] args) {
StaticResolution.sayHello();
}
}
指令:
0 getstatic #2 <java/lang/System.out>
3 ldc #3 <Hello world>
5 invokevirtual #4 <java/io/PrintStream.println>
8 return

分派
静态分派
所有依赖静态类型来决定调用哪个方法的分派动作,都称为静态分派。静态分派的最典型应用表现就是方法重载。
方法静态分派演示
/**
* 控制台打印
* hello, guy!
* hello, guy!
* @author Wang Chinda
* @date 2020/3/31
* @see
* @since 1.0
*/
public class StaticDispatch {
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
public void sayHello(Human human) {
System.out.println("hello, guy!");
}
public void sayHello(Man man) {
System.out.println("hello, man");
}
public void sayHello(Woman woman) {
System.out.println("Hello, women");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sd = new StaticDispatch();
sd.sayHello(man);
sd.sayHello(woman);
}
}
上面代码中的“Human”称为变量的“静态类型”,而后面的“Man”称为变量的”实际类型“。静态类型和实际类型再程序中都可能会发生变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型时在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。
// 实际类型变化
Human human = (new Random()).nextBoolean() ? new Man() : new Woman();
// 静态类型变化
sd.sayHello((Man) man); // 控制台打印 hello, man
sd.sayHello((Woman) woman); // 控制台打印 Hello, women
“静态类型”在代码被编译器编译之后,就已经确定类型引用,但是实际类型只有在程序运行时,才可以确定具体的引用类型。即,调用哪个方法以句柄所属类型匹配方法所携带的形参。
注意:编译器虽然能确定方法的重载版本,但很多情况下这个重载版本并不是唯一的,程序往往只能确定一个“相对更合适的”方法调用。
重载方法匹配优先级
/**
* @author Wang Chinda
* @date 2020/4/2
* @see
* @since 1.0
*/
public class OverLoad {
public static void sayHello(Object arg) {
System.out.println("hello Object!");
}
public static void sayHello(int arg) {
System.out.println("Hello int!");
}
public static void sayHello(long arg) {
System.out.println("hello long");
}
public static void sayHello(Character character) {
System.out.println("hello character");
}
public static void sayHello(char arg) {
System.out.println("hello char");
}
public static void sayHello(char... arg) {
System.out.println("hello char...");
}
public static void sayHello(Serializable arg) {
System.out.println("Hello serializable");
}
public static void main(String[] args) {
sayHello('a');
}
}
上面代码控制台打印:
hello char
'a'是char类型数据,最优匹配的当然是char类型形参方法调用。注释掉sayHello(char arg)方法,控制台打印:
Hello int!
这时发生了一次自动类型转换,'a'除了可以代表一个字符,还可以代表数字97(字符'a'的Unicode数值为十进制数字97),因此参数类型为int的重载方法最合适。我们继续注释掉sayHello(int arg)方法,控制台打印:
hello long
这时发生了两次自动类型转换,'a'转换为int的97之后,进一步转型为long的97L,此时参数类型为long的重载方法最合适。不过自动转型还会多次发生,按照char>int>long>float>double的顺序自动转型。我们继续注释掉sayHello(long arg)方法,控制台打印:
hello character
这时发生了一次自动装箱,'a'被包装为它的封装类型java.lang.Character,此时参数类型为Character的重载方法最合适。我们继续注释掉sayHello(Character character)方法,控制台打印:
Hello serializable
之所以输出Hello serializable是因为
java.lang.Character implements java.io.Serializable, Comparable<Character>
此时若是同时存在sayHello(Comparable arg)方法, 编译会抛出模糊的方法调用错误,并拒绝编译。
Ambiguous method call. Both sayHello (Serializable) in OverLoad and sayHello (Comparable) in OverLoad match
我们继续注释掉sayHello(Serializable arg)方法,控制台打印:
hello Object!
这时是char装箱后转型为父类,如果有多层级父类,越接近的优先级越高。我们继续注释掉sayHello(Object arg)方法,控制台打印:
hello char...
可见边长参数的重载优先级是最低的。
动态分派
在运行期间根据实际类型确定调用哪个目标方法的分派过程称为动态分派。
方法动态分派演示
/**
* 控制台打印:
* man say hello
* woman say hello
* woman say hello
*
* @author Wang Chinda
* @date 2020/4/2
* @see
* @since 1.0
*/
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
}
指令展示
0 new #2 <com/chinda/invoke/DynamicDispatch$Man>
3 dup
4 invokespecial #3 <com/chinda/invoke/DynamicDispatch$Man.<init>>
7 astore_1
8 new #4 <com/chinda/invoke/DynamicDispatch$Woman>
11 dup
12 invokespecial #5 <com/chinda/invoke/DynamicDispatch$Woman.<init>>
15 astore_2
16 aload_1
17 invokevirtual #6 <com/chinda/invoke/DynamicDispatch$Human.sayHello>
20 aload_2
21 invokevirtual #6 <com/chinda/invoke/DynamicDispatch$Human.sayHello>
24 new #4 <com/chinda/invoke/DynamicDispatch$Woman>
27 dup
28 invokespecial #5 <com/chinda/invoke/DynamicDispatch$Woman.<init>>
31 astore_1
32 aload_1
33 invokevirtual #6 <com/chinda/invoke/DynamicDispatch$Human.sayHello>
36 return
invokevirtual指令的运行解析过程:
- 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
- 如果在类型C中找到与常量中的描述符与简单名称都相符的方法,则进行访问权限的校验,如果通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。
- 否则,按照继承关系从下往上一次对C的各个父类进行第二步的搜索和验证过程。
- 如果之中没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
多态性的根源在于虚方法调用指令invokevirtual的执行逻辑,只会对方法有效,对字段是无效的,因为字段不使用这条指令。
字段没有多态性演示
/**
* 控制台打印
* I am Son, I have $0
* I am Son, I have $4
* This gay has $2
*
* @author Wang Chinda
* @date 2020/4/2
* @see
* @since 1.0
*/
public class FieldHasNoPolymorphic {
static class Father {
public int money = 1;
public Father() {
money = 2;
showMeTheMoney();
}
public void showMeTheMoney() {
System.out.println("I am Father, I have $" + money);
}
}
static class Son extends Father {
public int money = 3;
public Son() {
money = 4;
showMeTheMoney();
}
@Override
public void showMeTheMoney() {
System.out.println("I am Son, I have $" + money);
}
}
public static void main(String[] args) {
Father gay = new Son();
System.out.println("This gay has $" + gay.money);
}
}
字类初始化时,首先触发父类初始化,在父类初始化时,调用showMeTheMoney()虚方法,实际执行的是Son::showMeTheMoney()方法,此时子类还没有初始化,所以money值为0。初始化完父类初始化子类,此时money为4。执行打印时,调用的是父类中的属性,所以值为2。
单分派与多分派
方法的接收者和方法的参数统称为方法的宗量。根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。
单分派和多分派演示
/**
* 控制台打印
* father choose 360
* son choose qq
* @author Wang Chinda
* @date 2020/4/2
* @see
* @since 1.0
*/
public class Dispatch {
static class QQ{}
static class _360{}
public static class Father {
public void hardChoice(QQ arg) {
System.out.println("father choose qq");
}
public void hardChoice(_360 arg) {
System.out.println("father choose 360");
}
}
public static class Son extends Father {
@Override
public void hardChoice(QQ arg) {
System.out.println("son choose qq");
}
@Override
public void hardChoice(_360 arg) {
System.out.println("son choose 360");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.hardChoice(new _360());
son.hardChoice(new QQ());
}
}
指令演示
0 new #2 <com/chinda/invoke/Dispatch$Father>
3 dup
4 invokespecial #3 <com/chinda/invoke/Dispatch$Father.<init>>
7 astore_1
8 new #4 <com/chinda/invoke/Dispatch$Son>
11 dup
12 invokespecial #5 <com/chinda/invoke/Dispatch$Son.<init>>
15 astore_2
16 aload_1
17 new #6 <com/chinda/invoke/Dispatch$_360>
20 dup
21 invokespecial #7 <com/chinda/invoke/Dispatch$_360.<init>>
24 invokevirtual #8 <com/chinda/invoke/Dispatch$Father.hardChoice>
27 aload_2
28 new #9 <com/chinda/invoke/Dispatch$QQ>
31 dup
32 invokespecial #10 <com/chinda/invoke/Dispatch$QQ.<init>>
35 invokevirtual #11 <com/chinda/invoke/Dispatch$Father.hardChoice>
38 return
注意:invokevirtual #11 <com/chinda/invoke/Dispatch$Father.hardChoice> 静态分派指向的是Father::hardChoice()方法,但动态分派时,将方法指向到实际类型中的目标方法,即Son::hardChoice()。
基于栈的解释器执行过程
代码演示一
public int calc() {
int a = 100;
int b = 200;
int c = 300;
return (a + b) * c;
}
指令集
0 bipush 100
2 istore_1
3 sipush 200
6 istore_2
7 sipush 300
10 istore_3
11 iload_1
12 iload_2
13 iadd
14 iload_3
15 imul
16 ireturn
局部变量表、操作数栈深度
指令集概念模型
代码演示二
public void inc() {
int i = 1;
i = i++;
int j = i++;
int k = i + ++i * i++;
}
指令集
0 iconst_1
1 istore_1
2 iload_1
3 iinc 1 by 1
6 istore_1
7 iload_1
8 iinc 1 by 1
11 istore_2
12 iload_1
13 iinc 1 by 1
16 iload_1
17 iload_1
18 iinc 1 by 1
21 imul
22 iadd
23 istore_3
24 return
局部变量表、操作数栈深度
指令集概念模型
虚拟机最终会对执行过程做出一些列优化来提高性能,实际的运作过程会和概念模型差距非常大,产生差距的原因时虚拟机中解析器和即时编译器都会对输入的字节码进行优化,即使解释器中也不是按照字节码指令去逐条执行的。