直面底层之字节码看enum和switch

956 阅读7分钟

前言

本文从枚举的字节码方面看枚举,并学习下 swtich的实现 和 swtich(String) 的实现。


一、一个简单的枚举

public class EnumTest {
     enum EnumDay {
        MONDAY,
        TUESDAY,
        WENSDAY
    }

    public static void testEnum(EnumDay day){
        switch (day){
            case MONDAY:
                break;
            case TUESDAY:
                break;
            case WENSDAY:
                break;
            default:
                break;
        }
    }
}

我们定义了一个枚举类EnumDay, 定义了三个值 MONDAY,TUESDAY,WENSDAY

接下来我们通过 javac EnumTest 看下编译后生成了 EnumTest.class EnumTest1.classEnumTest1.class EnumTestEnumDay.class 三个class 文件。

我们看下 EnumTest 的字节码文件(javap -c EnumTest)

{
 
public javaplan.EnumTest(); // 无参的构造方法  
descriptor: ()V             // 构造方法描述符,入参没有,返回值是 V 代表void
flags: ACC_PUBLIC           // 方法访问标记 public
  
//操作数栈深度 1  局部变量表大小 1 参数个数 1 这里指this
Code:
stack=1, locals=1, args_size=1  
0: aload_0           //将局部变量表第0 个位置加载到操作数栈顶,即 this
1: invokespecial #1 // Method java/lang/Object."<init>":()V //用this 执行init 方法
4: return
LineNumberTable:
line 3: 0

//TestEnum 方法实现
public static void testEnum(javaplan.EnumTest$EnumDay);
descriptor: (Ljavaplan/EnumTest$EnumDay;)V  //方法描述符:入参是EnumDay,返回值是V,表示无返回值
flags: ACC_PUBLIC, ACC_STATIC  //方法描述符:public,static
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field javaplan/EnumTest$1.$SwitchMap$javaplan$EnumTest$EnumDay:[I
3: aload_0  //由于是static 方法,无this 指针作为默认参数,这里是把入参EnumDay 放入栈顶
4: invokevirtual #3 // Method javaplan/EnumTest$EnumDay.ordinal:()I  执行EnumDay.original 方法 放入局部变量表
7: iaload   //将 上一步的值加载到栈顶
8: tableswitch { // 1 to 3
1: 36     //如果是 1 跳转至 36行
2: 39     //如果是 2 跳转至 39行
3: 42     //如果是 3 跳转至 42行
default: 45 //否则跳转至 45行
}
36: goto 45 //跳转至45行
39: goto 45 //跳转至45行
42: goto 45 //跳转至45行
45: return


//内部类 EnumTest$1 和 EnumTest$EnumDay
InnerClasses:
static #6; //class javaplan/EnumTest$1
static final #9= #8 of #4; //EnumDay=class javaplan/EnumTest$EnumDay of class javaplan/EnumTest
上述代码流程看到生成了 三个类,也就比平常多了一个EnumDay,通过执行 EnumDay.original 然后判断该跳转执行哪一行,似乎并不能看出性能不好在哪里,接下来我们再看一个例子。

二、枚举类

public enum SingleEnumTest {
    YELLOW,
    RED,
    GREEN
}

依然我们编译成 字节码文件 再来看字节码:

public final class javaplan.SingleEnumTest extends java.lang.Enum<javaplan.SingleEnumTest> {
public static final javaplan.SingleEnumTest YELLOW;

public static final javaplan.SingleEnumTest RED;

public static final javaplan.SingleEnumTest GREEN;

public static javaplan.SingleEnumTest[] values();
Code:
0: getstatic #1 // Field $VALUES:[Ljavaplan/SingleEnumTest;
3: invokevirtual #2 // Method "[Ljavaplan/SingleEnumTest;".clone:()Ljava/lang/Object;
6: checkcast #3 // class "[Ljavaplan/SingleEnumTest;"
9: areturn

public static javaplan.SingleEnumTest valueOf(java.lang.String);
Code:
0: ldc #4 // class javaplan/SingleEnumTest
2: aload_0
3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #4 // class javaplan/SingleEnumTest
9: areturn



静态代码块

static {};
Code:

//生成静态示例 SingleEnumTest YELLOW
0: new #4 // class javaplan/SingleEnumTest
3: dup
4: ldc #7 // String YELLOW
6: iconst_0
7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #9 // Field YELLOW:Ljavaplan/SingleEnumTest;

//生成静态示例 SingleEnumTest RED
13: new #4 // class javaplan/SingleEnumTest
16: dup
17: ldc #10 // String RED
19: iconst_1
20: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
23: putstatic #11 // Field RED:Ljavaplan/SingleEnumTest;

//生成静态示例 SingleEnumTest GREEN
26: new #4 // class javaplan/SingleEnumTest
29: dup
30: ldc #12 // String GREEN
32: iconst_2
33: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
36: putstatic #13 // Field GREEN:Ljavaplan/SingleEnumTest;



//生成一个大小为3的数组

39: iconst_3
40: anewarray #4 // class javaplan/SingleEnumTest

//YELLOW 对象存入数组第0 个位置
43: dup
44: iconst_0
45: getstatic #9 // Field YELLOW:Ljavaplan/SingleEnumTest;
48: aastore

//RED 对象存入数组的第1 个位置
49: dup
50: iconst_1
51: getstatic #11 // Field RED:Ljavaplan/SingleEnumTest;
54: aastore

//GREEN对象存入数组的第2个位置
55: dup
56: iconst_2
57: getstatic #13 // Field GREEN:Ljavaplan/SingleEnumTest;
60: aastore

//数组对象放入局部变量表
61: putstatic #1 // Field $VALUES:[Ljavaplan/SingleEnumTest;
64: return
}
可以看到 每个枚举值 都是一个对象,且会生成一个数组,来保存枚举对象 这样会很耗内存 。上面我们提到 了枚举一般作为配合 swtich 使用,那么switch 的实现 有什么性能瓶颈么?

三、switch 的底层实现

(1) tableswtich:
//java代码:
 public void switch1(int i){
        switch (i){
            case 0:
                break;
            case 2:
                break;
            case 3:
                break;
            default:
                break;
        }
    }

//对应的字节码:
  public void switch1(int);
    Code:
       0: iload_1        //从局部变量表加载参数到栈顶即i
       1: tableswitch   { // 0 to 3
                     0: 32   //如果i= 0 跳转至32行
                     1: 41   //如果i= 1 跳转至41行
                     2: 35   //如果i= 2 跳转至35行
                     3: 38   //如果i= 3 跳转至38行
               default: 41   //其他 跳转至41行
          }
      32: goto          41
      35: goto          41
      38: goto          41
      41: return

可以看到逻辑非常简单,但是我们代码中并未写 case=1 的情况,生成的字节码指令默认给我们加上了case=1的情况,这样的话 那 如果 我中间隔100个 还会给我补全100个吗?接下来我们看下 switch 的另一中实现方式 lookupswtich

(2)lookupswitch
//java 代码
public void switch2(int i){
    switch (i){
        case 0:
            break;
        case 10:
            break;
        case 20:
            break;
        default:
            break;
    }
}

//对应的字节码
public void switch2(int);
    Code:
       0: iload_1   //从局部变量表加载参数到栈顶即i
       1: lookupswitch  { // 3
                     0: 36  //0 跳转至 36行
                    10: 39  // 10 跳转至39行
                    20: 42  // 20 跳转至 42行
               default: 45  // 其他跳转至 45行
          }
      36: goto          45
      39: goto          45
      42: goto          45
      45: return

逻辑也非常简单 ,由于跨度大,0 到 10 到20 底层采用了 lookupswtich 的实现,并没有补全 0 到10 和 10到20 中间的情况,可以看到 case 在不紧凑的情况下会采用 lookupswtich 来实现,lookup 将 case 值进行了排序,这样可以提高查找速率(lookup二分查找)感兴趣的朋友可以自己深入研究下。

(3)tableswitch 与lookupswtich 选择依据

很显然第一个依据是 case 是否紧凑,但这个只能作为大致的判断依据,准确的还要根据时间和空间两个维度来判断。

Tableswtich 性能计算:

tablespacecost = 4 + ((long) hi - lo + 1); hi 表示case 值的最大值 ,lo 表示case 值的最小值
tabletimecost = comparisons 比较次数 基本和 hi- lo +1 相等

LookUpSwtich 性能计算:

lookupspacecost = 3 + 2* tables; tables 表示 case 个数
lookuptimecost = tables
最终选择:
tablespacecost + 3*tabletimecost < lookupspacecost + 3 * lookuptimecost ? tableswtich : lookupswtich

可以看到,选择tableswtich或者 lookupswtich 会根据case 紧凑与否,并实际根据时间和空间消耗来选择


三、switch (String)的底层实现

JDK7 开始实现了 case 的值可以是 String,我们知道 enum 作为case 时是转为了int,那String 能和 int 产生关联的,我们是否想到了 hashcode,这只是我们的猜想,我们来看下具体的字节码。

//java 代码
public int stringSwitch(String name){
    switch (name){
        case "java":
            return 1;
        case "kotlin":
            return 2;
        default:
            return 3;
    }
}

//字节码
public int stringSwitch(java.lang.String);
    Code:
       0: aload_1                 //将入参 String加载到操作数栈顶
       1: astore_2                //将String 从操作数栈顶 放到局部变量表第二个位置
       2: iconst_m1               //将-1 加载到操作数栈顶
       3: istore_3                //将-1 从操作数栈顶 放到 局部变量表第三个位置
       4: aload_2                 //加载 局部变量表第二个位置即string 到栈顶
       5: invokevirtual #2       // Method java/lang/String.hashCode:()I,执行String hashCode
       8: lookupswitch  { // 2
           -1125574399: 50    //"kotlin" hashCode值跳转至 50行
               3254818: 36    // “java”  hashCode值跳转至 36行
               default: 61    //其他跳转至 61行
          }
      36: aload_2             //hashcode 值是 3254818 跳转至36行,加载String 到栈顶
      37: ldc           #3   // String java 从常量池加载字符串 "java"
      39: invokevirtual #4  // String.equals:(Ljava/lang/Object;)Z  判断两个字符串是否相等
      42: ifeq          61  //如果不相等跳转至 61行
      45: iconst_0          //将 0 加载到操作数栈顶
      46: istore_3          //将 0 存入局部变量表第三个位置
      47: goto          61  //跳转至 61行
      // 50-60行 处理字符串是 kotlin 的情况 和上述 java 字符串流程一致
      50: aload_2           
      51: ldc           #5                  // String kotlin
      53: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      56: ifeq          61
      59: iconst_1
      60: istore_3
      
      61: iload_3   //加载局部变量表第三个位置到操作数栈顶,java 存入的是 0,kotlin 存入的是 1
      62: lookupswitch  { // 2
                     0: 88  //0 跳转至 88行  
                     1: 90  //1 跳转至 90行
               default: 92  //其他  跳转至 92行
          }
      88: iconst_1  //将1 加载到操作数栈顶 
      89: ireturn   //返回 1
      90: iconst_2  //将2 加载到操作数栈顶
      91: ireturn   //返回 2
      92: iconst_3  //将3 加载到操作数栈顶
      93: ireturn   //返回3
a.Switch 传入String 的实现是 依赖hashCode
b.由于hashCode 偏向于稀疏,采用了 lookupswtich 实现
c.由于string 会存在hash冲突,解决冲突的方式是 相同的hashcode 会再次比较字符串是否相等
d. 针对不同的字符串比较结果 会采用新的一个case 来对应要返回的情况,第二个lookupswitch(或tableswtich)

四、总结

1、枚举作为类定义时,如果定义了几个枚举值,则会将每个枚举值生成一个静态变量,并初始化到数组中
2、swtich 会根据case 是否稀疏 ,并依赖时间和空间复杂度来决定是采用tableswtich 或者 lookupswitch
3、swtich(String) 依赖hashcode 作为case,并用字符串比较来解决hash 冲突,由于hashcode 稀疏,故底层采用的是 lookupswtich