很多人学习java不会去理解字节码指令,觉得能看懂字节码对写好java代码没什么帮助,但其实不然,不少java语法效果其实可以通过字节码来理解的,掀开字节码的面纱能更好的理解java代码的语法和实际执行结果之间的关系,看了本文会让你有一种字节码也不过如此的感觉。
名字解释
由于本文涉及java字节码指令,如果有一点计算机栈和堆的基本认知会更好理解,不清楚该知识点也能大概意会,不影响大家理解其大致的原理。这里简单介绍下本文涉及的几个常见的指令,以便更方便的阅读。
| 指令中反复出现字符 | 含义 | 举例 |
|---|---|---|
a |
对引用操作 | aload_1:读取栈顶一个32b大小的引用 |
i |
对栈上int长度的空间进行操作 | iload_1:读取栈顶一个32b大小的int值 istore_1:把一个int值赋值给栈顶的int变量 |
l |
对栈上long长度的空间进行操作 | lload_2:读取栈顶一个long长度(2个32b大小)的long值 lstore_2:把一个long值赋值给栈顶long长度(2个32b大小)的变量 |
store |
把某个值赋值给栈顶的变量 | 参考astore_1、istore_1、lstore_2 |
load |
读取栈顶的值 | 参考aload_1、iload_1、lload_2 |
eq |
是否等于1(小于为-1,等于为0,大于为1,false为0,true为1) | ifeq #10:如果等于1则继续执行紧跟的指令,否则跳转到第10个32b位置的指令处继续执行 |
ne |
跟eq反过来 |
ifne #12: 跟上面是反过来的意思 |
1 |
栈上int长度的空间(32b) | 参考aload_1、iload_1、istore_1 |
2 |
栈上long长度的空间(64b) | 参考lload_2、lstore_2 |
invokestatic |
调用静态方法 | |
invokevirtual |
调用实例方法 | |
invokespecial |
调用构造方法、父类方法、重写的子类方法 |
1. 通过java字节码理解代码执行顺序
案例一:
java
public class Test {
private final static Logger log = LoggerFactory.getLogger(Test.class);
// 写法一
public void log1(FakeObj fakeObj) {
// 日志框架会自己判断日志级别,如果最低日志级别是info,则该日志不会被打印出来
log.debug("params={}", JSONObject.toJSONString(fakeObj));
}
// 写法二
public void log2(FakeObj fakeObj) {
if (log.isDebugEnabled()) {
log.debug("params={}", JSONObject.toJSONString(fakeObj));
}
}
}
上面的代码编译后,使用javap -v Test.class命令查看其对应的字节码如下:
java
public void log1(org.example.int_to_long.FakeObj);
Code:
0: getstatic #7 // Field log:Lorg/slf4j/Logger;
3: ldc #13 // String params={}
5: aload_1
6: invokestatic #15 // Method com/alibaba/fastjson/JSONObject.toJSONString:(Ljava/lang/Object;)Ljava/lang/String;
9: invokeinterface #21, 3 // InterfaceMethod org/slf4j/Logger.debug:(Ljava/lang/String;Ljava/lang/Object;)V
14: return
public void log2(org.example.int_to_long.FakeObj);
Code:
0: getstatic #7 // Field log:Lorg/slf4j/Logger;
3: invokeinterface #27, 1 // InterfaceMethod org/slf4j/Logger.isDebugEnabled:()Z
8: ifeq 25 // if如果上一句执行结果是0(即false)则跳转到行号25处,是1(tru
)则继续
11: getstatic #7 // Field log:Lorg/slf4j/Logger;
14: ldc #13 // String params={}
16: aload_1
17: invokestatic #15 // Method com/alibaba/fastjson/JSONObject.toJSONString:(Ljava/lang/Object;)Ljava/lang/String;
20: invokeinterface #21, 3 // InterfaceMethod org/slf4j/Logger.debug:(Ljava/lang/String;Ljava/lang/Object;)V
25: return
通过字节码可以看出,log1中第6行是执行toJSONString()方法,然后执行的log.debug,而log2方法中是先执行isDebugEnabled后,根据其结果再执行toJSONString()和log.debug,由于生产环境中一般不开启debug日志级别,所以log2方法可以在不开启debug日志级别时不执行toJSONString(),故log2的性能更好。但是对于info、warn和error则不需要这样增加日志级别判断了,因为大部分时候生产环境都会开启这3个日志级别的。
2. 通过java字节码理解装箱和拆箱到底是怎样的
2.1 赋值时自动拆箱可能抛NullPointerException
java
public class Test {
public static void main(String[] args){
Long a = null;
long b = a;
}
}
上面的代码在把变量a赋值给b时就会抛出NullPointerException,使用javap -v Test.class命令查看其对应的字节码如下:
java
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: aconst_null // 把常量null的引用入栈
1: astore_1 // 给变量a赋值为null
2: aload_1 // 读取变量a的值
3: invokevirtual #2 // Method java/lang/Long.longValue:()J
6: lstore_2 // 把变量a转成long的结果赋值到临时变量b上
7: return
}
可以看到main方法行号为3的字节码调用了Long.longValue()方法,行号6的字节码才是把变量a赋值给b,显然由于a是null,所以调用longValue()方法自然会抛空指针了。
2.2 方法传参时自动拆箱抛NullPointerException
java
public class Test {
public static void main(String[] args){
Long a = null;
increment(a);
}
public static long increment(long n){
return n++;
}
}
上面把变量a传参给方法increment时就会抛NullPointerException,还是用javap -v Test.class查看其字节码如下:
java
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: aconst_null
1: astore_1
2: aload_1
3: invokevirtual #2 // Method java/lang/Long.longValue:()J
6: invokestatic #3 // Method increment:(J)J
9: pop2
10: return
public static long increment(long);
Code:
0: lload_0
1: dup2
2: lconst_1
3: ladd
4: lstore_0
5: lreturn
}
可以看到main方法行号为3的字节码调用了Long.longValue()方法,行号6的字节码才是真正的执行increment,显然由于a是null,所以调用longValue()方法也会抛空指针了。
2.3 用于大小比较或运算时拆箱抛NullPointerException
java
public class Test {
public static void main(String[] args){
Long n1 = null;
long n2 = 1L;
if(n1 == n2){
}
}
}
同样用javap -v Test.class查看字节码内容如下:
java
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: aconst_null
1: astore_1
2: lconst_1
3: lstore_2
4: aload_1
5: invokevirtual #2 // Method java/lang/Long.longValue:()J
8: lload_2
9: lcmp
10: ifne 13
13: return
}
同样的在行号8的字节码通过Long.longValue()方法把变量n1从Long转换成了long,然后行号8为加载变量n2,行号9才是比较两个long(注意不是比较Long)。
3. int和long直接计算时结果到底是int还是long
3.1 int+long计算
java
public class Test {
public static void main(String[] args) {
int i = 1;
long l = 1L;
// 看看这里到底是调用下面的哪个print方法
print(i+l);
}
private static void print(int i) {
System.out.println("i=" + i);
}
private static void print(long l) {
System.out.println("l=" + l);
}
}
这段代码输出结果是l=1 ,即第二个print方法,同样用javap -v Test.class查看字节码内容如下:
java
public static void main(java.lang.String[]);
Code:
stack=4, locals=4, args_size=1
0: iconst_1
1: istore_1
2: lconst_1
3: lstore_2
4: iload_1 // 加载变量i
5: i2l // 把变量i从int转成long
6: lload_2 // 加载变量l
7: ladd // 计算i+l
8: invokestatic #7 // Method print:(J)V
11: return
这说明编译器在编译的时候就已经把i+l计算后的类型固定为long了,所以调用的直接就是第二个print方法。
3.2 int和long的三元组计算
java
public class Test {
public static void main(String[] args) {
int i = 1;
long l = 1;
print(true ? i : l);
}
private static void print(int i) {
System.out.println("i=" + i);
}
private static void print(long l) {
System.out.println("l=" + l);
}
}
这段代码输出结果仍然是l=1 ,即第二个print方法,同样用javap -v Test.class查看字节码内容:
java
public static void main(java.lang.String[]);
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: lconst_1
3: lstore_2
4: iload_1 // 加载变量i
5: i2l // 把变量i从int转成long
6: invokestatic #7 // Method print:(J)V
9: return
可见尽管print方法的入参实际是变量int类型的i, 编译器仍然会把i强转成long,然后固定调用第二个print方法
4. 字符串相加跟StringBuilder其实是一样的效果
java
public class Test {
public void test1(String a, String b) {
String str = a + b;
System.out.println(str);
}
}
同样用javap -v Test.class查看字节码内容如下:
java
public void test1(java.lang.String, java.lang.String);
descriptor: (Ljava/lang/String;Ljava/lang/String;)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=4, args_size=3
0: aload_1 // 加载变量a
1: aload_2 // 加载变量B
2: invokedynamic #7, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
7: astore_3
8: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_3
12: invokevirtual #17 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15: return
SourceFile: "Test.java"
BootstrapMethods:
0: #41 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
Method arguments:
#39 \u0001\u0001
InnerClasses:
public static final #52= #48 of #50; // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
test1方法字节码的第2行是调用的 StringConcatFactory.makeConcatWithConstants方法,这个是,String; 这个指令实际上是在运行时动态地创建一个 StringBuilder,并将拼接的字符串常量加入其中,最终返回拼接后的字符串,也就是说跟我们自己直接写StringBuilder其实是差不多的,只是写起来更简洁一些 。
(本文提到的字节码中的行号其实是字节码指令占用空间的偏移量,为了便于理解,暂以行号来理解 ,同时为了方便阅读字节码,移除了字节码中的flags、descriptor、LineNumberTable、LocalVariableTable、StackMapTable、static静态语句块的内容)