Java 虚拟机基础(二)运行时数据区

发布于 2020-05-12  163 次阅读


三、运行时数据区(Run-Time Data Areas)

接上一篇 Java 虚拟机基础(一)类的加载机制

如果在上一篇没看懂运行是数据区到底是啥,那这篇文章就要解释一下这货到底是个啥了。

官方文档地址:2.5. Run-Time Data Areas

The Java Virtual Machine defines various run-time data areas that are used during execution of a program. Some of these data areas are created on Java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits. Other data areas are per thread. Per-thread data areas are created when a thread is created and destroyed when the thread exits.

官方文档中对运行时数据区的介绍非常的潦草。

简译:JVM 定义了在程序运行时所需要的各种运行时数据区。一部分数据区创建于 JVM 启动时,JVM停止时销毁,一部分在 Java线程 创建时创建,在Java线程结束时销毁。

其实,它的名字已经很清楚了,就是在程序运行时存放数据的区域。在官方文档中,运行时数据区这一章有6个小节。

运行时数据区

上面这张图应该很常见了吧,但是有趣的是,这张图是错误的!为什么?从这张图上看,似乎每一个区域有是相互独立的,但是实际上并不是。接下来将会讲解这些部分。

3.1 方法区(Method Area)

方法区,顾名思义,就是存放方法的区域。但是这可并不是这么简单,为啥呢?对于 JVM 而言,它该如何存方法呢?如果像普通代码一样存,那就是个快点的磁盘,没啥用。那它存的是啥呢?

其实,在类加载时,Java 代码早就被编译成 Java字节码了,而方法区存的也不是字节码,而是指令,系统指令。

而正如一般考试题一样,越明显的东西是越不常考的,而方法区最常 “考” 的也不是它存储的是方法,而是另一些东西。

方法区除了存储方法,还打杂存些别的东西?存啥呢?

大家先想一下,方法是写在哪里的?当然是  !在 Java 中无论是什么方法,就算是 main 方法也要写在类里!那方法区显然不应该只存方法,而应该连类也一起存!所以对于方法区而言,它存的可不只是方法,它还要存储类的结构,当然,类的常量和静态变量也会被存放在这里,毕竟它们也是类的结构的一部分。

了解到这,那很容易明白,方法区显然是可以被多个线程使用的,例如:你不能让每个线程都维护个自己的 String 类吧。这些相同且常用的的应该单独维护,然后所有线程共享才是合理的。所以其生命周期也自然是伴随着整个 JVM 生命周期。

1)方法区是线程共享的,在 JVM 启动时创建,在 JVM 停止时销毁;
2)方法区用于存储已被 JVM 加载的的 信息常量静态变量、及方法编译后的系统指令
3)当方法区所分配的内存不足时,会抛出 OutOfMemoryError 异常;

3.2 堆(Heap)

对于常接解 Java 的程序猿,无论是否看过 JVM 相关的东西,那都绝对会听说过 堆和栈。当然,这个堆指的可不只是数据结构中的堆。

The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.

在官方文档中,对这个堆的作用描述的很清楚。

简译:Java虚拟机具有一个在所有Java线程之间共享的。堆是运行时数据区,从中分配所有类的实例数组的内存。

清楚的不得了,它就是用来存类的实例数组的,我们 new MyClass() 的对象都会放到这里。

1)堆是线程共享的,生命周期伴随 JVM 的生命周期;
2)Java对象实例数组内存空间,都是在堆上分配的;

3)当堆所分配的内存不足时,会抛出 OutOfMemoryError 异常;

3.3 运行时常量池(Run-Time Constant Pool)

The Java Virtual Machine maintains a per-type constant pool , a run-time data structure that serves many of the purposes of the symbol table of a conventional programming language implementation.

简译:JVM 为每个类型都维护了一个常量池,这是一种运行时数据结构,它可以满足一般编程一语言常量表的功能。

Each run-time constant pool is allocated from the Java Virtual Machine's method area . The run-time constant pool for a class or interface is constructed when the class or interface is created by the Java Virtual Machine.

简译:每个运行时常量池都是从方法区分配的。当 JVM 创建类或接口时,会为其创建它的运行时常量池。

上面俩段话对运行时常量池的描述已经比较清楚了。它主要是用来实现常量表的。

常量我们知道,那常量表是啥?

