怎样用 Java 字节码指令去理解 JVM 运行时数据区的栈结构

开场白: “掷地铿锵嗟有力,杳无声处见刀光。全凭借,三言两语,便道尽,栈结构”——怎么可能?!>_<

栈其实是个很理论的概念。说理论,是因为栈并不指代内存里某块具体的区域,它只是一种数据结构。我们通常讲:某类型数据存放在栈中……这里的栈指的是以栈这种数据结构形式去存放数据的、主存硬件中某块实实在在的区域。

JVM Run-Time Data Areas (运行时数据区)里的这种不同区域的划分是个动态的概念。大致的、通俗的你可以这样理解:内存就是静态的硬件,当 JVM 运行时,静态的内存硬件便会被 JVM 动态地划分为一系列存放各种不同类型数据的区域,这些区域本质上是主存中一块一块的、被 JVM 划分好的物理内存。

这篇文章只讨论 Run-Time Data Areas 的栈结构,这里的栈结构说得确切些应该叫线程栈,为什么叫线程栈,是因为多个线程并发运行时,JVM 会给每一个线程分配不同的栈区域。如图:



接下来我们再来看下面这段代码:

public class Math {
    public static int initDate = 666;
    public static User user = new User();
    
    public int computes() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        int d = c / b;
        return d;
    }
    
    public static void main(String[] args) {
        Math math = new Math();
        int r = math.computes();
        System.out.println(r);
    }
}

class User {
    
}

运行结果:
15

使用 javap -c math > math.txt 反汇编得到字节码指令(也可使用开发工具上自带的反汇编工具 ):

Compiled from "Math.java"
public class Math {
  public static int initDate;

  public static User user;

  static {};
    Code:
       0: sipush        666
       3: putstatic     #12                 // Field initDate:I
       6: new           #14                 // class User
       9: dup
      10: invokespecial #16                 // Method User."<init>":()V
      13: putstatic     #19                 // Field user:LUser;
      16: return

  public Math();
    Code:
       0: aload_0
       1: invokespecial #23                 // Method java/lang/Object."<init>":()V
       4: return

----------------------分割线----------------------

  public int computes();
    Code:

//0: 将int类型常量1压入操作数栈的栈顶
       0: iconst_1

//1: 先将操作数栈的栈顶的int类型值弹出栈顶,然后存入局部变量表的1号索引位上的本地变量(由于computes()是非静态的方法,第0位索引位存放了当前对象的引用,所以是从1号索引位开始分配本地变量的)
       1: istore_1

       2: iconst_2

       3: istore_2

//4: 将局部变量表中1号索引位的int类型本地变量的值压入操作数栈的栈顶
       4: iload_1

       5: iload_2

//6: 先让栈顶依次弹出两个元素,将弹出栈顶的两个int型数值相加并将结果压入操作数栈的栈顶
       6: iadd

//7: 将单字节的int类型常量(-128~127)10压入操作数栈的栈顶
       7: bipush        10

//9: 先让栈顶依次弹出两个数值,将弹出栈顶的两个int型数值相乘并将结果压入操作数栈的栈顶
       9: imul

      10: istore_3

      11: iload_3

      12: iload_2

//13: 先让栈顶依次弹出两个数值,将弹出栈顶的两个int型数值相除并将结果压入操作数栈的栈顶
      13: idiv

//14: 先将操作数栈的栈顶的int类型值弹出栈顶,并存入局部变量表的4号索引位上的本地变量(由于之前局部变量表中存放本地变量的索引位已到3,故采用带参的istore指令)
      14: istore        4

//16: 将局部变量表中4号位的int类型本地变量的值压入操作数栈的栈顶(由于之前局部变量表中存放本地变量的索引位已到3,故采用带参的iload指令)
      16: iload         4

//18: 从当前方法返回此int类型值(从computes()返回到main()中),具体是将返回值15从conmeputes()方法栈帧里操作数栈的栈顶弹出,然后压入main()方法栈帧里操作数栈的栈顶。而后通过方法出口(即返回地址,存放mian()方法中当前指令的下一步指令的地址)找到main()方法当前指令的下一步指令,以执行main()方法当前指令的后续指令。最后将整个栈帧弹出线程栈。
      18: ireturn

