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

时间:2015-09-26 16:22:50

标签: java memory-management enums

我需要将序数files[i]值转换为Java中的枚举值。这很简单:

int

MyEnumType value = MyEnumType.values()[ordinal]; 方法是隐含的,我找不到它的源代码,因此问题。

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

2 个答案:

答案 0 :(得分:25)

是的,MyEnumType.values()每次都会创建一个用枚举元素填充的新数组。您只需使用==运算符即可对其进行测试。

MyEnumType[] arr1 = MyEnumType.values();
MyEnumType[] arr2 = MyEnumType.values();
System.out.println(arr1==arr2); //false

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

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

  • 您可以创建私有数组,并且只允许通过像

    这样的getter方法访问其内容
    private static final MyEnumType[] VALUES = values();// to avoid recreating array
    
    MyEnumType getByOrdinal(int){
        return VALUES[int];
    }
    
  • 您还可以将values()的结果存储在List等不可修改的集合中,以确保其内容不会被更改(现在这样的列表可以公开)。

    public static final List<X> VALUES = Collections.unmodifiableList(Arrays.asList(values()));
    

答案 1 :(得分:13)

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

  

我无法找到它的源代码

values()方法没有普通的源代码,而是由编译器生成的。对于javac,生成values()方法的代码位于com.sun.tools.javac.comp.Lower.visitEnumDef。对于ECJ(Eclipse&#39;编译器),代码位于 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]);
    }
}

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

javac MyEnumType.java && javap -c -p MyEnumType

输出中可见的是枚举的所有编译器生成的隐式成员,包括(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
}

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

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

values()

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

volatile

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

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

结论:

  • 枚举sun.misc.SharedSecrets.getJavaLangAccess().getEnumConstantsShared(MyEnumType.class)方法必须表现得好像总是分配一个新数组,以防来电者修改它。
  • 编译器或虚拟机可能会在某些情况下优化分配,但显然他们不会。
  • 在性能关键代码中,使用自己的数组副本非常值得。