JVM运行时数据区是什么?是干什么的?如果你有兴趣了解,请看本章内容。
下面这张图给我们展示出了JVM运行时数据区的结构,我把它分为两个部分:数据+指令,我们先来看看各个子模块的作用
- 程序计数器:指向当前线程正在执行的字节码指令的地址(行号)。它是线程私有的,独享的(为什么?)
那么为什么要有程序计数器?
原因:因为java的最小执行单位是线程,而线程执行指令最终还是要落在操作系统层面,操作系统层面即要在CPU上运行。在CPU上运行指令有一个不得不考虑的不稳定的因素-调度策略。
调度策略是基于时间片的,所谓调度策略,举个例子说明:我开启了一个线程,占用了CPU的资源在看电影,突然有人要和我开视频聊天,那么我开启了视频,即开启了另外一个线程,此时看电影这个线程被挂起。当聊天结束后,我又接着看我的电影,那么此时就要有一个能记录之前看电影这个线程的执行地址(行号),这样我才能接着之前继续看。这个可以帮助线程记录执行位置的东东就是程序计数器。(线程不具备记忆功能,不能记住自己的执行位置,因此“记忆”这个工作就得程序计数器去做)。
现在你也就能明白为什么它是线程私有的,独享的了吧。每个线程都有一个自己的程序计数器,用于记录本线程的执行地址。
- 虚拟机栈:存储当前线程运行方法时所需要的数据、指令、返回地址。
线程是一个执行者,它只负责做,不负责存储,因此线程在运行方法时所需要的数据、指令、返回地址就必须被存储起来,而存储这些数据的就是虚拟机栈。
虚拟机栈的结构:
虚拟机栈是一个栈,因此是后进先出的,即FILO。图中黄色部分表示的是一个虚拟机栈,绿色部分是一个栈帧,mehodOne()是一个方法,这里所表示的意思是一个方法就是一个栈帧。java程序中每一个方法都会在虚拟机栈中开辟一个栈帧,栈帧包括局部变量表、操作数栈、动态链接、出口,等等。下面以一个java方法来具体说明虚拟机栈的运行原理。
java代码:
public class JVMDemo {
private Object obj = new Object();
//局部变量
public void methodOne(int i) {
int j = 0;
int sum = i + j;
Object abc = obj;
long start = System.currentTimeMillis();
methodTwo();
return;
}
private void methodTwo() {
File file = new File("");
}
public static void main(String[] args) {
JVMDemo demo = new JVMDemo();
demo.methodOne(1);
}
}
想要弄清楚程序到底是怎么执行的,我们反编译一下,用命令行,执行javap指令,下面就是反编译后的文件
找到执行methodOne方法的代码:
public void methodOne(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=7, args_size=2
0: iconst_0
1: istore_2
2: iload_1
3: iload_2
4: iadd
5: istore_3
6: aload_0
7: getfield #12 // Field obj:Ljava/lang/Object;
10: astore 4
12: invokestatic #20 // Method java/lang/System.currentTimeMillis:()J
15: lstore 5
17: aload_0
18: invokespecial #26 // Method methodTwo:()V
21: return
LineNumberTable:
line 12: 0
line 13: 2
line 14: 6
line 15: 12
line 16: 17
line 17: 21
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 this Lcom/tongtong/jvm/HelloWorldDemo;
0 22 1 i I
2 20 2 j I
6 16 3 sum I
12 10 4 abc Ljava/lang/Object;
17 5 5 start J
着重看这一部分
Code:
stack=2, locals=7, args_size=2
0: iconst_0
1: istore_2
2: iload_1
3: iload_2
4: iadd
5: istore_3
6: aload_0
7: getfield #12 // Field obj:Ljava/lang/Object;
10: astore 4
12: invokestatic #20 // Method java/lang/System.currentTimeMillis:()J
15: lstore 5
17: aload_0
18: invokespecial #26 // Method methodTwo:()V
21: return
关于javap的指令集,已准备好,点开https://mp.youkuaiyun.com/postedit/82820634,里面有详细的指令集说明,可以对照着看上面的代码。
下面进行分析:
- iconst_0 将int类型常量0压入栈
- istore_2 将int类型值存入局部变量2
上面的两个步骤就对应这两个指令。那为什么一定要存在局部变量2中呢?为什么不是0或者1中呢?
以为局部变量表1中存储的是程序中的参数i,验证方法如下:
int sum = i + j;
这是methodOne方法中的一句代码,对应反编译中的这三个指令:
2: iload_1
3: iload_2
4: iadd
5: istore_3
我们来看看指令的具体含义:
- iload_1 :从局部变量1中装载int类型值
- iload_2 :从局部变量2中装载int类型值
- iadd : 执行int类型的加法
- istore_3 : 将int类型值存入局部变量3
很明显,iload_2装载的是j的值,那么iload_1装载的肯定是i的值,因此可以说明i是保存在局部变量1中的。那么问题来了,局部变量0中保存的是什么呢?答案是:this,具体为什么会是this,暂时还不清楚,有待后续学习。但是有一个地方,可以间接的说明这个问题,在反编译代码中有这么一句:args_size=2,表示的是方法中传入的参数的个数,在方法中明明只传了一个i,为什么会有两个?事实上,另一个参数就是this。
经过上述分析,我们可以得到下面这张图:
好了,证明结束。现在回过头来继续解析下面这四个指令:
- iload_1 :从局部变量1中装载int类型值
- iload_2 :从局部变量2中装载int类型值
- iadd : 执行int类型的加法
- istore_3 : 将int类型值存入局部变量3
前两个指令是分别从局部变量1和2中装载int类型值到操作数栈中,然后执行iadd,得到sum的值,接着执行istore_3,把sum保存到局部变量3中,下图即展示了这几个过程:
从以上分析中大概可以得出结论,操作数栈是用来保存操作数或者中间结果的。而局部变量表,顾名思义,就是用来保存局部变量的,它是32位的。最开始的时候,里面是空的,在真正执行程序时才会填充数据。
出了局部变量,程序中还定义了成员变量,即Object obj = new Object(),并且在methodOne()中还把obj付给了局部变量abc。吗,额这个过程又改怎样表示呢?
看上图的红线部分,obj保存在堆(heap)中,而abc是保存在局部变量表中,是obj的一个引用,它指向堆中的obj,保存的是obj的地址。
- 动态链接
动态链接突出的是java具有动态性,即java的运行时多态。Java的运行时多态就是用动态链接实现的。举个例子:
//定义接口
public interface UserService{
}
//定义类
public class UserController{
@Autowired
private UserService userService;
public void addUser(User user){
userService.save();
.......
}
}
以上面代码为例,我们定义了一个接口UserService,它有很多实现类(此处没给出具体实现),当我们注入UserService的实例时并执行save()方法时,事实上,jvm需要通过动态链接来动态的获取这个实例对应的具体实现类,因为这个接口可能有不止一个实现类,当执行方法时,我们要具体到某个实现类才可以。这也体现了java的运行时多态的特性。
- 出口
这个好理解,程序不可能一直执行,所以必须要有一个出口。出口大致分为两种:正常出口和异常出口。正常出口,就是return;程序执行到这里就会正常退出。另外一个就是程序发生异常,导致异常退出。
解决了上述的诸多疑问之后,现在提出几个问题。
1.如果我们在methodOne()方法中调用methodTwo()方法,那么此时虚拟机栈中的栈帧是怎样排列的?
2.如果我们在methodThree()方法中执行递归调用,虚拟机栈中会开辟几个栈帧?(1 or n)执行时又会出现什么问题?
对于问题1,很简单也很容易理解。当调用methodTwo()方法后,很显然在虚拟机栈中会开辟一个新的栈帧,此时这个栈帧会放在methosOne()栈帧的上面,理由:FILO(后进先出)。
而对于问题2,我们可以写个程序运行一下。
这个异常是不是很熟悉,这个结果即是问题2 的答案。如果只开辟一个栈帧,那么怎么可能会有StackOverflowError异常发生。很明显,是开辟了n个栈帧。另外,针对栈帧有个概念,叫做栈深度。表明栈的深度是有限的,如果栈的空间占用达到了极限,那么就会报这个错误。
本人不擅长写文章,所写的都是根据个人的理解来阐述的,如有错误地方,请大家批评指正,大家一起进步。今天就先写到这,后面的会尽快补上,谢谢大家。