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<Student> 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<Void>() {
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<Student> 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
是非稳定版本
两者功能一样,都用于垃圾收集时的信息打印。