优化 Java 反射字段访问

最近负责的一个 Java 服务经常报 CPU 占用高的告警。于是乘着一次 CPU 高的时候,去用 JFR profile 了一下。(使用 JFR 对 Java 应用 profile 可以参考这篇文章:Java 应用在线性能分析和火焰图)。把数据拖回来用 JMC 打开一看,发现热点都在反射上:

reflect_jfr

图里排在最前面的都是反射相关的函数,而且实际都是同一个地方引入的。那里为了读取一个私有字段,使用了类似下面的代码:

public class SomeSingleton {
    private Field field;
    public SomeSingleton() {
        field = someObject.getDeclaredField("fieldName");
        field.setAccessible(true);
    }

    private void someMethod() {
        Object value = field.get(someObject);
    }
}

这里已经 SomeSingleton 是个单例对象,field 只会反射一次,而且 setAccessible(true) 后已经避免了额外的访问权限检查。profile 出来的性能问题集中在 field.get(someObject) 上。

于是去看了下这块 Java 反射的源代码,发现一个有意思的地方。

Field.get 会创建并缓存一个 FieldAccessor 对象,通过这个对象访问实际的字段。而这个 FieldAccessor 对象会由于对象的修饰符不同而创建不同的实例(UnsafeFieldAccessorFactory.java)代码大致如下:

class UnsafeFieldAccessorFactory {
    static FieldAccessor newFieldAccessor(Field field, boolean override) {
        Class type = field.getType();
        boolean isStatic = Modifier.isStatic(field.getModifiers());
        boolean isFinal = Modifier.isFinal(field.getModifiers());
        boolean isVolatile = Modifier.isVolatile(field.getModifiers());
        boolean isQualified = isFinal || isVolatile;
        boolean isReadOnly = isFinal && (isStatic || !override);
        if (isStatic) {
            ...
            if (!isQualified) {
                ...
                } else {
                    return new UnsafeStaticObjectFieldAccessorImpl(field);
                }
            } else {
                ...
                } else {
                    return new UnsafeQualifiedStaticObjectFieldAccessorImpl(field, isReadOnly);
                }
            }
        } else {
            if (!isQualified) {
                ...
                } else {
                    return new UnsafeObjectFieldAccessorImpl(field);
                }
            } else {
                ...
                } else {
                    return new UnsafeQualifiedObjectFieldAccessorImpl(field, isReadOnly);
                }
            }
        }
    }
}

这里有四种情况的组合,有/没有 Static,有/没有 Qualified。

有 Qualified 的实现,对应 volatile 或 final 的字段,会忽略线程本地 cache。没有 Static 的那组,即访问的是实例对象,会有一次额外的检查对象实例是否匹配。即前面截图中的 ensureObject。(参见:UnsafeObjectFieldAccessorImpl.java)

我们访问的是一个实例对象,所以每次访问字段都经过了这一次实际并不需要的检查。为了优化这个操作,不得不用上了 unsafe 大法,写了个加速反射字段访问的工具类。

...
import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class UnsafeFieldAccessor {
    private static final Unsafe unsafe;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    private final long fieldOffset;

    public UnsafeFieldAccessor(Field field) {
        fieldOffset = unsafe.objectFieldOffset(field);
    }

    public Object getObject(Object object) {
        return unsafe.getObject(object, fieldOffset);
    }

    public void setObject(Object object, Object value) {
        unsafe.putObject(object, fieldOffset, value);
    }

    public int getInt(Object object) {
        return unsafe.getInt(object, fieldOffset);
    }

    public void setInt(Object object, int value) {
        unsafe.putInt(object, fieldOffset, value);
    }
}

这里是 Java8 的写法。Java9 及以上版本 Unsafe 的使用需要做一定调整。这里有 Object 和 int 类型的字段访问,其他基本类型,如 long, double ,或者需要触发清掉 cache 也可以依样画葫芦。

下面对比一下性能。在我自己的机器上,各自执行 10000000 次,通过反射和 UnsafeFieldAccessor 的时间。

测试代码:

public class UnsafeFieldAccessorTest {
    static class T {
        private Object obj;
    }

    public void benchmark() throws Exception {
        Field field = T.class.getDeclaredField("obj");
        field.setAccessible(true);
        UnsafeFieldAccessor accessor = new UnsafeFieldAccessor(field);
        T t = new T();
        Object v = new Object();
        int n = 10000000;
        long start;
        start = System.nanoTime();
        for (int i = 0; i < n; i++) {
            accessor.getObject(t);
        }
        System.out.printf("unsafe get: %d\n", System.nanoTime() - start);
        start = System.nanoTime();
        for (int i = 0; i < n; i++) {
            field.get(t);
        }
        System.out.printf("reflect get: %d\n", System.nanoTime() - start);
        start = System.nanoTime();
        for (int i = 0; i < n; i++) {
            accessor.setObject(t, v);
        }
        System.out.printf("unsafe set: %d\n", System.nanoTime() - start);
        start = System.nanoTime();
        for (int i = 0; i < n; i++) {
            field.set(t, v);
        }
        System.out.printf("reflect set: %d\n", System.nanoTime() - start);
    }
}

结果如下,单位是纳秒:

unsafe get: 6859493
reflect get: 22821121
unsafe set: 10714617
reflect set: 53694089

在能确保赋值的类型安全的前提下,使用 unsafe 绕过一些检查,可以大幅减少反射访问字段消耗的时间。

文章来源:

Author:xiezhenye
link:https://xiezhenye.com/2020/09/09/优化-java-反射字段访问/