IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    优化 Java 反射字段访问

    xiezhenye发表于 2020-09-09 11:29:04
    love 0

    最近负责的一个 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 绕过一些检查,可以大幅减少反射访问字段消耗的时间。



沪ICP备19023445号-2号
友情链接