一、javap命令简述
javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。
当然这些信息中,有些信息(如本地变量表、指令和代码行偏移量映射表、常量池中方法的参数名称等等)需要在使用javac编译成class文件时,指定参数才能输出,比如,你直接javac xx.java,就不会在生成对应的局部变量表等信息,如果你使用javac -g xx.java就可以生成所有相关信息了。如果你使用的eclipse,则默认情况下,eclipse在编译时会帮你生成局部变量表、指令和代码行偏移量映射表等信息的。
通过反编译生成的汇编代码,我们可以深入的了解java代码的工作机制。比如我们可以查看i++;这行代码实际运行时是先获取变量i的值,然后将这个值加1,最后再将加1后的值赋值给变量i。
通过局部变量表,我们可以查看局部变量的作用域范围、所在槽位等信息,甚至可以看到槽位复用等信息。
javap的用法格式:javap <options> <classes>
其中classes就是你要反编译的class文件。
在命令行中直接输入javap或javap -help可以看到javap的options有如下选项:
-help --help -? 输出此用法消息 -version 版本信息,其实是当前javap所在jdk的版本信息,不是class在哪个jdk下生成的。 -v -verbose 输出附加信息(包括行号、本地变量表,反汇编等详细信息) -l 输出行号和本地变量表 -public 仅显示公共类和成员 -protected 显示受保护的/公共类和成员 -package 显示程序包/受保护的/公共类 和成员 (默认) -p -private 显示所有类和成员 -c 对代码进行反汇编 -s 输出内部类型签名 -sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列) -constants 显示静态最终常量 -classpath <path> 指定查找用户类文件的位置 -bootclasspath <path> 覆盖引导类文件的位置
一般常用的是-v -l -c三个选项。
javap -v classxx,不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用到的常量池等信息。
javap -l 会输出行号和本地变量表信息。
javap -c 会对当前class字节码进行反编译生成汇编代码。
查看汇编代码时,需要知道里面的jvm指令,可以参考官方文档:
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html
另外通过jclasslib工具也可以看到上面这些信息,而且是可视化的,效果更好一些。
二、javap测试及内容详解
前面已经介绍过javap输出的内容有哪些,东西比较多,这里主要介绍其中code区(汇编指令)、局部变量表和代码行偏移映射三个部分。
如果需要分析更多的信息,可以使用javap -v进行查看。
另外,为了更方便理解,所有汇编指令不单拎出来讲解,而是在反汇编代码中以注释的方式讲解(吐槽一下,简书的markdown貌似不能改字体颜色,这一点很不爽)。
下面写段代码测试一下:
例子1:分析一下下面的代码反汇编之后结果:
public class TestDate { private int count = 0; public static void main(String[] args) { TestDate testDate = new TestDate(); testDate.test1(); } public void test1(){ Date date = new Date(); String name1 = "wangerbei"; test2(date,name1); System.out.println(date+name1); } public void test2(Date dateP,String name2){ dateP = null; name2 = "zhangsan"; } public void test3(){ count++; } public void test4(){ int a = 0; { int b = 0; b = a+1; } int c = a+1; } }
上面代码通过JAVAC -g 生成class文件,然后通过javap命令对字节码进行反汇编:$ javap -c -l TestDate
得到下面内容(指令等部分是我参照着官方文档总结的):
Warning: Binary file TestDate contains com.justest.test.TestDate Compiled from "TestDate.java" public class com.justest.test.TestDate { //默认的构造方法,在构造方法执行时主要完成一些初始化操作,包括一些成员变量的初始化赋值等操作 public com.justest.test.TestDate(); Code: 0: aload_0 //从本地变量表中加载索引为0的变量的值,也即this的引用,压入栈 1: invokespecial #10 //出栈,调用java/lang/Object."<init>":()V 初始化对象,就是this指定的对象的init()方法完成初始化 4: aload_0 // 4到6表示,调用this.count = 0,也即为count复制为0。这里this引用入栈 5: iconst_0 //将常量0,压入到操作数栈 6: putfield //出栈前面压入的两个值(this引用,常量值0), 将0取出,并赋值给count 9: return //指令与代码行数的偏移对应关系,每一行第一个数字对应代码行数,第二个数字对应前面code中指令前面的数字 LineNumberTable: line 5: 0 line 7: 4 line 5: 9 //局部变量表,start+length表示这个变量在字节码中的生命周期起始和结束的偏移位置(this生命周期从头0到结尾10),slot就是这个变量在局部变量表中的槽位(槽位可复用),name就是变量名称,Signatur局部变量类型描述 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lcom/justest/test/TestDate; public static void main(java.lang.String[]); Code: // new指令,创建一个class com/justest/test/TestDate对象,new指令并不能完全创建一个对象,对象只有在初,只有在调用初始化方法完成后(也就是调用了invokespecial指令之后),对象才创建成功, 0: new //创建对象,并将对象引用压入栈 3: dup //将操作数栈定的数据复制一份,并压入栈,此时栈中有两个引用值 4: invokespecial #20 //pop出栈引用值,调用其构造函数,完成对象的初始化 7: astore_1 //pop出栈引用值,将其(引用)赋值给局部变量表中的变量testDate 8: aload_1 //将testDate的引用值压入栈,因为testDate.test1();调用了testDate,这里使用aload_1从局部变量表中获得对应的变量testDate的值并压入操作数栈 9: invokevirtual #21 // Method test1:()V 引用出栈,调用testDate的test1()方法 12: return //整个main方法结束返回 LineNumberTable: line 10: 0 line 11: 8 line 12: 12 //局部变量表,testDate只有在创建完成并赋值后,才开始声明周期 LocalVariableTable: Start Length Slot Name Signature 0 13 0 args [Ljava/lang/String; 8 5 1 testDate Lcom/justest/test/TestDate; public void test1(); Code: 0: new #27 // 0到7创建Date对象,并赋值给date变量 3: dup 4: invokespecial #29 // Method java/util/Date."<init>":()V 7: astore_1 8: ldc #30 // String wangerbei,将常量“wangerbei”压入栈 10: astore_2 //将栈中的“wangerbei”pop出,赋值给name1 11: aload_0 //11到14,对应test2(date,name1);默认前面加this. 12: aload_1 //从局部变量表中取出date变量 13: aload_2 //取出name1变量 14: invokevirtual #32 // Method test2: (Ljava/util/Date;Ljava/lang/String;)V 调用test2方法 // 17到38对应System.out.println(date+name1); 17: getstatic #36 // Field java/lang/System.out:Ljava/io/PrintStream; //20到35是jvm中的优化手段,多个字符串变量相加,不会两两创建一个字符串对象,而使用StringBuilder来创建一个对象 20: new #42 // class java/lang/StringBuilder 23: dup 24: invokespecial #44 // Method java/lang/StringBuilder."<init>":()V 27: aload_1 28: invokevirtual #45 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder; 31: aload_2 32: invokevirtual #49 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 35: invokevirtual #52 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 38: invokevirtual #56 // Method java/io/PrintStream.println:(Ljava/lang/String;)V invokevirtual指令表示基于类调用方法 41: return LineNumberTable: line 15: 0 line 16: 8 line 17: 11 line 18: 17 line 19: 41 LocalVariableTable: Start Length Slot Name Signature 0 42 0 this Lcom/justest/test/TestDate; 8 34 1 date Ljava/util/Date; 11 31 2 name1 Ljava/lang/String; public void test2