栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

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

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

简介

Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。

在 Java 环境中运行时,对于任意一个类,能否知道这个类有哪些属性和方法。对于任意一个对象,能否调用它的任意一个方法。

Java 反射机制主要提供了以下功能:

在运行时判断任意一个对象所属的类。在运行时构造任意一个类的对象。在运行时判断任意一个类所具有的成员变量和方法。在运行时调用任意一个对象的方法。

反射的作用

    动态的加载类,动态的获取类的信息(属性,方法,构造器)动态的构造对象动态调用类和对象的任意方法,构造器获取泛型信息处理注解动态代理
反射调用的实现 源码分析

首先我们来看一个反射代码示例:

public class Student {
  private String name;
  private int age;

  public Student(){
    System.out.println("构造方法");
  }

  private Student(int age){
    this.age = age;
    System.out.println(age);
  }

  public void print(int num) {
    System.out.println("第" + num + "次,测试输出");
  }
}

public class ReflectionTest {

  public static void main(String[] args)
      throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
    Class c = Student.class;

    Constructor constructor = c.getConstructor();
    Student student = constructor.newInstance();

    Method method = c.getDeclaredMethod("print",int.class);
    method.setAccessible(true);
    method.invoke(student,1);
  }
}

上述代码我们通过反射来实现方法调用,即 Method.invoke,那么它是怎么实现的呢?通过查看源码可知:

public final class Method extends Executable {
  ....
     @CallerSensitive
    public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, obj, modifiers);
            }
        }
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            ma = acquireMethodAccessor();
        }
        return ma.invoke(obj, args);
    } 
  ....
}

继续向下阅读源码,可知创建 MethodAccessor 实例的是 ReflectionFactory,核心代码为:

public class ReflectionFactory {
  private static boolean noInflation = false;
  private static int inflationThreshold = 15;

  private static void checkInitted() {
    if (!initted) {
      AccessController.doPrivileged(new PrivilegedAction() {
        public Void run() {
          if (System.out == null) {
            return null;
          } else {
            String var1 = System.getProperty("sun.reflect.noInflation");
            if (var1 != null && var1.equals("true")) {
              ReflectionFactory.noInflation = true;
            }

            var1 = System.getProperty("sun.reflect.inflationThreshold");
            if (var1 != null) {
              try {
                ReflectionFactory.inflationThreshold = Integer.parseInt(var1);
              } catch (NumberFormatException var3) {
                throw new RuntimeException("Unable to parse property sun.reflect.inflationThreshold", var3);
              }
            }

            ReflectionFactory.initted = true;
            return null;
          }
        }
      });
    }
  }  

  public MethodAccessor newMethodAccessor(Method var1) {
    checkInitted();
    if (noInflation && !ReflectUtil.isVMAnonymousClass(var1.getDeclaringClass())) {
      return (new MethodAccessorGenerator()).generateMethod(var1.getDeclaringClass(), var1.getName(), var1.getParameterTypes(), var1.getReturnType(), var1.getExceptionTypes(), var1.getModifiers());
    } else {
      NativeMethodAccessorImpl var2 = new NativeMethodAccessorImpl(var1);
      DelegatingMethodAccessorImpl var3 = new DelegatingMethodAccessorImpl(var2);
      var2.setParent(var3);
      return var3;
    }
  }
}

上述测试用例结合源码分析,我们可以知道下述这些内容:

MethodAccessor 实现有两个版本,一个是 Java 实现的,另一个是 native code 实现的。Java 实现的版本在初始化时需要较多时间,但长久来说性能较好;native 版本正好相反,启动时相对较快,但运行时间长了之后速度就比不过 Java 版了。

这里引申出一个新的概念:inflation 机制。具体指:当反射被频繁调用时,动态生成一个类来做直接调用的机制,可以加速反射调用。

MethodAccessor 实现为什么会有两个版本?

主要是对性能的权衡,JDK1.4 开始引入如下优化措施:让 Java 方法在被反射调用时,开头若干次使用 native 版,等反射调用次数超过阈值时则生成一个专用的 MethodAccessor 实现类,生成其中的 invoke()方法的字节码,以后对该 Java 方法的反射调用就会使用 Java 版。

上面看到了 ReflectionFactory.newMethodAccessor()生产MethodAccessor的逻辑,在“开头若干次”时用到的DelegatingMethodAccessorImpl代码如下:

