是否可以在现有实例上调用构造函数?

Ole*_*cov 3 java reflection jvm unsafe

众所周知,使用sun.misc.Unsafe#allocateInstance它可以创建一个对象,而无需调用任何类构造函数。

是否可以做相反的事情:给定一个现有实例,调用它的构造函数?


澄清:这不是关于我在生产代码中要做的事情的问题。我对 JVM 内部结构和仍然可以完成的疯狂事情感到好奇。欢迎特定于某些 JVM 版本的答案。

apa*_*gin 6

JVMS \xc2\xa72.9 forbids invocation of constructor on already initialized objects:

\n\n
\n

Instance initialization methods may be invoked only within the Java\n Virtual Machine by the invokespecial instruction, and\n they may be invoked only on uninitialized class instances.

\n
\n\n

However, it is still technically possible to invoke constructor on initialized object with JNI. CallVoidMethod function does not make difference between <init> and ordinary Java methods. Moreover, JNI specification hints that CallVoidMethod may be used to call a constructor, though it does not say whether an instance has to be initialized or not:

\n\n
\n

When these functions are used to call private methods and constructors, the method ID must be derived from the real class of obj, not from one of its superclasses.

\n
\n\n

我已经验证以下代码可以在 JDK 8 和 JDK 9 中运行。JNI 允许您执行不安全的操作,但您不应在生产应用程序中依赖它。

\n\n

构造函数调用器.java

\n\n
public class ConstructorInvoker {\n\n    static {\n        System.loadLibrary("constructorInvoker");\n    }\n\n    public static native void invoke(Object instance);\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

构造函数Invoker.c

\n\n
#include <jni.h>\n\nJNIEXPORT void JNICALL\nJava_ConstructorInvoker_invoke(JNIEnv* env, jclass self, jobject instance) {\n    jclass cls = (*env)->GetObjectClass(env, instance);\n    jmethodID constructor = (*env)->GetMethodID(env, cls, "<init>", "()V");\n    (*env)->CallVoidMethod(env, instance, constructor);\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

测试对象.java

\n\n
public class TestObject {\n    int x;\n\n    public TestObject() {\n        System.out.println("Constructor called");\n        x++;\n    }\n\n    public static void main(String[] args) {\n        TestObject obj = new TestObject();\n        System.out.println("x = " + obj.x);  // x = 1\n\n        ConstructorInvoker.invoke(obj);\n        System.out.println("x = " + obj.x);  // x = 2\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

