个人随笔
目录
JVM(一):通过例子了解JVM内存区域中虚拟机栈的执行原理
2021-02-25 23:09:33

我们知道属于线程私有,每个线程虚拟机都会为它单独开辟一块独有的栈内存区域。每个栈里面主要组成成分为”栈帧”,每个方法对应一块”栈帧”内存区域。如下代码:

  1. public class Math {
  2. public int compute() {
  3. int a = 1;
  4. int b = 2;
  5. int c = (a+b)*10;
  6. return c;
  7. }
  8. public static void main(String[] args) {
  9. Math math = new Math();
  10. math.compute();
  11. }
  12. }

程序运行JVM会为主线程开辟一块内存区域,内存区域中有两块 “栈帧”,每个栈帧由四部分组成:
局部变量表、操作数栈、动态链接、方法出口,如下是一个整体执行流程图:

图的右边是JVM的内存区域,这里不去了解内存区域的原理,因为我这篇文章只是分析内存区域中的虚拟机栈的原理结构,我们根据上面的代码运行过程来分析下虚拟机栈。

该执行流程运行过程如下:

  • 1、当运行到main方法时,先在该线程的虚拟机栈中创建了一个main方法的栈帧。
  • 2、然后在局部变量表中创建一个math变量,而对象是放在堆中的,所以局部变量表中放的是math的引用
  • 3、4执行到math对象的compute()方法的时候,根据math对象头中的信息去方法区找到Math.class的类元信息,找到compute()方法所在的位置(该过程是在程序运行期间把符号引用变为直接引用,所以叫做动态链接)
  • 5、再在该虚拟机栈创建一个compute()方法对应的栈帧,然后开始执行compute()方法。

执行compute()方法流程如下

我们先看一下compute()方法的字节码指令:javap -c Math.class

  1. public int compute();
  2. Code:
  3. 0: iconst_1
  4. 1: istore_1
  5. 2: iconst_2
  6. 3: istore_2
  7. 4: iload_1
  8. 5: iload_2
  9. 6: iadd
  10. 7: bipush 10
  11. 9: imul
  12. 10: istore_3
  13. 11: iload_3
  14. 12: ireturn

上面只是抽取出来compute()方法的反汇编格式来描述:
我们这里先列一下上面锁需要用到的JVM栈和局部变量操作将常量压入栈的指令:

  1. iconst_1 int类型常量1压入操作数栈
  2. istore_1 int类型值存入局部变量1
  3. iconst_2 int类型常量2压入栈
  4. istore_2 int类型值存入局部变量2
  5. iload_1 从局部变量1中装载int类型值
  6. iload_2 从局部变量2中装载int类型值
  7. iadd 执行int类型的加法
  8. bipush 将一个8位带符号整数压入栈
  9. imul 执行int类型的乘法
  10. istore_3 int类型值存入局部变量3
  11. iload_3 从局部变量3中装载int类型值
  12. ireturn 从方法中返回int类型的数据

1、执行完第一步:iconst_1 将int类型常量1压入操作数栈
此时操作数栈中有值1

2、执行完第二步:istore_1 将int类型值存入局部变量1
此时局部变量表中的内容为a=1,操作数栈为空

3、执行完第三步:iconst_2 将int类型常量2压入栈
此时局部变量表中的内容为a=1,操作数栈为2,如下图

4、执行完第四步:istore_2 将int类型值存入局部变量2
此时局部变量表中的内容为a=1,b=2,操作数栈为空

5、执行完第五步:iload_1 从局部变量1中装载int类型值
此时局部变量表中的内容为b=2,操作数栈为1

6、执行完第六步:iload_2 从局部变量2中装载int类型值
此时局部变量表中的内容为空,操作数栈为2、1如下图

7、执行完第七步:iadd 执行int类型的加法
此时局部变量表中的内容为空,操作数栈为3

8、执行完第八步:bipush 将一个8位带符号整数压入栈
此时局部变量表中的内容为空,操作数栈为10、3如下图

9、执行完第九步:imul 执行int类型的乘法
此时局部变量表中的内容为空,操作数栈为30

10、执行完第十步:istore_3 将int类型值存入局部变量3
此时局部变量表中的内容为c=30,操作数栈为空

11、执行完第十一步:iload_3 从局部变量3中装载int类型值
此时局部变量表中的内容为空,操作数栈为30

12、执行完第十二步:ireturn 从方法中返回int类型的数据
此时局部变量表中的内容为空,操作数栈为空

此时执行到compute()栈帧的方法出口,然后销毁compute()栈帧返回到main()栈帧。

上面执行期间,程序计数器一直在计数。

栈内存设置

每个线程的栈内存区域使用的内存大小可以如下参数设置,如

  1. -Xss512K

表示栈内存大小设置为512K,默认值是1M,我们可以很容易的知道,栈内存越大
那么JVM能启动的线程数就越少,栈内存设置的越小,则线程数就越大。

StackOverflowError错误演示

其实思路很简单,我们做个递归程序,先设置栈内存为-Xss128k,我们看看能够建立多少个栈帧把栈内存撑爆,代码如下:

  1. public class TestStackOverflowError {
  2. private static int index =0;
  3. public void fun() {
  4. //创建一个栈帧
  5. index++;
  6. fun();
  7. }
  8. public static void main(String[] args) {
  9. TestStackOverflowError test = new TestStackOverflowError();
  10. try {
  11. test.fun();
  12. } catch (Throwable e) {
  13. // TODO Auto-generated catch block
  14. e.printStackTrace();
  15. System.out.println("建立的方法(栈帧)数目:"+index);
  16. }
  17. }
  18. }

简单的说一下执行流程,在运行到main方法的时候,建立了一个栈帧,然后新建对象,根据对象头找到该列方法区的元数据,然后根据符号引用动态链接找到fun方法的位置,开始执行,然后没执行一下就新建一个新的栈,我们在JVM参数中设置-Xss128k,执行程序可以看到日志为:

  1. java.lang.StackOverflowError
  2. at com.suibibk.jvm.TestStackOverflowError.fun(TestStackOverflowError.java:7)`
  3. ...
  4. at com.suibibk.jvm.TestStackOverflowError.main(TestStackOverflowError.java:13)
  5. 建立的方法(栈帧)数目:994

大概建立了994个栈帧,实际肯定要比这个多,应为还有别的方法,比如main方法,当我们把栈内存大小设置为512k,理论上栈内存变大,那么栈帧数就可以创建更多,日志如下:

  1. java.lang.StackOverflowError
  2. at com.suibibk.jvm.TestStackOverflowError.fun(TestStackOverflowError.java:7)`
  3. ...
  4. at com.suibibk.jvm.TestStackOverflowError.main(TestStackOverflowError.java:13)
  5. 建立的方法(栈帧)数目:10131

查看结果可以知道,跟预测的一样!

注:要用Throwable来捕获,不能用Exception,因为内存溢出是Error错误,不是Exception错误。

符号引用

我们执行javap -v TestStackOverflowError.class,类似红色圈起来的就是符号引用(嘿嘿,个人理解,以后再研究)

 375

啊!这个可能是世界上最丑的留言输入框功能~


当然,也是最丑的留言列表

有疑问发邮件到 : suibibk@qq.com 侵权立删
Copyright : 个人随笔   备案号 : 粤ICP备18099399号-2