JVM-从字节码到运行时(2)
基于栈的解释器执行过程
public int add(int);
descriptor: (I)I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=2
0: sipush 300
3: istore_2
4: aload_0
5: getfield #4 // Field a:I
8: sipush 200
11: iadd
12: iload_2
13: iadd
14: iload_1
15: imul
16: ireturn
LineNumberTable:
line 10: 0
line 11: 4
这是例子ByteCodeDemo
类中的add(int z)
方法。
descriptor: (I)I // 该方法入参类型为int,返参类型为int
flags: ACC_PUBLIC // 该方法的访问标志为PUBLIC
stack=2, locals=3, args_size=2
-
stack
表示操作数栈栈深为2 -
locals
表示局部变量表有3个变量槽位slot -
args_size
表示方法内变量数量,包括入参和方法内定义的局部变量
0: sipush 300
3: istore_2
4: aload_0
5: getfield #4 // Field a:I
8: sipush 200
11: iadd
12: iload_2
13: iadd
14: iload_1
15: imul
16: ireturn
左边数字为指令的偏移地址
,右边为助记符
。
关于助记符
、程序计数器
、局部变量表
和操作栈
在方法执行时的大致过程:
(此处应有图)
为什么说执行过程是基于栈呢?
Java虚拟机以方法作为最基本的执行单元,栈帧
则是用于支持虚拟机进行方法调用和方法执行背后的数据结构。每一个方法从调用开始到执行结束的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
运行时栈帧结构
每一个栈帧由局部变量表
、操作数栈
和固定帧
(桢数据)组成。其中,固定帧还包括方法指令
、常量池
、调用方栈底
和返回地址
等信息。
每一个线程中的方法调用链可能会很长,所以一个线程中会包含多个栈帧。在活动的线程中,只有栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,这个位于栈顶的栈帧叫当前栈帧
。
操作数栈
栈顶缓存
栈帧重叠
局部变量表
局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量表的容量以变量槽为最小单位。当JVM准备调用Java方法时,会为该方法创建栈帧,而栈帧中就包括局部变量表所需的空间,由于局部变量表建立在线程的堆栈空间中,因此是线程的私有数据,不会有线程安全问题。
局部变量表中第0位索引的变量槽存储该实力方法所在对象的引用,即this关键字,后续的其他参数将会传递至局部变量表中从1开始的连续位置上。所以,在非静态方法中,locals的数量至少为1,就是这个this。
局部变量表.png栈深与slot复用
上面说到每一个Java方法栈帧由3部分组成:局部变量表
、操作数栈
和固定帧
。由于固定帧的大小是固定不变的,因此Java方法栈帧的大小取决于局部变量表和操作数栈的大小,而由于栈帧重叠,可以粗略的认为操作数栈属于被调用者方法的栈帧的一部分,由此推断出,一个Java方法的栈帧大小主要取决于局部变量表的大小。当JVM设定默认的堆栈空间大小后-Xss
,一个Java线程所能调用的最大方法深度便直接取决于Java方法局部变量表的大小。所以,如果局部变量表所占空间大,则线程所能调用的最大方法的深度便会变小。
(此处应该验证)
由于局部变量表的大小会直接影响一个线程所能调用的方法深度,所以一个优化手段便是,使slot能够复用。slot复用能相对的减少局部变量表的大小,从而提高线程调用的最大方法深度。
(此处应该验证)
动态链接
每一个栈帧都包含和一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接
。Class文件常量池中的符号引用,会分为两部分解析:一部分是静态解析
,是在类加载阶段时替换为直接引用;一部分是动态解析
,将在每一次运行期间替换为直接直接,而动态解析这部分就是动态链接。
实现动态链接的关键便是在Java方法栈中持有一个指针,指向常量池,就能得到该Java方法的字节码指令,并根据字节码指令映射到机器指令,从而完成方法逻辑处理。
网友评论