运行时数据区
Java 运行时数据区共分为以下几个部分:程序计数器、Java 虚拟机栈、本地方法栈、Java堆和方法区。其中程序计数器、Java 虚拟机栈、本地方法栈是线程私有的,也就是每个线程都会有这几部分。Java 堆和方法区是线程共享的,下面分别看一下这些区域的作用。
image程序计数器:
程序计数器是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理线程恢复等基础功能都依赖程序计数器完成。每个线程都有一个独立的程序计数器。
Java 虚拟机栈:
Java 虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java方法执行 的内存模型。每个方法执行时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。从方法的调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈和出栈的过程。
本地方法栈:
与虚拟机栈类似,区别是虚拟机栈为虚拟机执行 Java 方法(即字节码)服务,本地方法栈为虚拟机使用到的 Native 方法服务。
Java 堆:
Java 堆是 Java 虚拟机管理的内存中最大的一块,被所有线程共享。此内存区域的唯一目的是存放对象实例。
方法区:
方法区与 Java 堆一样,被各个线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量。
虚拟机栈的结构
虚拟机栈描述的是 Java方法执行 的内存模型。我们本节主要介绍使用 ASM 修改方法,所以我们再重点看一下虚拟机栈的结构,如下图:
image局部变量表:
每个栈帧内部都包含一组称为局部变量表的变量列表。栈帧中局部变量表的长度由编译期决定,通过方法的 Code 属性保存及提供给栈帧使用。一个局部变量可以保存一个类型为 boolean、byte、char、short、float、reference 和 returnAddress 的数据,两个局部变量可以保存一个类型为 long 和 double 的数据。局部变量使用索引来进行定位访问,第一个局部变量的索引值为零。
Java 虚拟机使用局部变量表来完成方法调用时的参数传递,当一个方法被调用的时候,它的参数将会传递至从0开始的连续的局部变量表位置上。特别地,当一个实例方法被调用的时候,第0个局部变量一定是用来存储被调用的实例方法所在的对象的引用(即 Java 语言中的 "this" 关键字)。后续的其他参数将会传递至从1开始的连续的局部变量表位置上。
操作数栈:
栈帧中操作数栈的长度由编译期决定,操作数栈所属的栈帧在刚刚被创建的时候,操作数栈是空的。Java 虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用的时候,操作数栈也用来准备 调用方法的参数以及接收方法返回结果。
动态链接:
在 Class 文件里面,描述一个方法调用了其他方法,或者访问其成员变量是通过符号引用来表示的,动态链接的作用就是 将这些符号引用所表示的方法转换为实际方法的直接引用。
返回地址:
当一个方法开始执行后,要么方法正常调用完成,要么方法异常调用完成。无论是哪种方式完成,在方法退出之后,都需要返回到方法被调用的位置。
一个线程中的方法调用链路可能会很长,很多方法都处于同时执行的状态。对于执行引擎来说,在活动的线程中,位于当前栈顶的栈帧才是有效的,称之为当前帧,与这个栈帧相关联的方法称为当前方法。
虚拟机栈运行过程
下面我们再通过一个例子看一下栈帧是如何运行的。
首先编写 ClassFileAnalysis.java 源代码如下:
package com.asm.demo;
public class ClassFileAnalysis {
public static int add(int i, int j) {
int result = i + j;
return result;
}
}
-
执行 javac ClassFileAnalysis.java 生成 ClassFileAnalysis.class
-
反编译 javap -verbose ClassFileAnalysis.class
-verbose 表示打印方法参数和本地变量的数量以及栈区大小。
Classfile /Users/yangpeng/Desktop/ASM_METHOD/ClassFileAnalysis.class
Last modified Mar 5, 2019; size 283 bytes
MD5 checksum 8982796d91e9cd9cdebccf4c26c8ed52
Compiled from "ClassFileAnalysis.java"
public class com.asm.demo.ClassFileAnalysis
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#12 // java/lang/Object."<init>":()V
#2 = Class #13 // com/asm/demo/ClassFileAnalysis
#3 = Class #14 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 add
#9 = Utf8 (II)I
#10 = Utf8 SourceFile
#11 = Utf8 ClassFileAnalysis.java
#12 = NameAndType #4:#5 // "<init>":()V
#13 = Utf8 com/asm/demo/ClassFileAnalysis
#14 = Utf8 java/lang/Object
{
public com.asm.demo.ClassFileAnalysis();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public static int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=2
0: iload_0
1: iload_1
2: iadd
3: istore_2
4: iload_2
5: ireturn
LineNumberTable:
line 6: 0
line 7: 4
}
SourceFile: "ClassFileAnalysis.java"
可以看到类的完整信息,如类名、版本号、常量池方法信息等。由于我们分析的是使用 ASM 来操作方法,所以我们主要关注 add 方法的 Code 部分也就是如下:
image可以看到 add 方法本地变量表的大小是3,操作数栈的大小是2,参数的个数是2。共执行了6条指令。每条指令的含义如下:
- iload_0 : 将第1个int类型的本地变量推送至栈顶
- iload_1 : 将第2个int类型的本地变量推送至栈顶
- iadd : 将栈顶两个元素出栈,相加,将结果压入栈顶
- istore_2 : 将int类型的值存入第3个本地变量
- iload_2 : 将第3个int类型的本地变量推送至栈顶
- ireturn : 从当前方法返回int
更多指令请参考java虚拟机规范https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-6.html#jvms-6.5
假如我们调用 add 方法,传入的两个参数分别是2和3,也就是计算2和3相加的值,我们来看一下本地变量表和操作数栈是如何变化的。
image image初始状态下,本地变量表有我们传入的两个参数2和3,操作数栈是空的。
执行iload_0</br>
执行iload_0,之后2入栈
执行iload_1</br>
执行iload_1,之后3入栈
执行iadd</br>
执行iadd,2和3出栈,执行相加操作,计算结果5入栈
执行istore_2</br>
执行istore_2后,5出栈放入本地变量表
执行iload_2</br>
执行iload_2后,5入栈
执行ireturn</br>
执行ireturn后,将5作为结果返回
经过以上几步,方法就调用完成,我们计算出了2+3的值。
网友评论