使用Java在运行时省略实例字段

时间:2018-09-24 21:09:36

标签: java jvm metaprogramming jvm-hotspot

Java的断言机制允许禁用放置断言的情况,该断言基本上没有运行时成本(除了较大的类文件之外)。但这可能涵盖所有情况。

例如,许多Java集合都具有“快速失败”迭代器,这些迭代器试图检测您何时以线程不安全的方式使用它们。但这要求集合和迭代器本身都必须维护额外的状态,如果不存在这些检查,则不需要这些状态。

假设有人想做类似的事情,但是允许禁用检查,如果禁用了检查,它将在迭代器中保存一些字节,并在ArrayList中保存更多的字节,或者其他。

或者,假设我们正在进行某种对象池化,希望能够在运行时打开和关闭它。关闭时,它应该仅使用Java的垃圾回收并且不给引用计数腾出空间,例如这样(请注意,编写的代码非常破损):

class MyClass {
    static final boolean useRefCounts = my.global.Utils.useRefCounts();
    static {
        if(useRefCounts)
            int refCount; // want instance field, not local variable
    }
    void incrementRefCount(){
        if(useRefCounts) refCount++; // only use field if it exists;
    }
    /**return true if ready to be collected and reused*/
    boolean decrementAndTestRefCount(){
        // rely on Java's garbage collector if ref counting is disabled.
        return useRefCounts && --refCount == 0;
    }
}

上面的代码的麻烦在于,静态bock毫无意义。但是,是否有一些技巧可以使用低功率魔术来使沿这些路线的东西起作用? (如果允许使用高功率魔术,则核选项是生成MyClass的两个版本,并安排在开始时将正确的版本放在类路径上。)

2 个答案:

答案 0 :(得分:1)

注意:您可能根本不需要这样做。 JIT非常擅长内联运行时已知的常量,尤其是boolean并优化了未使用的代码。

int字段不是理想的,但是,如果您使用的是64位JVM,则对象大小可能不会更改。

在OpenJDK / Oracle JVM(64位)上,标头默认为12个字节。对象对齐为8个字节,因此对象将使用16个字节。该字段添加4个字节,对齐后也为16个字节。


要回答这个问题,您需要两个类(除非您使用生成的代码或黑客)

class MyClass {
    static final boolean useRefCounts = my.global.Utils.useRefCounts();

    public static MyClass create() {
        return useRefCounts ? new MyClassPlus() : new MyClass();
    }

    void incrementRefCount() {
    }

    boolean decrementAndTestRefCount() {
        return false;
    }
}

class MyClassPlus extends MyClass {
    int refCount; // want instance field, not local variable

    void incrementRefCount() {
        refCount++; // only use field if it exists;
    }

    boolean decrementAndTestRefCount() {
        return --refCount == 0;
    }
}

答案 1 :(得分:1)

如果您在使用引用计数的情况下接受的开销稍高,则可以使用外部存储,即

class MyClass {
    static final WeakHashMap<MyClass,Integer> REF_COUNTS
        = my.global.Utils.useRefCounts()? new WeakHashMap<>(): null;

    void incrementRefCount() {
        if(REF_COUNTS != null) REF_COUNTS.merge(this, 1, Integer::sum);
    }
    /**return true if ready to be collected and reused*/
    boolean decrementAndTestRefCount() {
        return REF_COUNTS != null
            && REF_COUNTS.compute(this, (me, i) -> --i == 0? null: i) == null;
    }
}

如果某人多次调用decrementAndTestRefCount()而不是incrementRefCount(),则行为上会有差异。当您的原始代码静默运行到负引用计数时,此代码将引发NullPointerException。在这种情况下,我更喜欢失败并例外...

如果您不使用该功能,上面的代码将使您仅需static个字段即可。大多数JVM消除关于static final变量状态的条件都应该没有问题。

请进一步注意,该代码允许MyClass实例在具有非零引用计数的同时获取垃圾,就像它是一个实例字段一样,但是当计数达到初始状态时,它也会主动删除映射再次为零,以最小化清理所需的工作。