java掌握

为了使JVM对动态语言更具吸引力,该平台的第七版已将invokedynamic引入了其指令集。 Java开发人员通常不会注意到此功能,因为该功能已隐藏在Java字节码中。 简而言之,通过使用invokedynamic ,可以将方法调用的绑定延迟到第一次调用之前。 例如,Java语言使用此技术来实现lambda表达式,这些表达式仅在首次使用时才需要出现。 这样做, invokedynamic已经发展成为一种基本的语言功能,我在先前的博客文章中对此进行了详细介绍 。 使用constantdynamic ,Java 11引入了类似的机制,只是它延迟了常量值的创建。 这篇文章描述了此功能的目的和内部工作原理,并展示了如何使用Byte Buddy库生成使用此新指令的代码。

Java中的常量值是什么?

在Java 5之前,Java程序中的常量值只能是字符串或原始类型。 这些常量作为文字内置在语言中,甚至由javac编译器假定以减小类文件的大小。 例如,在以下代码段中,从不实际读取only字段的值,而是在编译期间将其复制到其使用站点:

class ConstantSample {final String field = “foo”;void hello() {System.out.print(field);}
}

代替读取hello方法中的字段,生成的字节码将包含对常量foo的直接引用。 实际上,上述类绝不会尝试读取该字段的值,可以通过使用Java反射对其进行更改来验证该字段的值,此后调用hello仍会打印foo

为了表示这样的常量值,任何Java类文件都包含一个常量池,可以将其视为写出存在于类范围内的任何常量值的表。 这意味着在方法中使用或用作字段值的常量,但也包含描述类的其他不可变信息,例如类名或被调用方法的名称以及它们的声明类型名。 一旦在类的常量池中记录了值,就可以通过指向常量池中特定条目的偏移量来引用值。 这样做,在整个类中重复的值仅需要存储一次,因为偏移量当然可以多次引用。

因此,当在上述源代码中读取该字段时, javac发出一个字节码,该字节码引用常量池中foo值的偏移量,而不是发出对该字段的读取指令。 可以将字段声明为final,因为javac会忽略反射值更改的边缘情况。 通过发出读取常量的指令,与读取字段的指令相比, javac还节省了一些字节。 这就是使这种优化有利可图的原因,特别是因为在任何Java类中字符串和数字值都相当普遍。 较小的类文件可帮助Java运行时更快地加载类,而显式的常量性概念可帮助JVM的JIT和AOT编译器应用进一步的优化。

所描述的针对相同常数的偏移量的重用还隐含了重用值的标识。 由于用单个实例表示相等的字符串值,因此以下语句在Java中将声明为true:

assert “foo” == “foo”;

在幕后,foo的两个值都指向定义类的常量池中的相同常量池偏移量。 此外,JVM甚至可以通过遍历在常量池中找到的字符串来跨类对常量字符串进行重复数据删除。

恒定池存储的局限性

类文件的常量池中值的这种表格表示形式非常适合简单值,例如字符串和数字基元。 但是同时,当javac没有发现恒定的值时,它可能会带来非直观的后果。 例如,在以下类中, hello方法中未将唯一字段的值视为常量:

class NoConstantSample {final String field = “foo”.toString();void hello() {System.out.print(field);}
}

尽管toString方法对于字符串而言是微不足道的,但是这种情况对于不评估Java方法的javac仍然未知。 因此,编译器不能再发出恒定的池值作为print语句的输入。 相反,它必须发出该字段的字段读取指令,如前所述,该指令需要其他字节。 这次,如果通过使用反射更改了字段的值,则调用hello也将打印更新的值。

当然,这个例子是人为的。 但是不难想象,在实践中如何将经典方法限制为Java中的常量。 例如,想象一个定义为Math.max(CONST_A, CONST_B)的整数值。 当然,两个编译时常数的最大值本身就是常数。 但是,由于javac无法评估Java方法,因此派生值不是作为常量发现的,而只能在运行时进行计算。

在类文件的常量池中声明常量值的另一个问题是它对简单值的限制。 字符串和数值的表示当然很简单,但是比传统方法更复杂的Java对象需要更大的灵活性。 为了支持其他常量,Java类文件格式已经在Java 5中添加了类文字常量,其中诸如String.class类的值将不再被编译为对Class.forName("java.lang.String")的调用,而是一个常量。包含类引用的池条目。 Java 7发行版还在类文件规范中添加了新的常量池类型,以允许MethodTypeMethodHandle实例的常量表示。

