1. 前言
之前写过一篇java反射的文章《Java反射及性能》,但是感觉不是很完整,所以想把这块内容好好再梳理记录一下。
2. 简单的demo
- 本案例针对JDK1.8
常规的自定义反射使用,测试代码:
【TestRef.java】
public class TestRef {
public static void main(String[] args) {
try {
Class<?> clazz = Class.forName("com.allen.commons.entity.CommonTestEntity");
Object refTest = clazz.newInstance();
Method method = clazz.getMethod("defaultMethod");
//Method method1 = clazz.getDeclaredMethod("defaultMethod");
method.invoke(refTest);
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
---------------------------------------------------------------------------------------
【CommonTestEntity.java】
public class CommonTestEntity {
static {
System.out.println("CommonTestEntity执行类加载...");
}
public CommonTestEntity() {
System.out.println(this.getClass() + " | CommonTestEntity实例初始化 | " + this.getClass().getClassLoader());
}
public void defaultMethod() {
System.out.println("执行实例方法:defaultMethod");
}
}
3. jdk反射之Method获取
3.1. jdk反射大致的使用流程
- 创建class对象(类加载,使用当前方法所在类的ClassLoader来加载)
- 获取目标Method对象「反射调用的目标方法」(getMethod 和 getDeclaredMethod)
- 调用Method的invoke方法
3.2. getMethod 和 getDeclaredMethod区别
getMethod源码:
public Method getMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
Objects.requireNonNull(name);
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// 1. 检查方法权限
checkMemberAccess(sm, Member.PUBLIC, Reflection.getCallerClass(), true);
}
// 2. 获取方法
Method method = getMethod0(name, parameterTypes);
if (method == null) {
throw new NoSuchMethodException(methodToString(name, parameterTypes));
}
// 3. 返回方法
return method;
}
getDeclaredMethod源码:
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
Objects.requireNonNull(name);
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// 1. 检查方法是权限
checkMemberAccess(sm, Member.DECLARED, Reflection.getCallerClass(), true);
}
// 2. 获取方法
Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes);
if (method == null) {
throw new NoSuchMethodException(methodToString(name, parameterTypes));
}
// 3. 返回方法
return method;
}
- 获取方法的流程总结为三步走:
a.检查方法权限
b.获取方法 Method 对象
c.返回方法
3.2.1. 主要的两个区别
- getMethod 中 checkMemberAccess 传入的是 Member.PUBLIC,而 getDeclaredMethod 传入的是 Member.DECLARED 。代码中的注释:
注释里解释了 PUBLIC 和 DECLARED 的不同,PUBLIC 会包括所有的 public 方法,包括父类的方法,而 DECLARED 会包括所有自己定义的方法,public,protected,private 都在此,但是不包括父类的方法。
getMethod 中获取方法调用的是
getMethod0
,而getDeclaredMethod
获取方法调用的是privateGetDeclaredMethods
。privateGetDeclaredMethods 是获取类自身定义的方法,参数是 boolean publicOnly,表示是否只获取公共方法。其中getDeclaredMethod
源码如下【publicOnly为false】:getDeclaredMethod
其实是从privateGetDeclaredMethods
返回的方法列表里复制一个Method对象返回。复制的过程是在searchMethods里实现的,源码如下:
privateGetDeclaredMethods
源码如下:// Returns an array of "root" methods. These Method objects must NOT // be propagated to the outside world, but must instead be copied // via ReflectionFactory.copyMethod. private Method[] privateGetDeclaredMethods(boolean publicOnly) { checkInitted(); Method[] res; // 默认先从缓存获取 ReflectionData<T> rd = reflectionData(); if (rd != null) { // 走rd.declaredMethods分支 res = publicOnly ? rd.declaredPublicMethods : rd.declaredMethods; if (res != null) return res; } // No cached value available; request value from VM res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly)); if (rd != null) { if (publicOnly) { rd.declaredPublicMethods = res; } else { rd.declaredMethods = res; } } return res; }
① 首先通过
relectionData
缓存获取
② 如果缓存没有命中的话,通过getDeclaredMethods0
通过native方法从VM中获取方法
从上面的源码我们可以看到,在获取反射方法的时候Class对象会有一个叫ReflectionData
的缓存,关于这个缓存也是颇有些东西在里面,下面会有单独一小节详细介绍。
getMethod0
源码如下:private Method getMethod0(String name, Class<?>[] parameterTypes, boolean includeStaticMethods) { MethodArray interfaceCandidates = new MethodArray(2); Method res = privateGetMethodRecursive(name, parameterTypes, includeStaticMethods, interfaceCandidates); if (res != null) return res; // Not found on class or superclass directly interfaceCandidates.removeLessSpecifics(); return interfaceCandidates.getFirst(); // may be null }
其中
privateGetMethodRecursive
方法中也会调用到privateGetDeclaredMethods
方法和searchMethods
方法。
3.3. getMethod及getDeclaredMethod方法调用流程图
3.4. reflectionData
是Class中的一个缓存类,主要缓存的是每次从jvm里获取到的一些类属性,比如方法,字段等。
privateGetDeclaredMethods
每次在获取Method对象的时候,首先会查询reflectionData
缓存,如果有就直接返回,没有就从VM中查询目标Method,并缓存至reflectionData
。
从源码中可以知道,Class中的这个属性是SoftReference的,也就是在内存比较紧张的情况下可能会被回收。JVM可以通过-XX:SoftRefLRUPolicyMSPerMB
这个参数来控制回收的时机,一旦回收条件达成,只要触发GC,就会回收SoftReference引用对象。
如果reflectionData
回收了,之后调用privateGetDeclaredMethods
就会重新创建一个ReflectionData
对象,当然也需要重新从VM中获取Method相关数据,reflectionData
关联的Method,Field字段等都会重新生成新的对象。
3.4. method获取的性能问题
常规的反射调用到这边已经走过了一半的流程,讲到这边,有必要先进行一下小结。因为在这上半部分的流程中,就涉及到常规jdk反射的性能问题。
在常规的反射调用中,目标method的获取是十分消耗性能的,原因是:
- 首先需要checkAccess,检查方法可见性;
- 需要遍历方法并校验参数,主要涉及的方法为
searhMethods
;【同时relectionData
缓存失效后需要调用native方法从VM中获取方法并重新添加至缓存】 searhMethods
每次都会copy一个Method的备份然后返回;
针对反射上半部分流程的优化
- 缓存需要反射调用的目标方法;
- 缓存class对象,不要频繁的进行类加载操作;「性能极差」
坊间传闻,缓存目标Method对象,反射耗时大约是直接调用的25倍。如果不做缓存,反射耗时大约是直接调用的60倍。本人没有实际测试过😥。
4. jdk反射之invoke方法
先看invoke源码:
class Method {
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
Class<?> caller = Reflection.getCallerClass();
// 1. 检查权限
checkAccess(caller, clazz,
Modifier.isStatic(modifiers) ? null : obj.getClass(),
modifiers);
}
// 2. 获取 MethodAccessor
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
// 创建 MethodAccessor
ma = acquireMethodAccessor();
}
// 3. 调用 MethodAccessor.invoke
return ma.invoke(obj, args);
}
}
4.1. Method.invoke
Method部分源码:
public final class Method extends Executable {
private Class<?> clazz;
private String name;
private volatile MethodAccessor methodAccessor;
private Method root;
@CallerSensitive
public Object invoke(Object obj, Object... args)
}
4.1.1 root
每个实际的Java方法只有一个对应的Method对象作为root(实质上就是Method类的一个成员变量),root指向reflectionData
里缓存的某个方法。每次在通过反射获取Method对象时新创建Method对象把root封装起来,其实就是前文讲getDeclaredMethod
提到的方法copy操作,copy的具体源码如下:
4.1.2 MethodAccessor
Method.invoke()
实际上并不是自己实现的反射调用逻辑,而是委托给sun.reflect.MethodAccessor
来处理。
在第一次调用invoke()方法的时候,实现调用逻辑的MethodAccessor对象才会新建并更新给root,然后调用MethodAccessor.invoke()真正完成反射调用。
- 第一次调用
Method.invoke()
的时候会检查并获取MethodAccessor
,
- 获取的
MethodAccessor
其实都是从root Method获取的,如果root中的MethodAccessor
为null,则进行新建。
MethodAccessor
只是单方法接口,其invoke()方法与Method.invoke()
的对应。
public interface MethodAccessor {
Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException;
}
MethodAccessor
主要有三种实现
- DelegatingMethodAccessorImpl
- NativeMethodAccessorImpl
- GeneratedMethodAccessorXXX
其中DelegatingMethodAccessorImpl
是最终注入给Method的methodAccessor的,也就是Method所有的invoke方法都会调用这个DelegatingMethodAccessorImpl.invoke,正如其名,这个类是做代理的,其真正的实现是下面的两种:NativeMethodAccessorImpl
和GeneratedMethodAccessorXXX
。
创建MethodAccessor
实例的是ReflectionFactory
,源码如下:
NativeMethodAccessorImpl
是native code实现的,GeneratedMethodAccessorXXX
是java版本实现的,需要为反射调用的Method动态生成新类,XXX是连续递增的数字。
所有的方法反射都先调用NativeMethodAccessorImpl
,NativeMethodAccessorImpl
调用15次之后会生成一个GeneratedMethodAccessorXXX
类,转而使用java版本的invoke。这块逻辑可以看NativeMethodAccessorImpl
源码:
上图中new MethodAccessorGenerator().generateMethod
就是动态创建字节码并进行实例化的逻辑。
第一次创建GeneratedMethodAccessorXXX
类并实例化的时候,因为涉及类加载操作,【这块类加载的逻辑是:每个GeneratedMethodAccessorXXX
类都会实例化一个DelegatingClassLoader
类加载器进行类加载(方便GeneratedMethodAccessorXXX
类卸载)】,性能会有所影响;
- 动态生成的新类如下:
package sun.reflect;
// 这是反射目标方法所在的类
import com.allen.entities.CommonTestEntity;
import java.lang.reflect.InvocationTargetException;
import sun.reflect.MethodAccessorImpl;
public class GeneratedMethodAccessor1
extends MethodAccessorImpl {
/*
* Loose catch block
*/
public Object invoke(Object object, Object[] objectArray) throws InvocationTargetException {
CommonTestEntity commonTestEntity;
block5:
{
if (object == null) {
throw new NullPointerException();
}
commonTestEntity = (CommonTestEntity) object;
if (objectArray == null || objectArray.length == 0) break block5;
throw new IllegalArgumentException();
}
try {
// 执行真正的目标方法
commonTestEntity.defaultMethod();
return null;
} catch (Throwable throwable) {
throw new InvocationTargetException(throwable);
} catch (ClassCastException | NullPointerException runtimeException) {
throw new IllegalArgumentException(super.toString());
}
}
}
而15次的阈值在ReflectionFactory
中定义了
- invoke 超过15次调用后创建
GeneratedMethodAccessorXXX
的行为也叫反射膨胀,这个膨胀阈值可以自定义设置也可直接关闭膨胀功能。通过-Dsun.reflect.noInflation=true
和-Dsun.reflect.inflationThreshold=0
可以直接关闭膨胀功能,-Dsun.reflect.inflationThreshold
配置也能自定义配置膨胀阈值。-Dsun.reflect.inflationThreshold
最大值可以配置为Integer.MAX,通过增大该阈值或者直接关闭膨胀功能,可以解决频繁创建GeneratedMethodAccessorXXX
字节码和元空间内存问题。相关案例可见:《Potential native memory use in reflection delegating classloaders》
Java 版本的 MethodAccessorImpl 调用效率比 Native 版本要快 20 倍以上【纳秒级别,无法感知】,但是 Java 版本加载时要比 Native 多消耗 3-4 倍资源,所以默认会调用 Native 版本,如果调用次数超过 15 次以后,就会选择运行效率更高的 Java 版本。
4.2. invoke的性能问题
4.2.1. JIT 无法优化
Oracle官方文档提到:
4.2.2. Method#invoke方法会对参数做封装和解封操作
invoke 方法的参数是 Object[] 类型,也就是说,如果方法参数是简单类型(8中基本数据类型)的话,需要在此转化成 Object 类型,例如 long ,在 javac compile 的时候 用了Long.valueOf() 转型,也就大量了生成了Long 的 Object, 同时 传入的参数是Object[]数值,那还需要额外封装object数组。
而在上面 MethodAccessorGenerator#emitInvoke 方法里我们看到,生成的字节码时,会把参数数组拆解开来,把参数恢复到没有被 Object[] 包装前的样子,同时还要对参数做校验,这里就涉及到了解封操作。
4.2.3. 增加GC压力
- 封装和解封,产生了额外的不必要的内存浪费
- 频繁的反射调用可能生成大量的
GeneratedMethodAccessorXXX
类并创建很多DelegatingClassLoader
类加载器,占用堆内存的同时也会增加元空间的占用【同时引起元空间的碎片化】,引发更多的GC行为。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。