Java字节码浅析(三)

简介:

英文原文链接译文链接,原文作者:James Bloom,译者:有孚

从Java7开始,switch语句增加了对String类型的支持。不过字节码中的switch指令还是只支持int类型,并没有增加对其它类型的支持。事实上switch语句对String的支持是分成两个步骤来完成的。首先,将每个case语句里的值的hashCode和操作数栈顶的值(译注:也就是switch里面的那个值,这个值会先压入栈顶)进行比较。这个可以通过lookupswitch或者是tableswitch指令来完成。结果会路由到某个分支上,然后调用String.equlals来判断是否确实匹配。最后根据equals返回的结果,再用一个tableswitch指令来路由到具体的case分支上去执行。


01 public int simpleSwitch(String stringOne) {
02     switch (stringOne) {
03         case "a":
04             return 0;
05         case "b":
06             return 2;
07         case "c":
08             return 3;
09         default:
10             return 4;
11     }
12 }

这个字符串的switch语句会生成下面的字节码:


01 0: aload_1
02  1: astore_2
03  2: iconst_m1
04  3: istore_3
05  4: aload_2
06  5: invokevirtual #2                  // Method java/lang/String.hashCode:()I
07  8: tableswitch   {
08          default: 75
09              min: 97
10              max: 99
11               97: 36
12               98: 50
13               99: 64
14        }
15 36: aload_2
16 37: ldc           #3                  // String a
17 39: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
18 42: ifeq          75
19 45: iconst_0
20 46: istore_3
21 47: goto          75
22 50: aload_2
23 51: ldc           #5                  // String b
24 53: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
25 56: ifeq          75
26 59: iconst_1
27 60: istore_3
28 61: goto          75
29 64: aload_2
30 65: ldc           #6                  // String c
31 67: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
32 70: ifeq          75
33 73: iconst_2
34 74: istore_3
35 75: iload_3
36 76: tableswitch   {
37          default: 110
38              min: 0
39              max: 2
40                0: 104
41                1: 106
42                2: 108
43        }
44 104: iconst_0
45 105: ireturn
46 106: iconst_2
47 107: ireturn
48 108: iconst_3
49 109: ireturn
50 110: iconst_4
51 111: ireturn

这段字节码所在的class文件里面,会包含如下的一个常量池。关于常量池可以看下JVM内部细节中的_运行时常量池_一节。


01 Constant pool:
02   #2 = Methodref          #25.#26        //  java/lang/String.hashCode:()I
03   #3 = String             #27            //  a
04   #4 = Methodref          #25.#28        //  java/lang/String.equals:(Ljava/lang/Object;)Z
05   #5 = String             #29            //  b
06   #6 = String             #30            //  c
07  
08  #25 = Class              #33            //  java/lang/String
09  #26 = NameAndType        #34:#35        //  hashCode:()I
10  #27 = Utf8               a
11  #28 = NameAndType        #36:#37        //  equals:(Ljava/lang/Object;)Z
12  #29 = Utf8               b
13  #30 = Utf8               c
14  
15  #33 = Utf8               java/lang/String
16  #34 = Utf8               hashCode
17  #35 = Utf8               ()I
18  #36 = Utf8               equals
19  #37 = Utf8               (Ljava/lang/Object;)Z

注意,在执行这个switch语句的时候,用到了两个tableswitch指令,同时还有数个invokevirtual指令,这个是用来调用String.equals()方法的。在下一篇文章中关于方法调用的那节,会详细介绍到这个invokevirtual指令。下图演示了输入为”b”的情况下,这个swith语句是如何执行的。

