深入浅出 Java虚拟机(2)之运行时数据区
在上一期《 Java虚拟机(1)之类加载器》中,我们留下了一个非常重要的问题。
类加载器是加载类的,但类究竟是被加载到JVM的哪里去了呢?
它就是我们这一期的主角。JVM的存储模块,运行时数据区。
一.名词解释
运行时数据区,可以拆分成两个词,即:
- 运行时
- 数据区
名词 | 解释 |
---|---|
运行时 | 运行时,运行时,它是指谁在运行的时候?其实是JVM运行时,更通常的情况下,应该是控制模块,也就是字节码执行引擎运行的时候。 |
数据区 | 数据区,强调了其存在的作用。即,它应该是提供给JVM执行指令后存取数据的内存区域。 |
因此,运行时数据区,是为字节码执行引擎和类装载子系统提供存取内存数据的服务的。
所以,我们想要了解运行时数据区如何设计的,为啥要这样设计,我们就不得不提,字节码执行引擎到底是如何工作的。
二.字节码执行引擎工作流程
1.方法区
首先,用户必须显示的指定要执行的类,JVM才开始工作。
//编译Hello.java,生成class文件
javac Hello.java
//运行Hello,并传入param0,param1,param2 这3个参数
java Hello param0 param1 param2
java Hello,这条命令就是指定要加载执行Hello.class文件。
根据之前的知识,类加载器会将该class文件载入运行时数据区中。
然后呢?
JVM就会调用它的main方法。
此时,如果你是字节码执行引擎,你会怎么做?
你当然要找到运行时数据区里找到这个main方法对象,然后调用执行。
因此,运行时数据区里需要有一个专门的区域,来存放方法对象。
我们把这个区域称之为方法区。
PS1: 通过该区域,字节码执行引擎可以根据方法的符号引用,来找到方法的直接引用。
PS2: 类的许多信息也是被存储在这个区域
方法区只是一种概念,方法区有两种经典的实现方式。
- java7及以前,使用永久代来作为方法区
- java8及以后,使用元空间取代永久代来作为方法区
但是,它们有点不同
区别 | 永久代 | 元空间 |
---|---|---|
内存使用 | JVM内存 | 本地内存 |
字面量及类的静态变量 | 存在于方法区中 | 移动到其他地方了 |
2.程序计数器
既然,程序开始运行了,那么,我们就需要一块区域来记录程序当前运行的指令地址。
所以程序计数器就诞生了。
为啥说程序计数器需要一块区域,而不是说需要一个变量呢?
因为,在多线程的环境下,每个线程都需要记录自己当前运行的位置。
用更深奥的话来讲,程序计数器是线程私有的。
即对于每一个线程,都需要一个程序计数器。
我们在考虑运行时数据区的每块内容的时候,我们都会考虑,这块区域是不是线程私有的。
这对我们理解这块区域非常重要。
3.虚拟机栈
此时,main方法就被运行了。
main方法是程序的入口,我们称之为主线程。
在程序运行过程中,也可能会产生许多其他线程。
那么在运行时数据区中也需要有一块区域来存储它们,我们将这块区域称之为虚拟机栈。
注意:虚拟机栈也是线程私有的。即每一个线程都会对应一个栈
为啥要用栈这种数据结构?
因为方法调用的过程,与栈这种数据结构真的是天作之合。
方法在链式调用的过程中,先调用的方法往往是最后一个返回的。
栈在存取数据的过程中,先入栈的元素往往是最后一个出栈的。
举个例子:
比如方法main调用方法A,方法A调用方法B,方法B调用方法C。
那么C往往是最先结束的。而B次之,而A次次之,main则是最后结束的。
所以,同个线程内每当一个方法被调用的时候,就有东西被被压入栈中。当方法执行完毕,这个东西就出栈销毁。
这个东西是啥?换句话说,栈里存储的元素是啥?
我们给它取个名字,就叫栈帧。它其实就是和运行时的方法对应。
比如,递归的时候,不断调用同一个方法,就有相同方法的不同栈帧,被压入栈内。
栈帧有啥用?
栈帧,可以记录方法运行过程中的一些信息。方便字节码执行引擎运行的。比如:
-
局部变量表
-
操作数栈
-
其他控制信息
信息 | 功能 |
---|---|
局部变量表 | 当局部变量被赋值的时候,那么栈帧内的局部变量表就可以做相应的记录。 |
操作数栈 | 当要进行四则运算的时候,那么被运算的数,就会被丢入操作数栈中。等待计算处理 |
其他控制信息 | — |
4.堆
然而事实上,局部变量表存放不同数据类型的方式也不太一样,比如下面这段代码:
public class Demo {
public static void main(String[] args) {
int a = 3;
Object b = new Object();
}
}
其中局部变量表中的a,b都是局部变量,但是a,b所指向的数据则是不同的数据类型。
其中,3是基本数据类型,而new Object()是复合数据类型。
事实上,复合数据类型的数据并不会被直接存放在虚拟机栈中,因为其大小难以确定,并且可以动态变化。
而基本数据类型的大小都是固定的,可以被存放在栈中。
所以,在int a = 3;的过程中,3是被放入栈内的。
而new Object()的数据本身不应该被放入栈内。
那复合数据类型不放入栈中,运行时数据区就亟待一个新的内存区域,来存放这种复合的数据类型的数据。
我们将这个区域,称之为堆。
专门划出这个区域,具有非常深远的意义。
除了可以将上面的问题迎刃而解,并且还带来了其他好处。
其中,最显而易见的就是方便统一进行垃圾回收。
然而,堆中也是被做了非常详细的划分。可以移步下一期《深入浅出java虚拟机(3)之堆与垃圾回收》
这样,实际对象被保存在堆中。而局部变量进行复合数据类型的赋值的时候,栈的栈帧的局部变量表里存放的则是实际对象在堆中的地址。
5.本地方法栈
与虚拟机栈结构类似,唯一的区别是它是为JVM执行本地方法提供服务,也就是被native关键字标记的方法。本地方法,本身并不是java中方法。它往往是C或C++的代码,给Java调用其他语言的类库提供了帮助。
举个例子,Java创建线程的时候,就会使用到本地方法。因为java本身是没有能力创建线程的。
三.总结
通过本章,你大致了解了
- class文件究竟被加载到哪里去了?
- 运行时数据区到底分为哪几块,都是干什么的?
- 运行时数据区中哪几块是线程私有的,哪几块不是?
- 栈帧里有哪些信息,每块内容是干什么的?