JVM 上的编译时引用参数
Compile-Time By-Reference Parameters on the JVM
目前正在 JVM 上开发自定义编程语言,我希望该语言支持方法中的引用参数。我该怎么做呢?到目前为止,我已经想出了三种不同的方法来实现这一点。
- 包装对象
这背后的想法是创建一个包含字段当前值的包装对象,传递给 by-ref 方法调用,然后在调用后取消装箱。这是一种相当直接的方法,但需要大量 'garbage' 创建并立即丢弃的对象。
- 数组
简单的创建一个1元素类型的数组,将字段值放入数组,调用传递数组的方法,最后从数组中赋值给字段。这样做的好处是它确保了运行时类型安全,而不是需要额外转换的通用包装器 class。
- 不安全
这个稍微高级一点:使用sun.misc.Unsafe
分配一些本机内存space,将字段值存储在该内存上,调用方法并传递地址,重新分配字段从本机内存地址,并再次释放它。
Bonus:是否可以使用 Unsafe
class 实现指针和指针运算?
Wrapper Objects
[...] but requires a lot of 'garbage' objects that are created and immediately discarded.
如果此类包装器的生命周期仅限于调用点(+ 内联被调用方),那么编译器可能能够通过逃逸分析证明这一点,并通过将包装器对象分解为其原始成员并使用它们来避免分配直接在生成的代码中。
这基本上要求那些引用包装器永远不会存储到字段中并且仅作为方法参数传递
Unsafe
Use sun.misc.Unsafe to allocate some native memory space, store the field value on that memory
您不能在本机内存中存储对象引用。垃圾收集器不会知道它,因此可以更改您脚下的内存地址或 GC 对象(如果这是您唯一的参考)。
但是由于您正在创建自己的语言,因此您可以简单地将字段引用脱糖为对象引用 + 偏移量。 IE。传递两个参数(对象引用 + 长偏移量)而不是一个。如果您知道偏移量,则可以使用 Unsafe 来操作该字段。
显然这只适用于对象字段。不能以这种方式更改本地引用。
Bonus: Is it possible to implement pointers and pointer arithmetic using the Unsafe class?
对于非托管内存是。
对于托管堆中的内存,您只能指向对象本身并进行相对于对象头的指针运算。
而且您始终必须将对象引用存储在 Object
类型的字段中。将它们存储在 long
中会导致 GC 实现(至少是精确的实现)缺少引用。
编辑:您可能还对 JDK 正在进行的关于 VarHandles 的工作感兴趣。
在开发您的语言时,您可能需要牢记这一点。
您似乎错过了有关传递引用概念的重要一点:每当对引用进行写入时,引用的变量将被更新。这不同于任何像您这样的概念,它实际上会在持有人中传递一个副本并根据方法 return.
更新原始变量
您甚至可以在单线程用例中注意到差异:
foo(myField, ()-> {
// if myField is pass-by-reference, whenever foo() modifies
// it and calls this Runnable, it should see the new value:
System.out.println(myField);
});
当然,您可以让两个引用都访问同一个包装器,但是对于允许(几乎)任意代码的环境,这意味着您必须替换 every 引用到字段(最后,更改字段的内容)到包装器。
所以如果你想在 JVM 中实现一个干净的、真正的按值传递机制,它必须能够修改引用的工件,即字段或数组槽。对于局部变量,没有办法做到这一点,所以一旦创建了对局部变量的引用,就无法用持有者对象替换局部变量。
所以选项的种类是已知的,你可以传递一个 java.lang.reflect.Field
(不适用于数组槽),一对 java.lang.invoke.MethodHandle
或任意类型的对象(生成的类型) 提供读写访问权限。
实现此引用访问器类型时,您可以求助于 Unsafe
来创建匿名 class,就像 Java 的 lambda 表达式功能一样。事实上,你可以窃取从lambda表达式机制中启发自己很多:
- 在必须创建引用的地方放置一条
invokedynamic
指令,指向您的工厂方法并提供字段或数组槽的句柄
- 让工厂分析句柄并动态创建访问器实现,主要区别在于您的类型将有两个 操作,读取和写入
- 使用
Unsafe
创建 class(它可能会访问该字段,即使它是 private
)
- 如果该字段是静态的,则创建一个实例和 return 一个
CallSite
带有句柄 returning 该实例
- 否则return一个
CallSite
带有指向访问器构造函数的句柄class接受对象实例或数组
这样你只会在第一次使用时有开销,而后续使用将在 static
字段的情况下使用单例,或者为实例字段和数组即时构建访问器插槽。如果像普通对象一样频繁使用,HotSpots 逃逸分析可以忽略这些访问器实例创建。
目前正在 JVM 上开发自定义编程语言,我希望该语言支持方法中的引用参数。我该怎么做呢?到目前为止,我已经想出了三种不同的方法来实现这一点。
- 包装对象
这背后的想法是创建一个包含字段当前值的包装对象,传递给 by-ref 方法调用,然后在调用后取消装箱。这是一种相当直接的方法,但需要大量 'garbage' 创建并立即丢弃的对象。
- 数组
简单的创建一个1元素类型的数组,将字段值放入数组,调用传递数组的方法,最后从数组中赋值给字段。这样做的好处是它确保了运行时类型安全,而不是需要额外转换的通用包装器 class。
- 不安全
这个稍微高级一点:使用sun.misc.Unsafe
分配一些本机内存space,将字段值存储在该内存上,调用方法并传递地址,重新分配字段从本机内存地址,并再次释放它。
Bonus:是否可以使用 Unsafe
class 实现指针和指针运算?
Wrapper Objects [...] but requires a lot of 'garbage' objects that are created and immediately discarded.
如果此类包装器的生命周期仅限于调用点(+ 内联被调用方),那么编译器可能能够通过逃逸分析证明这一点,并通过将包装器对象分解为其原始成员并使用它们来避免分配直接在生成的代码中。
这基本上要求那些引用包装器永远不会存储到字段中并且仅作为方法参数传递
Unsafe Use sun.misc.Unsafe to allocate some native memory space, store the field value on that memory
您不能在本机内存中存储对象引用。垃圾收集器不会知道它,因此可以更改您脚下的内存地址或 GC 对象(如果这是您唯一的参考)。
但是由于您正在创建自己的语言,因此您可以简单地将字段引用脱糖为对象引用 + 偏移量。 IE。传递两个参数(对象引用 + 长偏移量)而不是一个。如果您知道偏移量,则可以使用 Unsafe 来操作该字段。
显然这只适用于对象字段。不能以这种方式更改本地引用。
Bonus: Is it possible to implement pointers and pointer arithmetic using the Unsafe class?
对于非托管内存是。
对于托管堆中的内存,您只能指向对象本身并进行相对于对象头的指针运算。
而且您始终必须将对象引用存储在 Object
类型的字段中。将它们存储在 long
中会导致 GC 实现(至少是精确的实现)缺少引用。
编辑:您可能还对 JDK 正在进行的关于 VarHandles 的工作感兴趣。 在开发您的语言时,您可能需要牢记这一点。
您似乎错过了有关传递引用概念的重要一点:每当对引用进行写入时,引用的变量将被更新。这不同于任何像您这样的概念,它实际上会在持有人中传递一个副本并根据方法 return.
更新原始变量您甚至可以在单线程用例中注意到差异:
foo(myField, ()-> {
// if myField is pass-by-reference, whenever foo() modifies
// it and calls this Runnable, it should see the new value:
System.out.println(myField);
});
当然,您可以让两个引用都访问同一个包装器,但是对于允许(几乎)任意代码的环境,这意味着您必须替换 every 引用到字段(最后,更改字段的内容)到包装器。
所以如果你想在 JVM 中实现一个干净的、真正的按值传递机制,它必须能够修改引用的工件,即字段或数组槽。对于局部变量,没有办法做到这一点,所以一旦创建了对局部变量的引用,就无法用持有者对象替换局部变量。
所以选项的种类是已知的,你可以传递一个 java.lang.reflect.Field
(不适用于数组槽),一对 java.lang.invoke.MethodHandle
或任意类型的对象(生成的类型) 提供读写访问权限。
实现此引用访问器类型时,您可以求助于 Unsafe
来创建匿名 class,就像 Java 的 lambda 表达式功能一样。事实上,你可以窃取从lambda表达式机制中启发自己很多:
- 在必须创建引用的地方放置一条
invokedynamic
指令,指向您的工厂方法并提供字段或数组槽的句柄 - 让工厂分析句柄并动态创建访问器实现,主要区别在于您的类型将有两个 操作,读取和写入
- 使用
Unsafe
创建 class(它可能会访问该字段,即使它是private
) - 如果该字段是静态的,则创建一个实例和 return 一个
CallSite
带有句柄 returning 该实例 - 否则return一个
CallSite
带有指向访问器构造函数的句柄class接受对象实例或数组
这样你只会在第一次使用时有开销,而后续使用将在 static
字段的情况下使用单例,或者为实例字段和数组即时构建访问器插槽。如果像普通对象一样频繁使用,HotSpots 逃逸分析可以忽略这些访问器实例创建。