一个类搞懂JAVA Class文件

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文件,都包括:

  1. 头部分:包括版本号等。51表示Java7,50表示Java6,以此类推
  2. 常量池Constant pool。
    在整个Class文件中其实都不再有任何常量,包括类名、签名、数字等等,与代码有关的所有常量都在这。另外有一点好玩的是字符串常量可能是另两个常量拼接而成。
  3. 签名相关。如可访问性,这里是ACC_PUBLIC, ACC_SUPER, ACC_ABSTRACT。其中ACC_SUPER是Java1.0.2以后对invokevirtual命令修改定义后的标识。
  4. 接口、字段、方法、属性等。属性区是个神奇的地方,包含很多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;

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

  1. 签名相关。如可访问性和方法签名
  2. 属性

你可能会好奇,代码放在哪,答案是在属性里。。。
在上面的javap结果中我们看到了三种属性:

  1. Code代码
  2. LineNumberTable行号表,是源代码的行号和Code中指令位置直接的映射,用于调试。比如System.out.println("Hello World");位于21行,被翻译成了从getstaticreturn之前的三条指令,也就是指令偏移位置的[0:, 8:),注意不包括偏移为8个指令,即return指令。
  3. LocalVariableTable本地变量表,比如上面指出了args变量的名字、本地变量数组的位置、指令作用域[0, 0+9),以及签名。这个也是用于调试

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

0x03 Code

在Code部分的一开始,我们看到
stack=2, locals=1, args_size=1
它表示这段代码

  1. stack栈长为2
    JVM指令是一种基于栈的指令,所有的指定都是类似于压栈、出栈、对栈顶参数做操作然后再压入栈顶。而stack就定义了这个栈的长度。
  2. locals本地变量数组长度为1(即args
  3. 该方法参数的数量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的初始化赋值
loadconst是很常见的指令。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_0iload_1分别会把thisa压入栈,这说明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指令,tableswitchlookupswitch的区别是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反编译的结果如下。这里要关注的就是RuntimeVisibleAnnotationsRuntimeInvisibleAnnotationsRuntimeInvisibleParameterAnnotations三个属性。@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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值