快捷搜索:

深扒JVM,对它进行“开膛破肚”式解析!

1. 打怪升级,你绕不开JVM

JVM,对Java程序员进阶而言,是一个绝对绕不开,也不能绕开的话题。

在你打怪升级、进阶蜕变的路上,势必会遇到项目上线中各种OOM、GC等问题,此时JVM的功底就至关重要了。

这篇文章,我们将从自己写的代码运行角度出发,将JVM“开膛破肚”。看看我们写的代码,在JVM的各区域都干了些啥?

多说一句,对于Java工程师的面试,JVM也是必问的一环,因此无论从面试还是实际工作,你都很有必要夯实自己的JVM功底。

扯得有点远,赶紧拉回来,马上进入正题!

2. JVM 区域划分

jvm的区域,大致有以下几块:

程序计数器

虚拟机栈

方法区

本地方法栈

接下来我们将JVM当成一个生物体,上述部分就是其不同器官。我们将从自己写的Java代码如何通过JVM来运行这一角度,来分析JVM里这些“器官”是如何支撑我们的Java代码跑起来的。

3. 程序计数器

假设我们有如下的一个类,就是最最基本的一个HelloWorld而已:

public class HelloWorld {

public static void main(String[] args) {

System.out.println("Hello World");

}

}

上面那段代码首先会存在于 “.java” 后缀的文件里,这个文件就是java源代码文件。

但是这个文件是面向我们程序员的,计算机是看不懂这段代码的。

所以此时就得通过编译器,把“.java”后缀的源代码文件编译为“.class”后缀的字节码文件。

这个“.class”后缀的字节码文件里,存放的就是对你写出来的代码编译好的字节码了。

字节码才是计算器可以理解的一种语言,而不是我们写出来的那一堆代码。这个字节码看起来大概是下面这样的:

注:这段字节码并不是完全对照着HelloWorld那个类来写的,就是给一段示例,让大家知道“.java”翻译成的“.class”是大概什么样子的。

大概给各位解释一下,图中比如“0: aload_0”这样的就是“字节码指令”,他对应了一条条机器指令,计算机只有读到这种机器码指令,才知道具体应该要干什么。

比如字节码指令可能会让计算机从内存里读取某个数据,或者把某个数据写入到内存里去。各种各样的指令,会指示计算机去干各种各样的事情。

所以到这里,大家首先明白的第一点:Java代码是会被翻译成字节码的,不同字节码指令指挥计算机干不同的事情。

那么在执行字节码指令的时候,JVM里的程序计数器作用是啥呢?

答案是:用来记录每个线程当前执行的字节码指令的位置,即记录当前线程目前执行到了哪一条字节码指令。

在实际中,会有多个线程并发执行各种不同的代码,所以每个线程都有自己的程序计数器,专门记录当前线程目前执行到了哪一条字节码指令。

下图更加清晰的展示出了他们之间的关系。

4. Java虚拟机栈

好,我们接着来看。大家都清楚,Java代码执行时,一定是线程来执行某个方法中的代码。就算是最基础的 HelloWorld ,也会有一个main线程来执行main方法里的代码。

在方法里,经常会定义一些方法内的局部变量,比如下面这样,在方法里定义了一个局部变量“name”。

public void sayHello() {

String name = "hello";

}

所以咱们JVM的这个“器官”就要出场了,JVM必须有一块区域是来保存每个方法内的局部变量等等数据的,这个区域就是Java虚拟机栈

为什么需要这个区域?因为每个线程都会去执行各种方法的代码,方法内还会嵌套调用其他的方法,所以每个线程都要有自己的Java虚拟机栈。

如果线程执行了一个方法,那么就会为这个方法调用创建对应的一个栈帧

栈帧里就有这个方法的局部变量表 、操作数栈、动态链接、方法出口等东西。这里别的东西不太好理解,后面我们再通过其他文章详细阐述,这里先理解一个局部变量就可以。

回到上面的例子,比如一个线程调用了上面写的“sayHello”方法,那么就会为“sayHello”方法创建一个栈帧,压入线程自己的Java虚拟机栈里面去。

在栈帧的局部变量表里就会有“name”这个局部变量,下图展示了这个过程。

接着如果“sayHello”方法调用了另外一个“greeting”方法 ,比如下面那样的代码:

这时会给“greeting”方法又创建一个栈帧,压入线程的Java虚拟机栈。

想想为啥会这样?因为sayHello方法里开始执行greeting方法了,而且greeting方法的栈帧的局部变量表里有一个“greet”变量,它是greeting方法的局部变量。

下图展示了这个过程:

您可能还会对下面的文章感兴趣: