- 使用javap -verbose 命令分析一个字节码文件时,将会分析该字节码的魔数、版本号、常量池、类信息、类的构造方法、类中的方法信息、类变量与成员变量等等。
- 魔数:所有的.class字节码文件的前4个字节都是魔数,固定为0xCAFEBABE
- 魔术之后的四个字节为版本信息,前两个字节表示minor version(次版本号),后两个字节表示major version(主版本号)。
- 常量池(constant pool):紧接着主版本号之后的就是常量池入口。
- 一个java类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看作是Class文件的资源仓库,比如说java类中定义的方法和变量信息,都是存储在常量池中。
- 常量池中主要存储两类常量:字面量与符号引用。字面量如文本字符串,Java中声明为final的常量值等。而符号引用如类和接口的全局限定名,字段的名称和描述符等。
- 常量池的总体结构:Java类所对应的常量池(常量表)主要由常量池数量与常量池数组这两部分构成。常量池数量紧跟在主版本后面,占据两个字节;常量池数组则紧跟在常量池数量之后。常量池数组与一般数组不同的是,常量池数组中不同的元素的类型、结构都是不同的,长度当然也就不同。但是每一种元素的第一个数据都是u1类型的,该字节是个标志位,占据一个字节。JVM在解析常量池时,会根据这个u1类型来获取元素的具体类型。值得注意的是,常量池数组中元素的个数 = 常量池数 - 1(其中0暂时不使用),目的是满足某些常量池索引值的数据在特定情况下需要表达{不引用任何一个常量池}的含义:根本原因在于,索引为0也是一个常量(保留常量),只不过他不位于常量表中,这个常量就对应null值。所以,常量池的索引从1而非0开始。
- jdk1.6及以前
- jdk新版本:
- 在JVM规范中,每个变量/字段都有描述信息,描述信息主要的作用是描述字段的数据类型、方法和参数列表(包括数量、类型、顺序)与返回值。 根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符表示,对象类型则使用字符L加对象的全限定名称来表示,如下所示:B - byte 、C -char、D - double、 F- float、 I- int、J - long、S - short、 z - Boolean、V - void , L-对象类型,如Ljava/lang/String;
- 对于数组类型来说,每一个维度使用一个前置的[来表示,如int[] 被记录为[I , String[][]被记录为[[Ljava/lang/String;
- 用描述符描述方法时,按照先参数列表,后返回值的顺序来描述。参数列表按照参数的严格顺序放在一组()类,如方法:String getRealNamebyNickname(int id , String name)的描述符为: (I,Ljava/lang/String;)Ljava/lang/String;
- class文件结构中出现的类或接口名称都是通过全限定形式表示的(也称为二进制名称)(全限定名称类似于java.lang包下的Object类的完全限定名为java.lang.Object)。
- Class字节码中有两种数据类型:
1)字节数据直接量:这是基本的数据类型。共细分为u1,u2,u4,u8四种,分别代表连续的1个字节、2个字节、4个字节、8个字节组成的整体数据。
2)表(数组):表是由多个基本数据或其他表,按照既定的顺序组成的大的数据集合。表是有结果的,他的结构体现在:组成表的成分所在的位置和顺序都是已经严格定义好的。 - 访问标志信息包括:Class文件是类还是接口;public?private?abstract?final?
- Java字节码整体结构
- Access Flags:如0x0021表示:0x0020与0x0001的并集,表示ACC_PUBLIC和ACC_SUPER
- 类索引、父类索引、接口索引
- 字段表用于描述类和接口中声明的变量。这里的变量包含了类级别变量以及实例变量(静态变量及成员变量),但不包括方法内部声明的局部变量。
- 字段表结构:
- 方法表结构:
方法表结构
类型
名称
数量
u2
access_flags
1
u2
name_index
1
u2
descriptor_index
1
u2
attributes_count
1
attribute_info
attributes
attributes_count
- (方法的)属性结构:
attribute_info{
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
} - 方法的属性结构:JVM预定义了部分attribute,但是编译器自己也可以实现自己的attribute写入Class文件里,供运行时使用。不同的attribute通过attribute_name_index来区分。
- 每一个方法都有一个Code attribute的属性:Code attribute的作用是保存该方法的结构,如所对应字节码的信息:
- max_stack表示这个方法运行的任何时刻所能达到的操作数栈的最大深度;max_locals表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量。
- exception_table,这里存放的是处理异常的信息;start_pc和end_pc表示在code数组中的从start_pc到end_pc(包含start_pc不包含end_pc)处的指令抛出的异常会由这个表项来处理。handler_pc表示处理异常的代码的开始处。catch_type表示会被处理的异常类型。它指向常量池里的一个异常类。
- java字节码对于异常的处理方式:统一采用异常表的方式来处理异常;当异常处理存在finally语句块时,现代化的jvm采取的处理方式是finally语句块的字节码拼接(复制)到每一个catch块后面,换句话说,程序中存在多少个catch块,就会在每一个catch块后面重复多少个finally语句块的字节码。finally在无异常信息时一个,在每一个捕获异常信息是均各一个,any异常一个。
-
Code_attribute{ u2 attribute_name_index; u4 attribute_length; u2 max_stack; u2 max_locals; u4 code_length; u1 code[code_length]; u2 exception_table_length{ u2 start_pc; u2 end_pc; u2 handler_pc; u2 catch_type; }exception_table[exception_table_length]; u2 attributes_count; attribute_info attributes[attributes_count]; }
- 好用的字节码查看工具:jclasslib;字节码打开软件可用Winhex;
- 对于java里的非静态方法(实例方法),其在编译后所生成的字节码当中,方法参数的数量总是会比源代码中方法的参数数量多一个(this),它位于方法的第一个参数位置处;这样就可以在java实例方法中使用this来访问当前对象的属性和其他方法。这个操作是在编译期间完成的,即由javac编译器在编译的时候将对this的访问转化为对一个普通实例方法参数的访问,接下来在运行期间,由JVM在调用实例方法时,自动向实例方法传入该this参数。所以在实例方法的局部变量表中至少会有一个指向当前对象的局部变量,这个局部变量就是this(编译器隐式传进来)。在java中每个方法都可以访问this。
- 程序代码、javap -verbose的结果:
public class TestSimpleByteCode2 { String str = "welcome"; private int x = 5; public static Integer in = 10; public static void main(String[] args){ TestSimpleByteCode2 test2 = new TestSimpleByteCode2(); test2.setX(10); in = 20; } public void setX(int x){ this.x = x; } }
Last modified 2019年5月9日; size 843 bytes MD5 checksum 4920c1b0f2779e10223cc163a84d5d4a Compiled from "TestSimpleByteCode2.java" public class Test5.TestSimpleByteCode2 minor version: 0 major version: 55 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #5 // Test5/TestSimpleByteCode2 super_class: #10 // java/lang/Object interfaces: 0, fields: 3, methods: 4, attributes: 1 Constant pool: #1 = Methodref #10.#34 // java/lang/Object."<init>":()V #2 = String #35 // welcome #3 = Fieldref #5.#36 // Test5/TestSimpleByteCode2.str:Ljava/lang/String; #4 = Fieldref #5.#37 // Test5/TestSimpleByteCode2.x:I #5 = Class #38 // Test5/TestSimpleByteCode2 #6 = Methodref #5.#34 // Test5/TestSimpleByteCode2."<init>":()V #7 = Methodref #5.#39 // Test5/TestSimpleByteCode2.setX:(I)V #8 = Methodref #40.#41 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #9 = Fieldref #5.#42 // Test5/TestSimpleByteCode2.in:Ljava/lang/Integer; #10 = Class #43 // java/lang/Object #11 = Utf8 str #12 = Utf8 Ljava/lang/String; #13 = Utf8 x #14 = Utf8 I #15 = Utf8 in #16 = Utf8 Ljava/lang/Integer; #17 = Utf8 <init> #18 = Utf8 ()V #19 = Utf8 Code #20 = Utf8 LineNumberTable #21 = Utf8 LocalVariableTable #22 = Utf8 this #23 = Utf8 LTest5/TestSimpleByteCode2; #24 = Utf8 main #25 = Utf8 ([Ljava/lang/String;)V #26 = Utf8 args #27 = Utf8 [Ljava/lang/String; #28 = Utf8 test2 #29 = Utf8 setX #30 = Utf8 (I)V #31 = Utf8 <clinit> #32 = Utf8 SourceFile #33 = Utf8 TestSimpleByteCode2.java #34 = NameAndType #17:#18 // "<init>":()V #35 = Utf8 welcome #36 = NameAndType #11:#12 // str:Ljava/lang/String; #37 = NameAndType #13:#14 // x:I #38 = Utf8 Test5/TestSimpleByteCode2 #39 = NameAndType #29:#30 // setX:(I)V #40 = Class #44 // java/lang/Integer #41 = NameAndType #45:#46 // valueOf:(I)Ljava/lang/Integer; #42 = NameAndType #15:#16 // in:Ljava/lang/Integer; #43 = Utf8 java/lang/Object #44 = Utf8 java/lang/Integer #45 = Utf8 valueOf #46 = Utf8 (I)Ljava/lang/Integer; { java.lang.String str; descriptor: Ljava/lang/String; flags: (0x0000) public static java.lang.Integer in; descriptor: Ljava/lang/Integer; flags: (0x0009) ACC_PUBLIC, ACC_STATIC public Test5.TestSimpleByteCode2(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: ldc #2 // String welcome 7: putfield #3 // Field str:Ljava/lang/String; 10: aload_0 11: iconst_5 12: putfield #4 // Field x:I 15: return LineNumberTable: line 14: 0 line 15: 4 line 16: 10 LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LTest5/TestSimpleByteCode2; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: new #5 // class Test5/TestSimpleByteCode2 3: dup 4: invokespecial #6 // Method "<init>":()V 7: astore_1 8: aload_1 9: bipush 10 11: invokevirtual #7 // Method setX:(I)V 14: bipush 20 16: invokestatic #8 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 19: putstatic #9 // Field in:Ljava/lang/Integer; 22: return LineNumberTable: line 19: 0 line 20: 8 line 21: 14 line 22: 22 LocalVariableTable: Start Length Slot Name Signature 0 23 0 args [Ljava/lang/String; 8 15 1 test2 LTest5/TestSimpleByteCode2; public void setX(int); descriptor: (I)V flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 2: putfield #4 // Field x:I 5: return LineNumberTable: line 24: 0 line 25: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this LTest5/TestSimpleByteCode2; 0 6 1 x I static {}; descriptor: ()V flags: (0x0008) ACC_STATIC Code: stack=1, locals=0, args_size=0 0: bipush 10 2: invokestatic #8 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 5: putstatic #9 // Field in:Ljava/lang/Integer; 8: return LineNumberTable: line 17: 0 } SourceFile: "TestSimpleByteCode2.java"
- Synchronized关键字字节码分析:使用javap -verbose -p 命令可以显示private方法的反编译结果
// 给实例方法上synchronized关键字,是给当前对象本身上锁(即setX); private synchronized void setX(int x){ this.x = x; } //给对象里的一个块上锁:monitorenter;monitorexit; // 除了程序正常执行流程完后使用monitorexit;还需要能在遇到异常时,在抛出异常之前执行monitorexit public void test1(String str){ synchronized (str){ System.out.println("hello word!"); } } // 对静态方法加synchronized关键字,是给当前类TestByteCodeComplex所对应的class对象上的锁。 public synchronized static void test2(){ }
private synchronized void setX(int); descriptor: (I)V flags: (0x0022) ACC_PRIVATE, ACC_SYNCHRONIZED Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 2: putfield #4 // Field x:I 5: return LineNumberTable: line 23: 0 line 24: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this LTest5/TestByteCodeComplex; 0 6 1 x I public void test1(java.lang.String); descriptor: (Ljava/lang/String;)V flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=4, args_size=2 0: aload_1 1: dup 2: astore_2 3: monitorenter 4: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream; 7: ldc #11 // String hello word! 9: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 12: aload_2 13: monitorexit 14: goto 22 17: astore_3 18: aload_2 19: monitorexit 20: aload_3 21: athrow 22: return Exception table: from to target type 4 14 17 any 17 20 17 any LineNumberTable: line 28: 0 line 29: 4 line 30: 12 line 31: 22 LocalVariableTable: Start Length Slot Name Signature 0 23 0 this LTest5/TestByteCodeComplex; 0 23 1 str Ljava/lang/String; StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 17 locals = [ class Test5/TestByteCodeComplex, class java/lang/String, class java/lang/Object ] stack = [ class java/lang/Throwable ] frame_type = 250 /* chop */ offset_delta = 4 public static synchronized void test2(); descriptor: ()V flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED Code: stack=0, locals=0, args_size=0 0: return LineNumberTable: line 35: 0
- <init>是指类的默认构造方法,<cinit>静态信息的初始化方法;
- 可重入锁:synchronized和ReentrantLock都是可重入锁,基于线程的分配而不是基于方法调用的分配;
- 自动装箱与自动拆箱:
public static void main(String[] args) { //自动装箱 Integer total = 99; //自定拆箱 int totalprim = total; } //对应的字节码指令: 0 bipush 99 2 invokestatic #2 <java/lang/Integer.valueOf> 5 astore_1 6 aload_1 7 invokevirtual #3 <java/lang/Integer.intValue> 10 istore_2 11 return
- stack frame:栈帧:栈帧是一种用于帮助虚拟机执行方法调用与方法执行的数据结构。栈帧在特定时刻只归属于特定的一个线程(不存在同步,并发等相关的一些问题)。栈帧本身是一种数据结构,封装了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息。
- 符号引用,直接引用:
有些符号引用是在类加载阶段或是第一次使用时就会转换为直接引用,这种转化叫做静态解析;
另外一些符号引用则是在每次运行期都会转化为直接引用,这种转化叫做动态链接,这体现为Java的多态性。 - 方法调用的字节码指令:
1)invokeinterface:调用接口中的方法,实际上是在运行期决定的,决定到底调用实现接口的哪个对象的特定方法
2)invokestatic:调用静态方法
3)invokespecial:调用自己的私有方法、构造方法以及父类的方法(都属于实例方法)
4)invokevirtual:调用虚方法,运行期动态查找的 (都属于实例方法)
5)invokedynamic:动态调用方法 - 静态解析的四种情形:静态方法;父类方法;构造方法;私有方法(无法被重写);这四类方法称作为非虚方法,他们是在类加载阶段就可以将符号引用转化为直接引用的。
- new关键字:为对象在堆上开辟一个内存空间(创建新的对象);压入栈顶;调用执行构造方法;将构造方法执行完后针对于在堆上所生成的对象的引用,将这个引用值返回,即将引用存储到局部变量当中。
- 方法的静态分派:在编译期间完全能确定的行为。
- 通过比较方法重载与方法重写,我们可以得到这样一个结论:方法重载是静态的,是编译期行为;方法重写是动态的,是运行期行为。
-
//对于:Grandpa p1 = new Father(); //p1的静态类型是Grandpa,而g1的实际类型(真正指向的类型)是Father //变量的静态类型是不会发生变化的,而变量的实际类型则是可以发生变化的(多态的一种体现), //实际类型是在运行期才能确定的 public class TestByteCodeMethod2 { // 对JVM,方法的重载是一种静态的行为,编译期就可以完全确定。 // 而方法重写是一种动态的行为 public void test(Grandpa grandpa){ System.out.println("grandpa"); } public void test(Father father){ System.out.println("father"); } public void test(Son son){ System.out.println("son"); } public static void main(String[] args) { Grandpa p1 = new Father(); Grandpa p2 = new Son(); TestByteCodeMethod2 testByteCode = new TestByteCodeMethod2(); testByteCode.test(p1); testByteCode.test(p2); // 输出:grandpa grandpa } } class Grandpa{ } class Father extends Grandpa{ } class Son extends Father{ }
//方法的动态分派 //方法的动态分派涉及到一个很重要的概念:方法接收者(方法是谁来调用的) //new之后 : invokevirtual字节码指令的多态查找流程:1找到操作数栈顶的第一个元素,它所指向的对象的实际类型(Apple) //找到常量池当中此对象中与Fruit.test()具有相同方法描述符和方法权限等相同的方法, // 找到了就直接返回类的此方法的直接引用(将符号引用转化为了直接引用); // 如果没有找到,则从子类向父类一直查找,直到查找到此方法,若一直找不到则会抛出异常(无此方法) public class TestByteCodeMethod3 { public static void main(String[] args) { Fruit apple = new Apple(); Fruit orange = new Orange(); apple.test(); orange.test(); apple = new Orange(); apple.test(); // 输出:Apple Orange Orange } } //方法重写 class Fruit{ public void test(){ System.out.println("Fruit"); } } class Apple extends Fruit{ @Override public void test(){ System.out.println("Apple"); } } class Orange extends Fruit{ @Override public void test(){ System.out.println("Orange"); } } ///////////////// // 对应的main方法的反编译结果中code如下(字节码指令): 0 new #2 <Test5/Apple> //创造新对象,分配内存空间 3 dup //复制操作数栈顶值 4 invokespecial #3 <Test5/Apple.<init>> //执行对象的构造方法 7 astore_1 //将引用存储到局部变量表中 8 new #4 <Test5/Orange> 11 dup 12 invokespecial #5 <Test5/Orange.<init>> 15 astore_2 16 aload_1 17 invokevirtual #6 <Test5/Fruit.test> 20 aload_2 21 invokevirtual #6 <Test5/Fruit.test> 24 new #4 <Test5/Orange> 27 dup 28 invokespecial #5 <Test5/Orange.<init>> 31 astore_1 32 aload_1 33 invokevirtual #6 <Test5/Fruit.test> 36 return