Enum#values()是否在每次调用时分配内存?

pla*_*que 25 java enums memory-management

我需要int在Java中将序数值转换为枚举值.这很简单:

MyEnumType value = MyEnumType.values()[ordinal];
Run Code Online (Sandbox Code Playgroud)

values()方法是隐式的,我无法找到它的源代码,因此问题.

是否MyEnumType.values()分配新阵列?如果确实如此,我应该在第一次调用时缓存数组吗?假设转换将经常被调用.

Psh*_*emo 25

是的,MyEnumType.values()每次创建一个充满枚举元素的新数组.您可以使用==操作员简单地测试它.

MyEnumType[] arr1 = MyEnumType.values();
MyEnumType[] arr2 = MyEnumType.values();
System.out.println(arr1==arr2); //false
Run Code Online (Sandbox Code Playgroud)

Java没有让我们创建不可修改数组的机制.因此,如果values()总是返回相同的数组实例,那么我们就有可能会有人为每个人更改其内容.
因此,直到将不可修改的数组机制引入Java,避免数组可变性问题values()方法必须创建并返回原始数组的副本.

如果你想避免重新创建这个数组,你可以简单地存储它并重用values()稍后的结果.有几种方法可以做到,比如.

  • 你可以创建私有数组,只允许通过getter方法访问其内容

    private static final MyEnumType[] VALUES = values();// to avoid recreating array
    
    MyEnumType getByOrdinal(int){
        return VALUES[int];
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 您还可以存储values()不可修改集合的结果,List以确保其内容不会被更改(现在这样的列表可以是公共的).

    public static final List<X> VALUES = Collections.unmodifiableList(Arrays.asList(values()));
    
    Run Code Online (Sandbox Code Playgroud)

  • 哦,关于这个的原因,这是为了安全 - 如果它每次返回相同的数组,有人可以更改内容,然后你会看到更改的数组,而不是原始和正确的数组. (3认同)
  • “您可以使用==运算符简单地对其进行测试”仅表明您的特定Java实现每次都会创建一个新数组。它并不表示它必须这样做。 (2认同)

Boa*_*ann 13

从理论上讲,该values()方法必须每次都返回一个新数组,因为Java没有不可变数组.如果它总是返回相同的数组,则无法阻止调用者通过修改数组来互相混淆.

我无法找到它的源代码

values()方法没有普通的源代码,是编译器生成的.对于javac,生成该values()方法的代码位于com.sun.tools.javac.comp.Lower.visitEnumDef中.对于ECJ(Eclipse的编译器),代码位于org.eclipse.jdt.internal.compiler.codegen.CodeStream.generateSyntheticBodyForEnumValues中.

找到该values()方法实现的更简单方法是通过反汇编编译的枚举.首先创建一些愚蠢的枚举:

enum MyEnumType {
    A, B, C;

    public static void main(String[] args) {
        System.out.println(values()[0]);
    }
}
Run Code Online (Sandbox Code Playgroud)

然后编译它,并使用JDK中包含的javap工具对其进行反汇编:

javac MyEnumType.java && javap -c -p MyEnumType
Run Code Online (Sandbox Code Playgroud)

在输出中可见的是枚举的所有编译器生成的隐式成员,包括(1)static final每个枚举常量的字段,(2)$VALUES包含所有常量的隐藏数组,(3)实例化每个常量的静态初始化块和分配每一个以它的命名的字段和至阵列,和(4)values()通过调用工作方法.clone()的上$VALUES阵列和返回结果:

final class MyEnumType extends java.lang.Enum<MyEnumType> {
  public static final MyEnumType A;

  public static final MyEnumType B;

  public static final MyEnumType C;

  private static final MyEnumType[] $VALUES;

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

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

  private MyEnumType(java.lang.String, int);
    Code:
       0: aload_0
       1: aload_1
       2: iload_2
       3: invokespecial #6                  // Method java/lang/Enum."<init>":(Ljava/lang/String;I)V
       6: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: invokestatic  #8                  // Method values:()[LMyEnumType;
       6: iconst_0
       7: aaload
       8: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      11: return

  static {};
    Code:
       0: new           #4                  // class MyEnumType
       3: dup
       4: ldc           #10                 // String A
       6: iconst_0
       7: invokespecial #11                 // Method "<init>":(Ljava/lang/String;I)V
      10: putstatic     #12                 // Field A:LMyEnumType;
      13: new           #4                  // class MyEnumType
      16: dup
      17: ldc           #13                 // String B
      19: iconst_1
      20: invokespecial #11                 // Method "<init>":(Ljava/lang/String;I)V
      23: putstatic     #14                 // Field B:LMyEnumType;
      26: new           #4                  // class MyEnumType
      29: dup
      30: ldc           #15                 // String C
      32: iconst_2
      33: invokespecial #11                 // Method "<init>":(Ljava/lang/String;I)V
      36: putstatic     #16                 // Field C:LMyEnumType;
      39: iconst_3
      40: anewarray     #4                  // class MyEnumType
      43: dup
      44: iconst_0
      45: getstatic     #12                 // Field A:LMyEnumType;
      48: aastore
      49: dup
      50: iconst_1
      51: getstatic     #14                 // Field B:LMyEnumType;
      54: aastore
      55: dup
      56: iconst_2
      57: getstatic     #16                 // Field C:LMyEnumType;
      60: aastore
      61: putstatic     #1                  // Field $VALUES:[LMyEnumType;
      64: return
}
Run Code Online (Sandbox Code Playgroud)

但是,该values()方法必须返回一个新数组的事实并不意味着编译器必须使用该方法.编译器可能会检测到的使用,MyEnumType.values()[ordinal]并且看到数组未被修改,它可以绕过该方法并使用底层$VALUES数组.上述main方法的反汇编表明javac 没有进行这样的优化.

我也测试了ECJ.反汇编显示ECJ还初始化一个隐藏数组来存储常量(虽然Java langspec不要求这样做),但有趣的是它的values()方法更喜欢创建一个空白数组,然后填充它System.arraycopy,而不是调用.clone().无论哪种方式,values()每次都返回一个新数组.像javac一样,它不会尝试优化序数查找:

final class MyEnumType extends java.lang.Enum<MyEnumType> {
  public static final MyEnumType A;

  public static final MyEnumType B;

  public static final MyEnumType C;

  private static final MyEnumType[] ENUM$VALUES;

  static {};
    Code:
       0: new           #1                  // class MyEnumType
       3: dup
       4: ldc           #14                 // String A
       6: iconst_0
       7: invokespecial #15                 // Method "<init>":(Ljava/lang/String;I)V
      10: putstatic     #19                 // Field A:LMyEnumType;
      13: new           #1                  // class MyEnumType
      16: dup
      17: ldc           #21                 // String B
      19: iconst_1
      20: invokespecial #15                 // Method "<init>":(Ljava/lang/String;I)V
      23: putstatic     #22                 // Field B:LMyEnumType;
      26: new           #1                  // class MyEnumType
      29: dup
      30: ldc           #24                 // String C
      32: iconst_2
      33: invokespecial #15                 // Method "<init>":(Ljava/lang/String;I)V
      36: putstatic     #25                 // Field C:LMyEnumType;
      39: iconst_3
      40: anewarray     #1                  // class MyEnumType
      43: dup
      44: iconst_0
      45: getstatic     #19                 // Field A:LMyEnumType;
      48: aastore
      49: dup
      50: iconst_1
      51: getstatic     #22                 // Field B:LMyEnumType;
      54: aastore
      55: dup
      56: iconst_2
      57: getstatic     #25                 // Field C:LMyEnumType;
      60: aastore
      61: putstatic     #27                 // Field ENUM$VALUES:[LMyEnumType;
      64: return

  private MyEnumType(java.lang.String, int);
    Code:
       0: aload_0
       1: aload_1
       2: iload_2
       3: invokespecial #31                 // Method java/lang/Enum."<init>":(Ljava/lang/String;I)V
       6: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #35                 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: invokestatic  #41                 // Method values:()[LMyEnumType;
       6: iconst_0
       7: aaload
       8: invokevirtual #45                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      11: return

  public static MyEnumType[] values();
    Code:
       0: getstatic     #27                 // Field ENUM$VALUES:[LMyEnumType;
       3: dup
       4: astore_0
       5: iconst_0
       6: aload_0
       7: arraylength
       8: dup
       9: istore_1
      10: anewarray     #1                  // class MyEnumType
      13: dup
      14: astore_2
      15: iconst_0
      16: iload_1
      17: invokestatic  #53                 // Method java/lang/System.arraycopy:(Ljava/lang/Object;ILjava/lang/Object;II)V
      20: aload_2
      21: areturn

  public static MyEnumType valueOf(java.lang.String);
    Code:
       0: ldc           #1                  // class MyEnumType
       2: aload_0
       3: invokestatic  #59                 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
       6: checkcast     #1                  // class MyEnumType
       9: areturn
}
Run Code Online (Sandbox Code Playgroud)

但是,JVM仍有可能进行优化,以检测数组被复制然后丢弃的事实,并避免它.为了测试这一点,我运行了以下一对基准测试程序,它们在循环中测试顺序查找,每次调用values()一次,另一次使用数组的私有副本.序数查找的结果被分配给一个volatile字段,以防止它被优化掉:

enum MyEnumType1 {
    A, B, C;

    public static void main(String[] args) {
        long t = System.nanoTime();
        for (int n = 0; n < 100_000_000; n++) {
            for (int i = 0; i < 3; i++) {
                dummy = values()[i];
            }
        }
        System.out.printf("Done in %.2f seconds.\n", (System.nanoTime() - t) / 1e9);
    }

    public static volatile Object dummy;
}

enum MyEnumType2 {
    A, B, C;

    public static void main(String[] args) {
        long t = System.nanoTime();
        for (int n = 0; n < 100_000_000; n++) {
            for (int i = 0; i < 3; i++) {
                dummy = values[i];
            }
        }
        System.out.printf("Done in %.2f seconds.\n", (System.nanoTime() - t) / 1e9);
    }

    public static volatile Object dummy;
    private static final MyEnumType2[] values = values();
}
Run Code Online (Sandbox Code Playgroud)

我在服务器VM上的Java 8u60上运行了这个.使用该values()方法的每个测试花费大约10秒,而使用私有阵列的每个测试花费大约2秒.使用-verbose:gcJVM参数显示values()在使用该方法时存在显着的垃圾收集活动,而在使用私有数组时则没有.在客户端虚拟机上运行相同的测试,私有阵列仍然很快,但values()方法变得更慢,花了一分多钟才完成.调用values()也需要更长时间,定义更多的枚举常量.所有这些表明该values()方法确实每次都分配一个新数组,并且避免它可能是有利的.

注意,这两个java.util.EnumSetjava.util.EnumMap需要使用枚举常量数组.为了性能,他们调用JRE专有代码来缓存存储在其中values() 的共享数组的结果java.lang.Class.您可以通过调用自己访问该共享阵列sun.misc.SharedSecrets.getJavaLangAccess().getEnumConstantsShared(MyEnumType.class),但依赖它是不安全的,因为这些API不是任何规范的一部分,可以在任何Java更新中更改或删除.

结论:

  • values()如果调用者修改它,则enum 方法必须表现得总是分配一个新数组.
  • 编译器或虚拟机可能会在某些情况下优化分配,但显然他们没有.
  • 在性能关键代码中,非常值得使用您自己的数组副本.