  public static void main(java.lang.String[]);
    Code:

//0: 在堆上为Math对象分配内存空间,并将对象内存地址指针(引用值)压入操作数栈的栈顶
       0: new           #1                  // class Math

//3: 复制操作数栈栈顶的数值(数值不能是long或double类型的)并将其复制后又压入操作数栈的栈顶
       3: dup

//4: 从操作数栈栈顶弹出一个对象引用的值,然后通过这个引用值找到并调用此对象的实例初始化方法(实例方法可理解为可以通过具体对象的引用去调用的方法。如对象引用变量.方法)
       4: invokespecial #34                 // Method "<init>":()V

//7: 先将操作数栈的栈顶的引用类型的值弹出栈顶,而后将操作数栈栈顶的引用型数值存入局部变量表中1号索引位的本地变量math(由于main方法里有个形式参数String[] args,它先于mian()方法内局部变量,在0索引位先存放了args这个数组型本地变量的值,所以后续的索引位从1开始)
       7: astore_1

//8: 将1号索引位的引用类型本地变量的值压入操作数栈的栈顶,这里压入的值其实就是this的值(指针)
       8: aload_1

//9: 从操作数栈栈顶弹出一个引用的值(即弹出this的值),然后通过这个引用值找到并调用computes()实例方法。invokevirtual是一种动态分派的方法操作指令:引用变量的类型并不能决定到底调用哪个类型的方法,而是根据具体对象属于哪个类来调用该类的这个方法。底层上看,这个指令是导致动态绑定(多态)的原因之一。
       9: invokevirtual #35                 // Method computes:()

//12: 先将操作数栈的栈顶的int类型值弹出栈顶,然后存入局部变量表的2号索引位上的本地变量
      12: istore_2

//13: 获取指定类的静态域(这里是获取PrintStream对象在堆中的地址指针),并将其值压入操作数栈的栈顶
      13: getstatic     #37                 // Field java/lang/System.out:Ljava/io/PrintStream;

//16: 将局部变量表中2号索引位的int类型本地变量的值(15)压入操作数栈的栈顶
      16: iload_2

//17: 从操作数栈的栈顶以此将弹出两个元素(先弹15,后弹PrintStream对象在堆中的地址指针),通过对象地址指针找到PrintStream.println()并将15这个值传入方法里,调用PrintStream.println()方法(结果在控制台打印15)
      17: invokevirtual #43                 // Method java/io/PrintStream.println:(I)V

//20: 从当前方法返回void。main()方法执行完毕
      20: return

----------------------分割线----------------------

}

上面代码中我已对分割线内的几乎所有字节码指令的功能作出解释。下面我准备画图去简要分析 Run-Time Data Areas 的栈结构中的数据是如何流转的,我将会在某些我认为重要的程序运行节点去作图。这些图所能表示的流程只限于分割线内的指令码。

main() code 0 这行指令码运行完时:



main() code 3 这行指令码运行完时:



main() code 4 这行指令码运行完时:

main() code 7 这行指令码运行完时:



main() code 8 这行指令码运行完时:

到此之后,下一步指令是调用 computes() 方法。

main() code 9 时,computes() code 0 后:



main() code 9 时,computes() code 1 后:



main() code 9 时,computes() code 5 后:

main() code 9 时,computes() code 6 后:



main() code 9 时,computes() code 9 后:

main() code 9 时,computes() code 13 后:

main() code 9 时,computes() code 16 后:

main() code 9 时,computes() code 18 后:

到此,computes() 方法执行结束,computes() 栈帧弹出线程栈。而后该继续执行 main() 方法内接下来的语句了。

main() code 12 这行指令码运行完时:


main() code 20 这行指令码运行完时:

到此,main() 方法执行结束,main() 栈帧从线程栈中弹出,整个线程栈中一切的一切皆归于平静_

搞这么麻烦到底为什么?其实这是一种学习代码,分析代码的方法。有时候很多取巧的东西不如这种笨办法:一步一步来,边分析边画图,边画图边分析,用 JVM 字节码指令对应其操作的区域去理解 Run-Time Data Areas 结构将会令人印象更加深刻!

目前只对线程栈进行了简单地分析,其实 Run-Time Data Areas 中的各个结构是相互联系的,一发动而牵全身。在 JVM 运行时,不仅栈存在数据流转,其余结构中也同样存在数据流转,其细节还是很复杂的。对此要始终保持敬畏之心。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 228,702评论 6 534
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 98,615评论 3 419
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 176,606评论 0 376
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,044评论 1 314
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 71,826评论 6 410
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,227评论 1 324
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,307评论 3 442
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,447评论 0 289
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 48,992评论 1 335
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,807评论 3 355
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,001评论 1 370
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,550评论 5 361
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,243评论 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,667评论 0 26
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,930评论 1 287
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,709评论 3 393
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,996评论 2 374

推荐阅读更多精彩内容