停止喂日志记录器! 提供更多修饰符! 延迟静态最终字段。 草图特征草图

在Java中,记录器在初始化类时就被初始化就足够了,为什么它们会乱扔整个启动程序? 约翰·罗斯来营救!


可能是这样的:


lazy private final static Logger LOGGER = Logger.getLogger("com.foo.Bar"); 

本文档扩展了最终变量的行为,使您可以选择在语言本身和JVM中支持延迟执行。 提出了通过更改粒度来改进现有懒惰计算机制的行为的方法:现在分类不是精确的,而是特定变量是精确的。



动机


Java已在延迟计算中深入构建。 几乎每个链接操作都可以提取惰性代码。 例如,执行<clinit>方法(类初始化程序的字节码)或使用引导程序方法(对于invokedynamic调用站点或CONSTANT_Dynamic常量)。


与使用引导程序方法的机制相比,类初始化器在粒度方面非常不礼貌,因为它们的约定是为整个类运行所有初始化代码,而不是将自身限于与类的特定字段相关的初始化。 这种粗略初始化的效果很难预测。 很难隔离使用类静态字段的副作用,因为计算一个字段会导致计算该类的所有静态字段。


如果触摸一个字段,则将影响所有这些字段。 在AOT编译器中,这使得优化静态字段引用特别困难,即使对于具有易于解析的常量值的字段也是如此。 一旦在这些字段之间至少有一个重新设计的静态字段混乱,就不可能完全分析此类的所有字段。 以前提出的用于对带有复杂初始化程序的常量字段进行常量卷积(在javac操作期间)的机制也表现出类似的问题。


重新设计的字段初始化的一个示例是记录器的初始化,该初始化在每个文件的每个步骤的不同项目中发生。


 private final static Logger LOGGER = Logger.getLogger("com.foo.Bar"); 

这种无害的外观初始化将在类的初始化期间启动大量工作,但是,在类初始化时真正需要记录器的可能性很小,或者根本不需要。 将其创建推迟到第一次真正使用的能力将简化初始化,并且在某些情况下,将完全避免这种初始化。


最终变量非常有用,它们是Java API的主要机制,用于指示值的恒定性。 惰性变量也很好用。 从Java 7开始,它们在JDK的内部起着越来越重要的作用,并带有@Stable批注。 JIT可以优化最终变量和稳定变量-比仅某些变量好得多。 添加惰性最终变量将使这种有用的用法模式变得更加普遍,从而有可能在更多地方使用。 最后,使用惰性最终变量将使JDK之类的库减少对<clinit>代码的依赖,从而减少启动时间并提高AOT优化的质量。


内容描述


可以使用新的lazy修饰符声明该字段,这是一个上下文关键字,仅被视为修饰符。 这样的字段称为惰性字段 ,并且还必须具有staticfinal修饰符。


惰性字段必须具有初始化程序。 编译器和运行时同意在首次使用变量时准确地启动初始化程序,而不是在初始化该字段所属的类时启动。


每个lazy static final字段在编译时都与代表其值的常量池元素相关联。 由于常量池本身的元素是惰性计算的,因此只需为与此元素关联的每个静态惰性最终变量简单分配正确的值就足够了。 (您可以将多个惰性变量绑定到一个元素,但这几乎不是有用或有意义的功能。)该属性名称为LazyValue ,并且它必须引用一个恒定的性别元素,该元素可以被ldc编码为可转换为惰性字段类型的值。 。 仅MethodHandle.invokeMethodHandle.invoke使用的强制类型转换。


因此,可以将惰性静态字段视为声明该字段的类中的常量池元素的命名别名。 诸如编译器之类的工具可能会尝试使用此字段。


惰性字段永​​远不会是常量变量(在JLS 4.12.4的意义上),并且明确地不参与常量表达式(在JLS 15.28的意义上)。 因此,即使它的初始化器是一个常量表达式,它也不会捕获ConstantValue属性。 相反,惰性字段捕获一种称为LazyValue的新类文件属性,JVM在链接到此特定字段时会查询该属性。 此新属性的格式与上一个属性相似,因为它也指向常量池中的一个元素,在这种情况下,该元素被解析为字段值。


链接了惰性静态字段后,执行类初始化程序的正常过程不应消失。 而是根据JVMS 5.5中定义的规则初始化任何声明类<clinit>方法。 换句话说,惰性静态字段的getstatic字节码执行与任何静态字段相同的链接。 初始化之后(或在当前线程已经开始的初始化过程中),JVM解析与该字段关联的常量池元素,并将从常量池获得的值存储在此字段中。


由于惰性static final不能为空,因此无法为它们分配任何值-即使在少数情况下,它们也可以用于空final变量。


在编译期间,所有惰性静态字段都独立于非惰性静态字段进行初始化,无论它们在源代码中的位置如何。 因此,对静态字段位置的限制不适用于惰性静态字段。 惰性静态字段初始化程序可以使用同一类的任何静态字段,无论它们在源中出现的顺序如何。 任何非静态字段的初始化程序或类初始化程序都可以访问惰性字段,而不管它们在源中相对于彼此的顺序如何。 通常,这样做不是最明智的主意,因为会丢失惰性值的全部含义,但可以在条件表达式或控制流中以某种方式使用它。 因此,可以将惰性静态字段更像另一个类的字段,在某种意义上,可以从声明它们的类的任何部分以任何顺序对其进行引用。


可以使用反射API通过使用java.lang.reflect.Field两个新API方法来检测惰性字段。 当且仅当字段具有lazy修饰符时,新的isLazy方法才返回true 。 当且仅当字段为lazy并且在isAssignedisAssigned初始化时,新的isAssigned方法才返回false 。 (根据种族的存在,它几乎可以在同一线程中的下一个调用上返回true)。 除了使用isAssigned之外,没有其他方法isAssigned字段是否已初始化。


