您的位置:首页 > 编程语言 > Java开发

一个类搞懂JAVA Class文件

2016-04-11 20:18 513 查看

0x00 Introduction

所有的Java代码最终交给JVM运行时都是需要转换成JVM的字节码,对于每一个类都需要组装成一个合法、完整的Class文件,被JVM载入后才能运行。

Java除了JLS作为语言标准外,还有一份The Java Virtual Machine Specification虚拟机规范,详细描述了Class文件的构成,以及JVM在载入时需要进行的检查、链接过程。这为Sun/Oracle之外的厂商自行实现JVM、编译器提供了可能。

最新的Java8的规范可从下面链接获取:

http://docs.oracle.com/javase/specs/jvms/se8/html/

本文代码是在JDK7 HotSpot下编译的

$ java -version
java version "1.7.0_75"
Java(TM) SE Runtime Environment (build 1.7.0_75-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.75-b04, mixed mode)


您可以从下面链接得到Java7的规范,网上还可以找到这份文件的中文翻译。

http://docs.oracle.com/javase/specs/jvms/se7/html/

作为一个Java码农,一定非常好奇Class的文件结构,同时对字节码的了解也利于性能调优。但是这份冗长的规范文件(如果整理成书将会有接近700页)其实大部分的时间都是在定义各种各样具体的数据结构以及各个指令的作用,如果你不打算亲自实现一个JVM虚拟机,则其中大部分的内容是不需要详细关注的。幸运的是,JDK其实提供了一个官方的反编译工具
javap
,用于快速查看Class文件的内容。

本文将提供一个示例类,将帮助您快速了解Class文件的结构,并了解大部分的字节码指令。

0x01 Class

定义一个类,为了方便起见,我把它放在了根包下

import java.util.*;

// A class includes most kinds of JAVA bytecode op
// javac BytecodeExample
// javap -c -v -p BytecodeExample
@Deprecated
public abstract class BytecodeExample<T extends List> {
}


编译后利用
javap -c -v -p BytecodeExample
反编译后得到下面的内容

对于javap命令,其中的

-c
参数表示反编译,如果没有该参数则看不到每个方法具体的代码

-v
参数表示verbose输出,会包括本地变量表、调试用的行号等信息

-p
参数表示输出private以上也就是所有的成员

Classfile /*****/target/classes/BytecodeExample.class
Last modified 2016-4-11; size 484 bytes
MD5 checksum d384ebc428f71935665589e7be64fdc3
Compiled from "BytecodeExample.java"
public abstract class BytecodeExample<T extends java.util.List> extends java.lang.Object
Signature: #14                          // <T::Ljava/util/List;>Ljava/lang/Object;
SourceFile: "BytecodeExample.java"
Deprecated: true
RuntimeVisibleAnnotations:
0: #19()
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER, ACC_ABSTRACT
Constant pool:
#1 = Methodref          #3.#20         //  java/lang/Object."<init>":()V
#2 = Class              #21            //  BytecodeExample
#3 = Class              #22            //  java/lang/Object
#4 = Utf8               <init>
#5 = Utf8               ()V
#6 = Utf8               Code
#7 = Utf8               LineNumberTable
#8 = Utf8               LocalVariableTable
#9 = Utf8               this
#10 = Utf8               LBytecodeExample;
#11 = Utf8               LocalVariableTypeTable
#12 = Utf8               LBytecodeExample<TT;>;
#13 = Utf8               Signature
#14 = Utf8               <T::Ljava/util/List;>Ljava/lang/Object;
#15 = Utf8               SourceFile
#16 = Utf8               BytecodeExample.java
#17 = Utf8               Deprecated
#18 = Utf8               RuntimeVisibleAnnotations
#19 = Utf8               Ljava/lang/Deprecated;
#20 = NameAndType        #4:#5          //  "<init>":()V
#21 = Utf8               BytecodeExample
#22 = Utf8               java/lang/Object
{
public BytecodeExample();
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 10: 0
LocalVariableTable:
Start  Length  Slot  Name   Signature
0       5     0  this   LBytecodeExample;
LocalVariableTypeTable:
Start  Length  Slot  Name   Signature
0       5     0  this   LBytecodeExample<TT;>;
}


这里您所看到的内容和原始的Class中文件所存储的内容、顺序是一致的。