如果有几个分支的hashcode是一样的话,比如说“FB”和”Ea”,它们的hashCode都是28,得简单的调整下equals方法的处理流程来进行处理。在下面的这个例子中,34行处的字节码ifeg 42会跳转到另一个String.equals方法调用,而不是像前面那样执行lookupswitch指令,因为前面的那个例子中hashCode没有冲突。(译注:这里一般容易弄混淆,认为ifeq是字符串相等,为什么要跳到下一处继续比较字符串?其实ifeq是判断栈顶元素是否和0相等,而栈顶的值就是String.equals的返回值,而true,也就是相等,返回的是1,false返回的是0,因此ifeq为真的时候表明返回的是false,这会儿就应该继续进行下一个字符串的比较)


01 public int simpleSwitch(String stringOne) {
02     switch (stringOne) {
03         case "FB":
04             return 0;
05         case "Ea":
06             return 2;
07         default:
08             return 4;
09     }
10 }

这段代码会生成下面的字节码:


01 0: aload_1
02  1: astore_2
03  2: iconst_m1
04  3: istore_3
05  4: aload_2
06  5: invokevirtual #2                  // Method java/lang/String.hashCode:()I
07  8: lookupswitch  {
08          default: 53
09            count: 1
10             2236: 28
11     }
12 28: aload_2
13 29: ldc           #3                  // String Ea
14 31: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
15 34: ifeq          42
16 37: iconst_1
17 38: istore_3
18 39: goto          53
19 42: aload_2
20 43: ldc           #5                  // String FB
21 45: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
22 48: ifeq          53
23 51: iconst_0
24 52: istore_3
25 53: iload_3
26 54: lookupswitch  {
27          default: 84
28            count: 2
29                0: 80
30                1: 82
31     }
32 80: iconst_0
33 81: ireturn
34 82: iconst_2
35 83: ireturn
36 84: iconst_4
37 85: ireturn

###循环语句

if-else和switch这些条件流程控制语句都是先通过一条指令比较两个值,然后跳转到某个分支去执行。

for循环和while循环这些语句也类似,只不过它们通常都包含一个goto指令,使得字节码能够循环执行。do-while循环则不需要goto指令,因为它们的条件判断指令是放在循环体的最后来执行。

有一些操作码能在单条指令内完成整数或者引用的比较,然后根据结果跳转到某个分支继续执行。而比较double,long,float这些类型则需要两条指令。首先会将两个值进行比较,然后根据结果把1,-1,0压入操作数栈中。然后再根据栈顶的值是大于小于或者等于0,来决定下一步要执行的指令的位置。这些指令在上一篇文章中有详细的介绍。

####while循环

while循环包含条件跳转指令比如if_icmpge 或者if_icmplt(前面有介绍)以及goto指令。如果判断条件不满足的话,会跳转到循环体后的第一条指令继续执行,循环结束(译注:这里判断条件和代码中的正好相反,如代码中是i<2,字节码内是i>=2,从字节码的角度看,是满足条件后循环中止)。循环体的末尾是一条goto指令,它会跳转到循环开始的地方继续执行,直到分支跳转的条件满足才终止。


1 public void whileLoop() {
2     int i = 0;
3     while (i < 2) {
4         i++;
5     }
6 }

编译完后是:


1 0: iconst_0
2 1: istore_1
3 2: iload_1
4 3: iconst_2
5 4: if_icmpge 13
6 7: iinc 1, 1
7 10: goto 2
8 13: return

if_icmpge指令会判断局部变量区中的1号位的变量(也就是i,译注:局部变量区从0开始计数,第0位是this)是否大于等于2,如果不是继续执行,如果是的话跳转到13行处,结束循环。goto指令使得循环可以继续执行,直到条件判断为真,这个时候会跳转到紧挨着循环体后边的return指令处。iinc是少数的几条能直接更新局部变量区里的变量的指令之一,它不用把值压到操作数栈里面就能直接进行操作。这里iinc指令把第1个局部变量(译注:第0个是this)自增1。

for循环和while循环在字节码里的格式是一样的。这并不奇怪,因为每个while循环都可以很容易改写成一个for循环。比如上面的while循环就可以改写成下面的for循环,当然了它们输出的字节码也是一样的:


