0%

【反八股系列】一、为什么我们要学习java虚拟机栈的原理?

曾经的八股文:

xxx

现在的八股文:

xxx

为了找回对编程最初的乐趣

决定自制反八股系列的知识视频

本系列的三大宗旨:

拒绝死记硬背

拒绝管中窥豹

拒绝浅尝则止


对于jvm的虚拟机内存结构,大家应该都能背会有“2个堆”、“2个栈”、“1个计数器” 这种内容,就像下面这张图一样。 其中java的栈是虚拟机指令执行的关键。

那么,为什么我们要学习背后的这个虚拟机栈呢? 我的代码能跑起来不就了可以吗?

哦,是面试要用么?然而个人不希望这些内容成为新一代的“科技八股文”。

因此本系列希望先从为什么学习入手,再深入到更深层次的东西,,希望能带来的是长久的收获,而非短暂的记忆。


为什么我们要学习背后的虚拟机栈。

首先,你debug的时候,你都不知道这个是什么东西,瞎猜可不行,你要知道暂停时现在是个请什么情况, IDE上的东西到底是什么,他们对程序运行又有什么影响

[debug图片]

其次,虚拟机栈的内容是很多重要知识的前置知识点。 垃圾收集的GC-ROOT与栈有关, JIT优化和栈有关。如果你不知道,那么在遇到相关知识点,只会产生“这是啥”、“这又是啥”的连锁反应, 就有可能陷入背诵八股文的折磨却不知所以然的地步。

最后,java虚拟机栈为我们生动展示了 一个小心的迷你的CPU执行逻辑。

对于很多没有学习过计算机底层原理(例如CSAPP这本书) 的人来说, 是完全不知道计算机是如何执行机器码指令的。 而java虚拟机栈可以更好理解 指令是如何运行的, 虽然这个指令不是真正的机器码执行, 而是jvm字节码指令。 但是通过字节码指令, 我们可以快速对应到java中常见的各种操作。

这对于很多入门时直奔删减改查的同学来说, 是不可多得的学习底层的机会。

另外数据结构里学习的栈的知识也会在这里得以应用。


清楚了上述好处后,我们开始深入了解虚拟机栈的细节。

首先,栈帧是什么?

不需要去记忆概念,就记得我们调试时,框框里的每一行,就是一个栈帧。 可以看到除了栈顶的方法正在执行外, 其他行都仿佛静止了一般, 因此就像拍照时的一帧。

对于栈帧里有什么, 经典背诵4件套:

局部变量表,

操作数栈,

动态链接,

方法返回地址,

其实与上面这4样配合的,还有个虚拟机栈所使用的“程序计数器”,才共同实现了jvm指令的执行。

首先对局部变量表而言, 为什么要有这个东西?

5 4 3 2 1 .

因为我们声明的局部变量a、b、c等, 都需要有一个地方存放, 但局部变量只有这个方法中才会使用, 所以才会在栈帧中开辟局部连量表的空间。

那么,变量表有多大呢?

编译时指定定死了

为什么能定死?

因为编译器很聪明,通过分析代码,他就能知道到底这个方法要用几个变量。

为什么我们需要思考这个问题?因为我们要考虑我们写的代码,可能会带来多大的局部栈的消耗。

例如,我在一个for循环里反复定义同一个变量,那局部变量表是不是在无限增大?我是不是要提前加大栈的分配内存?

其实不需要, 因为编译器支持变量表的复用, 它会知道你在重复声明变量,所以实际字节码指令中,它会明确写下“这个变量继续放到前面那个槽”的指令, 从而覆盖使用了之前的槽。

你问我怎么分析的? 请阅读《编译原理》。

然后操作数栈又是干什么的? 如果要做a+b,我直接从变量表上取a的值和b的值,加起来不就好了?

直接让CPU取a和b的值拿去算完回来,不就好了?

那我如果是 a + b*c呢

b*c的值放哪里?

如果是a+b*(c+d)呢?

这时候如果你学习过数据结构里栈的应用 ,就会知道 模拟一个计算器,往往需要一个栈。

而操作数栈就是这个作用。

当你学习jvm指令时,就会看到有专门的指令就是取栈顶或者把值推送到栈顶的指令。

这样做加法的时候,也就不用关心变量的地址了,只要你把栈顶的值存好,我直接拿去加就行。

那么动态链接又是个什么玩意?

就这么说, 你怎么知道这个方法此时要做哪些动作?

肯定有一段代码区(即jvm指令),让我一条条执行对吧?

那么这个代码区放哪呢?我总需要知道一个地址, 因此,动态链接,就是这个方法代码的位置。

那干嘛叫动态链接这么抽象啊?

因为有的方法往往是等运行的时候才知道地址, 所以统一就叫做动态链接了,这就是未来会提到的java多态的核心本质而。

那么返回地址比较好理解,方法执行完成, 返回上一层方法执行的位置。

等等,这个地址是实际的地址吗?例如0x2313212这种?

应该是把,不然怎么叫地址?

但是我已经有动态链接标记的方法指令的起始位置了,你为什么还要整这么长?

哦,那就用偏移值就可以了!

那现在再看,程序计数器,代表的是什么

很显然和方法返回地址一样, 也是指令偏移值, 这样通过动态链接 + 计数器, 就能知道这个方法当前执行到什么位置了, 即使发生了线程切换或者方法返回, 都不用担心了~!

那么再深度扩展一下,根据以上理解,是否能清楚下面这个的原因代码的原因

当你知道远离后,你就不需要记忆这种情况,而是一想背后的实现,就能明白这么写是不对的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test { 
public static void main(String[] args) {
System.out.println("value : " + getValue());
}

public static int getValue() {
int i = 1;
try {
return i;
} finally {
i++;
}
}
}

就是在执行 finally 语句块之前,try 或者 catch 语句块会保留其返回值到本地变量表(Local Variable Table)中。待 subroutine 执行完毕之后,再恢复保留的返回值到操作数栈中,然后通过 return 或者 throw 语句将其返回给该方法的调用者(invoker)

因为你要处理finally块时, 操作数栈要腾出来给finally使用, 因此返回值不能放在这,所以整了个局部变量ret放进去, 执行完成返回来。

https://www.jb51.net/article/74771.htm