枚举第I部分中的最大值数

第一部分,理论 | 第二部分,实用


根据Evgeny Mandrikov aka Godin的推文:


在其中,他想知道在Java enum中可以指定最大数量的值。 经过一系列实验和使用黑魔法ConstantDynamic( JEP 309 ),问题的作者得出了编号8191。

在由两篇文章组成的系列文章中,我们寻找枚举中元素数量的理论极限,尝试在实践中更接近它们,并找出JEP 309可以如何提供帮助。

侦察


回顾章节,我们首先看到枚举被分解。

首先,让我们看一下以下枚举的含义:

 public enum FizzBuzz { Fizz, Buzz, FizzBuzz; } 

编译和拆卸后:

javap -c -s -p -v FizzBu​​zz.class
 Classfile /dev/null/FizzBuzz.class Last modified 32 . 2019 .; size 903 bytes MD5 checksum add0af79de3e9a70a7bbf7d57dd0cfe7 Compiled from "FizzBuzz.java" public final class FizzBuzz extends java.lang.Enum<FizzBuzz> minor version: 0 major version: 58 flags: (0x4031) ACC_PUBLIC, ACC_FINAL, ACC_SUPER, ACC_ENUM this_class: #2 // FizzBuzz super_class: #13 // java/lang/Enum interfaces: 0, fields: 4, methods: 4, attributes: 2 Constant pool: #1 = Fieldref #2.#3 // FizzBuzz.$VALUES:[LFizzBuzz; #2 = Class #4 // FizzBuzz #3 = NameAndType #5:#6 // $VALUES:[LFizzBuzz; #4 = Utf8 FizzBuzz #5 = Utf8 $VALUES #6 = Utf8 [LFizzBuzz; #7 = Methodref #8.#9 // "[LFizzBuzz;".clone:()Ljava/lang/Object; #8 = Class #6 // "[LFizzBuzz;" #9 = NameAndType #10:#11 // clone:()Ljava/lang/Object; #10 = Utf8 clone #11 = Utf8 ()Ljava/lang/Object; #12 = Methodref #13.#14 // java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; #13 = Class #15 // java/lang/Enum #14 = NameAndType #16:#17 // valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; #15 = Utf8 java/lang/Enum #16 = Utf8 valueOf #17 = Utf8 (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; #18 = Methodref #13.#19 // java/lang/Enum."<init>":(Ljava/lang/String;I)V #19 = NameAndType #20:#21 // "<init>":(Ljava/lang/String;I)V #20 = Utf8 <init> #21 = Utf8 (Ljava/lang/String;I)V #22 = String #23 // Fizz #23 = Utf8 Fizz #24 = Methodref #2.#19 // FizzBuzz."<init>":(Ljava/lang/String;I)V #25 = Fieldref #2.#26 // FizzBuzz.Fizz:LFizzBuzz; #26 = NameAndType #23:#27 // Fizz:LFizzBuzz; #27 = Utf8 LFizzBuzz; #28 = String #29 // Buzz #29 = Utf8 Buzz #30 = Fieldref #2.#31 // FizzBuzz.Buzz:LFizzBuzz; #31 = NameAndType #29:#27 // Buzz:LFizzBuzz; #32 = String #4 // FizzBuzz #33 = Fieldref #2.#34 // FizzBuzz.FizzBuzz:LFizzBuzz; #34 = NameAndType #4:#27 // FizzBuzz:LFizzBuzz; #35 = Utf8 values #36 = Utf8 ()[LFizzBuzz; #37 = Utf8 Code #38 = Utf8 LineNumberTable #39 = Utf8 (Ljava/lang/String;)LFizzBuzz; #40 = Utf8 LocalVariableTable #41 = Utf8 name #42 = Utf8 Ljava/lang/String; #43 = Utf8 this #44 = Utf8 Signature #45 = Utf8 ()V #46 = Utf8 <clinit> #47 = Utf8 Ljava/lang/Enum<LFizzBuzz;>; #48 = Utf8 SourceFile #49 = Utf8 FizzBuzz.java { public static final FizzBuzz Fizz; descriptor: LFizzBuzz; flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM public static final FizzBuzz Buzz; descriptor: LFizzBuzz; flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM public static final FizzBuzz FizzBuzz; descriptor: LFizzBuzz; flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM private static final FizzBuzz[] $VALUES; descriptor: [LFizzBuzz; flags: (0x101a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC public static FizzBuzz[] values(); descriptor: ()[LFizzBuzz; flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=0, args_size=0 0: getstatic #1 // Field $VALUES:[LFizzBuzz; 3: invokevirtual #7 // Method "[LFizzBuzz;".clone:()Ljava/lang/Object; 6: checkcast #8 // class "[LFizzBuzz;" 9: areturn LineNumberTable: line 1: 0 public static FizzBuzz valueOf(java.lang.String); descriptor: (Ljava/lang/String;)LFizzBuzz; flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: ldc #2 // class FizzBuzz 2: aload_0 3: invokestatic #12 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; 6: checkcast #2 // class FizzBuzz 9: areturn LineNumberTable: line 1: 0 LocalVariableTable: Start Length Slot Name Signature 0 10 0 name Ljava/lang/String; private FizzBuzz(); descriptor: (Ljava/lang/String;I)V flags: (0x0002) ACC_PRIVATE Code: stack=3, locals=3, args_size=3 0: aload_0 1: aload_1 2: iload_2 3: invokespecial #18 // Method java/lang/Enum."<init>":(Ljava/lang/String;I)V 6: return LineNumberTable: line 1: 0 LocalVariableTable: Start Length Slot Name Signature 0 7 0 this LFizzBuzz; Signature: #45 // ()V static {}; descriptor: ()V flags: (0x0008) ACC_STATIC Code: stack=4, locals=0, args_size=0 0: new #2 // class FizzBuzz 3: dup 4: ldc #22 // String Fizz 6: iconst_0 7: invokespecial #24 // Method "<init>":(Ljava/lang/String;I)V 10: putstatic #25 // Field Fizz:LFizzBuzz; 13: new #2 // class FizzBuzz 16: dup 17: ldc #28 // String Buzz 19: iconst_1 20: invokespecial #24 // Method "<init>":(Ljava/lang/String;I)V 23: putstatic #30 // Field Buzz:LFizzBuzz; 26: new #2 // class FizzBuzz 29: dup 30: ldc #32 // String FizzBuzz 32: iconst_2 33: invokespecial #24 // Method "<init>":(Ljava/lang/String;I)V 36: putstatic #33 // Field FizzBuzz:LFizzBuzz; 39: iconst_3 40: anewarray #2 // class FizzBuzz 43: dup 44: iconst_0 45: getstatic #25 // Field Fizz:LFizzBuzz; 48: aastore 49: dup 50: iconst_1 51: getstatic #30 // Field Buzz:LFizzBuzz; 54: aastore 55: dup 56: iconst_2 57: getstatic #33 // Field FizzBuzz:LFizzBuzz; 60: aastore 61: putstatic #1 // Field $VALUES:[LFizzBuzz; 64: return LineNumberTable: line 3: 0 line 5: 13 line 7: 26 line 1: 39 } Signature: #47 // Ljava/lang/Enum<LFizzBuzz;>; SourceFile: "FizzBuzz.java" 


