曾经的八股文:
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 | public class Test { |
就是在执行 finally 语句块之前,try 或者 catch 语句块会保留其返回值到本地变量表(Local Variable Table)中。待 subroutine 执行完毕之后,再恢复保留的返回值到操作数栈中,然后通过 return 或者 throw 语句将其返回给该方法的调用者(invoker)
因为你要处理finally块时, 操作数栈要腾出来给finally使用, 因此返回值不能放在这,所以整了个局部变量ret放进去, 执行完成返回来。