【问题标题】:Does the Enum#values() allocate memory on each call?Enum#values() 是否在每次调用时分配内存?
【发布时间】:2015-12-24 07:05:46
【问题描述】:

我需要将序数 int 值转换为 Java 中的枚举值。很简单:

MyEnumType value = MyEnumType.values()[ordinal];

values() 方法是隐式的,我找不到它的源代码,因此提出了问题。

MyEnumType.values() 是否分配了一个新数组?如果是这样,我应该在第一次调用时缓存数组吗?假设转换会被非常频繁地调用。

【问题讨论】:

    标签: java memory-management enums


    【解决方案1】:

    是的。

    Java 没有让我们创建不可修改数组的机制。因此,如果values() 将返回相同的可变数组,我们就有可能有人为每个人更改其内容。

    因此,在将不可修改的数组引入 Java 之前,为安全起见,values() 必须返回包含所有值的新的/单独的数组。

    我们可以用==操作符来测试它:

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

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

    • 您可以创建私有数组并仅允许通过 getter 方法访问其内容,例如

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

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

    【讨论】:

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

    理论上,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]);
        }
    }
    

    然后编译,使用JDK自带的javap工具反汇编:

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

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

    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() 方法更喜欢创建一个空白数组然后用System.arraycopy 填充它,而不是调用@ 987654344@。无论哪种方式,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
    }
    

    然而,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();
    }
    

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

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

    结论:

    • 枚举values() 方法必须表现得好像它总是分配一个新数组,以防调用者修改它。
    • 在某些情况下,编译器或 VM 可能会优化该分配,但显然它们不会。
    • 在对性能要求严格的代码中,最好使用自己的数组副本。

    【讨论】:

      猜你喜欢
      • 2019-05-10
      • 1970-01-01
      • 2013-11-07
      • 1970-01-01
      • 2019-09-06
      • 1970-01-01
      • 2015-04-12
      • 1970-01-01
      • 2015-01-22
      相关资源
      最近更新 更多