3.1、虚拟机栈
栈是一种数据结构,先进后出(FILO),可用数据或链表实现。JVM虚拟机栈也是栈数据结构,它是用来解决程序运行问题的,作用就一个,就是JVM在执行java方法时,执行栈帧的不断入栈出栈操作。虚拟机栈内部结构
特点:
1、线程私有,每创建一个线程时,就会为线程创建一块独立的虚拟机栈内存放入当前线程,当线程执行结束,空间就被销毁,JVM多线程是通过线程轮流切换来获取执行时间的,当前正在运行的线程我们称为当前线程,每当线程执行一个方法时,就会在它本身的虚拟机栈中压入一个栈帧,我们把栈顶的栈帧称为当前栈帧,即就是正在执行的方法空间;
2、不存在垃圾回收问题;
3、规范中有两种异常:StackOverflowError、OutOfMemeryError。
StackOverflowError:当前线程栈深度大于所请求的JVM所允许的最大深度时,将抛出此异常;
OutOfMemeryError:当虚拟机栈申请不到足够内存时,将跑除此异常。
3.2 内存概念模型
3.2.1 栈帧
每一个方法执行及运行结束,就会有一个伴随一个栈帧入栈及出栈;
栈帧是虚拟栈中数据的存储单位;
栈帧线程私有的,也就代表方法中的局部变量都是线程安全的;
栈帧结构
局部变量表:
存放的变量为GC ROOT,它直接或者间接引用的对象不回收(后续讲解GC机制时会详解);
编译时大小便可知;
用于存放方法中的局部变量,8个基本类型、引用、returnAddress;
以Slot为存在单位,Slot为32位的,long和double类型占用2个Slot。
操作数栈:
用于临时存放方法计算中的中间结果; 不像局部变量一样,通过索引存储值,它是通过对栈顶入栈出栈来处理值的; 如果方法有返回值,那么返回值将会压入操作数中,并更新程序计数器中为下一条执行的指令。操作演示:
下面一步一步来操作下JVM栈的概念模型,开场肯定是煮栗子,再慢慢拨栗子吃:public class JvmStackTest {
public static void main(String[] args) {
int i=1;
int j=128;
int c=add(i,j);
System.out.println(c);
}
private static int add(int a,int b){
return a+b;
}
}
java有个小工具,javap,我们通过javap -v来查看下上面字节码:
Classfile /D:/newproject/test/target/test-classes/com/snailct/jvm/vmstack/JvmStackTest.class
Last modified 2020-9-17; size 724 bytes
MD5 checksum 281e06089b17ce6b278edcc0ac0eef80
Compiled from "JvmStackTest.java"
public class com.snailct.jvm.vmstack.JvmStackTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#28 // java/lang/Object."":()V
#2 = Methodref #5.#29 // com/snailct/jvm/vmstack/JvmStackTest.add:(II)I
#3 = Fieldref #30.#31 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Methodref #32.#33 // java/io/PrintStream.println:(I)V
#5 = Class #34 // com/snailct/jvm/vmstack/JvmStackTest
#6 = Class #35 // java/lang/Object
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/snailct/jvm/vmstack/JvmStackTest;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 i
#19 = Utf8 I
#20 = Utf8 j
#21 = Utf8 c
#22 = Utf8 add
#23 = Utf8 (II)I
#24 = Utf8 a
#25 = Utf8 b
#26 = Utf8 SourceFile
#27 = Utf8 JvmStackTest.java
#28 = NameAndType #7:#8 // "":()V
#29 = NameAndType #22:#23 // add:(II)I
#30 = Class #36 // java/lang/System
#31 = NameAndType #37:#38 // out:Ljava/io/PrintStream;
#32 = Class #39 // java/io/PrintStream
#33 = NameAndType #40:#41 // println:(I)V
#34 = Utf8 com/snailct/jvm/vmstack/JvmStackTest
#35 = Utf8 java/lang/Object
#36 = Utf8 java/lang/System
#37 = Utf8 out
#38 = Utf8 Ljava/io/PrintStream;
#39 = Utf8 java/io/PrintStream
#40 = Utf8 println
#41 = Utf8 (I)V
{
public com.snailct.jvm.vmstack.JvmStackTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/snailct/jvm/vmstack/JvmStackTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: sipush 128
5: istore_2
6: iload_1
7: iload_2
8: invokestatic #2 // Method add:(II)I
11: istore_3
12: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
15: iload_3
16: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
19: return
LineNumberTable:
line 5: 0
line 6: 2
line 7: 6
line 8: 12
line 9: 19
LocalVariableTable:
Start Length Slot Name Signature
0 20 0 args [Ljava/lang/String;
2 18 1 i I
6 14 2 j I
12 8 3 c I
}
从main线程开始运行,在main线程空间中分配栈空间,栈空间中分配栈帧内存
stack=2(操作数占用2个slot), locals=4(局部变量占用4个slot), args_size=1 一个参数。执行引擎开始执行字节码
0: iconst_1
iconst指令是把int值压入操作数栈顶。内存模型如下图:
1 istore_1
把操作数栈顶1个元素弹出压入局部变量。 内存模型如图:2 sipush 128
把int整型数值压入操作数栈: 后面的指令大家自己操作,在此就不多说。动态链接:
执行运行时常量池的引用; 每个栈帧内部都有一个指定运行常量池或者方法的引用,主要用于将符号引用转化为直接引用,白话就是多态机制的实现,后续详解返回地址:
方法返回分为两种:
1、正常退出,通过return返回值或者void(本质上也会有return);
2、异常退出,抛出异常。
不管哪种退出,都会返回到方法调用者的位置,这就需要记录程序计数器的值,不过异常退出是通过异常表来记录的,栈帧中不会保留这信息。
3.3 Q&A
1、方法中的哪些变量是线程安全的,哪些不是,为什么?
答:方法中的局部变量是线程安全的,因为方法是由虚拟机栈帧局部变量来存储的,为线程私有,作用域仅仅在方法内部。而参数和返回值变量可能会涉及到方法作用域外的一些操作,所以为非线程安全的。