  • @Holger 好问题!默认情况下,HotSpot JVM 在“Object.&lt;init&gt;”末尾注册终结器。也就是说,对于同一个对象,可以调用两次“finalize()”。使用“-XX:-RegisterFinalizersAtInit”标志,终结器会在对象分配后立即注册,因此“finalize()”将仅被调用一次。 (3认同)
  • 现在真正有趣的问题是……如果 `TestObject` 有一个重要的 `finalize()` 方法会发生什么? (2认同)

小智 5

似乎通过一些(非常可疑的)技巧,即使不通过自定义本机库,通过(ab)使用方法句柄,这是可能的。

该方法本质上是欺骗 JVM 认为它当前正在调用常规方法而不是构造函数。

我只需要添加一个强制性的“这可能不是一个好主意”,但这是我找到的唯一方法。我也无法证明它在不同 JVM 上的表现如何。

先决条件

为此,sun.misc.Unsafe需要一个 的实例。我不会在这里详细介绍如何获取它,因为您似乎已经有了它,但本指南解释了该过程。

第一步:获得值得信赖的MethodHandles.Lookup

接下来,java.lang.invoke.MethodHandles$Lookup需要 a 来获取构造函数的实际方法句柄。

这个类有一个权限系统,通过allowedModes中的属性来工作Lookup,该属性设置为一堆标志。有一个特殊的TRUSTED标志可以绕过所有权限检查。

不幸的是,该allowedModes字段已从反射中过滤掉,因此我们不能通过反射设置该值来简单地绕过权限。

尽管也可以绕过反射过滤器,但有一种更简单的方法:Lookup包含一个静态字段IMPL_LOOKUP,其中包含Lookup具有这些TRUSTED权限的 a 。我们可以通过使用反射来获取这个实例Unsafe

var field = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
var fieldOffset = unsafe.staticFieldOffset(field);

var lookup = (MethodHandles.Lookup) unsafe.getObject(MethodHandles.Lookup.class, fieldOffset);
Run Code Online (Sandbox Code Playgroud)

我们Unsafe在这里使用而不是setAccessibleand get,因为通过反射会导致较新的 java 版本中的模块系统出现问题。

第二步:寻找构造函数

现在我们可以获得我们MethodHandle想要调用的构造函数。我们使用Lookup刚刚获得的 a 来完成此操作,就像Lookup通常使用的 a 一样。

var type = MethodType.methodType(Void.TYPE, <your constructor argument types>);
var constructor = lookup.findConstructor(<your class>, type);
Run Code Online (Sandbox Code Playgroud)

第 3 步:获取MemberName

虽然 的签名findConstructor仅指定它返回 a MethodHandle,但它实际上返回 a java.lang.invoke.DirectMethodHandle$Constructor。该类型声明一个initMethod字段,其中包含java.lang.invoke.MemberName对我们的构造函数的引用。该MemberName类型无法从外部访问,因此与其进行的所有交互都通过Unsafe.

我们可以MemberName用同样的方式获得它Lookup

var constructorClass = Class.forName("java.lang.invoke.DirectMethodHandle$Constructor");
val initMethodField = constructorClass.getDeclaredField("initMethod");
val initMethodFieldOffset = unsafe.objectFieldOffset(initMethodField);

var initMemberName = unsafe.getObject(constructor, initMethodFieldOffset)
Run Code Online (Sandbox Code Playgroud)

第四步:欺骗Java

下一步是重要的部分。虽然 JVM 没有物理障碍阻止您像任何其他方法一样调用构造函数,但MethodHandle有一些检查可以确保您没有做一些可疑的事情。

大多数检查都可以通过使用 来规避TRUSTED Lookup,并且还剩下最后一项检查:

MemberName实例包含一堆标志,除其他外,这些标志告诉系统MemberName所指的是哪种成员。检查这些标志。

为了避免这种情况,我们可以简单地使用以下命令更改标志Unsafe

var constructorClass = Class.forName("java.lang.invoke.DirectMethodHandle$Constructor");
val initMethodField = constructorClass.getDeclaredField("initMethod");
val initMethodFieldOffset = unsafe.objectFieldOffset(initMethodField);

var initMemberName = unsafe.getObject(constructor, initMethodFieldOffset)
Run Code Online (Sandbox Code Playgroud)

标志的值来自java.lang.invoke.MethodHandleNatives.Constants#MN_IS_METHODjava.lang.invoke.MethodHandleNatives.Constants#MN_IS_CONSTRUCTOR

步骤5:获取REF_invokeVirtual方法句柄

现在我们有了一个完全合法的方法,它根本不是构造函数,我们只需要获取一个常规方法句柄来调用它。幸运的是,MethodHandles.Lookup.class有一个私有方法可以将 a 转换MemberName为 a(Direct)MethodHandle来进行各种调用:getDirectMethod

讽刺的是,我们实际上使用我们的全能查找来调用这个方法。

首先,我们获得MethodHandlefor getDirectMethod

var getDirectMethodMethodHandle = lookup.findVirtual(
        MethodHandles.Lookup.class,
        "getDirectMethod",
        MethodType.methodType(
                MethodHandle.class,
                byte.class,
                Class.class,
                memberNameClass,
                MethodHandles.Lookup.class
        )
);
Run Code Online (Sandbox Code Playgroud)

我们现在可以在查找中使用它,以获得MethodHandle我们的MemberName

var memberNameClass = Class.forName("java.lang.invoke.MemberName");
var flagsField = memberNameClass.getDeclaredField("flags");
var flagsFieldOffset = unsafe.objectFieldOffset(flagsField);
var flags = unsafe.getInt(initMemberName, flagsFieldOffset);

flags &= ~0x00020000; // remove "is constructor"
flags |= 0x00010000; // add "is (non-constructor) method"

unsafe.putInt(initMemberName, flagsFieldOffset, flags);
Run Code Online (Sandbox Code Playgroud)

(byte) 5参数代表“调用虚拟”,来自java.lang.invoke.MethodHandleNatives.Constants#REF_invokeVirtual.

第六步:利润?

我们现在可以handle像常规一样使用它MethodHandle来调用该类的任何现有实例的构造函数:

var getDirectMethodMethodHandle = lookup.findVirtual(
        MethodHandles.Lookup.class,
        "getDirectMethod",
        MethodType.methodType(
                MethodHandle.class,
                byte.class,
                Class.class,
                memberNameClass,
                MethodHandles.Lookup.class
        )
);
Run Code Online (Sandbox Code Playgroud)

有了这个handle,构造函数也可以被多次调用,并且实例实际上不必来自Unsafe#allocateInstance- 仅通过使用创建的实例也可以new