在上市我们遇到了

  • 枚举中定义的每个值的一个public static final字段
  • 专用合成字段$VALUESvalues()方法的实现细节
  • values()valueOf()方法的实现
  • 私人建设者
  • 静态初始化的块,实际上最有趣的事情发生在这里。 让我们更详细地考虑它。

以Java代码的形式,后者看起来像这样:

  static { Fizz = new FizzBuzz("Fizz", 0); Buzz = new FizzBuzz("Buzz", 1); FizzBuzz = new FizzBuzz("FizzBuzz", 2); $VALUES = new FizzBuzz[] { Fizz, Buzz, FizzBuzz }; } 

首先,创建枚举元素的实例。 创建的实例将立即写入相应的public static final字段。

然后创建一个数组,并用指向所有枚举元素实例的链接填充。 链接来自我们在上一段中初始化的类的字段。 填充的数组存储在private static final字段$VALUES

在那之后,清单就可以开始了。

瓶颈


我们在无聊的章节中寻找对枚举元素数量的限制。

您可以从JLS章节8.9.3节 “枚举成员”开始搜索:

JLS 8.9.3枚举成员
枚举类型E的成员都是以下所有:
...
*对于E声明主体中声明的每个枚举常量c,E具有
具有相同类型的隐式声明的E型公共静态最终字段
名称为c。 该字段具有变量初始化器,该初始化器实例化E并传递任何
c为为E选择的构造函数的参数。该字段具有相同的批注
作为c(如果有)。

