07.深入解密JVM反射机制——从Method.invoke到Inflation优化

图片[1]-07.深入解密JVM反射机制——从Method.invoke到Inflation优化-速优课

本文原创,转载请注明出处。关注博主,持续分享Java进阶知识。

一、开篇:反射的神奇用途

反射是Java语言中一个相当重要的特性,它允许正在运行的Java程序观测,甚至是修改程序的动态行为。

反射的典型应用场景:

应用场景说明
IDE自动提示敲入点号时,动态展示可访问的字段或方法
调试器在调试过程中枚举对象所有字段的值
Spring IoC根据配置文件动态加载和实例化Bean
ORM框架自动映射数据库表和Java对象
单元测试调用私有方法进行测试

反射的核心能力:

  • 通过Class对象枚举类中的所有方法和字段
  • 通过setAccessible(true)绕过Java语言的访问权限
  • 在运行时动态调用方法、访问字段、创建对象

二、反射调用的实现机制

2.1 Method.invoke的核心流程

public final class Method extends Executable {
    public Object invoke(Object obj, Object... args) throws ... {
        // 权限检查
        MethodAccessor ma = methodAccessor;
        if (ma == null) {
            ma = acquireMethodAccessor();
        }
        return ma.invoke(obj, args);
    }
}

关键点: Method.invoke实际上委派给了MethodAccessor接口来处理。

2.2 MethodAccessor的三种实现

实现类型说明特点
本地实现NativeMethodAccessorImpl通过本地方法调用,首次调用快
委派实现DelegatingMethodAccessorImpl作为中间层,可切换实现
动态实现GeneratedMethodAccessorX动态生成字节码,多次调用快

2.3 反射调用的完整链路

图片[2]-07.深入解密JVM反射机制——从Method.invoke到Inflation优化-速优课

示例:查看反射调用的栈轨迹

public class Test {
    public static void target(int i) {
        new Exception("#" + i).printStackTrace();
    }
​
    public static void main(String[] args) throws Exception {
        Class<?> klass = Class.forName("Test");
        Method method = klass.getMethod("target", int.class);
        method.invoke(null, 0);
    }
}

运行结果:

java.lang.Exception: #0
        at Test.target(Test.java:5)
        at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(...)
        at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(...)
        at java.lang.reflect.Method.invoke(...)
        at Test.main(Test.java:13)

三、Inflation机制:本地实现与动态实现的切换

3.1 Inflation的设计思想

Java虚拟机设置了一个阈值15(可通过-Dsun.reflect.inflationThreshold=调整):

  • 调用次数 < 15:使用本地实现(首次调用快)
  • 调用次数 >= 15:切换到动态实现(多次调用快)

为什么要这样设计?

实现类型首次调用性能多次调用性能
本地实现快(无需生成字节码)慢(Java→C++→Java切换)
动态实现慢(生成字节码耗时)快(直接使用invoke指令)

3.2 Inflation的切换过程

// v1版本:循环20次反射调用
import java.lang.reflect.Method;
​
public class Test {
    public static void target(int i) {
        new Exception("#" + i).printStackTrace();
    }
​
    public static void main(String[] args) throws Exception {
        Class<?> klass = Class.forName("Test");
        Method method = klass.getMethod("target", int.class);
        for (int i = 0; i < 20; i++) {
            method.invoke(null, i);
        }
    }
}

运行结果分析:

# 第14次调用(使用本地实现)
java.lang.Exception: #14
        at Test.target(Test.java:5)
        at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
​
# 第15次调用触发动态实现生成
[info][class,load] jdk.internal.reflect.GeneratedMethodAccessor1 source: __JVM_DefineClass__
​
# 第16次调用(使用动态实现)
java.lang.Exception: #16
        at Test.target(Test.java:5)
        at jdk.internal.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)

3.3 动态实现的字节码

动态生成的类伪代码:

package jdk.internal.reflect;
​
public class GeneratedMethodAccessor1 extends ... {
    @Override
    public Object invoke(Object obj, Object[] args) throws ... {
        Test.target((int) args[0]);  // 直接调用目标方法
        return null;
    }
}

3.4 关闭Inflation机制

可以通过-Dsun.reflect.noInflation=true关闭Inflation:

java -Dsun.reflect.noInflation=true Test

效果: 直接使用动态实现,跳过本地实现阶段。


四、反射调用的性能开销分析

4.1 性能开销的来源

开销1:方法查找

Class<?> klass = Class.forName("Test");           // 调用本地方法
Method method = klass.getMethod("target", int.class);  // 遍历公有方法

开销2:Object数组创建

// 编译后的字节码
59: aload_2                          // 加载Method对象
60: aconst_null                      // 第一个参数null
61: iconst_1
62: anewarray Object                 // 创建Object数组
65: dup
66: iconst_0
67: sipush 128
70: invokestatic Integer.valueOf     // 自动装箱
73: aastore                          // 存入数组
74: invokevirtual Method.invoke      // 反射调用

开销3:自动装箱/拆箱

基本类型参数需要装箱成包装类型,反射调用时又需要拆箱。

开销4:方法内联失败

多个不同的反射调用会污染类型profile,导致JIT无法内联。

4.2 性能优化实践

优化1:缓存查找结果

// 避免重复查找
Class<?> klass = Class.forName("Test");
Method method = klass.getMethod("target", int.class);

// 在循环中直接使用缓存的method对象
for (int i = 0; i < 1000000; i++) {
    method.invoke(null, i);
}