与字符串,类和原始值相反,Java编程语言不提供用于创建这些常量的文字。 相反,在javac需要有效表示方式的情况下,添加了此类常量的可能性以更好地支持invokedynamic指令。 本质上,lambda表达式由lambda的表达式类型签名MethodType以及对其实现的引用MethodHandle 。 如果必须为每个对lambda表达式的调用将两个值都创建为显式,非恒定参数,则使用此类表达式的性能开销肯定会超过其收益。

尽管此解决方案减轻了一些中间的麻烦,但它暗示着对Java的未来不满意,无法添加其他常量类型。 常量池条目的类型由单个字节编码,这严重限制了类文件中可能的常量类型的总数。 另一个麻烦是,对类文件格式的更改要求对处理类文件的任何工具进行级联调整,这使得希望使用更通用的方法来表达常量值。 通过引入constantdynamic ,Java虚拟机最终将在即将发布的Java 11版本中支持这种机制。

引入动态常数

动态常量不是通过处理文字表达式来创建的,而是通过调用产生该常量值作为结果的所谓的引导方法来创建的。 这与通过在运行时调用引导程序方法绑定绑定方法调用站点的invokedynamic指令非常相似,在运行时,将返回指向动态绑定调用站点的目标实现的指针。 作为主要区别,自举常量是不可变的,而动态绑定的方法调用可以在以后重定向到另一个实现。

从本质上讲,引导程序只不过是Java方法,这些方法对其签名有一些要求。 作为第一个参数,任何引导方法都将接收由JVM自动提供的MethodHandles.Lookup实例。 通过此类查找,可以使用类的特定实例表示的类的特权进行访问。 例如,当从任何类调用MethodHandles.lookup()时,对调用者敏感的方法将返回一个实例,例如,该实例允许读取调用类的私有字段,而对于从另一个内部创建的查找实例而言,这是不可能的类。 在使用bootstrap方法的情况下,查找表示在创建时定义动态常量的类,而不是在声明boostrap方法的类。 这样做,引导方法可以访问相同的信息,就像从常量定义类本身内部创建常量一样。 bootstrap方法作为第二个参数接收常量的名称,作为第三个参数,它接收常量的预期类型。 引导程序方法必须是静态的,或者是构造函数,其中构造的值表示常量。

在许多情况下,实现自举方法不需要这三个参数,但是它们的存在允许实现更通用的自举机制,从而有助于允许重用自举方法来创建多个常量。 如果需要,在声明引导方法时也可以省略最后两个参数。 但是,需要将MethodHandles.Lookup类型声明为第一个参数。 这样做是为了允许将来在第一个参数用作标记类型的情况下进一步允许调用模式。 这是与invokedynamic的另一个区别,它允许省略第一个参数。

有了这些知识,我们现在可以表示两个常量的先前最大值,该常量之前已提到为派生常量。 该值是通过以下引导方法微不足道地计算的:

public class Bootstrapper {public static int bootstrap(MethodHandles.Lookup lookup, String name, Class type) {return Math.max(CONST_A, CONST_B);}
}

由于作为第一个参数的查找实例具有定义该常量的类的特权,因此即使使用引导程序方法通常看不到这些值,也可以通过使用此查找来获取CONST_ACONST_B的值。 ,例如因为它们是私有的。 该类的javadoc详细解释了需要使用什么API来定位字段并读取其值。

为了创建动态常量,必须在类的常量池中引用引导程序方法作为动态常量类型的条目。 到目前为止,Java语言无法创建此类条目,据我所知,目前也没有其他语言在使用这种机制。 因此,我们将在本文后面探讨使用代码生成库Byte Buddy创建此类。 但是,在暗示注释中有常量池值的Java伪代码中,动态常量及其引导方法将被称为:

class DynamicConstant {// constant pool #1 = 10// constant pool #2 = 20// constant pool #3 = constantdyamic:Bootstrapper.bootstrap/maximum/int.classfinal int CONST_A = [constant #1], CONST_B = [constant #2];void hello() {System.out.print([constant #3]);}
}

首次执行hello方法后,JVM将通过调用Bootstrapper.bootstrap方法来解析指定的常量,该方法的最大值为常量名,而int.class为创建的常量的请求类型。 在从bootstrap方法接收到结果之后,JVM将用该结果替换对常量的任何引用,并且不再再次调用bootstrap方法。 如果在多个位置引用了动态常数,这也将是正确的。

避免自定义引导方法

