本文共 6542 字,大约阅读时间需要 21 分钟。
参考文献:
https://blog.csdn.net/bjweimengshu/article/details/100718955?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-6&spm=1001.2101.3001.4242
java代码运行于JVM中,而jvm要运行java代码首先要做的就是加载字节码,也就是.java文件经过编译变成的.class文件。jvm运行的指令就是.class字节码。所以通过学习字节码的规范和规则能提升我们对代码执行过程的理解。
这里恶补一下字节码相关的基础知识。
我们打开.class文件,发现字节码都是十六进制的编码。
字节码的组成有以下部分,正入上图所示,十六进制的字节码中就包含这些部分 。如下图所示。
1.魔数:如图所示,cafebabe就是java字节码的魔术,这个数字是用java之父,詹姆斯高斯林指定的,其含义 就是java的咖啡bean吧。
2.版本号:因为java有不同的版本,所以在java字节码中也有标记。0000 0034两个字节就表示java1.8版本。
3.常量池:在java代码中一般会有很多常量,比如我们System.out.println(”ok”),这里用引号写死在代码 中的就是常量。java在编译的时候就会将这些常量罗列出来,形成常量池。在调用的时候从常量池中获取这些值。因为对于代码而言,常量的数量是可以确定的,所以这块常量池用了一个字节来表示常量池的大小。这里的0027就表示该类中常量的个数。这里表示39,扣除本身,这里表示有38个常量。如下图所示即是该类的常量。
对于这些常量,都用过一种数据结构进行保存。tag+length+data。也就是识别符号+数据长度+真实的数据的方式。比如这里
01 0001 01表示tag为1,长度为1,值为1的常量。这里的表示的常量Constant_utf8_info.这里我们知道常量池主要存储一些常量。下图为java的常量和相关的tag、length以及data的说明。
4.访问标志:访问标志就是类,方法,属性的修饰。比如public,static、private、protected等这些。
在字节码中的十六进制关系如下图所示。
(5) 当前类名
访问标志后的两个字节,描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名。
(6) 父类名称
当前类名后的两个字节,描述父类的全限定名,同上,保存的也是常量池中的索引值。
(7) 接口信息
父类名称后为两字节的接口计数器,描述了该类或父类实现的接口数量。紧接着的n个字节是所有接口名称的字符串常量的索引值。
8.字段信息
字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量,但是不包含方法内部声明的局部变量。字段表也分为两部分,第一部分为两个字节,描述字段个数;第二部分是每个字段的详细信息fields_info。字段表结构如下图所示:
比如0001 0002 0004 0008 0000
字段1个 private 常量池中的4的值 8表示int 字段属性个数为0
9.方法表:方法表和之前的字段表一样包括两部分,首先就是方法的个数。然后是方法的详细信息。
具体对应关系下图所示,其中flags表示修饰符,stack表示栈的深度,locals表示本地变量表长度,args表示传入参数的长度。linueNumberTable表示源代码与字节码命令的对应关系,和debug模式有关系。LocalVariableTable表示本地变量表,用来申明方法中的变量。而代码的真正执行也就是这里的code区域。
(10)附加属性表
字节码的最后一部分,该项存放了在该文件中类或接口所定义属性的基本信息。
这里我们根据上述知识来解释一下示例代码的执行过程。
public class TestJavaClass { public static void main(String[] args) { int a=0; a=a+1; int b=a+1; if (b==2){ System.out.println("ok"); } }}
查看字节码的详细信息
javap -verbose TestJavaClass.class
Classfile /D:/github/simpleadmin/target/classes/com/scaffold/simple/admin/other/jclass/TestJavaClass.class //最后修改的时间 Last modified 2021-1-29; size 733 bytes//MD5值 MD5 checksum ce7047b29c79f7fc7a30953502e8b087//编译的来源 Compiled from "TestJavaClass.java"public class com.scaffold.simple.admin.other.jclass.TestJavaClass//java的版本,52表示java8 minor version: 0 major version: 52//共有类,兼容老java flags: ACC_PUBLIC, ACC_SUPER//常量池Constant pool: #1 = Methodref #6.#25 // java/lang/Object."":()V #2 = Fieldref #26.#27 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #28 // ok #4 = Methodref #29.#30 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #31 // com/scaffold/simple/admin/other/jclass/TestJavaClass #6 = Class #32 // java/lang/Object #7 = Utf8 #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/scaffold/simple/admin/other/jclass/TestJavaClass; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 args #17 = Utf8 [Ljava/lang/String; #18 = Utf8 a #19 = Utf8 I #20 = Utf8 b #21 = Utf8 StackMapTable #22 = Utf8 MethodParameters #23 = Utf8 SourceFile #24 = Utf8 TestJavaClass.java #25 = NameAndType #7:#8 // "":()V #26 = Class #33 // java/lang/System #27 = NameAndType #34:#35 // out:Ljava/io/PrintStream; #28 = Utf8 ok #29 = Class #36 // java/io/PrintStream #30 = NameAndType #37:#38 // println:(Ljava/lang/String;)V #31 = Utf8 com/scaffold/simple/admin/other/jclass/TestJavaClass #32 = Utf8 java/lang/Object #33 = Utf8 java/lang/System #34 = Utf8 out #35 = Utf8 Ljava/io/PrintStream; #36 = Utf8 java/io/PrintStream #37 = Utf8 println #38 = Utf8 (Ljava/lang/String;)V{ public com.scaffold.simple.admin.other.jclass.TestJavaClass(); descriptor: ()V flags: ACC_PUBLIC Code://栈长为1,本地变量长度1,传入参数长度为1 stack=1, locals=1, args_size=1 0: aload_0 //将本地变量表中1对应的数据入栈 1: invokespecial #1 // Method java/lang/Object."":()V 4: return LineNumberTable: line 8: 0 源代码第八行,对应code的0行 LocalVariableTable://this关键词,隐式传入 Start Length Slot Name Signature//slot为0表示this关键字 0 5 0 this Lcom/scaffold/simple/admin/other/jclass/TestJavaClass; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code://栈深为2,本地变量表 长度为3,传入参数为1 stack=2, locals=3, args_size=1//将0入栈 0: iconst_0//将栈顶元素放入到本地变量1中 1: istore_1//将本地变量压入栈中 2: iload_1//将1压入栈中 3: iconst_1//对栈中的元素进行相加,并放回到栈中 4: iadd//将栈顶元素放到本地变量1中 此时a=1 5: istore_1 //将本地变量1中的值入栈 6: iload_1//在栈中压入1 7: iconst_1//将栈中的元素相加,并重新压入栈中 8: iadd//将栈中的元素存储到本地 变量2中 此时b=2 9: istore_2//将本地变量2中的值压入栈中 10: iload_2//将常量2压入栈中 11: iconst_2//对比栈中两个元素是否相同,如果不同跳转到第23行 12: if_icmpne 23//从常量表中获取2的属性 15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;//将常量表中#3的常量进行绑定 18: ldc #3 // String ok//执行常量表中#4中的方法 20: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V//返回 23: return LineNumberTable://源代码的第11行,对应上述的第0行 line 11: 0 line 12: 2 line 13: 6 line 14: 10 line 15: 15 line 17: 23 LocalVariableTable: Start Length Slot Name Signature 0 24 0 args [Ljava/lang/String; //this关键字 2 22 1 a I //本地变量1, 10 14 2 b I //本地变量2 StackMapTable: number_of_entries = 1 frame_type = 253 /* append */ offset_delta = 23 locals = [ int, int ] MethodParameters: Name Flags args}SourceFile: "TestJavaClass.java"
总结:java通过编译将源文件编译为字节码文件,其字节码中方法的执行和常量池和代码区有很大的联系。其中代码区包括指令区、本地变量表操作数栈等。代码的运行通过操作数栈进行相关的逻辑操作,本地变量表用来定义局部变量和执行结果缓存,代码运行需要的用到的常量则直接从常量池中获取。