常量表就是常量的符号引用实际引用的对照表。(有关符号引用与实际引用详见上一篇

如果还不理解,那我们举个栗子:

在 Java 中,字符串类型不是基本类型,而是对象。这就意味着,字符串类型变量是引用(指针)

如下面的代码,字符串 s1 s2 s3 的地址互不相同。

结果全为 false

我们似乎可以理解,因为 s1 s2 s3 各创建一个新对象。

结果为 true

这段代码中,为什么 s2 这个拼接对象和 s1 是同一块地址?

答案其实在官方文档中就已经说明了。

The Java programming language requires that identical string literals (that is, literals that contain the same sequence of code points) must refer to the same instance of class String

简译:在 Java 编程语言要求,每个相同的 字符串文字 必需引用相同的 String 实例 。

很好理解,字符串文字 指的是用 “双引号” 直接写在代码中的字符串。而 String 是个对象,它存的可不仅仅是一个单纯的 char 型数组!这就意味着 String 要有个自己的空间来存这些东西!

所以,对于 JVM 而言,“Hello Word” 只不过应该是一个指向某个String实例的引用(指针)而以!而由谁来记录这些呢?

答案乎之欲出了,运行时常量池!

运行进常量池

由于 "abc" 和 "a"+"b"+"c" 是相同的 字符文字 所以其会指向相同的 String 实例。而 new 运算符会创建新的 String 实例,所以上面俩段代码的结果就变的清楚了。

而 "abc" 就是 符号引用,而 地址:0xF001(假设) 就是它的实际引用。

所以运行时常量池就是一个小表格(Map),存着 符号引用 和 实际引用 的对照表。

1)JVM会为每个类型都创建一个运行时常量池;
2)运行时常量池的空间是在方法区开辟的,可以说是方法区的一部分;
3)与方法区相同,线程共享,生命周期是JVM周期;

3.4 虚拟机栈(JVM Stacks)

Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread. 

简译:每个Java虚拟机线程都有一个私有Java虚拟机堆栈,与该线程同时创建。

A Java Virtual Machine stack is analogous to the stack of a conventional language such as C: it holds local variables and partial results, and plays a part in method invocation and return.

简译:JVM 栈类似于常规语言(例如C)的栈:它用于保存局部变量和部分结果,并在方法调用和返回中起作用。

第二段不太好理解。说人话,其实栈的最主要作用是用于方法运行。每调用一个方法,就会将栈内压入一个栈帧,方法结束在弹出。举个栗子:

方法

上面的代码的运行如下图一样:

栈的工作流程

这样看起来是不是清楚多了,但是还有个问题。压入栈的到底是啥呢?难到是方法本身么?显然不是。其实刚才已经提到了。

栈帧(Frame):每个栈帧对应一个调用的方法,可以理解为一个运行的空间。而栈帧里主要存储 局部变量表、操作数栈、动态链接 这几大模块。

局部变量表(Local Variables):用于存储方法用的局部变量。
操作数栈(Operand Stack):在栈帧中还有一个小栈,这个栈在初始的时候是空的,主要用于计算。也就是操作数栈。其运行的话,可以举个栗子。例如某方法要计算 a+b ,那则先将 变量 a 入栈,在将变量 b 入栈,然后进行相加,在将 a 和 b 弹出,将结果入栈。当然,这些都是使用 Java 字节码指令完成的。对于其它的操作也是类似如此,只是略为复杂。
动态链接( Dynamic Linking):当方法使用到常量的时候,动态链接就会指向运行时常量池中所要用的方法。这么也是为了方便使用。

1)JVM栈是线程私有的,生命周期与线程同步;
2)被线程执行的方法,为栈帧。一个方法对应一个栈帧;
3)如果方法调用太深,或无限递规使栈空间不足,会报 StackOverflowError 异常;

3.4 本地方法栈(Native Method Stacks)

如果你查看过 Java 源码,那肯定会发现 Java 源码中有很多地方使用了 native 关键字。这个关键字主要用于调用底层C语言。因为Java中很多底层是用 C语言 实现的,比如 hashcode 方法,所以Java在调用C的代码时候要有标注。

但C语言的可不是跑在 JVM 上的。那自然不能用 JVM 栈,所以JVM还有个专门用于调用 C代码 的栈,也就是本地方法栈。 

对于普通方法调用本地方法时,那就是用动态链接的方式。如下图:

动态链接示意图

3.1 程序计数器(PC Register)

对于有些计算机组成原理基础的应该都知道,我们以为计算机在多线程运作,但实际上,cpu只不过是一个线程运作一点时间,在切换另一个线程在动作一点时间,就这样疯狂切换。但这个时间是以纳秒(10 ^ -9 秒)为单位的,所以对于人而言,这些线程之前就是同时运行的。

但这也就出现一个问题,如果我这个线程跑到一半,到了时间片,可以程序没完成,根据规定,也必需撤出 CPU,那下次在轮到这个线程时,它怎么知道自己上次执行到哪?

这就是程序计数器的事情了,它的主要工作就是计录线程已经执行到哪了。

那回到最开始有关运行时数据区的描述中,程序计数器是计录线程执行到哪,肯定是线程私有的,所以它的生命周期也就是线程的开始与结束。

1)程序计数器是线程私有的,与Java线程同出生,同销毁;
2)程序计数器是用来记录线程所执行的位置;
3)如果当前执行到 native 类方法时,程序计数器为空;


欢迎来到欧喵的博客,喜欢就看看吧