class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
  private MethodAccessorImpl delegate;

  DelegatingMethodAccessorImpl(MethodAccessorImpl var1) {
    this.setDelegate(var1);
  }

  public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
    return this.delegate.invoke(var1, var2);
  }

  void setDelegate(MethodAccessorImpl var1) {
    this.delegate = var1;
  }
}

该方法相当于一个转换层,方便在 native 与 Java 版的 MethodAccessor 之间实现切换。

接着来看一看 NativeMethodAccessorImpl 实现:

class NativeMethodAccessorImpl extends MethodAccessorImpl {
  private final Method method;
  private DelegatingMethodAccessorImpl parent;
  private int numInvocations;

  NativeMethodAccessorImpl(Method var1) {
    this.method = var1;
  }

  public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
    if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
      MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
      this.parent.setDelegate(var3);
    }

    return invoke0(this.method, var1, var2);
  }

  void setParent(DelegatingMethodAccessorImpl var1) {
    this.parent = var1;
  }

  private static native Object invoke0(Method var0, Object var1, Object[] var2);
}

每次 NativeMethodAccessorImpl.invoke()方法被调用时,都会增加一个调用次数计数器,看超过阈值 inflationThreshold 没有;一旦超过,则调用 MethodAccessorGenerator.generateMethod()来生成 Java 版的 MethodAccessor 的实现类,并且改变 DelegatingMethodAccessorImpl 所引用的 MethodAccessor 为 Java 版。后续经由 DelegatingMethodAccessorImpl.invoke() 调用到的就是Java版的实现了。

我们简单看一下 MethodAccessorGenerator 的 generateMethod 方法实现:

该方法主要是通过 ClassFileAssembler 类来生成字节码,这种方式进行方法调用效率比 native 高,但换来的是要生成 class 文件,相当于拿空间换时间。不仅如此,由于生成字节码十分耗时,仅调用一次的话,反而是 native 实现要快上 3 到 4 倍。所以才会有个阈值限定,超过该阈值才采用字节码的方式。

该方法中的 generateName 方法会生成字节码文件的文件名和路径。

private static synchronized String generateName(boolean var0, boolean var1) {
    int var2;
    if (var0) {
      if (var1) {
        var2 = ++serializationConstructorSymnum;
        return "sun/reflect/GeneratedSerializationConstructorAccessor" + var2;
      } else {
        var2 = ++constructorSymnum;
        return "sun/reflect/GeneratedConstructorAccessor" + var2;
      }
    } else {
      var2 = ++methodSymnum;
      return "sun/reflect/GeneratedMethodAccessor" + var2;
    }
  }

那么如何查看 GeneratedMethodAccessor1.class 呢?

这里简单介绍一下如何操作,详细内容可以参考我的上篇文章,使用如下命令:

$ cd /Library/Java/JavaVirtualMachines/jdk-9.0.4.jdk/Contents/Home/bin/
$ jhsdb hsdb

打开 hsdb 可视化窗口后,在命令行窗口执行 jps 命令查看 pid。点击 File->Attach to…输入pid 点击 Tools -> Class Browser  GeneratedMethodAccessor 点击 save class。(关于 hsdb 的使用,后续会有文章来介绍,详细记录了个人的踩坑经历)

最终会在 jdk/bin 目录下生成一个名为 jdk 的文件夹,在里面存放了 GeneratedMethodAccessor1.class。

使用 javap 查看字节码内容,如下图所示:

具体源码为:

package jdk.internal.reflect;

import InvokeTest;
import java.lang.reflect.InvocationTargetException;

public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
  public Object invoke(Object paramObject, Object[] paramArrayOfObject) throws InvocationTargetException {
    try {
      if (paramArrayOfObject.length != 1)
        throw new IllegalArgumentException(); 
      Object object = paramArrayOfObject[0];
      if (object instanceof Integer) {
      
      } else {
        throw new IllegalArgumentException();
      } 
      try {
        InvokeTest.printException((object instanceof Byte) ? ((Byte)object).bytevalue() : ((object instanceof Character) ? ((Character)object).charValue() : ((object instanceof Short) ? ((Short)object).shortValue() : "JD-Core does not support Kotlin")));
        return null;
      } catch (Throwable throwable) {
        throw new InvocationTargetException(null);
      } 
    } catch (ClassCastException|NullPointerException classCastException) {
      throw new IllegalArgumentException(null.toString());
    } 
  }
}
测试案例

案例一

