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