JVM字节码与Java代码层调优
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM字节码与Java代码层调优相关的知识,希望对你有一定的参考价值。
jvm字节码指令
我们都知道,Java源代码不会像C/C++那样直接被编译为机器码,而是被编译成字节码,这造就了Java可以跨平台的特性。JVM实际执行的也是编译后的字节码,所以想要在Java代码层进行调优,就得对字节码有一定的了解。
.class文件是无法直接使用文本编辑器查看的,至于字节码的查看,我们可以使用javap这个jdk自带的工具。javap是 Java class文件分解器,可以反编译(即对javac编译的文件进行反编译),也可以查看java编译器生成的字节码,用于分解class文件。用法如下:
我们先来写一个简单的测试类代码:
public class Test1 {
public static void main(String[] args) {
int a = 2;
int b = 3;
int c = a + b;
System.out.println(c);
}
}
然后让我们来看看这些源代码编译后的字节码是长什么样的,进入该类的.class文件存放路径,打开cmd命令行,执行如下命令,将字节码重定向到一个.txt文件中:
javap -verbose Test1.class > Test1.txt
打开Test1.txt文件,该文件内容如下:
Classfile /D:/Java_Work/classesview/out/production/classesview/classesview/Test1.class // class文件的路径
Last modified 2018年7月27日; size 573 bytes // 最后一次修改时间以及该class文件的大小
MD5 checksum 6ccc47493e2c660409ad2f057996f117 // 该类的MD5值
Compiled from "Test1.java" // 源码文件名
public class classesview.Test1 // 包名及类名
minor version: 0 // 版本号
major version: 52 // 版本号
flags: (0x0021) ACC_PUBLIC, ACC_SUPER // 该类的权限修饰符
this_class: #4 // classesview/Test1 // 当前类,即this的指向
super_class: #5 // java/lang/Object // 父类,即super的指向
interfaces: 0, fields: 0, methods: 2, attributes: 1 // 接口数量、字段数量、方法数量、属性数量
Constant pool: // 常量池,这些有顺序的数字相当于是常量池里的一个索引
#1 = Methodref #5.#23 // java/lang/Object."<init>":()V // 方法引用(符号引用)
#2 = Fieldref #24.#25 // java/lang/System.out:Ljava/io/PrintStream; // 字段引用
#3 = Methodref #26.#27 // java/io/PrintStream.println:(I)V
#4 = Class #28 // classesview/Test1 // 类引用
#5 = Class #29 // java/lang/Object
#6 = Utf8 <init> // 这是字节码中构造函数的名称,而名称就是一段字符串,所以就会按照编码标识
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 Lclassesview/Test1;
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 args
#16 = Utf8 [Ljava/lang/String;
#17 = Utf8 a
#18 = Utf8 I
#19 = Utf8 b
#20 = Utf8 c
#21 = Utf8 SourceFile
#22 = Utf8 Test1.java
#23 = NameAndType #6:#7 // "<init>":()V // 返回值
#24 = Class #30 // java/lang/System
#25 = NameAndType #31:#32 // out:Ljava/io/PrintStream;
#26 = Class #33 // java/io/PrintStream
#27 = NameAndType #34:#35 // println:(I)V
#28 = Utf8 classesview/Test1
#29 = Utf8 java/lang/Object
#30 = Utf8 java/lang/System
#31 = Utf8 out
#32 = Utf8 Ljava/io/PrintStream;
#33 = Utf8 java/io/PrintStream
#34 = Utf8 println
#35 = Utf8 (I)V
{
public classesview.Test1(); // 构造函数
descriptor: ()V // 方法描述符,这里的V表示void
flags: (0x0001) 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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lclassesview/Test1;
public static void main(java.lang.String[]); // main方法
descriptor: ([Ljava/lang/String;)V // 方法描述符,[表示引用了一个数组类型,L则表示引用的类后面跟的就是类名
flags: (0x0009) ACC_PUBLIC, ACC_STATIC // 权限修饰符
Code:
// 操作数栈的深度2,当调用一个方法的时候,实际上在JVM里对应的是一个栈帧入栈出栈的过程
// 本地变量表最大长度(slot为单位),64位的是2,其他是1,索引从0开始,如果是非static方法索引0代表this,后面是入参,后面是本地变量
// 1个参数,实例方法多一个this参数
stack=2, locals=4, args_size=1
0: iconst_2 // 常量2压栈
1: istore_1 // 出栈保存到本地变量1里面
2: iconst_3 // 常量3压栈
3: istore_2 // 出栈保存到本地变量2里面
4: iload_1 // 局部变量1压栈
5: iload_2 // 局部变量2压栈
6: iadd // 栈顶两个元素相加,计算结果压栈
7: istore_3 // 出栈保存到局部变量3里面
8: getstatic #2 // 这里对应的是常量池里的#2
11: iload_3 // 局部变量3压栈
12: invokevirtual #3 // 这里对应的是常量池里的#3
15: return // return void
LineNumberTable: // 行号表
line 5: 0 // 源代码的第5行,0代表字节码里的0,也就是上面的常量2压栈那一行
line 6: 2 // 源代码的第6行,2代表字节码里的2,也就是上面的常量3压栈那一行
line 7: 4 // 以此类推...
line 8: 8
line 9: 15
LocalVariableTable: // 本地变量表
Start Length Slot Name Signature
0 16 0 args [Ljava/lang/String; // 索引为0,变量名称为args
2 14 1 a I // 索引为2,变量名称为a
4 12 2 b I // 索引为4,变量名称为b
8 8 3 c I // 索引为5,变量名称为c
}
SourceFile: "Test1.java" // 源码文件名
字节码里的指令与源代码的一个对应关系:
从以上的字节码中,可以看到和Java的源代码是不太一样的,字节码里面还会用描述符来描述字段和方法,描述符有时候也被称之为签名(Signature),字段描述符与源代码里的字段:
方法描述符与源代码里的方法:
JVM在执行字节码指令的时候,是基于栈的架构,与OS中基于寄存器的架构不太一样。基于栈的好处就是指令比较短,但是指令集就会比较长了。以下用了几张图片来描述执行以上main方法里的字节码指令时,操作数栈里的一个出栈入栈的过程:
i++ 与 ++i
我们经常会看到一个问题,i++ 与 ++i 哪个效率更高,在循环里应该使用哪一个好。虽然很多人都知道答案,但是可能不知道答案背后的原理。所以本小节将介绍一下 i++ 与 ++i 效率孰高孰低的原理。首先也是准备一些简单的测试代码,如下:
public class SelfAdd {
public static void main(String[] args) {
f3();
f4();
}
public static void f1() {
for(int i=0;i<10;i++) {
System.out.println(i);
}
}
public static void f2() {
for(int i=0;i<10;++i) {
System.out.println(i);
}
}
public static void f3() {
int i=0;
int j = i++;
System.out.println(j);
}
public static void f4() {
int i=0;
int j = ++i;
System.out.println(j);
}
}
以上代码编译后的字节码如下:
Classfile /D:/Java_Work/classesview/out/production/classesview/classesview/SelfAdd.class
Last modified 2018年7月27日; size 980 bytes
MD5 checksum f77d50197f39e7c67717f14297cbb504
Compiled from "SelfAdd.java"
public class classesview.SelfAdd
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #6 // classesview/SelfAdd
super_class: #7 // java/lang/Object
interfaces: 0, fields: 0, methods: 6, attributes: 1
Constant pool:
#1 = Methodref #7.#29 // java/lang/Object."<init>":()V
#2 = Methodref #6.#30 // classesview/SelfAdd.f3:()V
#3 = Methodref #6.#31 // classesview/SelfAdd.f4:()V
#4 = Fieldref #32.#33 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #34.#35 // java/io/PrintStream.println:(I)V
#6 = Class #36 // classesview/SelfAdd
#7 = Class #37 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lclassesview/SelfAdd;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 f1
#20 = Utf8 i
#21 = Utf8 I
#22 = Utf8 StackMapTable
#23 = Utf8 f2
#24 = Utf8 f3
#25 = Utf8 j
#26 = Utf8 f4
#27 = Utf8 SourceFile
#28 = Utf8 SelfAdd.java
#29 = NameAndType #8:#9 // "<init>":()V
#30 = NameAndType #24:#9 // f3:()V
#31 = NameAndType #26:#9 // f4:()V
#32 = Class #38 // java/lang/System
#33 = NameAndType #39:#40 // out:Ljava/io/PrintStream;
#34 = Class #41 // java/io/PrintStream
#35 = NameAndType #42:#43 // println:(I)V
#36 = Utf8 classesview/SelfAdd
#37 = Utf8 java/lang/Object
#38 = Utf8 java/lang/System
#39 = Utf8 out
#40 = Utf8 Ljava/io/PrintStream;
#41 = Utf8 java/io/PrintStream
#42 = Utf8 println
#43 = Utf8 (I)V
{
public classesview.SelfAdd();
descriptor: ()V
flags: (0x0001) 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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lclassesview/SelfAdd;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: invokestatic #2 // Method f3:()V
3: invokestatic #3 // Method f4:()V
6: return
LineNumberTable:
line 6: 0
line 7: 3
line 8: 6
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 args [Ljava/lang/String;
public static void f1();
descriptor: ()V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=0
0: iconst_0
1: istore_0
2: iload_0
3: bipush 10
5: if_icmpge 21
8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
11: iload_0
12: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
15: iinc 0, 1
18: goto 2
21: return
LineNumberTable:
line 28: 0
line 29: 8
line 28: 15
line 31: 21
LocalVariableTable:
Start Length Slot Name Signature
2 19 0 i I
StackMapTable: number_of_entries = 2
frame_type = 252 /* append */
offset_delta = 2
locals = [ int ]
frame_type = 250 /* chop */
offset_delta = 18
public static void f2();
descriptor: ()V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=0
0: iconst_0
1: istore_0
2: iload_0
3: bipush 10
5: if_icmpge 21
8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
11: iload_0
12: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
15: iinc 0, 1
18: goto 2
21: return
LineNumberTable:
line 51: 0
line 52: 8
line 51: 15
line 54: 21
LocalVariableTable:
Start Length Slot Name Signature
2 19 0 i I
StackMapTable: number_of_entries = 2
frame_type = 252 /* append */
offset_delta = 2
locals = [ int ]
frame_type = 250 /* chop */
offset_delta = 18
public static void f3();
descriptor: ()V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=0
0: iconst_0
1: istore_0
2: iload_0
3: iinc 0, 1
6: istore_1
7: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
10: iload_1
11: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
14: return
LineNumberTable:
line 72: 0
line 73: 2
line 74: 7
line 75: 14
LocalVariableTable:
Start Length Slot Name Signature
2 13 0 i I
7 8 1 j I
public static void f4();
descriptor: ()V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=0
0: iconst_0
1: istore_0
2: iinc 0, 1
5: iload_0
6: istore_1
7: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
10: iload_1
11: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
14: return
LineNumberTable:
line 93: 0
line 94: 2
line 95: 7
line 96: 14
LocalVariableTable:
Start Length Slot Name Signature
2 13 0 i I
7 8 1 j I
}
SourceFile: "SelfAdd.java"
我们先来看f4();和f3();,也就是i++与++i本身的字节码,这里没有涉及循环,两者的字节码与源代码的对比如下:
f4();
int i=0;
int j = ++i;
0: iconst_0 // 常量0压栈
1: istore_0 // 出栈保存到本地变量0里面,即代码中的变量i
2: iinc 0, 1 // 本地变量0加1
5: iload_0 // 本地变量0压栈,此时这个本地变量的值为1
6: istore_1 // 出栈保存到本地变量1里面,即代码中的变量j
f3();
int i=0;
int j = i++;
0: iconst_0 // 常量0压栈
1: istore_0 // 出栈保存到本地变量0里面,即代码中的变量i
2: iload_0 // 本地变量0压栈
3: iinc 0, 1 // 本地变量0加1,注意:这里是本地变量加1,不是操作数栈,栈里依旧是0
6: istore_1 // 出栈保存到本地变量1里面,即代码中的变量j
从字节码层面上,可以看到两者之间始终是区别于先+还是后+,并没有哪里少操作或多操作了一步。知道了 i++和++i 在字节码中的执行原理后,我们再来看看f1();和f2();方法里这种使用了循环的字节码,如下:
public static void f1();
descriptor: ()V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=0
0: iconst_0 // 常量0压栈
1: istore_0 // 出栈保存到本地变量0里面
2: iload_0 // 从本地变量0压栈
3: bipush 10 // 常量10压栈,因为取值为-128~127,所以采用bipush指令
5: if_icmpge 21 // 若判断的结果是大于就跳转到第21行
8: getstatic #4 // 对应常量池的#4
11: iload_0 // 本地变量0压栈
12: invokevirtual #5 // 对应常量池的#5
15: iinc 0, 1 // 本地变量0加1
18: goto 2 // goto到第2行
21: return // return void
public static void f2();
descriptor: ()V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=0
0: iconst_0
1: istore_0
2: iload_0
3: bipush 10
5: if_icmpge 21
8: getstatic #4
11: iload_0
12: invokevirtual #5
15: iinc 0, 1
18: goto 2
21: return
可以看到,这两个方法的字节码是一样的。所以实际上循环里无论是用++i还是i++效率都是一样的,当再有人问你这个问题的时候,就可以理直气壮的说它俩的效率是一样的了。因为在字节码里,它俩的指令就是一模一样的,没有任何区别 。
字符串拼接
我们都知道,在循环里拼接字符串的话,要使用StringBuilder或StringBuffer。如果直接使用 + 进行字符串拼接的话,效率就会很低。那么为什么使用 + 进行字符串拼接效率就低,而使用StringBuilder或StringBuffer进行字符串拼接效率就高呢?这就是本小节将要说明的一个问题。同样的,我们也是从字节码的角度来看,首先编写一些测试代码 ,如下:
public class StringAdd {
public static void main(String[] args) {
f1();
f2();
}
public static void f1() {
String src = "";
for (int i = 0; i < 10; i++) {
//每一次循环都会new一个StringBuilder
src = src + "A";
}
System.out.println(src);
}
public static void f2() {
//只要一个StringBuilder
StringBuilder src = new StringBuilder();
for (int i = 0; i < 10; i++) {
src.append("A");
}
System.out.println(src);
}
}
以上代码编译后的字节码如下,我这里只截取了f1();
和f2();
方法的部分字节码:
public static void f1();
descriptor: ()V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=0
0: ldc #4 // 将常量字符串压栈,对应常量池中的#4
2: astore_0 // 出栈保存到本地变量0里面
3: iconst_0 // 常量0压栈
4: istore_1 // 出栈保存到本地变量1里面
5: iload_1 // 本地变量1压栈
6: bipush 10 // 常量10压栈,因为取值为-128~127,所以采用bipush指令
8: if_icmpge 37 // 若判断结果为大于,则执行第37行
11: new #5 // class java/lang/StringBuilder 创建StringBuilder实例,如果是jdk1.4之前的版本,这里创建的是StringBuffer
14: dup // 复制StringBuilder实例的引用并压入栈顶
15: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V 使用空构造器创建的
18: aload_0 // 本地变量0压栈,aload是用于将对象引用压栈的指令
19: invokevirtual #7 // Method java/lang/StringBuilder.append: (Ljava/lang/String;)Ljava/lang/StringBuilder; 执行了append方法
22: ldc #8 // 字符串A压栈,取值-2147483648~2147483647采用ldc指令
24: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 执行了append方法
27: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 执行了toString方法
30: astore_0 // 将对象引用出栈保存到本地变量0里面
31: iinc 1, 1 // 本地变量1加1
34: goto 5 // 跳转到第5行
37: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
40: aload_0 // 本地变量0压栈,aload是用于将对象引用压栈的指令
41: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
44: return // return void
public static void f2();
descriptor: ()V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=0
0: new #5 // class java/lang/StringBuilder 创建StringBuilder实例
3: dup // 复制StringBuilder实例的引用并压入栈顶
4: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
7: astore_0 // 将对象引用出栈并保存到本地变量0里面
8: iconst_0 // 常量0压栈
9: istore_1 // 出栈保存到本地变量1里面
10: iload_1 // 本地变量1压栈
11: bipush 10 // 常量10压栈,因为取值为-128~127,所以采用bipush指令
13: if_icmpge 29 // 若判断结果为大于,则执行第29行
16: aload_0 // 本地变量0压栈,aload是用于将对象引用压栈的指令
17: ldc #8 // 字符串A压栈,取值-2147483648~2147483647采用ldc指令
19: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 执行了append方法
22: pop
23: iinc 1, 1 // 本地变量1加1
26: goto 10 // 跳转到第10行
29: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
32: aload_0 // 本地变量0压栈,aload是用于将对象引用压栈的指令
33: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
36: return // return void
从以上f1();
方法的字节码中,可以看到,在循环中使用 + 进行字符串拼接的话,每次循环都会new一个StringBuilder实例,同样的也是需要执行append方法来拼接字符串,最后还需要执行toString方法转换成字符串类型。而f2();
方法的字节码中,只创建了一次StringBuilder的实例,并且执行的指令也要少一些。所以使用StringBuilder进行字符串拼接,比使用 + 拼接的效率高。
Try-Finally字节码
除了以上小节所提到的问题外,还有一个问题也很常见,这是一个关于Try-Finally的题目。就是try里会return一个字符串,而finally里则会改变这个字符串。那么到底会返回改变前的字符串还是改变后的字符串。代码如下:
public class TryFinally {
public static void main(String[] args) {
System.out.println(f1());
}
public static String f1() {
String str = "hello";
try{
return str;
} finally {
str = "finally";
}
}
}
这个问题,我们同样可以从字节码的层面进行分析。将以上代码编译后的字节码如下,我这里只截取了f1();
方法的部分字节码,免得一些已经介绍过的内容占用篇幅:
public static java.lang.String f1();
descriptor: ()Ljava/lang/String;
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=0
0: ldc #5 // String hello 压栈
2: astore_0 // 出栈保存到本地变量0里
3: aload_0 // 本地变量0压栈
4: astore_1 // 出栈保存到本地变量1里
5: ldc #6 // String finally 压栈
7: astore_0 // 出栈保存到本地变量0里
8: aload_1 // 本地变量1压栈
9: areturn // 返回栈顶元素
10: astore_2 // 如果发生异常,操作栈里就会存在一个异常对象,此时就会把异常对象出栈保存到本地变量2里,然后执行以下指令
11: ldc #6 // String finally 压栈
13: astore_0 // 出栈保存到本地变量0里
14: aload_2 // 本地变量2压栈
15: athrow // 抛出异常,即把本地变量2里存储的异常对象给抛出
如上,从字节码中进行分析,我们就可以很清晰的看到f1();
方法到底会返回哪一个字符串。所以我们才要学会分析字节码,这样我们就能够看到代码执行的本质,而不是去死记硬背这些怪题,下次再遇到这种类似代码就不会一脸懵逼了。
String Constant Variable
在关于字符串拼接那一小节中,我们得知了在使用 + 进行字符串拼接的时候,实际上会创建StringBuilder实例来完成字符串的拼接。但使用 + 进行字符串拼接,背后就一定是StringBuilder吗?实际上是未必的,这取决于是常量拼接还是变量拼接。同样的,我们来编写一些测试代码,然后从字节码的层面上去观察。代码如下:
public class Constant {
public static void main(String[] args) {
f1();
new Constant().f2();
}
public static void f1() {
final String x = "hello";
final String y = x + "world";
String z = x + y;
System.out.println(z);
}
public void f2() {
final String x = "hello";
String y = x + "world";
String z = x + y;
System.out.println(z);
}
}
将以上代码编译后的字节码如下,我这里只截取了f1();
和f2();
方法的部分字节码:
public static void f1();
descriptor: ()V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=0
0: ldc #6 // 字符串hello压栈
2: astore_0 // 出栈保存到本地变量0里面
3: ldc #7 // 字符串helloworld压栈
5: astore_1 // 出栈保存到本地变量1里面
6: ldc #8 // 字符串hellohelloworld压栈
8: astore_2 // 出栈保存到本地变量2里面
9: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
12: aload_2 // 本地变量2压栈
13: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 此时操作栈里只有本地变量2,所以打印本地变量2
16: return // retrun void
public void f2();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: ldc #6 // 字符串hello压栈
2: astore_1 // 出栈保存到本地变量1里面
3: ldc #7 // 字符串helloworld压栈
5: astore_2 // 出栈保存到本地变量2里面
6: new #11 // class java/lang/StringBuilder 创建StringBuilder实例,因为到这一步就是变量进行拼接了
9: dup // 复制StringBuilder实例的引用并压入栈顶
10: invokespecial #12 // Method java/lang/StringBuilder."<init>":()V 调用构造函数完成实例的构造
13: ldc #6 // 字符串hello压栈
15: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 调用append方法拼接字符串
18: aload_2 // 本地变量2压栈
19: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 调用append方法拼接字符串
22: invokevirtual #14 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 调用toString方法转换为字符串类型
25: astore_3 // 出栈保存到本地变量3里面
26: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
29: aload_3 // 本地变量3压栈
30: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 此时操作栈里只有本地变量3,所以打印本地变量3
33: return // return void
从以上的字节码中,可以看到,当常量字符串进行拼接的时候,并没有使用SpringBuilder去完成拼接,而是直接使用了一个新的字符串进行赋值,这其实是JVM在编译时会对这种常量及字面常量进行替换,因为字节码里面是没有 + 的概念的。所以只有变量拼接才会使用SpringBuilder去完成拼接。
常用代码优化方法
常用代码优化方法:
- 尽量复用对象,不要循环创建对象,比如:for循环的字符串拼接
- 容器类初始化的时候最好是指定长度,例如List、Map等,可以减少动态扩容的次数
- ArrayList随机遍历快,LinkedList添加删除快
- 集合遍历时,尽量减少重复计算
- 尽量使用Entry来遍历Map,代码示例(1)
- 大数组复制使用System.arraycopy,因为该方法底层是使用C实现的,所以效率高些
- 局部变量尽量使用基本类型,而不是包装类型
- 不要手动调用System.gc(),因为GC是会耗时
- 及时消除过期对象的引用,防止内存泄露
- 尽量使用局部变量,减小变量的作用域
- 尽量使用非同步的容器,例如ArrayList和Vector中,应该选用ArrayList,因为Vector里大量使用了synchronized,会导致效率低下
- 尽量减小同步作用范围,例如synchronized方法和synchronized代码块中,应该选用synchronized代码块的方式完成同步
- 可以使用ThreadLocal缓存线程不安全的对象或重量级的对象,例如SimpleDateFormat
- 尽量使用延迟加载,例如在单例模式中就不要使用懒汉式的,代码示例(2)
- 尽量减少使用反射,如果是必须使用反射,则把反射出来的对象加缓存里,这样能避免使用反射的次数
- 尽量使用连接池、线程池、对象池、缓存
- 及时释放资源,I/O流、Socket、数据库连接对象
- 慎用异常,不要用抛异常来表示正常的业务逻辑
- String操作尽量少用支持正则表达式的方法,因为支持正则表达式方法的性能比较低
- 日志输出注意使用不同的日志级别,避免导致日志不停的输出
- 日志中参数拼接使用占位符,代码示例(3)
(1):
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
}
(2):
package classesview;
public class Singleton {
private Singleton() {
}
private static class SingletonHolder{
private static Singleton singleton = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.singleton;
}
}
(3):
log.info("orderId" + orderId); // 不推荐
log.info("orderId:{}", orderId); // 推荐
参考文档
java虚拟机规范
java语言规范
javap:
字段描述符
方法描述符
字节码指令:
常量池:
本地变量表:
- https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6.1
- https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.13
操作数栈:
Code属性:
LineNumberTable:
constant variable:
常量表达式
String.intern
String去重
以上是关于JVM字节码与Java代码层调优的主要内容,如果未能解决你的问题,请参考以下文章