首先从类加载的层面来看个 demo。

    Class c = Student.class;

    Constructor constructor = c.getConstructor();
    Student student = constructor.newInstance();

    Method method = c.getDeclaredMethod("print", int.class);
    method.setAccessible(true);
    for (int i = 0; i < 18; i++) {
      method.invoke(student, i + 1);
    }

来我们加上虚拟机参数 -verbose:class 启动执行如下:

我们发现执行到第 15 次的时候会又多加载一部分类,这说明从第前 15 次和后面的反射调用方式是不同的。

上述测试用例结合源码分析,我们可以知道下述这些内容:

MethodAccessor 实现有两个版本,一个是 Java 实现的,另一个是 native 实现的。需要注意的是inflationThreshold 的值是15,也就是说前15次是使用的 native 版本,之后使用的是 java 版本。

可以在启动命令里加上 -Dsun.reflect.noInflation=true,就会 RefactionFactorynoInflation 属性就变成 true 了,这样不用等到 15 次调用后,程序一开始就会用 java 版的 MethodAccessor 了。

或者换种测试方法,在启动命令里加上 -Dsun.reflect.inflationThreshold=10 -verbose:class,执行后可以发现在第10次输出后就会多加载一部分类。

案例二

在上文学习 JVM 处理异常时我们提到栈轨迹这个新概念,该操作会逐一访问当前线程的 Java 栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常。

接下来我们将利用异常和栈轨迹来演示反射具体的调用逻辑,查看如下 demo。

//不要包名
public class InvokeTest {

  public static void printException(int num) {
    new Exception("#" + num).printStackTrace();
  }

  public static void main(String[] args)
      throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    Class cl = Class.forName("InvokeTest");
    Method method = cl.getMethod("printException", int.class);
    method.invoke(null, 1);
  }
}

//接着执行编译和解析命令,JDK8
$ javac InvokeTest.java
$ java InvokeTest   
  java.lang.Exception: #1
        at InvokeTest.printException(InvokeTest.java:14)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at InvokeTest.main(InvokeTest.java:21)

可以看到,反射调用先是调用了 Method.invoke,然后进入 Java 实现(DelegatingMethodAccessorImpl),再然后进入本地实现(NativeMethodAccessorImpl),最后到达目标方法。

这两个实现类我们在上文源码分析已经讲述过,这里就不做过多叙述了。

结合源码分析,当反射连续调用超过 15次,类加载过程会有变化,那么调用链路会发生什么改变呢?查看如下 demo。

public class InvokeTest {

  public static void printException(int num) {
    new Exception("#" + num).printStackTrace();
  }

  public static void main(String[] args)
      throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    Class cl = Class.forName("InvokeTest");
    Method method = cl.getMethod("printException", int.class);
    for (int i = 1; i < 20; i++) {
      method.invoke(null, i);
    }
  }
} 

//接着执行编译和解析命令,JDK8
$ javac InvokeTest.java
$ java InvokeTest   
 
....
  java.lang.Exception: #15
        at InvokeTest.printException(InvokeTest.java:14)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at InvokeTest.main(InvokeTest.java:22)
java.lang.Exception: #16
        at InvokeTest.printException(InvokeTest.java:14)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at InvokeTest.main(InvokeTest.java:22)
java.lang.Exception: #17
        at InvokeTest.printException(InvokeTest.java:14)
        at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at InvokeTest.main(InvokeTest.java:22)

在上述结果中我们可以发现,当第 17次调用时才使用 GeneratedMethodAccessor1 字节码文件,在案例一中我们发现在第 15次和第 16次之间的类加载多了很多内容,那么在案例二中为什么不是第 16次使用字节码文件呢?

前 15次调用的是 NativeMethodAccessorImpl 文件中的 invoke 方法。

  public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
    if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
      MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
      this.parent.setDelegate(var3);
    }

    return invoke0(this.method, var1, var2);
  }

如果 debug 调试一下的话,可以发现当第 16次调用时,if 判断条件为 true,条件方法体中的代码实现和那些多出来的类有关,当类加载完毕后,会继续执行下面的 invoke0 方法,所以案例二中第 16次方法调用链路与前15次没有差异,等到来到第 17次,根本才会使用上次生成的字节码文件。

下图是 debug 调试过程中,第 17次调用的内容显示:

反射调用的开销

在上述测试案例二中,我们先后进行了 Class.forName,Class.getMethod 以及 Method.invoke 三个操作。其中,Class.forName 会调用本地方法,Class.getMethod 则会遍历该类的公有方法。如果没有匹配到,它还将遍历父类的公有方法。可想而知,这两个操作都非常费时。