对于任意的Class文件,都包括:

头部分:包括版本号等。51表示Java7,50表示Java6,以此类推

常量池Constant pool。

在整个Class文件中其实都不再有任何常量,包括类名、签名、数字等等,与代码有关的所有常量都在这。另外有一点好玩的是字符串常量可能是另两个常量拼接而成。

签名相关。如可访问性,这里是
ACC_PUBLIC, ACC_SUPER, ACC_ABSTRACT
。其中
ACC_SUPER
是Java1.0.2以后对
invokevirtual
命令修改定义后的标识。

接口、字段、方法、属性等。属性区是个神奇的地方,包含很多Java新特性的东西,比如@Annotation,比如表示已过时等等。

你要问,Class文件是用怎样的数据结构存储这些信息的?好吧,你适合直接直接看规范文件。

0x02 Hello World

码农的世界从Hello World开始,Java的世界从main函数开始(Stop,别找茬)。

于是我们加入一个打印Hello World的main函数,看看会编译成什么样

// public getstatic invokevirtual
public static void main(String[] args) {
System.out.println("Hello World");
}


javap后可以看到

public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc           #4                  // String Hello World
5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 21: 0
line 22: 8
LocalVariableTable:
Start  Length  Slot  Name   Signature
0       9     0  args   [Ljava/lang/String;


现在我们看到了一个方法被编译之后的样子。

一个方法包括:

签名相关。如可访问性和方法签名

属性

你可能会好奇,代码放在哪,答案是在属性里。。。

在上面的
javap
结果中我们看到了三种属性:

Code代码

LineNumberTable
行号表,是源代码的行号和Code中指令位置直接的映射,用于调试。比如
System.out.println("Hello World");
位于21行,被翻译成了从
getstatic
return
之前的三条指令,也就是指令偏移位置的
[0:, 8:)
,注意不包括偏移为8个指令,即
return
指令。

LocalVariableTable
本地变量表,比如上面指出了args变量的名字、本地变量数组的位置、指令作用域[0, 0+9),以及签名。这个也是用于调试

事实上,在Class属性中有很多类似2、3这样的用于调试的东西,而一些运行时生成的类或者非官方的编译器是没有这些信息的。

0x03 Code

在Code部分的一开始,我们看到

stack=2, locals=1, args_size=1


它表示这段代码

stack
栈长为2

JVM指令是一种基于栈的指令,所有的指定都是类似于压栈、出栈、对栈顶参数做操作然后再压入栈顶。而
stack
就定义了这个栈的长度。

locals
本地变量数组长度为1(即
args


该方法参数的数量
args_size
为1(即
args


上面的Hello World示例中共包含4条指令

以第一条指令为例

0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;


0:
表示指令偏移的起点,我们可以看到下一条指令是
3:
,这说明第一条指令占了3个字节。

JVM指令和常见的汇编一样,包括操作码和操作数,除了wide开头的指令,其他指令操作码都是一个字节,
getstatic
是助记符,实际的二进制数为
0xb2
,后面是一个2个字节的操作数,表示常量池位置#3,javap已经给了我们提示,就是
java/lang/System.out:Ljava/io/PrintStream
,这表示从
java/lang/System
类中获取
out
静态

字段,其类型是
Ljava/io/PrintStream
,然后推入栈顶。

下面我们人肉跟踪一下这段代码

0: getstatic     #3  // Field java/lang/System.out:Ljava/io/PrintStream;
// 获取System.out进入栈顶,现在栈=[System.out]
3: ldc           #4  // String Hello World
// 将常数#4(字符串Hello World)推入栈顶,现在栈=[System.out, "Hello World"]
5: invokevirtual #5  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
// 调用PrintStream.println方法,参数为栈顶元素(根据方法签名可以确定有2个参数,第一个是this),返回结果压入栈顶
8: return
// 返回


0x04 Field & Constructor

我们向示例类加入以下代码

// field
public static final int[][][] INT; // 15行
// putfield in generated constructor
private int a = 1; // 17行


// <clinit> putstatic multiarray
static {
INT = new int[1][2][3]; // 136行
}


你会发现
javap
后多出如下的东西

1、 字段声明

public static final int[][][] INT;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
private int a;
flags: ACC_PRIVATE


2、 如果没有定义构造函数,会自动生成的构造函数

这个函数会自动调用父类的构造函数,而
5:
6:
这是为了实现对字段a的初始化赋值

load
const
是很常见的指令。
xload_n
表示从第n个本地变量表中载入类型为x的元素压入栈顶(
locals
=>
stack
),其中
x=a
时表示是一个对象。
xconst_n
则表示在栈顶压入一个x类型的值为n的量,其中
x=i
表示int。

getstatic``putstatic``getfield``putfield
表示栈顶元素(载入、存入) * (静态、类字段)。

public BytecodeExample();
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1                  // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield      #2                  // Field a:I
9: return


3、 类构造函数

multianewarray
指令会生成一个多维数组。它有2个操作数,
#36=[[[I
表示构造的数组为
int[][][]
,而3表示用栈顶3个元素作为每个维度的size。

static {};
stack=3, locals=0, args_size=0
0: iconst_1
1: iconst_2
2: iconst_3
3: multianewarray #36,  3            // class "[[[I"
7: putstatic     #37                 // Field INT:[[[I
10: return


对于方法部分,我省略了
LineNumberTable
等信息,这和
javap
命令去掉-v参数的效果是一样的。

详细的指令定义可以从规范文档中得到,相信大部分的指令都是可以不需要查文档即能明白的,对于特殊的命令后面会特别阐述。

0x05 Number

我们向示例类加入以下代码

// this PrimitiveType ConditionalOp return
public int sum(byte b, short s, boolean z) {
return z ? 0 : b + s;
}


ifeq
表示如果等于则跳转到便宜
8:
ireturn
这是返回int型。

StackMapTable
是Java7规范引入的,用于帮助载入类时对Class进行验校,它会表示每个代码段中栈和本地变量表的长度、类型等信息。比如
frame_type = 8
表示指令偏移
[0:,8:)
直接不需要改变栈和本地变量表的长度。

需要注意的是,无论是
byte
还是
short
还是
boolean
,都是会独立占用一个栈的槽,也就是会被自动转换为32位int存储, 但同时在
LocalVariableTable
中标记了他们的类型。

LocalVariableTable
中前n个元素就是这个方法的入参,而且如果不是静态方法,第一个参数就是this。

stack=2, locals=4, args_size=4
0: iload_3
1: ifeq          8
4: iconst_0
5: goto          11
8: iload_1
9: iload_2
10: iadd
11: ireturn
LocalVariableTable:
Start  Length  Slot  Name   Signature
0      12     0  this   LBytecodeExample;
0      12     1     b   B
0      12     2     s   S
0      12     3     z   Z
StackMapTable: number_of_entries = 2
frame_type = 8 /* same */
frame_type = 66 /* same_locals_1_stack_item */
stack = [ int ]


我们向示例类加入以下代码

// private long box invokestatic
private static Long add(long a, Integer b) {
++b;
long r = a + b;
return r;
}


可以看到代码被编译后都是自动完成拆箱装箱操作的。

i2l
是将int转换为long。

特别注意的是
LocalVariableTable
中第一个变量
a
,它是一个long类型,占了2个槽,因为JVM规范规定了一个槽就是32位,无视机器是否是64位的。

stack=4, locals=5, args_size=2
0: aload_2
1: invokevirtual #6                  // Method java/lang/Integer.intValue:()I
4: iconst_1
5: iadd
6: invokestatic  #7                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
9: astore_2
10: lload_0
11: aload_2
12: invokevirtual #6                  // Method java/lang/Integer.intValue:()I
15: i2l
16: ladd
17: lstore_3
18: lload_3
19: invokestatic  #8                  // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
22: areturn
LocalVariableTable:
Start  Length  Slot  Name   Signature
0      23     0     a   J
0      23     2     b   Ljava/lang/Integer;
18       5     3     r   J


0x06 For Each

我们向示例类加入以下代码

// package_acc foreach arraylength locals varargs
static void forEach(List list, int... arr) {
for (int i : arr) {}
for (Object i : list) {}
}


这是一个包访问的方法,所以没有加上可访问性的标识。

可以看到这两个for each循环都做了隐式的处理:

1. 对于数据实际是转换成了
for(int i$=0; i$<arr.length; ++i$)


2. 对于List则转换成了迭代器。

为了实现这点,编译器自动加入了i$等变量,所以可以看到本地变量表
locals
的长度并不一定等于代码中声明的变量数量,而且在不同的作用域下,变量可能会复用一个
locals
槽,所以
locals
的长度可能比声明的变量数量多,也可能少。

if_icmpge
表示对于对于int进行比较,如果>=时跳转,相信这很容易理解。

static void forEach(java.util.List, int...);
flags: ACC_STATIC, ACC_VARARGS
Code:
stack=2, locals=6, args_size=2
0: aload_1
1: astore_2
2: aload_2
3: arraylength
4: istore_3
5: iconst_0
6: istore        4
8: iload         4
10: iload_3
11: if_icmpge     26
14: aload_2
15: iload         4
17: iaload
18: istore        5
20: iinc          4, 1
23: goto          8
26: aload_0
27: invokeinterface #9,  1            // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
32: astore_2
33: aload_2
34: invokeinterface #10,  1           // InterfaceMethod java/util/Iterator.hasNext:()Z
39: ifeq          52
42: aload_2
43: invokeinterface #11,  1           // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
48: astore_3
49: goto          33
52: return
LocalVariableTable:
Start  Length  Slot  Name   Signature
20       0     5     i   I
2      24     2  arr$   [I
5      21     3  len$   I
8      18     4    i$   I
49       0     3     i   Ljava/lang/Object;
33      19     2    i$   Ljava/util/Iterator;
0      53     0  list   Ljava/util/List;
0      53     1   arr   [I


0x07 Invoke

我们向示例类加入以下代码

// native
protected native int nativeFunc(int b);
// abstract
public abstract int hashCode();
// super. intern() invokespecial
void invoke() {
main(new String[0]);
nativeFunc(1);
super.toString().intern();
}


这里有三种invoke,加上之前的
invokeinterface
,他们的意义分别是

1.
invokestatic
调用静态方法

2.
invokeinterface
调用接口声明的方法

3.
invokevirtual
调用虚函数方法。JAVA中的方法都是虚函数,final函数主要是对于编译器和类载入检查用的。这也是见得最多的函数调用

4.
invokespecial
用于调用构造函数、父函数。

另外,
anewarray
用于创建一个一维的数组

protected native int nativeFunc(int);
flags: ACC_PROTECTED, ACC_NATIVE
public abstract int hashCode();
flags: ACC_PUBLIC, ACC_ABSTRACT

void invoke();
flags:
Code:
stack=2, locals=1, args_size=1
0: iconst_0
1: anewarray     #12                 // class java/lang/String
4: invokestatic  #13                 // Method main:([Ljava/lang/String;)V
7: aload_0
8: iconst_1
9: invokevirtual #14                 // Method nativeFunc:(I)I
12: pop
13: aload_0
14: invokespecial #15                 // Method java/lang/Object.toString:()Ljava/lang/String;
17: invokevirtual #16                 // Method java/lang/String.intern:()Ljava/lang/String;
20: pop
21: return


0x08 Class Cast

我们向示例类加入以下代码

// new instanceof checkcast
static void tryCast() {
ArrayList arrayList = new ArrayList();
boolean b = arrayList instanceof List;
Object obj = arrayList;
List list = (List) obj;
}


注意
0:``3:``4:
共同组成了一个new的操作,其中
0:
在栈上创一个空的对象,而
4:
对它实施初始化。
<init>
是构造函数的方法名,
<clinit>
是类构造函数的方法名。

我曾经想过,如果只有new不执行
invokespecial
是否就可以实现对任意的类实现无参构造,但是很遗憾,如果没有
invokespecial
的话会通不过HotSpot的验校。

instancoef
如果检查成功的话,会压栈1,否则压栈0

checkcast
在检查失败后直接抛出
ClassCastException
(规范中规定了例如cast失败、类载入错误等一堆JVM级的异常)

stack=2, locals=4, args_size=0
0: new           #18                 // class java/util/ArrayList
3: dup
4: invokespecial #19                 // Method java/util/ArrayList."<init>":()V
7: astore_0
8: aload_0
9: instanceof    #20                 // class java/util/List
12: istore_1
13: aload_0
14: astore_2
15: aload_2
16: checkcast     #20                 // class java/util/List
19: astore_3
20: return


0x09 Generic Type

在类的定义中您可能已经看到了泛型,方法中的泛型和其大致相同。

我们向示例类加入以下代码

// generic null SignatureOfClass
static <A extends BytecodeExample & List> A generic(List<? super HashSet> set) {
return null;
}


请注意其中的各种方法签名,有时是使用
/
,有时则使用
.


请注意
LocalVariableTypeTable
属性,如果方法中含有与泛型有关的局部变量时,其就会出现在
LocalVariableTypeTable
中,这可能是对旧jvm的适配

aconst_null
指令会将null放入栈顶

static <A extends BytecodeExample & java/util/List> A generic(java.util.List<? super java.util.HashSet>);
flags: ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: aconst_null
1: areturn
LocalVariableTypeTable:
Start  Length  Slot  Name   Signature
0       2     0   set   Ljava/util/List<-Ljava/util/HashSet;>;
Signature: #104                         // <A:LBytecodeExample;:Ljava/util/List;>(Ljava/util/List<-Ljava/util/HashSet;>;)TA;


0x0A Final & InnerClass

我们向示例类加入以下代码

// final AnonymousClass invokeinterface
final boolean isFinal(final int a, List<String> list) {
Comparator<String> comparator = new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2) * a;
}
};
Collections.sort(list, comparator);
return true;
}


您可能会发现多了一个
BytecodeExample$1
的类,这是编译器自动生成的“匿名类”。

stack=4, locals=4, args_size=3
0: new           #21                 // class BytecodeExample$1
3: dup
4: aload_0
5: iload_1
6: invokespecial #22                 // Method BytecodeExample$1."<init>":(LBytecodeExample;I)V
9: astore_3
10: aload_2
11: aload_3
12: invokestatic  #23                 // Method java/util/Collections.sort:(Ljava/util/List;Ljava/util/Comparator;)V
15: iconst_1
16: ireturn


[0:,9:)
的代码也和之间的
new
不同,
aload_0
iload_1
分别会把
this
a
压入栈,这说明final参数是通过构造函数传入匿名对象的实例的,同时它还保留了外部类实例的
this
引用。

我们可以查看下
javap BytecodeExample$1


class BytecodeExample$1 implements java.util.Comparator<java.lang.String> {
final int val$a;
final BytecodeExample this$0;
BytecodeExample$1(BytecodeExample, int);
public int compare(java.lang.String, java.lang.String);
public int compare(java.lang.Object, java.lang.Object);
}


0x0B switch

我们向示例类加入以下代码,您也可以
javap BytecodeExample$Color
以了解一个枚举类

// enum InnerClass
enum Color {
RED(1), GREEN(2), BLUE(3);
Color(int a) {}
}
// string_switch lookupswitch tableswitch
static void switchFunc(Color c, String s) {
switch (s) {
case "1": return;
}
switch(c) {
case RED: return;
case GREEN: return;
case BLUE: return;
default: return;
}
}


Java从Java7开始可以对String做switch,在
[0:,61:)
中,编译的代码首先求
hashCode
然后做
equals
以确定是否match。

[61:,end]
则是采用了
tableswitch
指令,
tableswitch
lookupswitch
的区别是
tableswitch
用于处理连续的值。事实上,在上面的代码中如果只有2个case,那么我的编译器就会使用
lookupswitch


stack=2, locals=4, args_size=2
0: aload_1
1: astore_2
2: iconst_m1
3: istore_3
4: aload_2
5: invokevirtual #24                 // Method java/lang/String.hashCode:()I
8: lookupswitch  { // 1
49: 28
default: 39
}
28: aload_2
29: ldc           #25                 // String 1
31: invokevirtual #26                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
34: ifeq          39
37: iconst_0
38: istore_3
39: iload_3
40: lookupswitch  { // 1
0: 60
default: 61
}
60: return
61: getstatic     #27                 // Field BytecodeExample$2.$SwitchMap$BytecodeExample$Color:[I
64: aload_0
65: invokevirtual #28                 // Method BytecodeExample$Color.ordinal:()I
68: iaload
69: tableswitch   { // 1 to 3
1: 96
2: 97
3: 98
default: 99
}
96: return
97: return
98: return
99: return


0x0C Exception

我们向示例类加入以下代码

// exception
static void exception() throws RuntimeException {
try {
throw new NullPointerException();
} catch (RuntimeException e) {
e.printStackTrace();
} catch (Error | Exception e) {
throw e;
} finally {
return;
}
}


Exception table
展示了catch部分的实现,会依次罗列异常处理器。
[from, to)
表示了字节码偏移量的区间,也就是try{}部分的代码。

Exception table
展示了finally部分的处理,finally会作为any的异常处理器出现。

Java7支持用
|
来声明一组异常,在编译器中被自动调整为了他们共同的父类
Throwable
,所以
LocalVariableTable
的第二个本地变量的类型是
Throwable


athrow
指令抛出栈顶异常。

static void exception() throws java.lang.RuntimeException;
flags: ACC_STATIC
Code:
stack=2, locals=2, args_size=0
0: new           #30                 // class java/lang/NullPointerException
3: dup
4: invokespecial #31                 // Method java/lang/NullPointerException."<init>":()V
7: athrow
8: astore_0
9: aload_0
10: invokevirtual #33                 // Method java/lang/RuntimeException.printStackTrace:()V
13: return
14: astore_0
15: aload_0
16: athrow
17: astore_1
18: return
Exception table:
from    to  target type
0     8     8   Class java/lang/RuntimeException
0     8    14   Class java/lang/Error
0     8    14   Class java/lang/Exception
0    13    17   any
14    18    17   any
LocalVariableTable:
Start  Length  Slot  Name   Signature
9       4     0     e   Ljava/lang/RuntimeException;
15       2     0     e   Ljava/lang/Throwable;
Exceptions:
throws java.lang.RuntimeException


0x0D Synchronized

我们向示例类加入以下代码

// synchronized monitorenter/monitorexit
static synchronized void synchronizedFunc() {
Object o = new Object();
synchronized (o) {
}
}


虽然您可能了解到
synchronized
关键字无论是作用在方法上还是代码块上,其在JVM中的处理并没有很大的不同,但是在Class文件的规范定义中,是完全不同的。

synchronized
作用于方法时是以签名的形式存在,而对代码块则是
monitorenter/monitorexit


特别注意的是,
synchronized
代码块隐式加上了finally块,用于防止有异常时没有释放锁。

static synchronized void synchronizedFunc();
flags: ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=3, args_size=0
0: new           #29                 // class java/lang/Object
3: dup
4: invokespecial #1                  // Method java/lang/Object."<init>":()V
7: astore_0
8: aload_0
9: dup
10: astore_1
11: monitorenter
12: aload_1
13: monitorexit
14: goto          22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from    to  target type
12    14    17   any
17    20    17   any


0x0E Annotation

我们向示例类加入以下代码

@Deprecated
@Nullable
static void annotation(@Nullable int resource) {
}


javap
反编译的结果如下。这里要关注的就是
RuntimeVisibleAnnotations
RuntimeInvisibleAnnotations
RuntimeInvisibleParameterAnnotations
三个属性。@Nullable注解的
RetentionPolicy
并没有
Runtime
,所以是
RuntimeInvisibleAnnotations
的。

static void annotation(int);
flags: ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 110: 0
LocalVariableTable:
Start  Length  Slot  Name   Signature
0       1     0 resource   I
Deprecated: true
RuntimeVisibleAnnotations:
0: #126()
RuntimeInvisibleAnnotations:
0: #128()
RuntimeInvisibleParameterAnnotations:
0:
0: #128()


0x0F try-finally

JAVA中有一个经典的问题,就是try-finally中的多return问题

static int multiReturn() {
try {
return 1;
} finally {
return 2;
}
}


javap
如下,你知道应该返回什么吗?

static int multiReturn();
flags: ACC_STATIC
Code:
stack=1, locals=2, args_size=0
0: iconst_1
1: istore_0
2: iconst_2
3: ireturn
4: astore_1
5: iconst_2
6: ireturn
Exception table:
from    to  target type
0     2     4   any
4     5     4   any


完整的代码请戳

https://github.com/zillionbrains/exercise/blob/master/bytecode_example/BytecodeExample

如果您有编译问题,请尝试移除108、109行代码的 @Nullable
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java jvm class