1 public void forLoop() {
2     for(int i = 0; i < 2; i++) {
3  
4     }
5 }

####do-while循环

do-while循环和for循环,while循环非常类似,除了一点,它是不需要goto指令的,因为条件跳转指令在循环体的末尾,可以用它来跳转回循环体的起始处。


1 public void doWhileLoop() {
2     int i = 0;
3     do {
4         i++;
5     } while (i < 2);
6 }

这会生成如下的字节码:


1 0: iconst_0
2 1: istore_1
3 2: iinc          1, 1
4 5: iload_1
5 6: iconst_2
6 7: if_icmplt    2
7 10: return

目录
相关文章
|
6月前
|
安全 Java API
JDK 11中的动态类文件常量:探索Java字节码的灵活性与动态性
在JDK 11中,Java语言引入了一个新的特性,允许在运行时动态地修改类文件常量。这一特性为Java开发者提供了更大的灵活性,使他们能够根据需要在运行时更改类文件中的常量值。本文将深入探讨动态类文件常量的工作原理、优点、限制以及在实际项目中的应用。
151 11
|
9天前
|
Java
轻松上手Java字节码编辑:IDEA插件VisualClassBytes全方位解析
本插件VisualClassBytes可修改class字节码,包括class信息、字段信息、内部类,常量池和方法等。
58 6
|
22天前
|
Java 数据格式 索引
使用 Java 字节码工具检查类文件完整性的原理是什么
Java字节码工具通过解析和分析类文件的字节码,检查其结构和内容是否符合Java虚拟机规范,确保类文件的完整性和合法性,防止恶意代码或损坏的类文件影响程序运行。
|
22天前
|
Java API Maven
如何使用 Java 字节码工具检查类文件的完整性
本文介绍如何利用Java字节码工具来检测类文件的完整性和有效性,确保类文件未被篡改或损坏,适用于开发和维护阶段的代码质量控制。
|
1月前
|
Java
如何从Java字节码角度分析问题|8月更文挑战
如何从Java字节码角度分析问题|8月更文挑战
|
2月前
|
Arthas Java 测试技术
Java字节码文件、组成,jclasslib插件、阿里arthas工具,Java注解
Java字节码文件、组成、详解、分析;常用工具,jclasslib插件、阿里arthas工具;如何定位线上问题;Java注解
Java字节码文件、组成,jclasslib插件、阿里arthas工具,Java注解
|
6月前
|
设计模式 Java 程序员
【设计模式】JAVA Design Patterns——Bytecode(字节码模式)
【设计模式】JAVA Design Patterns——Bytecode(字节码模式)
|
2月前
|
Java API 开发者
【Java字节码操控新篇章】JDK 22类文件API预览:解锁Java底层的无限可能!
【9月更文挑战第6天】JDK 22的类文件API为Java开发者们打开了一扇通往Java底层世界的大门。通过这个API,我们可以更加深入地理解Java程序的工作原理,实现更加灵活和强大的功能。虽然目前它还处于预览版阶段,但我们已经可以预见其在未来Java开发中的重要地位。让我们共同期待Java字节码操控新篇章的到来!
|
2月前
|
Java API 开发者
【Java字节码的掌控者】JDK 22类文件API:解锁Java深层次的奥秘,赋能开发者无限可能!
【9月更文挑战第8天】JDK 22类文件API的引入,为Java开发者们打开了一扇通往Java字节码操控新世界的大门。通过这个API,我们可以更加深入地理解Java程序的底层行为,实现更加高效、可靠和创新的Java应用。虽然目前它还处于预览版阶段,但我们已经可以预见其在未来Java开发中的重要地位。让我们共同期待Java字节码操控新篇章的到来,并积极探索类文件API带来的无限可能!
|
3月前
|
监控 Java API
分布式链路监控系统问题之对Java应用实现字节码增强的方式的问题如何解决
分布式链路监控系统问题之对Java应用实现字节码增强的方式的问题如何解决