值得注意的是,通过查看源码可知,以 getMethod 为代表的查找方法操作,会执行 copyMethod 来拷贝结果。因此,我们应当避免在热点代码中使用返回 Method 数组的 getMethods 或者 getDeclaredMethods 方法,以减少不必要的堆空间消耗。

如上述案例所示,Class.forName 和 Class.getMethod 本身使用次数并不多,在实践中,我们往往会在应用程序中缓存 Class.forName 和 Class.getMethod 的结果。因此,下面我就只关注反射调用本身的性能开销。

接下来我们通过最直观(并不严谨)的方式来比较直接调用和反射调用的性能差距.它会将反射调用循环二十亿次。此外,它还将记录下每跑一亿次的时间。(测试机为 Mac,JDK8)

将取最后五个记录的平均值,作为预热后的峰值性能。

首先我们来执行直接调用所需耗时。

public class InvokeCapabilityTest {

  public static void target(int i) { // 空方法
  }

  public static void main(String[] args) throws Exception {
    normalTest();
  }

  public static void normalTest() {
    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;
      }
      InvokeCapabilityTest.target(128);
    }
  }
}

最后得出的结果是一亿次直接调用耗费的时间大约在为 95 ms,如果注释 InvokeCapabilityTest.target(128),执行上述代码,发现输出的时间大概也是 95ms。其原因在于这段代码属于热循环,同样会触发即时编译。并且,即时编译会将对 Test.target 的调用内联进来(方法内联指的是编译器在编译一个方法时,将某个方法调用的目标方法也纳入编译范围内,并用其返回值替代原方法调用这么个过程。后续会专门介绍内联这一知识),从而消除了调用的开销。

接着执行反射调用的耗时。

public class InvokeCapabilityTest {

  public static void target(int i) { // 空方法
  }

  public static void main(String[] args) throws Exception {
    invokeTest();
  }

  public static void invokeTest() throws Exception {
    Class klass = Class.forName("com.msdn.java.hotspot.invoke.InvokeCapabilityTest");
    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);
    }
  }
}

反射调用耗时大约为 275ms,测得的结果约为 2.9倍。我们来看一下反射调用前的字节码操作。

 				50: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
        53: lload         5
        55: lload_2
        56: lsub
        57: invokevirtual #13                 // Method java/io/PrintStream.println:(J)V
        60: lload         5
        62: lstore_2
        63: aload_1
        64: aconst_null
        65: iconst_1
        66: anewarray     #14                 // class java/lang/Object
        69: dup
        70: iconst_0
        71: sipush        128
        74: invokestatic  #15                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        77: aastore
        78: invokevirtual #16                 // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
        81: pop

分析结果如下:

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

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

反射调用前的字节码操作除了带来性能开销外,还可能占用堆内存,使得 GC 更加频繁。我们可以修改 JVM 参数为 -XX:+PrintGCDetails -XX:+PrintGCDateStamps,然后执行程序查看程序 GC 日志,这里只截取部分输出结果:

