Java反射机制探秘

本文深入探讨了Java反射的原理,包括根据符号获取方法调用的实现,以及Method.invoke的内部机制。当反射调用次数超过一定阈值时,Java会动态生成字节码以优化性能。然而,这种优化伴随着Object数组的使用、基本类型的装箱拆箱和方法内联的挑战。反射调用的开销主要体现在变长参数、自动装箱和可能失去内联机会。了解这些可以帮助开发者在使用反射时做出更明智的选择。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。

反射

本质其实就是根据 符号 拿到符号表以及直接地址 进行调用。

反射调用的实现原理

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);
  }
}
  • 委派给了 MethodAccessor 接口实现,
  • MethodAccessor 接口提供了两种实现,一种是本地方法实现,另一种是 Java 实现

那么如何拿到执行地址呐?有两种方式:
在这里插入图片描述
考虑到许多反射调用仅会执行一次,Java 虚拟机设置了一个阈值 15(可以通过 -Dsun.reflect.inflationThreshold= 来调整),当某个反射调用的调用次数在 15 之下时,采用本地实现;当达到 15 时,便开始动态生成字节码,并将委派实现的委派对象切换至动态实现,这个过程我们称之为 Inflation。

刚开始生成字节码比较耗时,但是随着 JIT 的即时编译,会越来越快

反射调用的开销

  1. 变长参数方法导致的 Object 数组
    • 由于 Method.invoke 是一个变长参数方法,在字节码层面它的最后一个参数会是 Object 数组(。Java 编译器会在方法调用处生成一个长度为传入参数数量的 Object 数组,并将传入参数一一存储进该数组中
  2. 基本类型的自动装箱、拆箱
    • 由于 Object 数组不能存储基本类型,Java 编译器会对传入的基本类型参数进行自动装箱
  3. 最重要的方法内联
    • 由于 Java 虚拟机的关于上述调用点的类型 profile(注:对于 invokevirtual 或者 invokeinterface,Java 虚拟机会记录下调用者的具体类型,我们称之为类型 profile)无法同时记录这么多个类,因此可能造成所测试的反射调用没有被内联的情况

Method.invoke一直会被内联,但是它里面的 MethodAccesor.invoke 则不一定。
实际上,在C2编译之前循环代码已经运行过非常多次,也就是说MethodAccesor.invoke已经看到多次调用至target()的动态实现。在profile里会显示为有target1,有target2,但是profile不完整,即还有一大部分的调用者类型没有记录。
这时候C2会选择不inline这个MethodAccesor.invoke调用,直接做虚调用。

参考

JVM系列之:JVM是如何实现反射的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值