一个类搞懂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对世界各个时区(TimeZone)的通用转换处理方法(转载)
- java-注解annotation
- java-模拟tomcat服务器
- java-用HttpURLConnection发送Http请求.
- java-WEB中的监听器Lisener
- Android IPC进程间通讯机制
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- 介绍一款信息管理系统的开源框架---jeecg
- 聚类算法之kmeans算法java版本
- java实现 PageRank算法
- PropertyChangeListener简单理解
- c++11 + SDL2 + ffmpeg +OpenAL + java = Android播放器
- 插入排序
- 冒泡排序
- 堆排序
- 快速排序