2022-01-13T21:57:14.880-0800: [GC (Allocation Failure) [PSYoungGen: 267264K->0K(257536K)] 268202K->938K(432640K), 0.0004327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2022-01-13T21:57:14.926-0800: [GC (Allocation Failure) [PSYoungGen: 257024K->0K(247296K)] 257962K->938K(422400K), 0.0004015 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2022-01-13T21:57:14.972-0800: [GC (Allocation Failure) [PSYoungGen: 246784K->0K(237568K)] 247722K->938K(412672K), 0.0006462 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2022-01-13T21:57:15.017-0800: [GC (Allocation Failure) [PSYoungGen: 237056K->0K(285184K)] 237994K->938K(460288K), 0.0006034 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2022-01-13T21:57:15.070-0800: [GC (Allocation Failure) [PSYoungGen: 284672K->0K(273920K)] 285610K->938K(449024K), 0.0004569 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2022-01-13T21:57:15.120-0800: [GC (Allocation Failure) [PSYoungGen: 273408K->0K(263168K)] 274346K->938K(438272K), 0.0005023 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
287

简要介绍一下输出内容:

1、在方括号中”PSYoungGen:”后面的”267264K->0K(257536K)”代表的是”GC前该内存区域已使用的容量->GC后该内存区域已使用的容量(该内存区域总容量)”

2、在方括号之外的”268202K->938K(432640K)”代表的是”GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)”

3、再往后的”0.0004327 secs”代表该内存区域GC所占用的时间,单位是秒。

性能优化一

针对上述代码,我们可以如何修改,从而提升性能,减少 GC 的频繁调用呢?

在已知 invoke 方法第二个参数为 Object 数组,Java 会对传入的基本数据类型进行自动装箱的前提下,我们尝试避免在循环体内多次对基本数据类型进行装箱处理。

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

还是上述代码示例,如果我们将 128 放在循环外定义,如下所示:

long current = System.currentTimeMillis();
Integer num = new Integer(128);
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, num);
}

上述优化后的代码测得的结果大约为 222ms。

通过上述措施对性能有一点优化,接着查看修改后的代码的 GC 日志。最后发现程序不会触发 GC,为什么呢?网上的说法是这样的,原本的反射调用被内联了,从而使得即时编译器中的逃逸分析将原本新建的 Object 数组判定为不逃逸的对象。如果一个对象不逃逸,那么即时编译器可以选择栈分配甚至是虚拟分配,也就是不占用堆空间。但是,个人并不认同这种说法,首先 JDK8 默认会开启逃逸分析,如果关闭逃逸分析,那么不管是哪种方式都会触发 GC的,方法内联与逃逸分析并无绝对的因果关系。接下来我们再来看两个案例,主要是针对 invoke 第二个参数的赋值处理。

情况一:

//修改 JVM参数-Djava.lang.Integer.IntegerCache.high=128
method.invoke(null, 128);

该种情况下,测试得到的结果大约为 290ms,再输出 GC 日志,结果发现并没有触发 GC,为什么呢?因为增加 Integer 的缓存范围,避免了在循环中重复创建 Integer 对象。

情况二:

long current = System.currentTimeMillis();
Object[] args = new Object[1];
args[0] = 128;
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, args);
}

这种情况是在在循环体外自定义好数组对象,当作 invoke 的参数, 时耗大约为 340ms,性能反而更糟糕了。注意,这里也没有触发 GC。为什么呢?编译器在进行代码分析时,无法确定这个数组会不会中途被更改,因此无法优化掉访问数组的操作,导致性能变差。不过因为已经在循环体外创建了对象,所以并没有触发 GC。

关于这三种情况性能和 GC 的分析大致如此,对此进行一下总结:

1、基于如下前提:如果一个对象不逃逸,那么即时编译器可以选择栈分配甚至是虚拟分配,也就是不占用堆空间。(如果不存在逃逸行为,即时编译器可以对该对象进行如下优化:同步消除、标量替换和栈上分配。关于逃逸行为的分析基有两种:方法逃逸和线程逃逸,这里就不详细介绍了。)

2、不管是否发生逃逸,如果循环中不需要频繁创建对象,那么是否占用堆空间,都不会触发 GC的。

3、方法内联可以提升性能,方法内联和逃逸分析没有绝对的因果关系。

性能优化二

在前文我们提到 Method.invoke 方法基于 MethodAccessor 有两种实现方式:一个是 Java 实现的,另一个是 native code 实现的。长久来说,Java 生成字节码的方式性能更好。在我们演示案例中有介绍到 -Dsun.reflect.noInflation=true 参数,使用该参数,则不需要等到第17次反射调用才使用字节码文件,而是一开始就使用字节码文件。

此外,每次反射调用都会检查目标方法的权限,在 Java 正常的方法调用是不会检查权限的。

那么我们接着优化一进行优化。

//JVM参数配置:-Dsun.reflect.noInflation=true

Class klass = Class.forName("com.msdn.java.hotspot.invoke.InvokeCapabilityTest");
Method method = klass.getMethod("target", int.class);
method.setAccessible(true);// 关闭权限检查
long current = System.currentTimeMillis();
Integer num = new Integer(128);
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, num);
}

执行上述代码,测得的结果大约为 151ms,性能进一步提升。

至此,关于反射调用的优化措施全部介绍完毕,接下来我们对反射调用的性能开销和对应的优化措施进行总结。

1、Method.invoke 中的第二个参数是一个可以变长度的 Object 数组,数组中存放的都是对象类型。如果我们存入参数是基本类型,可以提前装箱,减少性能损耗。

2、可以关闭反射调用的 inflation 机制(具体操作为增加 JVM参数:-Dsun.reflect.noInflation=true ),从而取消本地实现,并且直接使用 Java 字节码实现。注意:如果反射调用次数过低,则没必要关闭 inflation 机制。

