java 虚拟机栈 例子,JVM(一):通过例子了解JVM内存区域中虚拟机栈的执行原理

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

publicclassMath{

publicintcompute(){

inta=1;

intb=2;

intc=(a+b)*10;

returnc;

}

publicstaticvoidmain(String[]args){

Mathmath=newMath();

math.compute();

}

}

程序运行JVM会为主线程开辟一块内存区域,内存区域中有两块 “栈帧”,每个栈帧由四部分组成:

局部变量表、操作数栈、动态链接、方法出口,如下是一个整体执行流程图:

bb82a1e73a1fa88dc1f2be99714e6d7e.png

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

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

1、当运行到main方法时,先在该线程的虚拟机栈中创建了一个main方法的栈帧。

2、然后在局部变量表中创建一个math变量,而对象是放在堆中的,所以局部变量表中放的是math的引用

3、4执行到math对象的compute()方法的时候,根据math对象头中的信息去方法区找到Math.class的类元信息,找到compute()方法所在的位置(该过程是在程序运行期间把符号引用变为直接引用,所以叫做动态链接)

5、再在该虚拟机栈创建一个compute()方法对应的栈帧,然后开始执行compute()方法。

执行compute()方法流程如下

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

publicintcompute();

Code:

0:iconst_1

1:istore_1

2:iconst_2

3:istore_2

4:iload_1

5:iload_2

6:iadd

7:bipush10

9:imul

10:istore_3

11:iload_3

12:ireturn

上面只是抽取出来compute()方法的反汇编格式来描述:

我们这里先列一下上面锁需要用到的JVM栈和局部变量操作将常量压入栈的指令:

iconst_1将int类型常量1压入操作数栈

istore_1将int类型值存入局部变量1

iconst_2将int类型常量2压入栈

istore_2将int类型值存入局部变量2

iload_1从局部变量1中装载int类型值

iload_2从局部变量2中装载int类型值

iadd执行int类型的加法

bipush将一个8位带符号整数压入栈

imul执行int类型的乘法

istore_3将int类型值存入局部变量3

iload_3从局部变量3中装载int类型值

ireturn从方法中返回int类型的数据

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

此时操作数栈中有值1

2、执行完第二步:istore_1 将int类型值存入局部变量1

此时局部变量表中的内容为a=1,操作数栈为空

3、执行完第三步:iconst_2 将int类型常量2压入栈

此时局部变量表中的内容为a=1,操作数栈为2,如下图

9a9e0a1e499161322b1f02b947510a6b.png

4、执行完第四步:istore_2 将int类型值存入局部变量2

此时局部变量表中的内容为a=1,b=2,操作数栈为空

5、执行完第五步:iload_1 从局部变量1中装载int类型值

此时局部变量表中的内容为b=2,操作数栈为1

6、执行完第六步:iload_2 从局部变量2中装载int类型值

此时局部变量表中的内容为空,操作数栈为2、1如下图

c44b82a4eb5f32672e5bd206d8e10905.png

7、执行完第七步:iadd 执行int类型的加法

此时局部变量表中的内容为空,操作数栈为3

8、执行完第八步:bipush 将一个8位带符号整数压入栈

此时局部变量表中的内容为空,操作数栈为10、3如下图

853af6708a6747164ad5cdd7cc41af37.png

9、执行完第九步:imul 执行int类型的乘法

此时局部变量表中的内容为空,操作数栈为30

10、执行完第十步:istore_3 将int类型值存入局部变量3

此时局部变量表中的内容为c=30,操作数栈为空

11、执行完第十一步:iload_3 从局部变量3中装载int类型值

此时局部变量表中的内容为空,操作数栈为30

12、执行完第十二步:ireturn 从方法中返回int类型的数据

此时局部变量表中的内容为空,操作数栈为空

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

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

栈内存设置

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

-Xss512K

表示栈内存大小设置为512K,默认值是1M,我们可以很容易的知道,栈内存越大

那么JVM能启动的线程数就越少,栈内存设置的越小,则线程数就越大。

StackOverflowError错误演示

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

publicclassTestStackOverflowError{

privatestaticintindex=0;

publicvoidfun(){

//创建一个栈帧

index++;

fun();

}

publicstaticvoidmain(String[]args){

TestStackOverflowErrortest=newTestStackOverflowError();

try{

test.fun();

}catch(Throwablee){

// TODO Auto-generated catch block

e.printStackTrace();

System.out.println("建立的方法(栈帧)数目:"+index);

}

}

}

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

java.lang.StackOverflowError

at com.suibibk.jvm.TestStackOverflowError.fun(TestStackOverflowError.java:7)`

...

at com.suibibk.jvm.TestStackOverflowError.main(TestStackOverflowError.java:13)

建立的方法(栈帧)数目:994

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

java.lang.StackOverflowError

at com.suibibk.jvm.TestStackOverflowError.fun(TestStackOverflowError.java:7)`

...

at com.suibibk.jvm.TestStackOverflowError.main(TestStackOverflowError.java:13)

建立的方法(栈帧)数目:10131

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

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

符号引用

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

d830089f22b334d8f60678c126ccc90c.png

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值