JVM笔记(2)-笑谈Java字节码指令
基础不牢,地动山摇,Java仍然是业界主流的开发语言之一,Java生态圈中有大量的组件框架,也包括大量大数据的组件如Hadoop等。我们想要更熟练透彻地掌握这些组件框架并更好地开发自己的程序,深入学习JVM的基础很有必要。本篇深入浅出来讲述Java字节码指令运行的过程,避免过度深入太多细节,让学习者可以对JVM解析字节码以及运行指令的过程有一个宏观的认识,对继续深入学习JVM相关知识有总体的把控。
1.准备程序和字节码
Java:
public class ByteCodeT { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public void hi() { String s12 = "hi ".concat(name); hello(s12); } public String hello(String s2) { return s2; } public String getToken() { return UUID.randomUUID() .toString() .replace("-", "") + name.hashCode(); } }
在class文件目录输入命令打印出字节码(-v是打印全面字节码信息,-p是涵盖所有成员):
javap -v -p ByteCodeT
字节码:
警告: 二进制文件ByteCodeT包含com.lims.pracpro.jdkprac.ByteCodeT Classfile /E:/Code/flickeringproject/pracpro/target/classes/com/lims/pracpro/jdkprac/ByteCodeT.class Last modified 2020-9-17; size 1319 bytes MD5 checksum 1c5e715a75b59bba2b0228e7757fa33d Compiled from "ByteCodeT.java" public class com.lims.pracpro.jdkprac.ByteCodeT minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #18.#40 // java/lang/Object."<init>":()V #2 = Fieldref #17.#41 // com/lims/pracpro/jdkprac/ByteCodeT.name:Ljava/lang/String; #3 = String #42 // hi #4 = Methodref #43.#44 // java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/String; #5 = Methodref #17.#45 // com/lims/pracpro/jdkprac/ByteCodeT.hello:(Ljava/lang/String;)Ljava/lan g/String; #6 = Class #46 // java/lang/StringBuilder #7 = Methodref #6.#40 // java/lang/StringBuilder."<init>":()V #8 = Methodref #47.#48 // java/util/UUID.randomUUID:()Ljava/util/UUID; #9 = Methodref #47.#49 // java/util/UUID.toString:()Ljava/lang/String; #10 = String #50 // - #11 = String #51 // #12 = Methodref #43.#52 // java/lang/String.replace:(Ljava/lang/CharSequence;Ljava/lang/CharSeque nce;)Ljava/lang/String; #13 = Methodref #6.#53 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBu ilder; #14 = Methodref #43.#54 // java/lang/String.hashCode:()I #15 = Methodref #6.#55 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; #16 = Methodref #6.#49 // java/lang/StringBuilder.toString:()Ljava/lang/String; #17 = Class #56 // com/lims/pracpro/jdkprac/ByteCodeT #18 = Class #57 // java/lang/Object #19 = Utf8 name #20 = Utf8 Ljava/lang/String; #21 = Utf8 <init> #22 = Utf8 ()V #23 = Utf8 Code #24 = Utf8 LineNumberTable #25 = Utf8 LocalVariableTable #26 = Utf8 this #27 = Utf8 Lcom/lims/pracpro/jdkprac/ByteCodeT; #28 = Utf8 getName #29 = Utf8 ()Ljava/lang/String; #30 = Utf8 setName #31 = Utf8 (Ljava/lang/String;)V #32 = Utf8 hi #33 = Utf8 s12 #34 = Utf8 hello #35 = Utf8 (Ljava/lang/String;)Ljava/lang/String; #36 = Utf8 s2 #37 = Utf8 getToken #38 = Utf8 SourceFile #39 = Utf8 ByteCodeT.java #40 = NameAndType #21:#22 // "<init>":()V #41 = NameAndType #19:#20 // name:Ljava/lang/String; #42 = Utf8 hi #43 = Class #58 // java/lang/String #44 = NameAndType #59:#35 // concat:(Ljava/lang/String;)Ljava/lang/String; #45 = NameAndType #34:#35 // hello:(Ljava/lang/String;)Ljava/lang/String; #46 = Utf8 java/lang/StringBuilder #47 = Class #60 // java/util/UUID #48 = NameAndType #61:#62 // randomUUID:()Ljava/util/UUID; #49 = NameAndType #63:#29 // toString:()Ljava/lang/String; #50 = Utf8 - #51 = Utf8 #52 = NameAndType #64:#65 // replace:(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/S tring; #53 = NameAndType #66:#67 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #54 = NameAndType #68:#69 // hashCode:()I #55 = NameAndType #66:#70 // append:(I)Ljava/lang/StringBuilder; #56 = Utf8 com/lims/pracpro/jdkprac/ByteCodeT #57 = Utf8 java/lang/Object #58 = Utf8 java/lang/String #59 = Utf8 concat #60 = Utf8 java/util/UUID #61 = Utf8 randomUUID #62 = Utf8 ()Ljava/util/UUID; #63 = Utf8 toString #64 = Utf8 replace #65 = Utf8 (Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String; #66 = Utf8 append #67 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #68 = Utf8 hashCode #69 = Utf8 ()I #70 = Utf8 (I)Ljava/lang/StringBuilder; { private java.lang.String name; descriptor: Ljava/lang/String; flags: ACC_PRIVATE public com.lims.pracpro.jdkprac.ByteCodeT(); 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 11: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/lims/pracpro/jdkprac/ByteCodeT; public java.lang.String getName(); descriptor: ()Ljava/lang/String; flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field name:Ljava/lang/String; 4: areturn LineNumberTable: line 15: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/lims/pracpro/jdkprac/ByteCodeT; public void setName(java.lang.String); descriptor: (Ljava/lang/String;)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: aload_1 2: putfield #2 // Field name:Ljava/lang/String; 5: return LineNumberTable: line 19: 0 line 20: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lcom/lims/pracpro/jdkprac/ByteCodeT; 0 6 1 name Ljava/lang/String; public void hi(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=1 0: ldc #3 // String hi 2: aload_0 3: getfield #2 // Field name:Ljava/lang/String; 6: invokevirtual #4 // Method java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/Stri ng; 9: astore_1 10: aload_0 11: aload_1 12: invokevirtual #5 // Method hello:(Ljava/lang/String;)Ljava/lang/String; 15: pop 16: return LineNumberTable: line 23: 0 line 24: 10 line 25: 16 LocalVariableTable: Start Length Slot Name Signature 0 17 0 this Lcom/lims/pracpro/jdkprac/ByteCodeT; 10 7 1 s12 Ljava/lang/String; public java.lang.String hello(java.lang.String); descriptor: (Ljava/lang/String;)Ljava/lang/String; flags: ACC_PUBLIC Code: stack=1, locals=2, args_size=2 0: aload_1 1: areturn LineNumberTable: line 28: 0 LocalVariableTable: Start Length Slot Name Signature 0 2 0 this Lcom/lims/pracpro/jdkprac/ByteCodeT; 0 2 1 s2 Ljava/lang/String; public java.lang.String getToken(); descriptor: ()Ljava/lang/String; flags: ACC_PUBLIC Code: stack=4, locals=1, args_size=1 0: new #6 // class java/lang/StringBuilder 3: dup 4: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V 7: invokestatic #8 // Method java/util/UUID.randomUUID:()Ljava/util/UUID; 10: invokevirtual #9 // Method java/util/UUID.toString:()Ljava/lang/String; 13: ldc #10 // String - 15: ldc #11 // String 17: invokevirtual #12 // Method java/lang/String.replace:(Ljava/lang/CharSequence;Ljava/lan g/CharSequence;)Ljava/lang/String; 20: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/la ng/StringBuilder; 23: aload_0 24: getfield #2 // Field name:Ljava/lang/String; 27: invokevirtual #14 // Method java/lang/String.hashCode:()I 30: invokevirtual #15 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 33: invokevirtual #16 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 36: areturn LineNumberTable: line 32: 0 line 33: 10 line 34: 17 line 35: 27 line 32: 36 LocalVariableTable: Start Length Slot Name Signature 0 37 0 this Lcom/lims/pracpro/jdkprac/ByteCodeT; } SourceFile: "ByteCodeT.java"
可以看出,几个方法的复杂度:getName<setName<hi<getToken;我们一步一步来看。
选择方法getName():
一共三行指令:
第一行,aload_0,看起来应该是加载东西的,先不管;
第二行,getfield,后面引用了常量池的2号常量,查看常量池可知,2号常量是字段引用(即为String name)
第三行,areturn,即是return返回。
仔细想来,getfield指令传入了参数“Field name:Ljava/lang/String;”并不完整,后半部分只是Ljava/lang/String类型,整个过程究竟取谁的name值?这里get的是谁的field?
由此可以想到第一步的指令是为第二步的取值准备了数据。
结合局部变量表来看,aload_0正是取了局部变量表中第0个槽的数据,意思是将Slot 0中的值压入栈中,其值为this,就是当前类的实例,即getfield的主体。
2.解析
Jvm大帝是神之旨意的履行者(Jvm大帝就是虚拟机,神就是开发者,神之旨意是开发者写好并编译后的字节码...),当Jvm大帝带领Java世界运行进入了一个新的方法后,会为这个方法在栈内存大陆上创造两个重要的领域:局部变量表和操作数栈。
局部变量表里一般会包含this指针(针对实例方法,静态方法当然无此)、方法的所有传入参数和方法中所开辟的本地变量。
我们再引入另外一个比喻,如果把运行Java方法理解为拍戏,那么局部变量表里的各个局部变量就是这部戏的核心主角,或者说领衔主演,而操作数栈正是这部戏的舞台。所谓操作数栈搭台,局部变量唱戏,是也。那么aload_0就是告诉Jvm导演(大帝已沦落为导演),请0号演员this同志登台(压栈),演后边的本子。
当然了,这个比喻并不完全恰当,因为操作数栈并不是“舞台”的结构,而是栈的结构。但是这个比喻可以很好地说明局部变量表和操作数栈之间的关系,以及aload_0的作用。
getfield做的操作:this入栈——》弹出this——》取值
3.方法执行深入
方法执行的操作:
上图展示指令、操作数栈、局部变量表三者的运作关系
关于字节码指令,一是指令的功能,二是指令操作的数据类型。先从功能说起,指令主要可以分为如下几类:
- 存储和加载类指令:主要包括load系列指令、store系列指令和ldc、push系列指令,主要用于在局部变量表、操作数栈和常量池三者之间进行数据调度;(关于常量池前面没有特别讲解,这个也很简单,顾名思义,就是这个池子里放着各种常量,好比片场的道具库)
- 对象操作指令(创建与读写访问):比如我们刚刚的putfield和getfield就属于读写访问的指令,此外还有putstatic/getstatic,还有new系列指令,以及instanceof等指令。
- 操作数栈管理指令:如pop和dup,他们只对操作数栈进行操作。
- 类型转换指令和运算指令:如add/div/l2i等系列指令,实际上这类指令一般也只对操作数栈进行操作。
- 控制跳转指令:这类里包含常用的if系列指令以及goto类指令。
- 方法调用和返回指令:主要包括invoke系列指令和return系列指令。这类指令也意味这一个方法空间的开辟和结束,即invoke会唤醒一个新的java方法小宇宙(新的栈和局部变量表),而return则意味着这个宇宙的结束回收。
指令操作的数据类型来讲:指令开头或尾部的一些字母,就往往表明了它所能操作的数据类型:
a对应对象,表示指令操作对象性数据,比如aload和astore、areturn等等。
i对应整形。也就有iload,istore等i系列指令。
f对应浮点型。
l对应long,b对应byte,d对应double,c对应char。
另外地,ia对应int array,aa对应object array,da对应double array。不在一一赘述。
解析复杂方法:
解析:
- (红色第1部分)new指令执行,引用常量池6号参数,创建一个StringBuilder对象,在内存中开辟空间(并将其引用入栈),用于实现连接字符串功能,类似C++中的运算符重载。
- dup指令执行,复制栈顶,并入栈,此时栈深为2(2个引用)——(一般new指令后会跟dup复制一份引用,其中一个引用用于init初始化,一个引用给程序员用于赋值等使用)
- invokespecial指令执行,使用常量池7号符号引用(<init>方法初始化),此时用到了栈顶的1个引用参数,栈深变为1。
- (黄色第2部分)invokestatic指令执行,调用UUID.randomUUID()静态方法,结果压栈,并弹出,调用String的toString方法,结果再压栈,此时栈深2。
- (绿色第3部分)字符串“-”、“”入栈,栈深为4,弹出栈顶3个元素,调用String的replace方法,最后一个元素调用方,其余两个为参数,结果入栈,此时栈深2。
- (青色第5部分)弹出栈顶2个元素,调用StringBuilder的append方法,最后一个元素为调用方,先弹出的为参数,结果入栈,栈深为1。
- (粉色第5部分)aload_0指令执行,将局部变量表的第0个变量压栈(this入栈),栈深为2,;弹出栈顶调用方,调用getfield方法,结果入栈,栈深为2;弹出栈顶调用方,调用hashCode方法,结果入栈,栈深为2。
- (洋红第6部分)弹出栈顶2个元素,调用StringBuilder的append方法,最后弹出元素为调用方,先弹出元素为append参数,结果入栈,栈深1;弹出栈顶,调用toString方法,结果压栈,栈深1。
- areturn返回栈顶,栈空,结果返回,方法调用完成。