3、每次反射调用都会检查目标方法的权限,而这个检查同样可以在 Java 代码里关闭。

扩展

通常来说,使用反射 API 的第一步便是获取 Class 对象。在 Java 中常见的有这么三种。

    使用静态方法 Class.forName 来获取。调用对象的 getClass() 方法。直接用类名 +“.class”访问。对于基本类型来说,它们的包装类型(wrapper classes)拥有一个名为“TYPE”的 final 静态字段,指向该基本类型对应的 Class 对象。

除此之外,Class 类和 java.lang.reflect 包中还提供了许多返回 Class 对象的方法。例如,对于数组类的 Class 对象,调用 Class.getComponentType() 方法可以获得数组元素的类型。

一旦得到了 Class 对象,我们便可以正式地使用反射功能了。下面我列举了较为常用的几项。

    使用 newInstance() 来生成一个该类的实例。它要求该类中拥有一个无参数的构造器。使用 isInstance(Object) 来判断一个对象是否该类的实例,语法上等同于 instanceof 关键字(JIT 优化时会有差别,我会在本专栏的第二部分详细介绍)。使用 Array.newInstance(Class,int) 来构造该类型的数组。使用 getFields()/getConstructors()/getMethods() 来访问该类的成员。除了这三个之外,Class 类还提供了许多其他方法,详见[4]。需要注意的是,方法名中带 Declared 的不会返回父类的成员,但是会返回私有成员;而不带 Declared 的则相反。

当获得了类成员之后,我们可以进一步做如下操作。

    使用 Constructor/Field/Method.setAccessible(true) 来绕开 Java 语言的访问限制。使用 Constructor.newInstance(Object[]) 来生成该类的实例。使用 Field.get/set(Object) 来访问字段的值。使用 Method.invoke(Object, Object[]) 来调用方法。
字节码使用附录

打印内联决策。这使您可以查看哪些方法被内联。

-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining

示例:

sun.reflect.GeneratedMethodAccessor1::invoke (124 bytes)
  @ 84   java.lang.Integer::intValue (5 bytes)   accessor
    @ 98   com.msdn.java.hotspot.invoke.InvokeCapabilityTest::target (1 bytes)   inline (hot)
      !              @ 6   sun.reflect.GeneratedMethodAccessor1::invoke (124 bytes)   inline (hot)
        -> TypeProfile (6243/6243 counts) = sun/reflect/GeneratedMethodAccessor1
        @ 84   java.lang.Integer::intValue (5 bytes)   accessor
          @ 98   com.msdn.java.hotspot.invoke.InvokeCapabilityTest::target (1 bytes)   inline (hot)
            @ 15   sun.reflect.Reflection::quickCheckMemberAccess (10 bytes)   inline (hot)
              @ 1   sun.reflect.Reflection::getClassAccessFlags (0 bytes)   (intrinsic)
                @ 6   java.lang.reflect.Modifier::isPublic (12 bytes)   inline (hot)
                  @ 56   sun.reflect.DelegatingMethodAccessorImpl::invoke (10 bytes)   inline (hot)
                    -> TypeProfile (5458/5458 counts) = sun/reflect/DelegatingMethodAccessorImpl
                    !                @ 6   sun.reflect.GeneratedMethodAccessor1::invoke (124 bytes)   inline (hot)
                      -> TypeProfile (6161/6161 counts) = sun/reflect/GeneratedMethodAccessor1
                      @ 84   java.lang.Integer::intValue (5 bytes)   accessor
                        @ 98   com.msdn.java.hotspot.invoke.InvokeCapabilityTest::target (1 bytes)   inline (hot)

如上述结果中的 inline,即表示 target 被内联到 GeneratedMethodAccessor1 的 invoke 方法中。

逃逸分析的相关命令

开启逃逸分析:-XX:+DoEscapeAnalysis,在 JDK1.8 中是默认开启的关闭逃逸分析:-XX:-DoEscapeAnalysis显示分析结果:-XX:+PrintEscapeAnalysis

GC日志输出命令

-verbose:gc 是稳定版本

-XX:+PrintGC 是非稳定版本

两者功能一样,都用于垃圾收集时的信息打印。

参考文献

GC日志查看分析

求你了,GC 日志打印别再瞎配置了

详解 JVM 逃逸分析

逃逸分析

深入理解Java虚拟机:(十三)方法内联

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/756598.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号