isAssigned需要isAssigned调用才能解决与解决循环依赖相关的罕见问题。也许我们可以不用实现此方法就可以做到。但是,编写带有惰性变量的代码的人有时希望仔细了解是否将该值设置为此类变量,其方式与互斥锁用户有时希望从互斥锁中查找是否已锁定(但实际上并不想被锁定)相同。


惰性的final字段有一个不寻常的限制:永远不要将它们初始化为默认值。 也就是说,惰性引用字段不应初始化为null ,数字类型不应具有null值。 惰性布尔值只能使用一个值-true初始化,因为false是其默认值。 如果惰性静态字段的初始化程序返回其默认值,则此字段的链接将失败,并显示相应的错误。


为此引入了限制。 允许JVM实现将默认值保留为内部看门狗值,以标记未初始化字段的状态。 默认值已在准备时设置的任何字段的初始值中设置(在JLS 5.4.2中进行了描述)。 因此,该值自然会在任何字段的生命周期开始时就已经存在,因此是用作监视该字段状态的看门狗值的逻辑选择。 使用这些规则,您永远无法从惰性静态字段获取原始默认值。 为此,JVM可以例如将惰性字段实现为到相应常量池元素的不可变链接。


可以通过将值(可能等于默认值)包装在某些方便类型的盒子或容器中来避免对默认值的限制。 零数字可以包装在非零的Integer引用中。 非基本类型可以包装在Optional中,如果它为null,则为空。


为了在实现功能方面保持自由,特别低估了isAssigned方法的要求。 如果JVM可以证明可以在没有可观察到的外部影响的情况下初始化惰性静态变量,则它可以随时进行初始化。 在这种情况下,即使从未调用过getfieldisAssigned也将返回true 。 对isAssigned施加的唯一要求是,如果它返回false ,则在当前线程中不应观察到任何变量初始化的副作用。 如果他返回true ,那么当前线程将来可能会观察到初始化的副作用。 这样的约定允许编译器在其自己的字段中用getstatic替换ldc ,这使JVM不监视常量池中具有公共或退化元素的最终变量的详细状态。


多个线程可以进入竞争状态以初始化惰性的final字段。 正如CONSTANT_Dynamic所发生的那样,JVM选择了该比赛的任意获胜者,并将该获胜者的值提供给参与该比赛的所有线程,并将其写入所有随后的获取值的尝试。 为了绕开比赛,特定的JVM实现可以尝试使用CAS操作,如果平台支持它们,则比赛的获胜者将看到先前的默认值,而失败者将看到赢得比赛的非默认值。


因此,用于最终变量的单个分配的现有规则继续有效,现在捕获了延迟计算的所有困难。


相同的逻辑适用于使用最终字段的安全发布-惰性和非惰性字段都相同。


请注意,一个类可以将静态字段转换为惰性静态字段,而不会破坏二进制兼容性。 在两种情况下, getstatic客户语句getstatic相同的。 当变量声明更改为惰性时,将以不同的方式链接getstatic


替代解决方案


您可以将嵌套类用作惰性变量的容器。


您可以定义诸如库API之类的内容,以管理惰性值或(通常)管理任何单调数据。


重构它们将要创建惰性静态变量的内容,以使它们变为空静态方法,并以某种方式使用ldc CONSTANT_Dynamic常量发布其主体。


(注意:以上变通办法没有提供一种二进制兼容的方法来从其<clinit>逐步解耦现有的静态常量)


如果我们谈论提供更多功能,则可以允许惰性字段为非静态或非最终字段,同时保持静态字段和非静态字段的行为之间的当前对应关系和类比。 常量池不能是非静态字段的存储库,但它仍可以包含引导程序方法(取决于当前实例)。 冻结的数组(如果已实现)可以采用延迟选项。 这些研究为基于此文档的未来项目奠定了良好的基础。 顺便说一下,这些机会使我们决定禁止默认值更加有意义。


惰性变量必须使用其自己的初始化表达式进行初始化。 有时,这似乎是一个非常令人不愉快的限制,使我们回到了空的最终变量发明的时代。 回想一下,这些空的最终变量可以使用任意代码块(包括try-finally逻辑)进行初始化,并且可以成组而不是同时进行初始化。 将来,将有可能尝试将相同的可能性应用于延迟的最终变量。 可能将一个或多个惰性变量与初始化代码的专用块关联,该初始化代码的任务是恰好为每个变量分配一次,这与类初始化器或对象构造函数一样。 解构函数出现后,此类功能的体系结构会变得更加清晰,因为它们解决的任务在某种意义上是相交的。


分钟的广告。 Joker 2018大会将很快举行,届时将有许多Java和JVM的知名专家。 在官方网站上查看发言人和报告的完整列表。

作者


John Rose是Oracle的JVM工程师和架构师。 首席工程师达芬奇机器项目(OpenJDK的一部分)。 首席工程师JSR 292(在Java平台上支持动态类型的语言)专门研究动态调用和相关主题,例如类型分析和高级编译器优化。 以前,他从事内部类的研究,在SPARC,Unsafe API上建立了最初的HotSpot端口,还开发了许多动态,并行和混合语言,包括Common Lisp,Scheme(“ esh”)和C ++的动态绑定器。


译者


Oleg Chirukhin-在撰写本文时,他正在JUG.ru Group公司担任社区经理,他从事Java平台的普及。 在加入JRG之前,他参与了银行和政府信息系统,自定义编程语言生态系统以及在线游戏的开发。 当前的研究兴趣包括虚拟机,编译器和编程语言。

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


All Articles