优化2:扩大Integer缓存范围

java -Djava.lang.Integer.IntegerCache.high=128 Test

优化3:关闭权限检查

method.setAccessible(true); // 关闭权限检查

优化4:关闭Inflation机制

java -Dsun.reflect.noInflation=true Test

4.3 性能对比测试

优化措施性能开销(相对于直接调用)
默认反射调用约2.7倍
扩大Integer缓存约1.8倍
关闭权限检查 + 关闭Inflation约1.3倍
污染类型profile后约6.7倍

五、类型Profile污染与内联失败

5.1 类型Profile的作用

JVM会记录invokevirtual/invokeinterface调用点的具体类型,用于优化方法内联。

默认TypeProfileWidth=2:每个调用点最多记录2种类型。

5.2 Profile污染的影响

// v5版本:污染类型profile
public static void polluteProfile() throws Exception {
    Method method1 = Test.class.getMethod("target1", int.class);
    Method method2 = Test.class.getMethod("target2", int.class);
    for (int i = 0; i < 2000; i++) {
        method1.invoke(null, 0);
        method2.invoke(null, 0);
    }
}

结果: 类型profile被污染,测试方法的反射调用无法内联,性能从1.3倍下降到6.7倍。

5.3 解决方案

方案1:提高TypeProfileWidth

java -XX:TypeProfileWidth=3 Test

效果: 性能从6.7倍提升到2.8倍。

方案2:使用Lambda或方法引用代替反射

// 使用方法引用
IntConsumer consumer = Test::target;
consumer.accept(128);  // 性能接近直接调用

六、反射API简介

6.1 获取Class对象的三种方式

// 方式1:Class.forName
Class<?> klass1 = Class.forName("com.example.Test");

// 方式2:对象.getClass()
Test obj = new Test();
Class<?> klass2 = obj.getClass();

// 方式3:类名.class
Class<?> klass3 = Test.class;
Class<?> intClass = int.class;
Class<?> intArrayClass = int[].class;

6.2 常用反射操作

操作方法说明
创建实例klass.newInstance()需要无参构造器
判断实例klass.isInstance(obj)等同于obj instanceof Test
创建数组Array.newInstance(klass, length)构造指定类型的数组
获取字段klass.getFields()获取公有字段(含父类)
获取方法klass.getMethods()获取公有方法(含父类)
获取构造器klass.getConstructors()获取公有构造器
获取所有字段klass.getDeclaredFields()获取所有字段(不含父类)
获取所有方法klass.getDeclaredMethods()获取所有方法(不含父类)

6.3 成员对象的操作

// 绕过访问限制
method.setAccessible(true);
field.setAccessible(true);
constructor.setAccessible(true);

// 调用方法
Object result = method.invoke(obj, args);

// 访问字段
Object value = field.get(obj);
field.set(obj, newValue);

// 创建实例
Object instance = constructor.newInstance(args);

七、实战:Method对象的唯一性

7.1 实验代码

import java.lang.reflect.Method;

public class Test {
    public static void target(int i) {}

    public static void main(String[] args) throws Exception {
        Class<?> klass = Class.forName("Test");
        Method method = klass.getMethod("target", int.class);
        method.setAccessible(true);
        
        polluteProfile();
        
        // 性能测试...
    }

    public static void polluteProfile() throws Exception {
        Method method1 = Test.class.getMethod("target", int.class);
        Method method2 = Test.class.getMethod("target", int.class);
        
        System.out.println(method1 == method2);      // true? false?
        System.out.println(method1.equals(method2)); // true? false?
        
        for (int i = 0; i < 2000; i++) {
            method1.invoke(null, 0);
            method2.invoke(null, 0);
        }
    }
}

7.2 思考问题

  1. method1 == method2 的结果是什么?
  2. method1.equals(method2) 的结果是什么?
  3. 这对类型profile有什么影响?
  4. 性能测试结果会怎样?

八、总结

核心要点回顾

  1. 反射调用机制:Method.invoke → DelegatingMethodAccessorImpl → NativeMethodAccessorImpl/GeneratedMethodAccessorX
  2. Inflation机制:前15次使用本地实现,第16次开始使用动态实现
  3. 性能开销来源:方法查找、Object数组创建、自动装箱、方法内联失败
  4. 优化策略:缓存结果、关闭权限检查、关闭Inflation、使用Lambda替代
  5. 类型Profile污染:多个反射调用会导致内联失败,性能大幅下降

关键理解

  • 反射调用的性能开销主要来自装箱和内联失败
  • Inflation机制平衡了首次调用和多次调用的性能
  • 在生产环境中,反射调用的性能可能比基准测试差很多
  • 对于高频调用,应考虑使用Lambda或代码生成代替反射

写在最后

反射是Java的强大特性,但使用时需要注意性能问题。通过理解其底层实现机制,我们可以在需要时进行有效的性能优化。对于性能敏感的场景,应优先考虑使用Lambda、方法引用或代码生成等替代方案。

如果觉得本文对你有帮助,欢迎点赞、收藏、关注!后续我会继续分享更多JVM相关的深度解析文章。

📚 关注我,一起深入探索Java虚拟机的奥秘!

© 版权声明
THE END
喜欢就支持一下吧
点赞10
相关推荐
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容

温馨提示:
1、本内容转载于网络,版权归原作者所有!
2、本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
3、本内容若侵犯到你的版权利益,请联系我们,会尽快给予删除处理!