随着Java语言的不断的发展,Java的应用场景慢慢被扩大,各类优雅解决问题的技术也不断衍生,如AOP技术,清晰理解Java运行原理就显得颇有必要,本篇文章重点讲解Java字节码相关知识。java
Java文件经过编译器生成的是class字节码文件,字节码文件也有文件本身的格式,这里不详细展开,直接经过Java本身带的工具查看一下。 首先咱们的测试类文件以下:segmentfault
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生成的内容以下:bash
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;
复制代码
这里包含整个类里面的字符串,包含声明的类信息,属性等
最后是方法的信息:ui
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文件,下面介绍字节码相关的基础知识。this
上述字节码中类,属性以及方法中均有flag信息,这个就是修饰符,在字节码中类访问修饰符及对应值以下所示:spa
标志符名称 | 标志符值 | 释义 |
---|---|---|
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 | 枚举修饰符 |
上面介绍的是类的访问修饰符,那么属性以及方法的也是相似的,只是相对而言比较简单,这里就不继续展开了。.net
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;
复制代码
这里简单解释一下,类方法最少有一个参数,这个参数就是类对象自己,至关于this关键字,并且下标是0。
上面已经介绍了字节码相关的基础知识,可是没有详细说明字节码指令相关内容,本节就重点介绍字节码指令内容,字节码指令主要分为以下几类:
上面已经基本介绍完字节码全部的内容了,这里实战讲解方法操做流程。先记住下面这个点:
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
复制代码
方法解释:
堆栈操做解释:
0:压栈一个String类型的对象,值为:"/Users/Desktop/resources.arsc"
上面操做结束后,方法栈和局部变量以下所示:
7-30:
31-42
上面逻辑基本就这样分析,这个方法比较长,就不继续向下分析,都是同样的步骤
操做栈流程的关键: 全部的操做都伴随着压栈和出栈的逻辑,如方法调用,使用到的在栈中的类和参数会被出栈,若是方法有返回值,则将返回值压栈。
字节码知识仍是比较重要的,理解字节码知识能清晰的理解JVM运行机制,同时为后面AOP直接操做字节码打下基础。
参考:
segmentfault.com/a/119000000… my.oschina.net/ta8210/blog…