一文看懂Java字节码
前言
随着Java语言的不断的发展,Java的应用场景慢慢被扩大,各种优雅解决问题的技术也不断衍生,如AOP技术,清晰理解Java运行原理就显得很有必要,本篇文章重点讲解Java字节码相关知识。
字节码基础
Java文件通过编译器生成的是class字节码文件,字节码文件也有文件自己的格式,这里不详细展开,直接通过Java自己带的工具查看一下。 首先我们的测试类文件如下:
public class Person {
public String name;
public int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
定义了一个Person类,里面有name和age的属性,编译后生成Person.class文件,直接使用Java工具dump这个class文件,dump命令如下:
javap -v -p Person.class
dump生成的内容如下:
public class com.sec.resourceparse.Person
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#27 // java/lang/Object."<init>":()V
#2 = Fieldref #4.#28 // com/sec/resourceparse/Person.name:Ljava/lang/String;
#3 = Fieldref #4.#29 // com/sec/resourceparse/Person.age:I
#4 = Class #30 // com/sec/resourceparse/Person
#5 = Class #31 // java/lang/Object
#6 = Utf8 name
#7 = Utf8 Ljava/lang/String;
#8 = Utf8 age
#9 = Utf8 I
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 Lcom/sec/resourceparse/Person;
#17 = Utf8 getName
#18 = Utf8 ()Ljava/lang/String;
#19 = Utf8 setName
#20 = Utf8 (Ljava/lang/String;)V
#21 = Utf8 getAge
#22 = Utf8 ()I
#23 = Utf8 setAge
#24 = Utf8 (I)V
#25 = Utf8 SourceFile
#26 = Utf8 Person.java
#27 = NameAndType #10:#11 // "<init>":()V
#28 = NameAndType #6:#7 // name:Ljava/lang/String;
#29 = NameAndType #8:#9 // age:I
#30 = Utf8 com/sec/resourceparse/Person
#31 = Utf8 java/lang/Object
{
public java.lang.String name;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC
public int age;
descriptor: I
flags: ACC_PUBLIC
public com.sec.resourceparse.Person();
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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/sec/resourceparse/Person;
public java.lang.String getName();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field name:Ljava/lang/String;
4: areturn
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/sec/resourceparse/Person;
public void setName(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #2 // Field name:Ljava/lang/String;
5: return
LineNumberTable:
line 13: 0
line 14: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/sec/resourceparse/Person;
0 6 1 name Ljava/lang/String;
这里截取了部分内容,先简单看一下,首先是类信息的介绍:
public class com.sec.resourceparse.Person
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
类名,编译的JDK版本,以及访问修饰符 然后字符串池:
Constant pool:
#1 = Methodref #5.#27 // java/lang/Object."<init>":()V
#2 = Fieldref #4.#28 // com/sec/resourceparse/Person.name:Ljava/lang/String;
#3 = Fieldref #4.#29 // com/sec/resourceparse/Person.age:I
#4 = Class #30 // com/sec/resourceparse/Person
#5 = Class #31 // java/lang/Object
#6 = Utf8 name
#7 = Utf8 Ljava/lang/String;
这里包含整个类里面的字符串,包含声明的类信息,属性等 最后是方法的信息:
public java.lang.String getName();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field name:Ljava/lang/String;
4: areturn
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/sec/resourceparse/Person;
这里主要是方法名,访问修饰符,以及操作栈执行流程信息 看完整个类的class文件,下面介绍字节码相关的基础知识。
访问修饰符
上述字节码中类,属性以及方法中均有flag信息,这个就是修饰符,在字节码中类访问修饰符及对应值如下所示:
标志符名称 | 标志符值 | 释义 |
---|---|---|
ACC_PUBLIC | 0x0001 | Public 类型 |
ACC_FINAL | 0x0010 | Final类型 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义 |
ACC_INTERFACE | 0x0200 | 接口修饰符 |
ACC_ABSTRACT | 0x0400 | abstract修饰符 |
ACC_SYNTHETIC | 0x1000 | 标志这个类并非由用户代码生成 |
ACC_ANNOTATION | 0x2000 | 注解修饰符 |
**ACC_ENUM | 0x400 | 枚举修饰符 |
上面介绍的是类的访问修饰符,那么属性以及方法的也是类似的,只是相对而言比较简单,这里就不继续展开了。
类型对照表
JAVA中有基本类型,数组,以及对象,字节码中对类型的表示有所区别,对照表如下所示:
类型 | 字节码表示 | 释义 |
---|---|---|
byte | B | 字节 |
boolean | Z | bool |
char | C | 字符 |
short | S | 短整型 |
int | I | 整型 |
float | F | 浮点数 |
long | J | 长整型 |
double | D | 浮点数 |
void | V | 空返回值 |
类 | Ljava/lang/Object; | 对象类型 |
数组 | [] | [ |
其中类是以L开头,中间是类路径,最后以;结尾,上面的数组是单个数组,要结合其他类型一起使用,如int[]的字节码是[I,int[][]的字节码是[[I.
方法解析
上面已经介绍了访问修饰符以及JAVA字节码中类型对照,下面讲解一下方法的解析,拿上面的方法举例,如下所示:
public java.lang.String getName();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field name:Ljava/lang/String;
4: areturn
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/sec/resourceparse/Person;
- descriptor:方法描述,描述的是方法参数以及返回值类型,其格式为: (参数类型)返回值类型,这里表示方法为无参且返回值为String
- flags: 为方法的访问修饰符,这里表示为Public
- Code:具体方法栈的描述
- stack:栈分配最大深度
- locals:方法内局部变量个数
- args_size:方法参数数量
- LineNumberTable:方法行数信息(不关注,没有细看)
- LocalVariableTable:局部变量对照表
这里简单解释一下,类方法最少有一个参数,这个参数就是类对象本身,相当于this关键字,而且下标是0。
字节码指令
上面已经介绍了字节码相关的基础知识,但是没有详细说明字节码指令相关内容,本节就重点介绍字节码指令内容,字节码指令主要分为如下几类:
- 存储与加载类指令 加载参数到操作栈,或者将操作栈中的数据存到局部变量中,主要包括load系列指令,store和push等指令
- 对象操作指令 对象指令主要包括new生成对象,从对象中获取属性等操作,如getField和putField以及getStatic和putStatic等
- 栈管理指令 pop和dup等压栈和推出栈指令
- 运算指令 运算指令主要是对数据进行加减乘除等指令,这里也只会在操作栈中执行
- 控制跳转指令 ifelse等条件判断指令,还有goto等
- 方法调用和返回指令 主要包括invoke系列指令和return系列指令,其中invoke是执行方法的指令,return是返回系列指令
操作栈流程
上面已经基本介绍完字节码所有的内容了,这里实战讲解方法操作流程。先记住下面这个点: JAVA方法执行都是基于栈进行的,方法调用指令调用后都会出栈,如果方法有返回值,则将返回值压栈
先分析一个简单的:
public java.lang.String getName();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field name:Ljava/lang/String;
4: areturn
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/sec/resourceparse/Person;
1.aload_0:这里是将第0个参数,压栈,参数的类型是对象(前面分析过是this)


再介绍一个稍微复杂一点的列子:
public class Manager {
public static void main(String [] args) {
String resPath = "/Users/Desktop/resources.arsc";
FileInputStream ins = null;
ByteArrayOutputStream ous = null;
try {
ins = new FileInputStream(new File(resPath));
ous = new ByteArrayOutputStream();
int length = -1;
byte data[] = new byte[4 * 1024];
while ((length = ins.read(data)) != -1) {
ous.write(data, 0, length);
}
byte[] resData = ous.toByteArray();
ParseUtils.parseRes(resData);
} catch (Exception e) {
e.printStackTrace();
}
}
}
对应的字节码如下所示:
public com.sec.resourceparse.Manager();
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 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/sec/resourceparse/Manager;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=5, locals=7, args_size=1
0: ldc #2 // String /Users/Desktop/resources.arsc
2: astore_1
3: aconst_null
4: astore_2
5: aconst_null
6: astore_3
7: new #3 // class java/io/FileInputStream
10: dup
11: new #4 // class java/io/File
14: dup
15: aload_1
16: invokespecial #5 // Method java/io/File."<init>":(Ljava/lang/String;)V
19: invokespecial #6 // Method java/io/FileInputStream."<init>":(Ljava/io/File;)V
22: astore_2
23: new #7 // class java/io/ByteArrayOutputStream
26: dup
27: invokespecial #8 // Method java/io/ByteArrayOutputStream."<init>":()V
30: astore_3
31: iconst_m1
32: istore 4
34: sipush 4096
37: newarray byte
39: astore 5
41: aload_2
42: aload 5
44: invokevirtual #9 // Method java/io/FileInputStream.read:([B)I
47: dup
48: istore 4
50: iconst_m1
51: if_icmpeq 66
54: aload_3
55: aload 5
57: iconst_0
58: iload 4
60: invokevirtual #10 // Method java/io/ByteArrayOutputStream.write:([BII)V
63: goto 41
66: aload_3
67: invokevirtual #11 // Method java/io/ByteArrayOutputStream.toByteArray:()[B
70: astore 6
72: aload 6
74: invokestatic #12 // Method com/sec/resourceparse/ParseUtils.parseRes:([B)V
77: goto 87
80: astore 4
82: aload 4
84: invokevirtual #14 // Method java/lang/Exception.printStackTrace:()V
87: return
这个Manager中只声明了一个static的main方法,但是字节码中有一个init的方法,其实就是默认的无参构造方法,先看一下这个方法的字节码:
public com.sec.resourceparse.Manager();
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 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/sec/resourceparse/Manager;
1.aload_0:将this对象压入栈中 2.invokespecial:调用栈顶对象的特殊方法init方法,由于init的返回值类型为V,所以调用后栈顶就为空 3.return:由于栈顶没有值,所以直接执行return指令就可以了
再重点看下另外一个方法:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=5, locals=7, args_size=1
0: ldc #2 // String /Users/Desktop/resources.arsc
2: astore_1
3: aconst_null
4: astore_2
5: aconst_null
6: astore_3
7: new #3 // class java/io/FileInputStream
10: dup
11: new #4 // class java/io/File
14: dup
15: aload_1
16: invokespecial #5 // Method java/io/File."<init>":(Ljava/lang/String;)V
19: invokespecial #6 // Method java/io/FileInputStream."<init>":(Ljava/io/File;)V
22: astore_2
23: new #7 // class java/io/ByteArrayOutputStream
26: dup
27: invokespecial #8 // Method java/io/ByteArrayOutputStream."<init>":()V
30: astore_3
31: iconst_m1
32: istore 4
34: sipush 4096
37: newarray byte
39: astore 5
41: aload_2
42: aload 5
44: invokevirtual #9 // Method java/io/FileInputStream.read:([B)I
47: dup
48: istore 4
50: iconst_m1
51: if_icmpeq 66
54: aload_3
55: aload 5
57: iconst_0
58: iload 4
60: invokevirtual #10 // Method java/io/ByteArrayOutputStream.write:([BII)V
63: goto 41
66: aload_3
67: invokevirtual #11 // Method java/io/ByteArrayOutputStream.toByteArray:()[B
70: astore 6
72: aload 6
74: invokestatic #12 // Method com/sec/resourceparse/ParseUtils.parseRes:([B)V
77: goto 87
80: astore 4
82: aload 4
84: invokevirtual #14 // Method java/lang/Exception.printStackTrace:()V
87: return
方法解释:
- ([Ljava/lang/String;)V:参数是一个String的一维数组,无返回值
- flags:访问修饰符为static 和 public的
堆栈操作解释: 0:压栈一个String类型的对象,值为:"/Users/Desktop/resources.arsc"

- 弹出栈顶元素并且存在局部变量1中
- 将null压入栈中
- 弹出栈顶元素null,并且存在局部变量2中
- 将null压入栈中
- 弹出栈顶元素null,并且存在局部变量3中
上面操作结束后,方法栈和局部变量如下所示:

7-30:
- new一个java/io/FileInputStream对象,并且压入栈中
- dup:将上面产生的对象再压入栈中,当前栈有2个FileInputStream对象了
- new一个java/io/File对象,并且压入栈中
- dup:将上面产生的File对象再压入栈中,当前栈有2个File对象了
- aload_1:将局部变量1压入栈中,也就是String值压入栈中
- invokespecial:调用File的init方法,参数是String,无返回值
说明: 5-6就是将创建出来的File对象,调用其构造方法的过程,这里应该弄清楚为什么创建对象后要压2次栈了
- invokespecial:调用FileInputStream对象的方法,参数是File,无返回值
- astore_2:将栈顶元素存到局部变量2中
- new一个java/io/ByteArrayOutputStream对象,并且压入栈中
- dup:将上面产生的对象再压入栈中,当前栈中有2个ByteArrayOutputStream对象
- invokespecial:调用ByteArrayOutputStream的init方法
- 将栈顶元素存到局部变量3中
31-42
- iconst_m1:将-1压入栈中
- istore 4:将栈顶弹出,存到局部变量4中
- sipush 4096:将4096 int类型的数压入栈顶
- newarray :取出栈的数,创建数组,并压入栈中
- astore 5:弹出栈的元素并且存入局部变量5中
- aload_2:将局部变量2压入栈中
- aload 5:将局部变量5压入栈中
- invokevirtual:执行FileInputStream的read方法,参数为byte数组,返回值为int
- dup:复制栈顶元素,并且压入栈中
- istore 4:弹出栈顶元素并且存到局部变量4中
- iconst_m1:将-1再压入栈中
- if_icmpeq 66:比较栈顶2个int数是否相等,相等就直接跳到66行,负责执行下面的逻辑
上面逻辑基本就这样分析,这个方法比较长,就不继续向下分析,都是一样的步骤
操作栈流程的关键: 所有的操作都伴随着压栈和出栈的逻辑,如方法调用,使用到的在栈中的类和参数会被出栈,如果方法有返回值,则将返回值压栈。
总结
字节码知识还是比较重要的,理解字节码知识能清晰的理解JVM运行机制,同时为后面AOP直接操作字节码打下基础。
参考: segmentfault.com/a/119000000… my.oschina.net/ta8210/blog…
转载自:https://juejin.cn/post/6844903925498413064