1.通过javap命令查看class文件的字节码内容
最简单的一个案例
public class Test { public static void main(String[] args) { int a = 2; int b = 5; int c = b-a; System.out.println(c); } }
先使用javac命令进行编译,Test.class,再使用
javap ‐v Test1.class > Test.txt
Test.txt内容如下
内容大致分为4个部分:
- 第一部分:显示了生成这个class的java源文件、版本信息、生成时间等。
- 第二部分:显示了该类中所涉及到常量池,共26个常量。
- 第三部分:显示该类的构造器,编译器自动插入的。
- 第四部分:显示了main方的信息。(这个是需要我们重点关注的)
Classfile /E:/Astudy/JVM/Test.class Last modified 2020-3-8; size 398 bytes MD5 checksum a638d93d1191aa144f8140f0fcfcd644 Compiled from "Test.java" public class Test minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: //常量池 #1 = Methodref //方法引用 #5.#14 // java/lang/Object."<init>":()V #2 = Fieldref //字段引用 #15.#16 // java/lang/System.out:Ljava/io/PrintStream; #3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V #4 = Class //类 #19 // Test #5 = Class //UTF-8编码的字符串 #20 // java/lang/Object #6 = Utf8 <init> #7 = Utf8 ()V #8 = Utf8 Code #9 = Utf8 LineNumberTable #10 = Utf8 main #11 = Utf8 ([Ljava/lang/String;)V #12 = Utf8 SourceFile #13 = Utf8 Test.java #14 = NameAndType //字段或方法的符号引用 #6:#7 // "<init>":()V #15 = Class #21 // java/lang/System #16 = NameAndType #22:#23 // out:Ljava/io/PrintStream; #17 = Class #24 // java/io/PrintStream #18 = NameAndType #25:#26 // println:(I)V #19 = Utf8 Test #20 = Utf8 java/lang/Object #21 = Utf8 java/lang/System #22 = Utf8 out #23 = Utf8 Ljava/io/PrintStream; #24 = Utf8 java/io/PrintStream #25 = Utf8 println #26 = Utf8 (I)V { public Test(); //无参构造 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 1: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=1 0: iconst_2 //将int类型的2压入栈 1: istore_1 //出栈一个变量放入局部变量表中下标为1的位置,下标为0的位置存放的是this指针,此时栈为空, 2: iconst_5 //将int类型的5压入栈 3: istore_2 //出栈一个变量放入局部变量表中下标为2的位置,下标为0的位置存放的是this指针,此时栈为空, 4: iload_2 //从局部变量表中取出下标为2(实际值此处为5)的变量压入操作数栈中 5: iload_1 //从局部变量表中取出下标为1(实际值此处为2)的变量压入操作数栈中 6: isub //在操作数栈中做减操作,结果为3 7: istore_3 //出栈一个变量放入局部变量表中下标为3的位置,下标为0的位置存放的是this指针,此时栈为空, 8: getstatic #2 //去常量池中引用"#2"符号引用的类与方法 // Field java/lang/System.out:Ljava/io/PrintStream; 11: iload_3 //从局部变量表中取出下标为3(实际值此处为3)的变量压入操作数栈中 12: invokevirtual #3 //调度对象的实现方法 // Method java/io/PrintStream.println:(I)V 15: return LineNumberTable: line 3: 0 line 4: 2 line 5: 4 line 6: 8 line 7: 15 } SourceFile: "Test.java"
常量池
Constant TypeValue 说明
- CONSTANT_Class 7 类或接口的符号引用
- CONSTANT_Fieldref 9 字段的符号引用
- CONSTANT_Methodref 10 类中方法的符号引用
- CONSTANT_InterfaceMethodref 11 接口中方法的符号引用
- CONSTANT_String 8 字符串类型常量
- CONSTANT_Integer 3 整形常量
- CONSTANT_Float 4 浮点型常量
- CONSTANT_Long 5 长整型常量
- CONSTANT_Double 6 双精度浮点型常量
- CONSTANT_NameAndType 12 字段或方法的符号引用
- CONSTANT_Utf8 1 UTF-8编码的字符串
- CONSTANT_MethodHandle 15 表示方法句柄
- CONSTANT_MethodType 16 标志方法类型
- CONSTANT_InvokeDynamic 18 表示一个动态方法调用点
图示:你们一定没见过画的这么丑的图
二:i++和++i的区别
java代码
public class Test2 { public static void main(String[] args) { new Test2().method1(); new Test2().method2(); } public void method1() { int i = 1; int a = i++; System.out.println(a); //打印1 } public void method2() { int i = 1; int a = ++i; System.out.println(a); //打印2 } }
执行javac之后执行javap
字节码节选
i++
stack=2, locals=3, args_size=1 0: iconst_1 //将int类型的1压入栈 1: istore_1 //出栈一个变量放入局部变量表中下标为1的位置,下标为0的位置存放的是this指针,此时栈为空, 2: iload_1 //从局部变量表中取出下标为1(实际值此处为1)的变量压入操作数栈中 3: iinc 1, 1 //将局部变量表中下标为1的变量进行加1操作 6: istore_2 //出栈一个变量放入局部变量表中下标为2的位置,这一步没有对操作栈中的数进行操作,直接出栈到变量表中 7: getstatic #6 //去常量池中引用"#6"符号引用的类与方法 // Field java/lang/System.out:Ljava/io/PrintStream; 10: iload_2 //从局部变量表中取出下标为1(实际值此处为1)的变量压入操作数栈中 11: invokevirtual #7 //执行println方法 // Method java/io/PrintStream.println:(I)V 14: return
++i
stack=2, locals=3, args_size=1 0: iconst_1 //将int类型的1压入栈 1: istore_1 //出栈一个变量放入局部变量表中下标为1的位置,下标为0的位置存放的是this指针,此时栈为空, 2: iinc 1, 1 //将局部变量表中下标为1的变量进行加1操作 5: iload_1 //从局部变量表中取出下标为1(实际值此处为2)的变量压入操作数栈中 6: istore_2 //出栈一个变量放入局部变量表中下标为2的位置,(实际值此处为2) 7: getstatic #6 //去常量池中引用"#6"符号引用的类与方法 // Field java/lang/System.out:Ljava/io/PrintStream; 10: iload_2 //从局部变量表中取出下标为1(实际值此处为2)的变量压入操作数栈中 11: invokevirtual #7 //执行println方法 // Method java/io/PrintStream.println:(I)V 14: return
区别:
i++
- 只是在本地变量中对数字做了相加,并没有将数据压入到操作栈
- 将前面拿到的数字1,再次从操作栈中拿到,压入到本地变量中
++i
- 将本地变量中的数字做了相加,并且将数据压入到操作栈
- 将操作栈中的数据,再次压入到本地变量中
三:字符串拼接
使用StringBuilder和"+"号拼接哪个效率高?
public class Test3 { public static void main(String[] args) { new Test3().m1(); new Test3().m2(); } public void m1() { String s1 = "123"; String s2 = "456"; String s3 = s1 + s2; System.out.println(s3); } public void m2() { String s1 = "123"; String s2 = "456"; StringBuilder sb = new StringBuilder(); sb.append(s1); sb.append(s2); String s3 = sb.toString(); System.out.println(s3); } }
直接上字节码
字符串在类加载的时候会保存至常量池当中
Constant pool: #1 = Methodref #14.#25 // java/lang/Object."<init>":()V #2 = Class #26 // Test3 #3 = Methodref #2.#25 // Test3."<init>":()V #4 = Methodref #2.#27 // Test3.m1:()V #5 = Methodref #2.#28 // Test3.m2:()V #6 = String #29 // 123 #7 = String #30 // 456 #8 = Class #31 // java/lang/StringBuilder #9 = Methodref #8.#25 // java/lang/StringBuilder."<init>":()V #10 = Methodref #8.#32 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #11 = Methodref #8.#33 // java/lang/StringBuilder.toString:()Ljava/lang/String; #12 = Fieldref #34.#35 // java/lang/System.out:Ljava/io/PrintStream; #13 = Methodref #36.#37 // java/io/PrintStream.println:(Ljava/lang/String;)V #14 = Class #38 // java/lang/Object #15 = Utf8 <init> #16 = Utf8 ()V #17 = Utf8 Code #18 = Utf8 LineNumberTable #19 = Utf8 main #20 = Utf8 ([Ljava/lang/String;)V #21 = Utf8 m1 #22 = Utf8 m2 #23 = Utf8 SourceFile #24 = Utf8 Test3.java #25 = NameAndType #15:#16 // "<init>":()V #26 = Utf8 Test3 #27 = NameAndType #21:#16 // m1:()V #28 = NameAndType #22:#16 // m2:()V #29 = Utf8 123 #30 = Utf8 456 #31 = Utf8 java/lang/StringBuilder #32 = NameAndType #39:#40 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #33 = NameAndType #41:#42 // toString:()Ljava/lang/String; #34 = Class #43 // java/lang/System #35 = NameAndType #44:#45 // out:Ljava/io/PrintStream; #36 = Class #46 // java/io/PrintStream #37 = NameAndType #47:#48 // println:(Ljava/lang/String;)V #38 = Utf8 java/lang/Object #39 = Utf8 append #40 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #41 = Utf8 toString #42 = Utf8 ()Ljava/lang/String; #43 = Utf8 java/lang/System #44 = Utf8 out #45 = Utf8 Ljava/io/PrintStream; #46 = Utf8 java/io/PrintStream #47 = Utf8 println #48 = Utf8 (Ljava/lang/String;)V
"+"号拼接
stack=2, locals=4, args_size=1 0: ldc #6 //将常量池中的"123"压入栈 // String 123 2: astore_1 //将引用类型(String)的值存入局部变量表中的下标为1的位置,下标为0的位置为this 3: ldc #7 //将常量池中的"456"压入栈 // String 456 5: astore_2 //将引用类型(String)的值存入局部变量表中的下标为2的位置 6: new #8 //创建一个新的StringBuilder对象 // class java/lang/StringBuilder 9: dup //复制栈顶部一个字长内容 10: invokespecial #9 //根据编译时类型来调用实例方法 // Method java/lang/StringBuilder."<init>":()V 13: aload_1 //从局部变量表中取出下标为1的变量(此处为"123")放入到操作数栈中 14: invokevirtual #10 //调用StringBuilder的append()方法 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 17: aload_2 //从局部变量表中取出下标为1的变量(此处为"465")放入到操作数栈中 18: invokevirtual #10 //调用StringBuilder的append()方法 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: invokevirtual #11 //调用StringBuilder的toString()方法 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 24: astore_3 //将引用类型(String)的值存入局部变量表中的下标为3的位置 25: getstatic #12 //获取静态字段System.out // Field java/lang/System.out:Ljava/io/PrintStream; 28: aload_3 //从局部变量表中取出下标为3的变量(此处为"123456")放入到操作数栈中 29: invokevirtual #13 //调用System.out 的println()方法 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 32: return
StringBuilder
Code: stack=2, locals=5, args_size=1 0: ldc #6 //将常量池中的"123"压入栈 // String 123 2: astore_1 //将引用类型(String)的值存入局部变量表中的下标为1的位置,下标为0的位置为this 3: ldc #7 //将常量池中的"456"压入栈 // String 456 5: astore_2 //将引用类型(String)的值存入局部变量表中的下标为2的位置 6: new #8 //创建一个新的StringBuilder对象 // class java/lang/StringBuilder 9: dup //复制栈顶部一个字长内容 10: invokespecial #9 //根据编译时类型来调用实例方法 // Method java/lang/StringBuilder."<init>":()V 13: astore_3 //将引用类型(String)的值存入局部变量表中的下标为3的位置 14: aload_3 //从局部变量表中取出下标为3的变量放入到操作数栈中 15: aload_1 //从局部变量表中取出下标为1的变量(此处为"123")放入到操作数栈中 16: invokevirtual #10 //调用StringBuilder的append()方法 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 19: pop //弹出栈顶端一个字长的内容 20: aload_3 //从局部变量表中取出下标为3的变量放入到操作数栈中 21: aload_2 //从局部变量表中取出下标为2的变量放入到操作数栈中 22: invokevirtual #10 //调用StringBuilder的append()方法 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 25: pop //弹出栈顶端一个字长的内容 26: aload_3 //从局部变量表中取出下标为3的变量放入到操作数栈中 27: invokevirtual #11 //调用StringBuilder的toString()方法 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 30: astore 4 //将引用类型或returnAddress类型值存入局部变量下标为4 32: getstatic #12 //获取静态字段System.out // Field java/lang/System.out:Ljava/io/PrintStream; 35: aload 4 //从局部变量表中取出下标为4的变量(此处为"123456")放入到操作数栈中 37: invokevirtual #13 //调用System.out 的println()方法 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 40: return
从通过字节码的方式我们可以看出,+号其实还是使用了StringBuilder对象的append方法,所以他们的效率是一样的。但是真的是这样吗?如果是循环拼接呢?
循环拼接
使用+拼接
Code: stack=2, locals=3, args_size=1 0: ldc #6 //去常量池中找字符串,结果为空 // String 2: astore_1 //将引用类型(String)的值存入局部变量表中的下标为1的位置,下标为0的位置为this 3: iconst_0 //将int类型的0压入栈 4: istore_2 //将int类型的0弹出栈,存入局部变量表中下标为2的位置 5: iload_2 //将局部变量表中下标为2的值压入栈中 6: iconst_5 //将int类型的5压入栈 7: if_icmpge 35 //将操作数栈中的两个数进行比较,如果一个int(第一次循环值0)类型值大于或者等于另外一个int(值为5)类型值,则跳转35处执行 10: new #7 //创建一个StringBuilder对象 // class java/lang/StringBuilder 13: dup //复制栈顶部一个字长内容 14: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V 17: aload_1 //从局部变量表中取出下标为1的String变量压入操作数栈中 18: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: iload_2 //从局部变量表中取出下标为2的int类型变量压入操作数栈中 22: invokevirtual #10 //调用append方法 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 25: invokevirtual #11 //调用toString方法 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 28: astore_1 //将引用类型(String)的值存入局部变量表中的下标为1的位置,下标为0的位置为this 29: iinc 2, 1 //将局部变量表中下标为2的值加1 32: goto 5 //跳转到5 35: getstatic #12 //获取System.out // Field java/lang/System.out:Ljava/io/PrintStream; 38: aload_1 //从局部变量表中取出下标为1的String变量压入操作数栈中 39: invokevirtual #13 //调用println方法 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 42: return
使用StringBuilder
flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: new #7 //创建一个StringBuilder对象 // class java/lang/StringBuilder 3: dup //复制栈顶部一个字长内容 4: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V 7: astore_1 //将引用类型(String)的值存入局部变量表中的下标为1的位置,下标为0的位置为this 8: iconst_0 //将int类型的0压入栈 9: istore_2 //将int类型的0弹出栈,存入局部变量表中下标为2的位置 10: iload_2 //将局部变量表中下标为2的值压入栈中 11: iconst_5 //将int类型的5压入栈 12: if_icmpge 27 //将操作数栈中的两个数进行比较,如果一个int(第一次循环值0)类型值大于或者等于另外一个int(值为5)类型值,则跳转27处执行 15: aload_1 //从局部变量表中取出下标为1的String变量压入操作数栈中 16: iload_2 //从局部变量表中取出下标为2的int类型变量压入操作数栈中 17: invokevirtual #10 //调用append方法 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 20: pop //弹出栈顶端一个字长的内容 21: iinc 2, 1 //将局部变量表中下标为2的值加1 24: goto 10 //跳转到10 27: getstatic #12 //获取System.ou // Field java/lang/System.out:Ljava/io/PrintStream; 30: aload_1 //从局部变量表中取出下标为1的String变量压入操作数栈中 31: invokevirtual #11 //调用toString方法 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 34: invokevirtual #13 //调用println方法 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 37: return
结论,使用+号每一次循环都会创建一个StringBuilder对象每次拼接完之后都会调用toString()方法,这样的话效率肯定没有直接用StringBuildr效率高,因为StringBuildr只创建了一个对象,调用了一次toString()方法
代码优化建议
1、尽可能使用局部变量
调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中速度较快,其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。
2、尽量减少对变量的重复计算
明确一个概念,对方法的调用,即使方法中只有一句语句,也是有消耗的。所以例如下面的操作
for (int i = 0; i < list.size(); i++) {...}
建议替换为:
int length = list.size(); for (int i = 0, i < length; i++) {...}
3、尽量采用懒加载的策略,即在需要的时候才创建
String str = "aaa"; if (i == 1){ list.add(str); }//建议替换成 if (i == 1){ String str = "aaa"; list.add(str); }
4、异常不应该用来控制程序流程
异常对性能不利。抛出异常首先要创建一个新的对象,Throwable接口的构造函数调用名为fillInStackTrace()的本地同步方 法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,Java虚拟机就必须调整调用堆栈,因为在处理过程中创建 了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。
5、不要将数组声明为public static final
因为这毫无意义,这样只是定义了引用为static final,数组的内容还是可以随意改变的,将数组声明为public更是一个安全漏洞,这意味着这个数组可以被外部类所改变。
6、不要创建一些不使用的对象,不要导入一些不使用的类
如果代码中出现"The value of the local variable i is not used"、"Theimport java.util is never used",那么请删除这些无用的内容
7、程序运行过程中避免使用反射
反射是Java提供给用户一个很强大的功能,功能强大往往意味着效率不高。不建议在程序运行过程中使用尤其是频繁使用反射机制,特别是 Method的invoke方法。
如果确实有必要,一种建议性的做法是将那些需要通过反射加载的类在项目启动的时候通过反射实例化出一个对象并放入内存。
8、使用数据库连接池和线程池
这两个池都是用于重用对象的,前者可以避免频繁地打开和关闭连接,后者可以避免频繁地创建和销毁线程
9、容器初始化时尽可能指定长度
容器初始化时尽可能指定长度,如:new ArrayList<>(10); new HashMap<>(32); 避免容器长度不足时,扩容带来的性能损耗。
10、ArrayList随机遍历快,LinkedList添加删除快
11、使用Entry遍历Map
Map<String,String> map = new HashMap<>(); for (Map.Entry<String,String> entry : map.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); }
12、不要手动调用System.gc();
13、String尽量少用正则表达式
正则表达式虽然功能强大,但是其效率较低,除非是有需要,否则尽可能少用。
replace() 不支持正则
replaceAll() 支持正则
如果仅仅是字符的替换建议使用replace()
14、日志的输出要注意级别
// 当前的日志级别是error LOGGER.info("保存出错!" + user);
15、对资源的close()建议分开操作
try{ XXX.close(); YYY.close(); }catch (Exception e){ ... } // 建议改为 try{ XXX.close(); }catch (Exception e){ ... } try{ YYY.close(); }catch (Exception e){ ... }
3.1、尽可能使用局部变量
调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中速度较快,其他变
量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随
着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。
3.2、尽量减少对变量的重复计算
明确一个概念,对方法的调用,即使方法中只有一句语句,也是有消耗的。所以例如下
面的操作:
建议替换为:
这样,在list.size()很大的时候,就减少了很多的消耗。
3.3、尽量采用懒加载的策略,即在需要的时候才创建
for (int i = 0; i < list.size(); i++)
{...}
int length = list.size();
for (int i = 0, i < length; i++)
{...}
3.4、异常不应该用来控制程序流程
异常对性能不利。抛出异常首先要创建一个新的对象,Throwable接口的构造函数调用
名为fillInStackTrace()的本地同步方 法,fillInStackTrace()方法检查堆栈,收集调用跟踪
信息。只要有异常被抛出,Java虚拟机就必须调整调用堆栈,因为在处理过程中创建 了
一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。
3.5、不要将数组声明为public static final
因为这毫无意义,这样只是定义了引用为static final,数组的内容还是可以随意改变的,
将数组声明为public更是一个安全漏洞,这意味着这个数组可以被外部类所改变。
3.6、不要创建一些不使用的对象,不要导入一些不使用的
类
这毫无意义,如果代码中出现"The value of the local variable i is not used"、"The
import java.util is never used",那么请删除这些无用的内容
3.7、程序运行过程中避免使用反射
反射是Java提供给用户一个很强大的功能,功能强大往往意味着效率不高。不建议在程序
运行过程中使用尤其是频繁使用反射机制,特别是 Method的invoke方法。
如果确实有必要,一种建议性的做法是将那些需要通过反射加载的类在项目启动的时候
通过反射实例化出一个对象并放入内存。
String str = "aaa";
if (i == 1){
list.add(str);
}
//建议替换成
if (i == 1){
String str = "aaa";
list.add(str);
}
3.8、使用数据库连接池和线程池
这两个池都是用于重用对象的,前者可以避免频繁地打开和关闭连接,后者可以避免频
繁地创建和销毁线程。
3.9、容器初始化时尽可能指定长度
容器初始化时尽可能指定长度,如:new ArrayList<>(10); new HashMap<>(32); 避免容
器长度不足时,扩容带来的性能损耗。
3.10、ArrayList随机遍历快,LinkedList添加删除快
3.11、使用Entry遍历Map
避免使用这种方式:
3.12、不要手动调用System.gc();
3.13、String尽量少用正则表达式
正则表达式虽然功能强大,但是其效率较低,除非是有需要,否则尽可能少用。
replace() 不支持正则
replaceAll() 支持正则
如果仅仅是字符的替换建议使用replace()。
3.14、日志的输出要注意级别
Map<String,String> map = new HashMap<>();
for (Map.Entry<String,String> entry : map.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
}
Map<String,String> map = new HashMap<>();
for (String key : map.keySet()) {
String value = map.get(key);
}