在大多数情况下,创建动态常量不需要实现单独的引导方法。 为了涵盖大多数用例,JVM绑定的类java.lang.invoke.ConstantBootstraps已经实现了几种通用的引导方法,可用于创建大多数常量。 作为核心,类的invoke方法允许通过提供方法引用作为常量值的工厂来定义常量。 为了使这种通用方法有效,引导程序方法可以接收任意数量的附加参数,这些参数本身必须是恒定值。 然后,在描述动态常量的条目时,将这些参数作为对其他常量池条目的引用。

这样做,可以通过提供Math.max方法的句柄以及CONST_ACONST_B的两个常量值作为附加参数来计算上述最大值。 然后, ConstantBootstraps中的invoke方法的实现将使用这两个值来调用Math.max并返回结果,其中bootstrap方法大致实现如下:

class ConstantBootstraps {static Object invoke(MethodHandles.Lookup lookup, String name, Class type,MethodHandle handle, Object[] arguments) throws Throwable {return handle.invokeWithArguments(arguments);}
}

当将其他参数提供给引导方法时,它们将按其顺序分配给每个其他方法参数。 为了允许更灵活的引导程序方法(例如上面的invoke方法),最后一个参数也可以是Object数组类型,以接收任何多余的参数,在这种情况下为两个整数值。 如果引导程序方法不接受提供的参数,则JVM将不会调用引导程序方法,而是在失败的常量解析期间引发BootstrapMethodError

使用这种方法,使用ConstantBootstraps.invoke的伪代码将不再需要单独的引导程序方法,而是看起来像下面的伪代码:

class AlternativeDynamicConstant {// constant pool #1 = 10// constant pool #2 = 20// constant pool #3 = MethodHandle:Math.max(int,int)// constant pool #4 = constantdyamic:ConstantBootstraps.invoke/maximum/int.class/#3,#1,#2final int CONST_A = [constant #1], CONST_B = [constant #2];void hello() {System.out.print([constant #4]);}
}

嵌套动态常数

如前所述,引导方法的参数必须是其他常量池条目。 由于动态常量存储在常量池中,因此可以嵌套动态常量,这使此功能更加灵活。 这带有直观的限制,即动态常量的初始化不得包含圆圈。 例如,如果解决了Qux值,将从顶部到底部调用以下引导程序方法:

static Foo boostrapFoo(MethodHandles.Lookup lookup, String name, Class type) {return new Foo();
}static Bar boostrapBar(MethodHandles.Lookup lookup, String name, Class type, Foo foo) {return new Bar(foo);
}static Qux boostrapQux(MethodHandles.Lookup lookup, String name, Class type, Bar bar) {return new Qux(bar);
}

当需要JVM解析Qux的动态常量时,它将首先解析Bar这将再次触发Foo的先前初始化,因为每个值都取决于前一个。

当表达静态常量池条目类型(例如空引用)不支持的值时,也可能需要嵌套动态常量。 在Java 11之前,空值只能表示为字节码指令,而不能表示为常量池值,其中字节码均未暗示null的类型。 为了克服此限制, java.lang.invoke.ConstantBootstraps提供了几种便捷的方法,例如nullValue ,该方法允许将键入的null值引导为动态常量。 然后可以将此null值作为参数提供给另一个引导程序方法,该方法希望将null作为参数。 同样,不可能在常量池中表示只能表示引用类型的原始类型文字,例如int.class 。 相反, javac int.class例如int.class转换为对静态Integer.TYPE字段的读取,该字段在启动时通过对JVM的本地调用来解析其int.class值。 同样, ConstantBootstraps提供了primitiveType引导程序方法,可以轻松地将这些值表示为动态常量。

为什么要关心常数值?

以上所有内容听起来都像是一种技术诀窍,除了静态字段已经提供的功能外,对Java平台的添加并不多。 但是,动态常数的潜力很大,但尚未开发。 作为最明显的用例,动态常量可用于正确实现惰性值。 惰性值通常用于仅在使用时按需表示昂贵的对象。 从今天开始,惰性值通常是通过使用所谓的双重检查锁定来实现的 ,这种模式例如由scalac编译器为其lazy关键字实现:

class LazyValue {volatile ExpensiveValue value;void get() {T value = this.value;if (value == null) {synchronized (this) {value = this.value;if (value == null) {value = new ExpensiveValue();}}}return value;}
}

尽管值一旦初始化就永远不会改变,但上述构造需要在每次读取时都进行易失性读取。 这意味着不必要的开销,可以通过将惰性值表示为仅在曾经使用过时才进行引导的动态常量来避免该开销。 尤其是在Java核心库中,这对于延迟许多从未使用过的值的初始化很有用,例如在Locale类中,尽管大多数JVM仅使用运行中的机器标准语言,但Locale类却为任何受支持的语言初始化了值。 通过避免初始化这些多余的值,JVM可以更快地启动,并避免将内存用于无效值。

