JVM是如何实现反射的

news/2024/5/19 5:20:37 标签: jvm, 反射

在我们聊起JVM是如何实现反射的之前,我们先来说一下什么是反射

反射反射就是在运行过程中获取类的信息,并能调用类的方法。

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

举例来说,我们可以通过class对象枚举该类中的所有方法,我们还可以通过java的反射包里的Method.setAccessible绕过java语言的访问权限,在私有方法类以外的地方调用里面的方法。

说一个反射在我们开发中很常见的情况吧,在我们使用IntelliJ Idea进行开发的时候,我们使用.的时候,会自动告诉我们可以调用什么方法,这就是开发中一种十分常见的反射的效果。

在我们使用Web开发中,SpringIOC的依赖反转就是依赖于Java的反射机制,但我们也同时可以感受到,反射是一种很浪费性能的事情,在Oracle官方也特意提到了其对于性能消耗过大的事情。

接下来就来说一下反射的实现机制,以及性能糟糕的原因。

反射调用的实现

首先我们来看看方法的反射调用是如何实现的,就是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);
  }
}

我们可以通过上面的方法发现,它实际上将反射的业务委派给了MethodAccessor来实现,MethodAccessor是一个接口,它有两个具体的接口实现:一个通过本地方法来实现反射调用,另一个则使用了委派模式。为了方便记忆,我便用了“本地使用”和“委派实现”来指代这两者。

每个Method实例的第一次反射调用都会生成一个委派实现,它所委派的具体实现便是一个本地实现。本地实现非常容易理解。当进入了JVM内部之后,我们便拥有了Method实例指向具体的方法地址。这时候,反射调用就会将参数都准备好,自动填充到对应的方法内。

那我们使用委派实现的具体作用是什么呢,直接交给本地不好吗?

其实,Java的反射机制还设立了另一种动态生成字节码的实现,简称动态实现,并且委派实现的意义就是在于,可以在本地实现和动态实现中切换。

在这里我说一下,动态实现的总体速度是比本地实现快上几十倍的,但是问题在于,生成字节码然后解码的过程倒是很浪费资源,所以,如果你就调用一次方法去反射,得不偿失啊。

所以JVM就规定了反射次数的一个规范:当调用invoke方法<15次,就本地实现,当≥15次,就用动态生成字节码的方法反射

举一个反射调用20次代码实现

// v1 版本
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);
    }
  }
}

# 使用 -verbose:class 打印加载的类
$ java -verbose:class Test
...
java.lang.Exception: #14
        at Test.target(Test.java:5)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl .invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl .invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl .invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:564)
        at Test.main(Test.java:12)
[0.158s][info][class,load] ...
...
[0.160s][info][class,load] jdk.internal.reflect.GeneratedMethodAccessor1 source: __JVM_DefineClass__
java.lang.Exception: #15
       at Test.target(Test.java:5)
       at java.base/jdk.internal.reflect.NativeMethodAccessorImpl .invoke0(Native Method)
       at java.base/jdk.internal.reflect.NativeMethodAccessorImpl .invoke(NativeMethodAccessorImpl.java:62)
       at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl .invoke(DelegatingMethodAccessorImpl.java:43)
       at java.base/java.lang.reflect.Method.invoke(Method.java:564)
       at Test.main(Test.java:12)
java.lang.Exception: #16
       at Test.target(Test.java:5)
       at jdk.internal.reflect.GeneratedMethodAccessor1 .invoke(Unknown Source)
       at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl .invoke(DelegatingMethodAccessorImpl.java:43)
       at java.base/java.lang.reflect.Method.invoke(Method.java:564)
       at Test.main(Test.java:12)
...

我们可以从上面日志中看出,从第15次反射调用invoke的时候,虚拟机加载了额外的类,这就是动态实现的证明,并且可以看出,在后面的实现,都是依赖于动态实现去进行反射调用。

这种规范,被我们定义为Inflation

反射调用的开销

下面,我们就来说一下反射带给我们额外性能的开销。我们在刚才的例子中,使用到了

Class.forName():调用本地方法
Class.getMethod():遍历该类的共有方法。如果没有匹配到,还会遍历父类的共有方法

我们可以看出上面的两个方法实现都是很耗时的,尤其是getMethod(),我们至少要避免getMethod方法在热点代码中少用。

以下贴出一个例子,会将反射执行二十亿次

