随着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; 复制代码
这里简单解释一下,类方法最少有一个参数,这个参数就是类对象本身,相当于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)
2.getfield:从当前栈顶对象获取name的属性,并且将其压入栈中
3.areturn:当前栈顶是一个String类型的值,所以返回的使用要使用areturn
再介绍一个稍微复杂一点的列子:
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"
2-6:
上面操作结束后,方法栈和局部变量如下所示:
7-30:
31-42
上面逻辑基本就这样分析,这个方法比较长,就不继续向下分析,都是一样的步骤
操作栈流程的关键:所有的操作都伴随着压栈和出栈的逻辑,如方法调用,使用到的在栈中的类和参数会被出栈,如果方法有返回值,则将返回值压栈。
字节码知识还是比较重要的,理解字节码知识能清晰的理解JVM运行机制,同时为后面AOP直接操作字节码打下基础。
参考:
segmentfault.com/a/119000000… my.oschina.net/ta8210/blog…