本文是我多年之前的老博客(android-performance.com)的一篇文章,老博客很久没有维护了,把一些有用的文章转移过来。
javap 是 jdk 自带的一个工具,可以反编译 class 文件,是我们在做 java 代码性能分析时必不可少的一个工具。
我们先写个简单的代码,然后我们在逐个分析 javap 解析出来的内容。
public class TestJavap {
public static int add(int a, int b) {
int r = a + b;
return r;
}
public static void main(String[] args) {
int r = add(15, 16);
System.out.println(r);
}
}
执行 javap -v TestJavap
之后获得的内容如下:
D:\workspace\test_java\bin>javap -v TestJavap.class
Classfile /D:/workspace/test_java/bin/TestJavap.class
Last modified 2013-12-31; size 643 bytes
MD5 checksum 03f49f751716ceb852c190bfb54cbb2f
Compiled from "TestJavap.java"
public class TestJavap
SourceFile: "TestJavap.java"
minor version: 0
major version: 50
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // TestJavap
#2 = Utf8 TestJavap
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 LTestJavap;
#14 = Utf8 add
#15 = Utf8 (II)I
#16 = Utf8 a
#17 = Utf8 I
#18 = Utf8 b
#19 = Utf8 r
#20 = Utf8 main
#21 = Utf8 ([Ljava/lang/String;)V
#22 = Methodref #1.#23 // TestJavap.add:(II)I
#23 = NameAndType #14:#15 // add:(II)I
#24 = Fieldref #25.#27 // java/lang/System.out:Ljava/io/PrintStream;
#25 = Class #26 // java/lang/System
#26 = Utf8 java/lang/System
#27 = NameAndType #28:#29 // out:Ljava/io/PrintStream;
#28 = Utf8 out
#29 = Utf8 Ljava/io/PrintStream;
#30 = Methodref #31.#33 // java/io/PrintStream.println:(I)V
#31 = Class #32 // java/io/PrintStream
#32 = Utf8 java/io/PrintStream
#33 = NameAndType #34:#35 // println:(I)V
#34 = Utf8 println
#35 = Utf8 (I)V
#36 = Utf8 args
#37 = Utf8 [Ljava/lang/String;
#38 = Utf8 SourceFile
#39 = Utf8 TestJavap.java
{
public TestJavap();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LTestJavap;
public static int add(int, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=2
0: iload_0
1: iload_1
2: iadd
3: istore_2
4: iload_2
5: ireturn
LineNumberTable:
line 4: 0
line 5: 4
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 a I
0 6 1 b I
4 2 2 r I
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: bipush 15
2: bipush 16
4: invokestatic #22 // Method add:(II)I
7: istore_1
8: getstatic #24 // Field java/lang/System.out:Ljava/io/PrintStream;
11: iload_1
12: invokevirtual #30 // Method java/io/PrintStream.println:(I)V
15: return
LineNumberTable:
line 9: 0
line 10: 8
line 11: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 args [Ljava/lang/String;
8 8 1 r I
}
很长很恐怖,是吧。。。(如果是一个实际项目的class文件,那会恐怖得令人发指),别急,让我们来一点一点地分析:
Classfile /D:/workspace/test_java/bin/TestJavap.class
Last modified 2013-12-31; size 643 bytes
MD5 checksum 03f49f751716ceb852c190bfb54cbb2f
Compiled from "TestJavap.java"
public class TestJavap
SourceFile: "TestJavap.java"
minor version: 0
major version: 50
这部分不用多说,大家一看就明白。主要就是记录一些基础的版本信息。minor version: 0 major version: 50 指的是这个class文件编译时所使用的 jdk 版本号。
Constant pool:
#1 = Class #2 // TestJavap
#2 = Utf8 TestJavap
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
.......
Constant Pool (常量池),在java虚拟机中是个重要的概念。我们可以这样理解一下,这个“池子”记录了java程序运行所需要的所有符号,包括变量名、方法名、类名、字符串等一切符号。在下面的介绍中你会看到,在java方法执行时会经常引用常量池中的内容。#1,#2 这样的数字可以理解为常量池中的每一项的“索引地址”,字节码指令会经常使用这个索引来引用对应的符号。
这里张图可以加深对常量池的理解:
(图1:java 虚拟机的数据结构)
(图2:java class 文件结构)
是不是觉得jvm运行时离不开常量池
更多关于常量池的介绍可以参考:《Javaclassfile-The constant pool》, 以及《深入java虚机》一书
下面是重点,我们会详细介绍方法字节码表示的含义。
比如方法 add 对应的java代码和字节码表示为:
public static int add(int a, int b) {
int r = a + b;
return r;
}
......
public static int add(int, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=2
0: iload_0
1: iload_1
2: iadd
3: istore_2
4: iload_2
5: ireturn
LineNumberTable:
line 4: 0
line 5: 4
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 a I
0 6 1 b I
4 2 2 r I
......
其中 flags: ACC_PUBLIC, ACC_STATIC 这一行我觉得不用细讲,一看就明白,这是类或方法的访问标识,用来定义他们的访问权限的。还有 ACC_FINAL ACC_ABSTRACT 等。他们和 public 、static、final 、abstract 这些关键字是对应的。
下面我们先来介绍 LocalVariableTable(局部变量表)
我们要先有记住一点,jvm是基于栈的运算,先看一下上面的图1(Java虚拟机运行时的数据结构)。每个java线程在运行时,jvm都会为其分配一个“栈空间”(就是一个内存区域),主要包括一个PC寄存器(记录当前线程运行的下一条指令),JVM栈空间,本地栈空间(本地代码,一般是C写的lib可以理解为JNI的方式调用的代码,和我们自己写的java代码无关了)。当某个java方法运行时,jvm会创建一个“栈帧”(也是一段内存空间),我们要介绍的LocalVariableTable就是“栈帧”的一部分,另外“栈帧”还包括我们常听说的“操作数栈(Operand Stack)”和对常量池的引用(Reference To Constant Pool)。局部变量表中记录了一个java方法运行时锁需要的局部变量名(Name 这一列), Signature 是类型描述符,I就表示int类型(更多类型描述符参见:《Chapter 4. The class File Format》
这个还要介绍一个“Slot”
的概念,一个 Slot 就可以理解为一个 32 位(4字节)的内存单位。在我们的例子中,参数 a、b 临时变量 r 都是 int 类型,在 java 中,int 类型就是一个4字节长度,即1个slot。在我们的例子中,LocalVariableTable 中有三个变量,都是 int 类型,需要 3 个 slot,所以看到 locals =3 这一行 就应该明白是什么意思了吧。
我们在深入一点,把 a,b 和r 都换成 Long 类型,在 javap -v
一下,看看会变成什么样子:
代码:
public static long add(long a, long b) {
long r = a + b;
return r;
}
对应的字节码为:
.......
public static long add(long, long);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=6, args_size=2
0: lload_0
1: lload_2
2: ladd
3: lstore 4
5: lload 4
7: lreturn
LineNumberTable:
line 4: 0
line 5: 5
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 a J
0 8 2 b J
5 3 4 r J
.......
是不是能找到点感觉啦? 因为在 java 中 long 类型是 64 位,8 字节,要占用 2 个 slot,所以 3 个变量共占用 6 个 slot,所以这里 locals = 6。Slot 这里一列也不一样了,是吧,说明,Slot 这一列可以看作变量空间的入口索引位置(Signature 下的 J 是 long 类型的类型描述符)。
stack 指的是栈的宽度——就是执行这个方法时,为这个方法的操作数栈定义多少个slot,注意,这个宽度足以容纳当前方法所有运算所需要的操作数,下面我们举例说明。
上面的例子中,只有一个 a + b
的操作,每个参数都是 long 型(即 2 个slot), 执行这个加法运算的过程是这样的,lload_0 指令把 LocalVariableTable 中索引为 0 的操作数(变量a)压入操作数栈中,lload_2 把索引为 2 的操作数也压入栈中,注意,这里操作数栈中已经压入了两个 long 类型,共 4 个 slot,然后 ladd 指令从栈中弹出这两个操作数(此时操作数栈空了),运算结束后在把运算结构再次压入栈中,此时操作数栈中只有一个long类型的数据(占用 2 个 slot),然后 lstore 把栈中的结果保存在局部变量表中索引为 4 的位置(即变量r)。在这个过程中,“最多”占用 4 个 slot(就是把 a 和 b 都压入栈中的时候),所以 stack=4
。
Code 代码前的标号是字节码指令的偏移(java的字节码文件组织得是很紧凑的,每个字节都有其具体的含义)。 jvm 中每个字节码占用 1 个字节,上面了例子中,lload_0 、lload_2、ladd 3 个指令由于没有操作数,所以它们几个的偏移量分别为0,1,2。第四个指令 lstore 后面跟了一个操作数索引参数(1个字节),其占用2个字节,所以下一个质量的偏移量是从 5开始,一次类推。更多java字节码质量参见:Java bytecode instruction listings
LineNumberTable 记录字节码行号和源代码行号的对应关系。 比如 line 4: 0,左边的4代表源码的行号,后边的0代表字节码的起始偏移地址。这个信息是用来调试用了,我们经常看到的java抛出的异常时锁所携带的线程调用栈的信息,就是跟这个表有关系。
出处:https://www.coderxing.com/javap-verbose.html
本文为原创文章,采用署名-相同方式共享 3.0 中国大陆(CC BY-SA 3.0 CN))进行许可,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。