// v2 版本
mport 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);

    long current = System.currentTimeMillis();
    for (int i = 1; i <= 2_000_000_000; i++) {
      if (i % 100_000_000 == 0) {
        long temp = System.currentTimeMillis();
        System.out.println(temp - current);
        current = temp;
      }

      method.invoke(null, 128);
    }
  }
}


   59: aload_2                         // 加载 Method 对象
   60: aconst_null                     // 反射调用的第一个参数 null
   61: iconst_1
   62: anewarray Object                // 生成一个长度为 1 的 Object 数组
   65: dup
   66: iconst_0
   67: sipush 128
   70: invokestatic Integer.valueOf    // 将 128 自动装箱成 Integer
   73: aastore                         // 存入 Object 数组中
   74: invokevirtual Method.invoke     // 反射调用

并且,进行反射调用的过程中,它内部还会进行其他两个操作。

  1. 由于Method.invoke是一个变长参数方法,在字节码层面它的最后一个参数是Object数组。Java编译器会在方法调用处生成一个长度为传入参数数量的Object数组,并将传入参数一一存储进该数组中。

  2. 由于Object数组不能存储基本类型,Java编译器会对传入的基本类型参数进行自动装箱。

这两个操作除了带来性能开销外,还可能占用堆内存,使GC更加频繁。

那么,如何消除掉这部分开销呢?

关于第二个自动装箱,Java缓存了[-128,127]中所有整数所对应的Integer对象,当需要自动装箱的整数在这个范围内的时候就会自动返回缓存的Integer,否则就需要新建一个Integer对象。

所以为了能不新建Integer对象,可以将缓存的范围也扩大。


http://www.niftyadmin.cn/n/784469.html

相关文章

微信小程序获取用户信息接口调整目的以及使用方法介绍

微信小程序获取用户信息接口调整目的以及使用方法介绍微信小程序已经调整了获取用户信息的接口&#xff0c;还不知道的开发者看一下官网给出的理由和方法&#xff1a; 为优化用户体验&#xff0c;使用 wx.getUserInfo 接口直接弹出授权框的开发方式将逐步不再支持。从2018年4月…

将数据写入文本,非乱码(转)

#include<iostream.h> #include<fstream.h> void main() { ofstream outData("f:\\data.txt"); //在f盘下建立文件。 ifstream inData; int x,a[10]; for(int i0;i<10;i) { cin>>x; outData<<x<<" "; …

Protobuf集成Netty进行简单信息传递的讲解

首先不知道Protobuf的可有先看看这篇文章https://blog.csdn.net/qq_41936805/article/details/100771566 代码已上传至github https://github.com/2NaCl/netty-protobuf-demo/tree/master 我们使用protobuf进行了序列化和反序列化的操作&#xff0c;并且也知道了这个操作相对于…

使用js是想防止表单重复提交的效果

直接上代码&#xff1a; <html><head><title>Form表单</title><script type"text/javascript">var isCommitted false;//表单是否已经提交标识&#xff0c;默认为falsefunction dosubmit(){if(isCommittedfalse){isCommitted true;/…

Netty整合protobuf解决多message的多协议实现的项目演示

本篇博客涉及到的代码已上传至github&#xff1a;https://github.com/2NaCl/netty-protobuf-moreMsg 之前我们实现了通过Netty整合protobuf进行简单的客户端给服务端发送消息的demo&#xff0c;从功能上来讲protobuf确实不太能看出来有其他的业务能力&#xff0c;但是从性能来…

功能式Python中的探索性数据分析

欢迎大家前往腾讯云社区&#xff0c;获取更多腾讯海量技术实践干货哦~ 这里有一些技巧来处理日志文件提取。假设我们正在查看一些Enterprise Splunk提取。我们可以用Splunk来探索数据。或者我们可以得到一个简单的提取并在Python中摆弄这些数据。 在Python中运行不同的实验似乎…

C指针---指向指针的指针(转)

一&#xff0e; 回顾指针概念&#xff1a; 今天我们又要学习一个叫做指向另一指针地址的指针。让我们先回顾一下指针的概念吧&#xff01; 当我们程序如下申明变量&#xff1a; short int i; char a; short int * pi; 程序会在内存某地址空间上为各变量开辟空间&#xff…

二叉树基础(上):什么样的二叉树适合用数组来存储?

其实这段时间一直在刷lc&#xff0c;但是还是感觉树图写的不太行&#xff0c;就重新来学习总结了。 首先提出疑问&#xff1a;二叉树有哪几种存储方式&#xff1f;什么样的二叉树适合用数组来存储&#xff1f; 待着问题&#xff0c;然后来学习今天的内容 树 我们首先来看&a…