这些字段以与相应字段相同的顺序隐式声明
枚举常量,在主体中显式声明的任何静态字段之前
E的声明
...
*以下隐式声明的方法:
 /** * Returns an array containing the constants of this enum * type, in the order they're declared. This method may be * used to iterate over the constants as follows: * * for(E c : E.values()) * System.out.println(c); * * @return an array containing the constants of this enum * type, in the order they're declared */ public static E[] values(); /** * Returns the enum constant of this type with the specified * name. * The string must match exactly an identifier used to declare * an enum constant in this type. (Extraneous whitespace * characters are not permitted.) * * @return the enum constant with the specified name * @throws IllegalArgumentException if this enum type has no * constant with the specified name */ public static E valueOf(String name); 



因此,每个枚举类都有一个values()方法,该方法返回一个包含此枚举中声明的所有元素的数组。 因此,真空中的球形枚举不能包含超过Integer.MAX_VALUE + 1元素。

继续前进。 Java中的枚举表示为java.lang.Enum类的后代,因此它们受JVM中类固有的所有限制。

让我们看一下JVMS§4.1“ ClassFile结构”中给出的类文件结构的高级描述:

 ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; } 

正如我们从JLS§8.9.3中已经知道的那样,将为结果类中的每个枚举元素创建一个同名字段。 类中的字段数定义了一个16位无符号fields_count ,这将我们限制为一个类文件或65_534枚举元素中的65_535个字段。 $VALUES数组保留一个字段,该字段的一个克隆返回values()方法。 规范中没有明确说明,但是不太可能提出更优雅的解决方案。

字段,方法,类,常量值等的名称存储在常量池中。
如果您对常量池的内部结构一无所知,建议您阅读lany古老文章 。 尽管自从将其写入常量池以来,已经出现了许多新颖有趣的东西,但基本原理仍然保持不变。
类常量池的大小也受65_535个元素的数量限制。 正确形成的类的常量池永远不会为空。 至少会有一个此类的名称。

例如,由javac从OpenJDK 14-ea + 29编译的空枚举类的常量池中没有调试信息,包含29次出现。

因此,一个枚举中65_534个元素的数量也无法达到。 在最佳情况下,我们可以指望65_505或接近这个数字。

这个冗长的介绍中的最后一个和弦是:

只能在静态初始化块中将该值<clinit>static final字段,该块在类文件级别由称为<clinit>的方法表示。 任何方法的字节码不能超过65_535个字节。 一个熟悉的数字,不是吗?

一条静态静态写入指令占用3个字节,这给了我们大约65_535 / 3 = 21_845估计值。 实际上,这个估计数被夸大了。 该指令采用该值从堆栈的顶部写入该字段,前面的指令之一放在该堆栈的顶部。 并且该指令还占用了宝贵的字节。 但是,即使您不考虑这一点,所得到的数字仍然明显小于65_505。

总结:

  • 类文件格式将枚举元素的最大数量限制为大约65_505
  • 静态最终字段初始化机制对我们的限制更大。 理论上-最多21_845个元素,实际上这个数字甚至更少

在本系列的最后一篇文章中,我们将重点讨论不健康的优化和类文件的生成。

Source: https://habr.com/ru/post/zh-CN483392/


All Articles