一、关于javap命令
摘自官网:
javap可以反汇编一个或多个class文件。输出取决于所使用的选项。不使用任何选项时,该javap命令将打印public、protected 字段,以及method。
该javap命令不支持多版本JAR包。使用命令的类路径形式可以查看所有JAR文件(无论是否为multirelease)中的基本条目。使用URL形式,可以使用参数的URL形式来指定要反汇编的类的特定版本。
javap命令可以做什么?
首先我们回顾一下为什么java语言可以跨平台:
java程序编译之后的代码,并不是能被硬件系统直接运行的代码,而是一种“中间码”——字节码(.class 文件),然后不同的硬件平台上安装有不同的Java虚拟机(JVM),由JVM来把字节码再“翻译”成所对应的硬件平台能够执行的代码。因此对于Java编程者来说,不需要考虑硬件平台是什么。所以Java可以跨平台。
也就是说由jvm虚拟机将class文件里面的字节码翻译成对应的硬件平台能够执行的代码。通俗一点就是class文件里面的字节码是jvm一系列的操作步骤,它会将这些步骤翻译给硬件系统去执行,比如创建一个对象,步骤大致为:
1、开辟空间
2、初始化
3、引用赋值
而javap命令,将这些步骤反汇编过来,使我们可以查看我们写的代码,jvm虚拟机是如何一步一步的去运行它的。
例如:
java源码:
public class LazySingleton {
public static void main(String[] args) {
LazySingleton lazySingleton = new LazySingleton();
}
}
执行命令
javap -c -l LazySingleton
得到反汇编信息
public class com.test.design.singleton.LazySingleton {
//默认的构造方法,在构造方法执行时主要完成一些初始化操作,包括一些成员变量的初始化赋值等操作
public com.test.design.singleton.LazySingleton();
Code:
0: aload_0 //从本地变量表中加载索引为0的变量的值,也就是this的引用,压入栈
1: invokespecial #1 //出栈,调用java/lang/Object."<init>":()V 初始化对象,就是this指定的对象的init()方法完成初始化
4: return //默认构造方法结束,返回
//指令与代码行数的偏移对应关系,每一行第一个数字对应代码行数,第二个数字对应前面code中指令前面的数字
LineNumberTable:
line 3: 0
//局部变量表,start+length表示这个变量在字节码中的生命周期起始和结束的偏移位置(this生命周期从头0到结尾5),slot就是这个变量在局部变量表中的槽位(槽位可复用),name就是变量名称,Signatur局部变量类型描述
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/test/design/singleton/LazySingleton;
public static void main(java.lang.String[]);
Code:
// new指令,创建一个class com/test/design/singleton/LazySingleton对象,new指令并不能完全创建一个对象,对象只有在调用初始化方法完成后(也就是调用了invokespecial指令之后),对象才创建成功
0: new #2 //class com/test/design/singleton/LazySingleton 创建对象,并将对象引用压入栈
3: dup //将操作数栈定的数据复制一份,并压入栈,此时栈中有两个引用值,另外一个是this
4: invokespecial #3 // Method "<init>":()V pop出栈引用值,调用其构造函数,完成对象的初始化
7: astore_1 //pop出栈引用值,将其(引用)赋值给局部变量表中的变量lazySingleton, _1代码变量表中的slot(索引)
8: return //方法结束,返回
LineNumberTable:
line 5: 0
line 6: 8
//局部变量表,args是main方法的参数变量,
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
8 1 1 lazySingleton Lcom/test/design/singleton/LazySingleton;
}
i++ 与 ++i
public class LazySingleton {
public static void main(String[] args) {
int i = 0;
i = i++;
int j = 0;
j = ++j;
int k = 0;
k = j++ + j++ ;
}
}
反汇编分析
public class com.test.design.singleton.LazySingleton {
public com.test.design.singleton.LazySingleton();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public static void main(java.lang.String[]);
Code:
0: iconst_0 // 1、将常量0压入栈顶
1: istore_1 // 2、把它赋值给第一个变量i
2: iload_1 // 3、第一个变量i的值存入到栈顶
3: iinc 1, 1 // 4、将第一个常量(i) 加一,而第3步存入到栈中的值没有变
6: istore_1 // 5、将栈中的值赋给第一个常量i,从这里可以看出i++是将值压入栈顶,再执行自增操作,然后将栈中的值赋给自己。
7: iconst_0 // 1、将常量0压入栈顶
8: istore_2 // 2、把它赋值给第二个变量j
9: iinc 2, 1 // 3、将第二个常量(j)加一,而第3步存入到栈中的值没有变
12: iload_2 // 4、将第二个变量j的值存入到栈顶
13: istore_2 // 5、将栈中的值再赋值给第二个常量j,从这里可以看出++j是自增,将变量j的值压入栈中,再将栈的值赋值自己。
14: iconst_0 // 1、将常量0压入栈顶
15: istore_3 // 2、把它赋值给第三个常量k
16: iload_2 // 3、将第二个常量j存入栈顶 (值为1)
17: iinc 2, 1 // 4、将第二个常量j加一
20: iload_2 // 5、将第二个常量j存入栈顶(值为2),此时栈中有0、1、2
21: iinc 2, 1 // 6、将第二个常量j加一
24: iadd // 7、将栈定存了常量的2个值弹出相加,再将结果存入栈中,此时栈中有0、1、2、3
25: istore_3 // 8、将栈顶的值赋值给第3个常量k,
26: return
LineNumberTable:
line 5: 0
line 6: 2
line 7: 7
line 8: 9
line 10: 14
line 11: 16
line 12: 26
}
结论:
i++ 先将值存入栈中,再运算,下一次使用变量时,再将变量的值压入栈中,再运算
++i 是先运算再将值压入栈中,
总结
1、通过javap命令可以查看一个java类反汇编、常量池、变量表、指令代码行号表等等信息。
2、平常,我们比较关注的是java类中每个方法的反汇编中的指令操作过程,这些指令都是顺序执行的,可以参考官方文档查看每个指令的含义,很简单: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.areturn
3、通过对前面例子代码反汇编中各个指令操作的分析,可以发现,一个方法的执行通常会涉及下面几块内存的操作: (1)java栈中:局部变量表、操作数栈。这些操作基本上都值操作。
(2)java堆。通过对象的地址引用去操作。
(3)常量池。
(4)其他如帧数据区、方法区(jdk1.8之前,常量池也在方法区)等部分,测试中没有显示出来,这里说明一下。
4、通过javap命令,分析代码的效率高低
在做值相关操作时:
一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可能是指,可能是对象的引用)被压入操作数栈。
一个指令,也可以从操作数数栈中取出一到多个值(pop多次),完成赋值、加减乘除、方法传参、系统调用等等操作。
注意
关于指令,涉及到一个知识点,指令重排,jvm翻译给硬件系统的指令,并不是会按顺序执行,比如创建对象的指令:
1、开辟空间
2、初始化
3、引用赋值
cpu可能会对这些指令进行重排(指令重排),最终执行的指令可能是:
1、开辟空间
2、引用赋值
3、初始化
在单线程环境下不会有影响,多线程环境开发需要注意。