運行時數據區
Java 運行時數據區共分爲以下幾個部分:程序計數器、Java 虛擬機棧、本地方法棧、Java堆和方法區。其中程序計數器、Java 虛擬機棧、本地方法棧是線程私有的,也就是每個線程都會有這幾部分。Java 堆和方法區是線程共享的,下面分別看一下這些區域的作用。
程序計數器:
程序計數器是當前線程所執行的字節碼的行號指示器,字節碼解釋器工作時就是改變這個計數器的值來選取下一條需要執行的字節碼指令。分支、循環、跳轉、異常處理線程恢復等基礎功能都依賴程序計數器完成。每個線程都有一個獨立的程序計數器。
Java 虛擬機棧:
Java 虛擬機棧也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是 Java方法執行 的內存模型。每個方法執行時會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。從方法的調用開始到執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧和出棧的過程。
本地方法棧:
與虛擬機棧類似,區別是虛擬機棧爲虛擬機執行 Java 方法(即字節碼)服務,本地方法棧爲虛擬機使用到的 Native 方法服務。
Java 堆:
Java 堆是 Java 虛擬機管理的內存中最大的一塊,被所有線程共享。此內存區域的唯一目的是存放對象實例。
方法區:
方法區與 Java 堆一樣,被各個線程共享,用於存儲已被虛擬機加載的類信息、常量、靜態變量。
虛擬機棧的結構
虛擬機棧描述的是 Java方法執行 的內存模型。我們本節主要介紹使用 ASM 修改方法,所以我們再重點看一下虛擬機棧的結構,如下圖:
局部變量表:
每個棧幀內部都包含一組稱爲局部變量表的變量列表。棧幀中局部變量表的長度由編譯期決定,通過方法的 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 部分也就是如下:
可以看到 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相加的值,我們來看一下本地變量表和操作數棧是如何變化的。
初始狀態下,本地變量表有我們傳入的兩個參數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的值。