JVM学习笔记6:反射

反射的实现

  • 横向比对C#,几乎是是一样的

核心是invoke函数

1
2
3
4
5
6
7
8
9
10
11
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);
}
}

调用过程

  • 如果打印stack trace可以看到西面的代码,从中可以看到java的反射本质是依次调用DelegatingMethodAccessorImplNativeMethodAccessorImpl,最后调用对象方法。
  • 这个思路说白了就是在C++层面(native开头的API)查询到对象函数的地址,然后调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
method.invoke(null, 0);
}
}

-------------------------------------------

java.lang.Exception: #0
at Test.target(Test.java:5)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl .invoke0(Native Method)
a t java.base/jdk.internal.reflect.NativeMethodAccessorImpl. .invoke(NativeMethodAccessorImpl.java:62)
t java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.i .invoke(DelegatingMethodAccessorImpl.java:43)
java.base/java.lang.reflect.Method.invoke(Method.java:564)
t Test.main(Test.java:131
  • 当多次调用(这个次数由Dsun.reflect.inflationThreshold决定)后,JVM会动态构建一个函数的字节码,并将委派实现的委派对象切换至动态实现,这个过程我们称之为 Inflation。
1
2
3
4
5
6
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)

反射的开销

外部消耗

  • 从上面的实现可以看出,反射需要调用forName、getMethod等方法,这些方法调用本身就会产生消耗。Class.forName 会调用本地方法,Class.getMethod 则会遍历该类的公有方法。如果没有匹配到,它还将遍历父类的公有方法。
  • 以 getMethod 为代表的查找方法操作,会返回查找得到结果的一份拷贝。因此,我们应当避免在热点代码中使用返回 Method 数组的 getMethods 或者 getDeclaredMethods 方法,以减少不必要的堆空间消耗。

内部消耗

  • 在invoke函数中需要传递一个object数组,这个是会产生内存消耗的。

  • 同时,基础类型转object有装箱消耗。不过java会对[-128,127]之间的数字对应的Integer对象有cache。

  • 还有java会做逃逸分析,也就是说下面代码中invoke函数中的object会被认为是不逃逸的,那么即时编译器可以选择栈分配甚至是虚拟分配,也就是不占用堆空间。

  • 最后是内联函数的实现,由于 Java 虚拟机调用点的类型 profile(注:对于 invokevirtual 或者 invokeinterface,Java 虚拟机会记录下调用者的具体类型,我们称之为类型 profile)无法同时记录这么多个类,因此可能造成所测试的反射调用没有被内联的情况。可以提高 Java 虚拟机关于每个调用能够记录的类型数目(对应虚拟机参数 -XX:TypeProfileWidth,默认值为 2,这里设置为 3)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);

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);
}
}
}
# Java
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×