作者声明:创作过程中使用了AI辅助
平时写业务或者造框架的时候,反射肯定少不了。依赖注入、ORM、序列化这些底层全靠它撑着。但大家一直都在吐槽“反射慢”,它到底能有多慢?有救吗?
最近专门花时间跑了个微基准测试,把老熟人 Method.invoke 和稍微新一点的 MethodHandle 拉出来溜了溜。为了摸清底细,我不仅测了“直接调”和“加缓存”的性能差距,还把反射里的各个前置操作(比如查字段、查方法、读写属性)全拆开测了一遍。
测试场景
先简单交代下测试场景:
我搞了个很普通的 Target 类,里面有公共字段 value、私有字段 hidden 和一个普通的 add 方法。
测试参照组是我们平时手写的直接调用(比如直接 target.add(a,b) )。
而反射组除了测最终的执行(Call/Read/Write),还把寻找元数据的过程(比如 Class.forName,或者 getMethod)也单独拿出来算了开销。
测试数据
咱们直接上最原始的跑分数据。这里分为四种情况,大家可以重点对比“反射平均耗时”和“直接平均耗时”这两列。
1. Method.invoke(非缓存)
这种情况就是平时最容易踩坑的写法,每次操作都重新去拿元数据。
| 测试项目 | 反射调用次数 | 反射平均耗时 | 反射总耗时 | 直接调用次数 | 直接平均耗时 | 直接总耗时 |
|---|---|---|---|---|---|---|
| Get Type | 100000 | 153ns | 15.36ms | 100000 | 3ns | 0.302ms |
| Get Field | 100000 | 31ns | 3.13ms | 100000 | 10ns | 1.02ms |
| Get Property | 100000 | 720ns | 72.06ms | 100000 | 8ns | 0.837ms |
| Get Method | 100000 | 36ns | 3.61ms | 100000 | 7ns | 0.724ms |
| Read Field | 100000 | 10ns | 1.04ms | 100000 | 1ns | 0.181ms |
| Read Property | 100000 | 644ns | 64.47ms | 100000 | 1ns | 0.187ms |
| Write Field | 100000 | 9ns | 0.986ms | 100000 | 1ns | 0.176ms |
| Write Property | 100000 | 697ns | 69.74ms | 100000 | 1ns | 0.183ms |
| Call Method | 100000 | 34ns | 3.42ms | 100000 | 2ns | 0.202ms |
2. Method.invoke(缓存)
只要把 Method 或者 Field 提前提出来,你看看底下读写和调用的耗时降了多少。
| 测试项目 | 反射调用次数 | 反射平均耗时 | 反射总耗时 | 直接调用次数 | 直接平均耗时 | 直接总耗时 |
|---|---|---|---|---|---|---|
| Get Type | 100000 | 155ns | 15.55ms | 100000 | 1ns | 0.173ms |
| Get Field | 100000 | 17ns | 1.76ms | 100000 | 10ns | 1.07ms |
| Get Property | 100000 | 720ns | 72.03ms | 100000 | 13ns | 1.33ms |
| Get Method | 100000 | 19ns | 1.90ms | 100000 | 10ns | 1.06ms |
| Read Field | 10000000 | 4ns | 41.53ms | 10000000 | 0ns | 0.563ms |
| Read Property | 10000000 | 5ns | 59.47ms | 10000000 | 0ns | 0.565ms |
| Write Field | 10000000 | 4ns | 44.92ms | 10000000 | 0ns | 0.285ms |
| Write Property | 10000000 | 7ns | 72.51ms | 10000000 | 0ns | 0.285ms |
| Call Method | 10000000 | 11ns | 114.66ms | 10000000 | 0ns | 2.47ms |
3. MethodHandle(非缓存)
新宠 MethodHandle 登场。如果在循环里现抓现用,不仅不快,反而更惨。
| 测试项目 | 反射调用次数 | 反射平均耗时 | 反射总耗时 | 直接调用次数 | 直接平均耗时 | 直接总耗时 |
|---|---|---|---|---|---|---|
| Get Type | 100000 | 154ns | 15.48ms | 100000 | 1ns | 0.172ms |
| Get Field | 100000 | 14ns | 1.47ms | 100000 | 2ns | 0.262ms |
| Get Property | 100000 | 645ns | 64.60ms | 100000 | 2ns | 0.264ms |
| Get Method | 100000 | 20ns | 2.07ms | 100000 | 2ns | 0.264ms |
| Read Field | 100000 | 210ns | 21.02ms | 100000 | 0ns | 0.006ms |
| Read Property | 100000 | 951ns | 95.16ms | 100000 | 0ns | 0.007ms |
| Write Field | 100000 | 215ns | 21.56ms | 100000 | 0ns | 0.004ms |
| Write Property | 100000 | 985ns | 98.54ms | 100000 | 0ns | 0.004ms |
| Call Method | 100000 | 300ns | 30.03ms | 100000 | 0ns | 0.025ms |
4. MethodHandle(缓存)
提前准备好 Handle 后,它的威力才真正发挥出来。
| 测试项目 | 反射调用次数 | 反射平均耗时 | 反射总耗时 | 直接调用次数 | 直接平均耗时 | 直接总耗时 |
|---|---|---|---|---|---|---|
| Get Type | 100000 | 154ns | 15.46ms | 100000 | 1ns | 0.175ms |
| Get Field | 100000 | 10ns | 1.04ms | 100000 | 2ns | 0.258ms |
| Get Property | 100000 | 635ns | 63.58ms | 100000 | 2ns | 0.260ms |
| Get Method | 100000 | 17ns | 1.76ms | 100000 | 2ns | 0.262ms |
| Read Field | 10000000 | 3ns | 33.39ms | 10000000 | 0ns | 0.563ms |
| Read Property | 10000000 | 4ns | 41.80ms | 10000000 | 0ns | 0.566ms |
| Write Field | 10000000 | 3ns | 32.06ms | 10000000 | 0ns | 0.285ms |
| Write Property | 10000000 | 3ns | 35.32ms | 10000000 | 0ns | 0.286ms |
| Call Method | 10000000 | 4ns | 44.29ms | 10000000 | 0ns | 2.37ms |
个人体会
看着上面的表格,不知道你有什么感觉?我个人的最大体会是下面这几点:
1. 缓不缓存,那就是天与地的差别
你看数据,只要把 Method 或者 MethodHandle 提前缓存下来,真正调用时的耗时直接掉到了 3~11ns!虽然比直接调用(0~2ns)还是慢一丢丢,但这几纳秒的差距在绝大多数业务代码里根本感受不到,基本等同于热点代码的性能。
如果不缓存,每次都要走一遍安全检查和查表,开销直接放大十几甚至上百倍。
2. MethodHandle 真的比 invoke 强吗?
- 缓存情况下:两者基本五五开,MethodHandle 确实略占上风(3~4ns vs 4~11ns)。
- 非缓存情况下:MethodHandle 反而吃瘪了。因为它在
unreflect动态生成 LambdaForm 的时候,第一步的代价比老反射还要高。 - 顺带一提,我这里测试用的是带类型转换的
mh.invoke(),如果你的场景极其严苛,可以用invokeExact,性能还能再榨出几滴来。
3. “内省”这玩意儿是真的重
仔细看表格里 Get Property 和 Read/Write Property 这几项,只要沾上 PropertyDescriptor 去走内省解析,反射耗时直接飙到 600~900ns。底层各种机制全套跑下来,开销极大,属于性能重灾区。
几条建议
根据这些数据,以后咱们写反射代码或者封装小框架时,可以参考下面这几套连招:
- 能缓存就绝对不现查。 拿到
Method或MethodHandle后,赶紧扔进ConcurrentHashMap或者提升为静态常量。这一步做好了,性能问题就解决 90% 了。 - 避开 PropertyDescriptor 这种重武器。 如果是在高频调用的热路径里,别用内省去推断 getter/setter。老老实实拼接方法名,用
getMethod查出来存好,会快得多。 - 热点路径优先上 MethodHandle。 配合
invokeExact加上提前预绑定(bindTo或者asType),能让你获得极其接近原生代码的性能体验。
(最后叠个甲:测试是在 Windows + JDK 17 下跑的,没上 JMH,System.nanoTime 可能会有抖动。所以数值大家看看就好,主要是感受一下各个操作相对的量级差距。)