目前正在开发JVM上的自定义编程语言,我希望该语言支持方法中的by-reference参数。我该怎么做呢?到目前为止,我能够提出三种不同的方法来实现这一目标。
这背后的想法是创建一个包装器对象,该对象创建时包含字段的当前值,传递给by-ref方法调用,然后在调用后取消装箱。这是一种相当直接的方法,但需要大量的垃圾。创建并立即丢弃的对象。
只需创建一个包含1个元素的类型数组,将字段值放入数组中,调用传递数组的方法,最后从数组中分配字段。关于这一点的好处是它确保运行时类型安全,而不是通用的包装类,这需要额外的强制转换。
这个稍高级:使用sun.misc.Unsafe
分配一些本机内存空间,将字段值存储在该内存中,调用方法并传递地址,从本机内存地址重新分配字段,并再次释放它。
Bonus :是否可以使用Unsafe
类实现指针和指针算法?
答案 0 :(得分:4)
包装对象 [...]但需要大量的垃圾'创建并立即丢弃的对象。
如果这样的包装器的生命周期限于一个callsite(+ inlined callee),那么编译器可能能够通过转义分析来证明这一点,并通过将包装器对象分解为其原始成员并直接在其中使用它们来避免分配生成的代码。
这基本上要求那些引用包装器永远不会存储到字段中,只能作为方法参数传递
不安全 使用sun.misc.Unsafe分配一些本机内存空间,将字段值存储在该内存
上
您无法在本机内存中存储对象引用。垃圾收集器不会知道它,因此可能会改变你脚下的内存地址或GC对象,如果这是你唯一的参考。
但是,既然您正在创建自己的语言,那么您可以简单地将字段引用转移到对象引用+偏移量中。即传递两个参数(对象ref +长偏移)而不是一个。如果您知道偏移量,则可以使用“不安全”来操纵该字段。
显然这只适用于对象字段。本地参考不能以这种方式改变。
Bonus:是否可以使用Unsafe类实现指针和指针算法?
是的非托管内存。
对于托管堆中的内存,只允许指向对象本身并相对于对象头执行指针运算
并且您始终必须在Object
- 类型字段中存储对象引用。将它们存储在long
中会导致GC实现(至少是精确的)缺少参考。
编辑:您可能也对JDK中有关VarHandles的正在进行的工作感兴趣。 在开发您的语言时,您可能需要牢记这一点。
答案 1 :(得分:3)
似乎你错过了关于传递引用概念的重要观点:每当写入引用时,引用的变量将被更新。这与您的任何概念不同,它实际上会在持有者中传递副本,并在方法返回时更新原始变量。
即使在单线程用例中,您也可以注意到差异:
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);
});
当然,您可以使两个引用访问相同的包装器,但对于允许(几乎)任意代码的环境,这意味着您必须替换对该字段的每个引用(在最后,将字段的内容更改为包装器。
因此,如果要在JVM中实现干净,真实的按值传递机制,它必须能够修改引用的工件,即字段或数组插槽。对于局部变量,没有办法做到这一点,因此一旦创建了对它的引用,就无法用持有者对象替换局部变量。
所以选项的类型已经知道了,您可以传递java.lang.reflect.Field
(不适用于数组插槽),一对java.lang.invoke.MethodHandle
或任意类型的对象(生成类型)提供读写访问权。
实现此引用访问器类型时,您可以求助于Unsafe
来创建一个匿名类,就像Java的lambda表达式一样。事实上,你可以窃取从lambda表达机制中激励自己:
invokedynamic
指令,指向工厂方法并提供字段或数组插槽的句柄Unsafe
创建该类(可能会访问该字段,即使其private
)CallSite
,其句柄返回该实例CallSite
,其句柄指向接受对象实例或数组的访问者类的构造函数这样,您在第一次使用时只会有开销,而后续使用将在static
字段的情况下使用单例,或者在运行中构建一个访问器,例如字段和数组插槽。如果频繁使用,就像普通对象一样,HotSpots转义分析可以省略这些存取器实例的创建。