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