另一个重要的用例是使用常量表达式优化编译器。 不难想象,为什么编译器比可变值更喜欢处理常量。 例如,如果编译器可以合并两个常量,则此合并的结果可以永久替换先前的值。 如果原始值会随时间变化,那么这当然是不可能的。 尽管即时编译器可能仍认为可变值在运行时实际上是恒定的,但提前编译器则依赖于某些明确的恒定性概念。 通过确保引导程序方法无副作用,例如,将来的Java版本可以对其编译时进行评估,其中常量动态可以用作轻量级宏机制,以扩大使用Graal用Java编写的本机映像的范围。

我会使用此功能吗?

在Java 7中引入invokedynamic时,从Java语言的角度来看,这个新的字节码功能尚未使用。 但是,从Java 8开始,可以在大多数类文件中找到作为lambda表达式的实现的invokedynamic指令。 同样,Java 11尚未使用恒定动力学功能,但是可以预料将来会有所改变。

在对暴露constantdynamic几个潜在的API,已经讨论了最新JVMLS(这样也会使通过API invokedynamic访问)。 这对于库作者来说特别有用,它允许他们更好地解析关键执行路径,但也可以释放一些潜力来改善javac的常量检测,例如扩大非捕获lambda表达式的范围,在这种情况下,字段或变量访问可能是如果在编译过程中发现常量,则通过读取常量来代替。 最后,这种新机制为将来的语言增强提供了潜力,例如一个惰性关键字,它避免了替代JVM语言中当前等效项的开销。

常量动态功能对于经常需要使用其他信息来增强现有类的Java代理也很有用。 Java代理通常无法通过添加静态字段来更改类,因为这会干扰基于反射的框架,并且由于在重新定义已加载的类时大多数JVM禁止更改类格式。 但是,这两种限制都不适用于在运行时期间添加的动态常量,在动态常量中,Java代理现在可以轻松地用附加信息标记类。

使用字节好友创建动态常量

尽管缺少对constantdynamic的语言支持,但是版本11的JVM已经完全能够处理包含动态常量的类文件。 使用字节代码生成库Byte Buddy,我们可以创建此类文件并将其加载到JVM的早期访问版本中 。

在Byte Buddy中,动态常量由JavaConstant.Dynamic的实例表示。 为了方便起见,Byte Buddy为工厂提供了由java.lang.invoke.ConstantBoostraps类声明的任何引导方法,例如前面讨论的invoke方法。

举一个简单的例子,下面的代码创建Callable的子类,并将call方法的返回值定义为示例类的动态常量。 为了引导常量,我们将Sample的构造函数提供给上述的invoke方法:

public class Sample {public static void main(String[] args) throws Throwable {Constructor<? extends Callable<?>> loaded = new ByteBuddy().subclass(Callable.class).method(ElementMatchers.named("call")).intercept(FixedValue.value(JavaConstant.Dynamic.ofInvocation(Sample.class.getConstructor()))).make().load(Sample.class.getClassLoader()).getLoaded().getConstructor();Callable<?> first = loaded.newInstance(), second = loaded.newInstance();System.out.println("Callable instances created");System.out.println(first.call() == second.call());}public Sample() { System.out.println("Sample instance created"); }
}

如果运行代码,请注意如何仅创建一个Sample实例,如本文所述。 还要注意如何仅在首次调用call方法时以及在创建Callable实例之后才懒惰地创建实例。

要运行上面的代码,您当前必须使用-Dnet.bytebuddy.experimental=true运行Byte Buddy才能解锁对此功能的支持。 Java 11最终确定并准备发布时,情况将发生变化,其中Byte Buddy 1.9.0将是第一个立即支持Java 11的版本。 另外,在处理动态常量时,最新的Byte Buddy版本中仍然存在一些粗糙的地方。 因此,最好从master分支构建Byte Buddy或使用JitPack 。 要查找有关Byte Buddy的更多信息,请访问bytebuddy.net 。

翻译自: https://www.javacodegeeks.com/2018/08/hands-on-java-constantdynamic.html

java掌握

java掌握_掌握Java 11的Constantdynamic相关推荐

  1. java书籍_学习Java最好的10本书,从入门到精通

    在当代,学习Java等编程课程的主要方式是视频资源,如果你想学,在网上五分钟之内就可以找到一堆学习视频,瞬间将你的硬盘填满.但是这些课程质量良莠不齐,对于小白来说很难辨别好坏. 但是书籍不同,书籍都是 ...

  2. 易语言 java支持_开源Java客户端可以连接易语言服务器

    我们的服务端处理客户端的连接请求是同步进行的, 每次接收到来自客户端的连接请求后, 都要先跟当前的客户端通信完之后才能再处理下一个连接请求. 这在并发比较多的情况下会严重影响程序的性能, 为此,我们可 ...

  3. java编译_解析 Java 即时编译器原理。

    ↑ 点击上面 "时代Java"关注我们,关注新技术,学习新知识! 一.导读 常见的编译型语言如C++,通常会把代码直接编译成CPU所能理解的机器码来运行.而Java为了实现&quo ...

  4. java 示例_最佳Java示例

    java 示例 什么是Java? (What is Java?) Java is a programming language developed by Sun Microsystems in 199 ...

  5. mac java安全_关于 Java for Mac OS X 10.4 发行版 7 的安全性内容

    Java CVE-ID:CVE-2008-1185.CVE-2008-1186.CVE-2008-1187.CVE-2008-1188.CVE-2008-1189.CVE-2008-1190.CVE- ...

  6. 尚学堂java培训_送给 Java 自学者或者初学者的最全知识清单,2020 年 Java 就该这么学...

    最近逛知乎,发现有很多想自学 Java 或者 Java 初学者提问,不知道如何学习 Java?我接触 Java 快 8 年的时间了,一直从事 Java 开发工作,自己一直升级打怪,对于如何更好的学习 ...

  7. 21点 小游戏 java代码_基于Java的21点扑克游戏的实现

    在上次写的比较牌点的扑克游戏上Java扑克游戏(多人多牌数比较游戏)的实现中,添加21点游戏规则,实现21点牌类游戏.具体实现步骤如下:[需要源代码的留QQ,大家一起探讨探讨哈,谢谢啦!] 抽象出规则 ...

  8. java海报_使用java画一张海报

    PS: 没找到合适的海报背景,就随便找了一张,使用技术都是相同的 1. 添加依赖 这俩其实跟本章节的核心技术没有关系,是为了获取QQ昵称和QQ头像而引入的. org.jsoup jsoup 1.11. ...

  9. java头像_用java实现给你的头像) +n

    首先, 其实应该把标题改为--给任意图片右上角套上红色消息数目框; 代码如下: //主程序: package cn.sourcecodes.main; import cn.sourcecodes.ut ...

最新文章

  1. kali最新国内更新源sources
  2. 如何在DNN模块中插入一个图片--在模块中引用资源文件
  3. 南京理工大学计算机学院教授严捍,2019年7月1日学术报告二则(宋巍 教授,南京理工大学;张鹏程 副教授,河海大学)...
  4. linux故障排查书籍,Linux系统故障排查和修复技巧.docx
  5. 使用Javascript来创建一个响应式的超酷360度全景图片查看幻灯效果
  6. TypeError: cannot perform reduce with flexible type
  7. shell脚本修改文本中匹配行之前的行的方法
  8. 【简单排序算法】:简单选择排序、直接插入排序和冒泡排序
  9. linux搭建ddos发包机脚本_分享一个linux下自动封IP防御DDOS的脚本-网络教程与技术 -亦是美网络...
  10. 计算机与科学的论文,计算机与科学技术论文要求.doc
  11. Linux下的Scala安装
  12. 微信公共平台开发(一):服务器配置
  13. JS实现获取今天星期几
  14. 所谓的成长就是认知升级-成长就是应付自如
  15. 中国与印度软件工程师之比较
  16. 如何进行英文文献检索
  17. 使用国外著名大学数字图书馆资源方法
  18. 【图像融合】基于matlab主成分结合小波离散变换PCA-DWT图像融合【含Matlab源码 2199期】
  19. 5G+急诊救治 - 5G救护车远程会诊智慧医疗解决方案
  20. POJ 2924 Gauß in Elementary School(水~)

热门文章

  1. 【每日一题】7月7日题目精讲—最短路
  2. 小G的项链(Manacher)
  3. 【无码专区9】序列统计(带权并查集 + 前缀和建边 + dp)
  4. CF1192B Dynamic Diameter(LCT)
  5. YBTOJ:前缀匹配(AC自动机)
  6. ssl初一组周六模拟赛【2018.4.21】
  7. codeforces1435 D. Shurikens
  8. K8S Learning(8)—— Service
  9. 所有和Java中代理有关的知识点都在这了
  10. jsoup解析HTML用法小结