从noInflation看Java Method.invoke

旧文,现重新整理下:

1. 缘起

最近有同事发来如下一段异常,程序已开始运行正常,只是很快就会莫名其妙的抛这个异常,不知道如何着手解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Exception in thread "main" java.lang.NoClassDefFoundError: sun/reflect/ConstructorAccessorImpl
at sun.misc.Unsafe.defineClass(Native Method)
at sun.reflect.ClassDefiner.defineClass(ClassDefiner.java:63)
at sun.reflect.MethodAccessorGenerator$1.run(MethodAccessorGenerator.java:399)
at sun.reflect.MethodAccessorGenerator$1.run(MethodAccessorGenerator.java:394)
at java.security.AccessController.doPrivileged(Native Method)
at sun.reflect.MethodAccessorGenerator.generate(MethodAccessorGenerator.java:393)
at sun.reflect.MethodAccessorGenerator.generateConstructor(MethodAccessorGenerator.java:92)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:55)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at java.lang.Class.newInstance(Class.java:442)
at com.thomas.classloader.unload.MonitorHotSwap.main(MonitorHotSwap.java:34)
Caused by: java.lang.ClassNotFoundException: sun.reflect.ConstructorAccessorImpl
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
...

我把他的代码稍作简化,主要部分是类似下面的逻辑,功能是程序监控某几个文件的变动,发现符合条件就会编译并会重新加载class,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class HotSwapURLClassLoader extends URLClassLoader {
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = null;
clazz = findLoadedClass(name);
if (clazz != null) {
//...一段热替换逻辑
}
if (name.startsWith("java.")) {
try {
ClassLoader system = ClassLoader.getSystemClassLoader();
clazz = system.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
return customLoad(name, this);
}
...
}
class X {
...
public void hot(String msg) {
//System.out.println(String.format("ver:%s, classLoader: %s.", ver, this.getClass().getClassLoader()));
}
}

上面的异常,不同于常遇到的异常,我们根据异常栈逆推可能不会很容易的定位到root cause。所以我在hot方法加了一条日志信息,即注释打开,运行多次,发现每次都是

1
2
ver:15, classLoader: com.thomas.unload.HotSwapURLClassLoader@4e25154f.
Exception in thread "main" java.lang.NoClassDefFoundError: sun/reflect/ConstructorAccessorImpl

这让我想起了JVM参数有inflationThreshold=15

2. 复现

我们可以通过下面demo来深入问题,运行时候加入 -verbose或者 -XX:+TraceClassLoading 参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class App {
public static void main(String[] args) throws Exception {
int size = args.length > 0 ? Integer.valueOf(args[0]) : 1_000_000;
Class<?> clz = Class.forName("X");
Object o = clz.newInstance();
Method m = clz.getMethod("hot", String.class);
Thread.sleep(20_000);
for(int i=0; i<500; i++) {
for(int j=0; j<size; j++){
m.invoke(o, "hello"+i);
}
Thread.sleep(50);
}
System.out.println("---finish loop----");
Thread.sleep(20_000);
}
}

我们可以看到输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
...
[Loaded sun.reflect.DelegatingMethodAccessorImpl from /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/rt.jar]
Hello, hello0
...
Hello, hello14
[Loaded sun.reflect.ClassFileConstants from /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded sun.reflect.AccessorGenerator from /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded sun.reflect.MethodAccessorGenerator from /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded sun.reflect.ByteVectorFactory from /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded sun.reflect.ByteVector from /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded sun.reflect.ByteVectorImpl from /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded sun.reflect.ClassFileAssembler from /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded sun.reflect.UTF8 from /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded sun.reflect.Label from /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded sun.reflect.Label$PatchInfo from /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.util.ArrayList$Itr from /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded sun.reflect.MethodAccessorGenerator$1 from /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded sun.reflect.ClassDefiner from /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded sun.reflect.ClassDefiner$1 from /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded sun.reflect.GeneratedMethodAccessor1 from __JVM_DefineClass__]
Hello, hello15
Hello, hello16
Hello, hello17
[Loaded java.lang.Shutdown from /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/rt.jar]

注意到最开始load的是DelegatingMethodAccessorImpl 可是到第15次执行该方法的时候就开始load MethodAccessorGenerator -> sun.reflect.GeneratedMethodAccessor1了, 前面都是from jre/lib/rt.jar的,而 GeneratedMethodAccessor1 from __JVM_DefineClass__,那么这个类是怎么generate出来的呢?
让我们从Method源码开始吧.

3. Class.getMethod

先看Method的获取,clazz.getMethod获取public方法,这里也可以使用getDeclaredMethod,后者可以获取declared[含private/protect/public]的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@CallerSensitive
public Method getMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true);
Method method = getMethod0(name, parameterTypes, true);
if (method == null) {
throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes));
}
return method;
}
private void checkMemberAccess(int which, Class<?> caller, boolean checkProxyInterfaces) {
final SecurityManager s = System.getSecurityManager();
if (s != null) {
...
final ClassLoader ccl = ClassLoader.getClassLoader(caller);
final ClassLoader cl = getClassLoader0();
if (which != Member.PUBLIC) {
if (ccl != cl) {
s.checkPermission(SecurityConstants.CHECK_MEMBER_ACCESS_PERMISSION);
}
}
this.checkPackageAccess(ccl, checkProxyInterfaces);
}
}

上述,Reflection.getCallerClass()实现是一个native方法,用于获取调用这个方法的Class对象,完整源码见sun/reflect/Reflection.c, 实现最后在jvm.cpp, 这个Reflection.getCallerClass获取栈帧调用者要比我们平时使用Throwable/thread stackTrace/SecurityManager的getClassContext()都要高效,只是1.8抛弃了,目前是JDK内部使用的方法, 而且该方法有CallerSensitive注解。
程序通过forname,即1.8中,forName0(className, true, ClassLoader.getClassLoader(caller), caller) 是通过caller class的classloader加载类,然后调用getMethod,即最终通过
privateGetMethodRecursive,即其实是依赖 searchMethods(privateGetDeclaredMethods(true),从缓存或JVM中获取该Class中的方法列表,searchMethods则用于从返回的方法列表里找到一个匹配的对象,该对象名称和参数的方法匹配。
forname和getMethod具体写起来也很长,但为了不影响本文主题,不深入探讨。
下面,让我们进入正题。

4. MethodAccessor vs ReflectionFactory

使用JAVA版本:java version “1.8.0_102”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public final class Method extends Executable {
...
private volatile MethodAccessor methodAccessor;
// For sharing of MethodAccessors. This branching structure is
// currently only two levels deep (i.e., one root Method and
// potentially many Method objects pointing to it.)
//
// If this branching structure would ever contain cycles, deadlocks can
// occur in annotation code.
private Method root;
@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);
}
private MethodAccessor acquireMethodAccessor() {
// First check to see if one has been created yet, and take it
// if so
MethodAccessor tmp = null;
if (root != null) tmp = root.getMethodAccessor();
if (tmp != null) {
methodAccessor = tmp;
} else {
// Otherwise fabricate one and propagate it up to the root
tmp = reflectionFactory.newMethodAccessor(this);
setMethodAccessor(tmp);
}
return tmp;
}
...
}

JAVA1.8里,Method extends Executable,Executable新见于1.8,是一个适用于method和constructor的通用类,里面有一些paramter和anntation等通用function Executable extends AccessibleObject; 有人可能会问 CallerSensitive 是什么用处,见这里
可以看到实现MethodAccessor代理实现了Method.invoke, MethodAccessor在首次调用初始化进入acquireMethodAccessor方法中,这里root即为自己,root.getMethodAccessor()为null,所以会有reflectionFactory.newMethodAccessor来new一个。
看reflectionFactory.newMethodAccessor(this), 源码位于jdk/src/share/classes/sun/reflect下,点此在线浏览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package sun.reflect;
...
public class ReflectionFactory {
// "Inflation" mechanism. Loading bytecodes to implement
// Method.invoke() and Constructor.newInstance() currently costs
// 3-4x more than an invocation via native code for the first
// invocation (though subsequent invocations have been benchmarked
// to be over 20x faster). Unfortunately this cost increases
// startup time for certain applications that use reflection
// intensively (but only once per class) to bootstrap themselves.
// To avoid this penalty we reuse the existing JVM entry points
// for the first few invocations of Methods and Constructors and
// then switch to the bytecode-based implementations.
//
// Package-private to be accessible to NativeMethodAccessorImpl
// and NativeConstructorAccessorImpl
private static boolean noInflation = false;
private static int inflationThreshold = 15;
...
public MethodAccessor newMethodAccessor(Method method) {
checkInitted();
if (noInflation && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
return new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
} else {
NativeMethodAccessorImpl acc =
new NativeMethodAccessorImpl(method);
DelegatingMethodAccessorImpl res =
new DelegatingMethodAccessorImpl(acc);
acc.setParent(res);
return res;
}
}
}

4.1 noInflation

当noInflation为true,默认false,除非设置 -Dsun.reflect.noInflation=true,则这时由MethodAccessorGenerator生成一个MethodAccessor,具体逻辑跟接下来要讲述的NativeMethodAccessorImpl的一个分支是一样的。

4.2 NativeMethodAccessorImpl

noInflation为false,则通过new NativeMethodAccessorImpl并将其赋给代理类使用(这里不是很清楚为何代理,猜测可能编码简便,也使得只需通过setDelegate方式解除引用,NativeMethodAccessorImpl可以被回收掉)。
让我们看看NativeMethodAccessorImpl的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class NativeMethodAccessorImpl extends MethodAccessorImpl {
private Method method;
private DelegatingMethodAccessorImpl parent;
private int numInvocations;
NativeMethodAccessorImpl(Method method) {
this.method = method;
}
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
// We can't inflate methods belonging to vm-anonymous classes because
// that kind of class can't be referred to by name, hence can't be
// found from the generated bytecode.
if (++numInvocations > ReflectionFactory.inflationThreshold()
&& !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
MethodAccessorImpl acc = (MethodAccessorImpl)
new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
parent.setDelegate(acc);
}
return invoke0(method, obj, args);
}
void setParent(DelegatingMethodAccessorImpl parent) {
this.parent = parent;
}
private static native Object invoke0(Method m, Object obj, Object[] args);
}

可以看到当numInvocations超过ReflectionFactory的inflationThreshold(上文ReflectionFactory默认15 ),这时便会通过new MethodAccessorGenerator生成,也就是对应了上文我们说的ReflectionFactory里的noInflation为true的逻辑。

4.3 NativeMethodAccessorImpl.invoke0

先来看NativeMethodAccessorImpl的invoke0,这是默认的逻辑,即ReflectionFactory提到的
Inflation, Loading bytecodes to implement Method.invoke() and Constructor.newInstance() currently costs 3-4x more than an invocation via native code for the first invocation (though subsequent invocations have been benchmarked to be over 20x faster).
java bytecode版本运行快,但是初始化耗时,而native版本启动快,但若长久运行效率不如bytecode版本。
在有的文章里说,”调用JNI方法耗时“,如马化腾先生说的,这句话对也不对。因为这句话是没有指明的是耗时的究竟是“调用”这个操作还是“JNI方法”,而且这个“调用”指的是哪些(call,参数校验,java和C的参数/结果转换…)?不过还好平时基本不会用到,所以许多技术交流或者面试这么问这么答对的时候不会有什么大问题。
那么究竟是哪个耗时呢?事实上从下面这张火焰图可以明显看出来。

5. JMH BenchMark

MethodAccessorGenerator的代码稍后再做分析,让我们先在此暂停,看一下注释里面提到的二者性能并做一个对比, 这里简化了JMH的日志输出,仅保留VM参数和结果。

5.1 JMH对比结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# JMH 1.10 (released 776 days ago, please consider updating!)
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/bin/java
# VM options: -Djmh.ignoreLock=true -Dsun.reflect.inflationThreshold=180000000
# Warmup: 1 iterations, 1 s each
...
Benchmark (size) Mode Cnt Score Error Units
ReflectiveBenchMarkTest.testDirect 1000000 avgt 3 0.048 ± 0.003 ms/op
ReflectiveBenchMarkTest.testDirect:·threads.alive 1000000 avgt 3 5.000 ± 0.001 threads
ReflectiveBenchMarkTest.testDirect:·threads.daemon 1000000 avgt 3 4.000 ± 0.001 threads
ReflectiveBenchMarkTest.testDirect:·threads.started 1000000 avgt 3 5.000 threads
ReflectiveBenchMarkTest.testInvoke 1000000 avgt 3 290.985 ± 73.334 ms/op
ReflectiveBenchMarkTest.testInvoke:·threads.alive 1000000 avgt 3 5.000 ± 0.001 threads
ReflectiveBenchMarkTest.testInvoke:·threads.daemon 1000000 avgt 3 4.000 ± 0.001 threads
ReflectiveBenchMarkTest.testInvoke:·threads.started 1000000 avgt 3 5.000 threads
------------------
# JMH 1.10 (released 776 days ago, please consider updating!)
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/bin/java
# VM options: -Djmh.ignoreLock=true -Dsun.reflect.inflationThreshold=1
# Warmup: 1 iterations, 1 s each
...
ReflectiveBenchMarkTest.testDirect 1000000 avgt 3 0.049 ± 0.012 ms/op
ReflectiveBenchMarkTest.testDirect:·threads.alive 1000000 avgt 3 5.000 ± 0.001 threads
ReflectiveBenchMarkTest.testDirect:·threads.daemon 1000000 avgt 3 4.000 ± 0.001 threads
ReflectiveBenchMarkTest.testDirect:·threads.started 1000000 avgt 3 5.000 threads
ReflectiveBenchMarkTest.testInvoke 1000000 avgt 3 3.082 ± 1.054 ms/op
ReflectiveBenchMarkTest.testInvoke:·threads.alive 1000000 avgt 3 5.000 ± 0.001 threads
ReflectiveBenchMarkTest.testInvoke:·threads.daemon 1000000 avgt 3 4.000 ± 0.001 threads
ReflectiveBenchMarkTest.testInvoke:·threads.started 1000000 avgt 3 5.000 threads

在我的 Mac 2.9 GHz Intel Core i5,8 GB 1867 MHz DDR3,使用JMH实测几次下来性能差距还是inflationThreshold=1和80000000差距还是蛮大的,约80倍(1000000次分别290.985ms,3.082ms),远大于文中提的20X(1000000次分别0.049ms,3.082ms)[或者测试有误?当然也不排除可能是参数较少的缘故],而优化的invoke与原生的方法访问性能差60倍,但可以计算到,在反射上的耗时是纳秒计的,这在许多业务系统相对而言几乎可以忽略不计的。

5.2 JMH BenchMark代码

(未想到好的测试first invocation 3-4X的方法,故这里只测两种方法性能)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(value = 1, jvmArgsAppend = { "-Djmh.ignoreLock=true", "-Dsun.reflect.inflationThreshold=180000000"})//"-XX:+TraceClassLoading",
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class ReflectiveBenchMarkTest extends MethodTest {
Method m;
Object o;
X xx = new X();
@Param({ "1000", "1000000" })
int size;
@Setup
public void setUp() throws Exception {
Class<?> clz = Class.forName("org.thomas.chs.X");
o = clz.newInstance();
m = clz.getMethod("hot", String.class);
}
@TearDown
public void tearDown() throws Exception {
}
@Benchmark
public void testInvoke() throws Exception {
for (int i = 0; i < size; i++) {
m.invoke(o, "hello");
}
}
@Benchmark
public void testDirect() throws Exception {
for (int i = 0; i < size; i++) {
xx.hot("hello");
}
}
}

6. JAVA Flame Graph

下面是针对上文的App类做的一个JAVA火焰图,代码在这 使用 https://github.com/jvm-profiling-tools/async-profiler 的profiler, 在CentOS 7系统。

1
2
3
4
java -Dsun.reflect.inflationThreshold=10000000 App 10000000
pgrep java
./profiler.sh -d 80 -o flamegraph -f /tmp/traces.txt 7760
../FlameGraph/flamegraph.pl --colors=java /tmp/traces.txt > ../flamegraph.svg


由于blog不支持插入svg,可以点击 这里看svg原件, 上面看各个方法的CPU采样会很清楚。
上面采样基于nflationThreshold=10_000_000, 上图可以看到sun/Freflect/DelegatingMethodAccessorImpl.invoke上方被分成了GeneratedMethodAccessor1和NativeMethodAccessorImpl两部分, 并且他们的耗时比还是很明显的3:1,但其实程序在NativeMethodAccessorImpl循环一次,GeneratedMethodAccessor1循环499次。

7. GeneratedMethodAccessor1是什么?

7.1 GeneratedMethodAccessor1的生成

那么GeneratedMethodAccessor1到底是什么?这要看这里通过ASM生成的字节码了。
可以在线浏览代码MethodAccessorGenerator,这里只贴出部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/** This routine is not thread-safe */
private MagicAccessorImpl generate(final Class<?> declaringClass,
String name,
Class<?>[] parameterTypes,
Class<?> returnType,
Class<?>[] checkedExceptions,
int modifiers,
boolean isConstructor,
boolean forSerialization,
Class<?> serializationTargetClass)
{
ByteVector vec = ByteVectorFactory.create();
asm = new ClassFileAssembler(vec);
this.declaringClass = declaringClass;
this.parameterTypes = parameterTypes;
this.returnType = returnType;
this.modifiers = modifiers;
this.isConstructor = isConstructor;
this.forSerialization = forSerialization;
asm.emitMagicAndVersion();
...
final String generatedName = generateName(isConstructor, forSerialization);
asm.emitConstantPoolUTF8(generatedName);
asm.emitConstantPoolClass(asm.cpi());
thisClass = asm.cpi();
if (isConstructor) {
...
} else {
asm.emitConstantPoolUTF8("sun/reflect/MethodAccessorImpl");
}
asm.emitConstantPoolClass(asm.cpi());
...
targetMethodRef = asm.cpi();
if (isConstructor) {
asm.emitConstantPoolUTF8("newInstance");
} else {
asm.emitConstantPoolUTF8("invoke");
}
invokeIdx = asm.cpi();
if (isConstructor) {
asm.emitConstantPoolUTF8("([Ljava/lang/Object;)Ljava/lang/Object;");
} else {
asm.emitConstantPoolUTF8
("(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;");
}
...
final byte[] bytes = vec.getData();
// Note: the class loader is the only thing that really matters
// here -- it's important to get the generated code into the
// same namespace as the target class. Since the generated code
// is privileged anyway, the protection domain probably doesn't
// matter.
return AccessController.doPrivileged(
new PrivilegedAction<MagicAccessorImpl>() {
public MagicAccessorImpl run() {
try {
return (MagicAccessorImpl)
ClassDefiner.defineClass
(generatedName,
bytes,
0,
bytes.length,
declaringClass.getClassLoader()).newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new InternalError(e);
}
}
});
}

7.2 GeneratedMethodAccessor1长啥样

其实不管你是否了解ASM,通过注释可以看出大概这段jvm code是干啥的了,不过有没有办法得到这段字节码呢?记得有个jvm参数可以获取匿名类的字节码,而这里我们也可以通过javaagent方式得到这段字节码的。如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CustomAgent implements ClassFileTransformer {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new CustomAgent());
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// System.out.println(className);
if (className.indexOf("GeneratedMethodAccessor")!= -1) {
String fileName = className.substring(className.lastIndexOf("/")+1)+".class";
try {
Files.write(Paths.get("/Users/thomaslau/ztemp/java/", fileName), classfileBuffer);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("succed writing" + className);
}
return classfileBuffer;
}
}

Manifest.mf文件指定 Premain-Class: thomas.CustomAgent,之后运行

java -javaagent:/Users/thomaslau/custagent.jar App

得到的GeneratedMethodAccessor1.class见附录
下面需要用到decompile工具,有的使用jd,不过要注意需要支持到1.8,有些不能正确反编译,所以我使用了Luyten+Procyon, 一款Java Decompiler Gui for Procyon,曾经评测得分较高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package sun.reflect;
import java.lang.reflect.*;
public class GeneratedMethodAccessor1 extends MethodAccessorImpl
{
public Object invoke(final Object o, final Object[] array) throws InvocationTargetException {
if (o == null) {
throw new NullPointerException();
}
X x;
String s;
try {
x = (X)o;
if (array.length != 1) {
throw new IllegalArgumentException();
}
s = (String)array[0];
x.hot(s);
return null;
}
catch (ClassCastException | NullPointerException ex) {
final Object o3;
throw new IllegalArgumentException(o3.toString());
}
try {
x.hot(s);
return null;
}
catch (Throwable t) {
throw new InvocationTargetException(t);
}
}
}

7.3 MagicAccessorImpl

最后,generate返回的是MagicAccessorImpl,因为generate这里为ConstructorAccessorImpl和MethodAccessorImpl共用,二者都是MagicAccessorImpl子类,然后再在各自的方法里强制转换一次。 这里package(sun.reflect)属性的MagicAccessorImpl作用是:

is a marker class in the hierarchy. All subclasses of this class are
"magically" granted access by the VM to otherwise inaccessible
fields and methods of other classes. It is used to hold the code
for dynamically-generated FieldAccessorImpl and MethodAccessorImpl
subclasses. (Use of the word "unsafe" was avoided in this class's
name to avoid confusion with {@link sun.misc.Unsafe}.)

这也表明了至少generate底层是支持invoke 到private方法的。
public Object invoke(final Object o, final Object[] array)
从这一句我们也可以看到,即便是GeneratedMethodAccessor1,这个Method的对象/参数/返回都退化成了Object,导致需要一些类型check,但这是由外层方法决定的,MethodHandle避免了这类问题。

8. NativeMethodAccessorImpl.invoke0

最后,让我们看下GeneratedMethodAccessor1之外的选择,invoke0这个native方法,可以从前面的SVG看C代码的调用栈:

CPU采样也可以看到X.hot的cpu占比远小于JVM_InvokeMethod
从NativeAccessor.c找到jvm.cpp, 点这里jvm.cpp,让我们大概了解下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
JVM_ENTRY(jobject, JVM_InvokeMethod(JNIEnv *env, jobject method, jobject obj, jobjectArray args0))
JVMWrapper("JVM_InvokeMethod");
Handle method_handle;
if (thread->stack_available((address) &method_handle) >= JVMInvokeMethodSlack) {
method_handle = Handle(THREAD, JNIHandles::resolve(method));
Handle receiver(THREAD, JNIHandles::resolve(obj));
objArrayHandle args(THREAD, objArrayOop(JNIHandles::resolve(args0)));
oop result = Reflection::invoke_method(method_handle(), receiver, args, CHECK_NULL);
jobject res = JNIHandles::make_local(env, result);
if (JvmtiExport::should_post_vm_object_alloc()) {
oop ret_type = java_lang_reflect_Method::return_type(method_handle());
assert(ret_type != NULL, "sanity check: ret_type oop must not be NULL!");
if (java_lang_Class::is_primitive(ret_type)) {
// Only for primitive type vm allocates memory for java object.
// See box() method.
JvmtiExport::post_vm_object_alloc(JavaThread::current(), result);
}
}
return res;
} else {
THROW_0(vmSymbols::java_lang_StackOverflowError());
}
JVM_END

这里往下就是jvm内部,对JIT的黑盒了. Reflection.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// This would be nicer if, say, java.lang.reflect.Method was a subclass
// of java.lang.reflect.Constructor
oop Reflection::invoke_method(oop method_mirror, Handle receiver, objArrayHandle args, TRAPS) {
oop mirror = java_lang_reflect_Method::clazz(method_mirror);
int slot = java_lang_reflect_Method::slot(method_mirror);
bool override = java_lang_reflect_Method::override(method_mirror) != 0;
objArrayHandle ptypes(THREAD, objArrayOop(java_lang_reflect_Method::parameter_types(method_mirror)));
oop return_type_mirror = java_lang_reflect_Method::return_type(method_mirror);
BasicType rtype;
if (java_lang_Class::is_primitive(return_type_mirror)) {
rtype = basic_type_mirror_to_basic_type(return_type_mirror, CHECK_NULL);
} else {
rtype = T_OBJECT;
}
instanceKlassHandle klass(THREAD, java_lang_Class::as_Klass(mirror));
Method* m = klass->method_with_idnum(slot);
if (m == NULL) {
THROW_MSG_0(vmSymbols::java_lang_InternalError(), "invoke");
}
methodHandle method(THREAD, m);
return invoke(klass, method, receiver, override, ptypes, rtype, args, true, THREAD);
}

这里除了invoke_method,还有上文constructor也对应invoke_constructor,后者主要在initialize检查,没有这些参数校验,但最终也是调用invoke方法。
如果你留意链接里的火焰图,就会发现NativeMethodAccessorImpl.invoke0顶上的一条比较宽的是JavaCalls::call_helper(JavaValue result, methodHandle m, JavaCallArguments* args, TRAPS),没错这里就是最终调用java方法的地方了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void JavaCalls::call_helper(JavaValue* result, methodHandle* m, JavaCallArguments* args, TRAPS) {
methodHandle method = *m;
JavaThread* thread = (JavaThread*)THREAD;
assert(thread->is_Java_thread(), "must be called by a java thread");
assert(method.not_null(), "must have a method to call");
assert(!SafepointSynchronize::is_at_safepoint(), "call to Java code during VM operation");
assert(!thread->handle_area()->no_handle_mark_active(), "cannot call out to Java here");
...
// do call
{ JavaCallWrapper link(method, receiver, result, CHECK);
{ HandleMark hm(thread); // HandleMark used by HandleMarkCleaner
StubRoutines::call_stub()(
(address)&link,
// (intptr_t*)&(result->_value), // see NOTE above (compiler problem)
result_val_address, // see NOTE above (compiler problem)
result_type,
method(),
entry_point,
args->parameters(),
args->size_of_parameters(),
CHECK
);
result = link.result(); // circumvent MS C++ 5.0 compiler bug (result ...
}
}
}

reflection.cpp invoke等代码过长,就不贴这里了,感兴趣的可以移步到Reflection.cpp#invoke

9. 其他

有的会通过机器指令验证,如:

1
2
3
>java -server -Dsun.reflect.inflationThreshold=180000000 -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+PrintCompilation '-XX:CompileCommand=compileonly sun/reflect/NativeMethodAccessorImpl.invoke' App > pure_native_infla18M.txt
Java HotSpot(TM) 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output
>java -server -Dsun.reflect.inflationThreshold=1 -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+PrintCompilation '-XX:CompileCommand=compileonly sun/reflect/GeneratedMethodAccessor1.invoke' App > pure_generate_infla1.txt

上面确实可以看到各种inflationThreshold情况下起作用的是GeneratedMethodAccessor1还是NativeMethodAccessorImpl,不过再看就是看到一些callq指令了,性能分析需要引入其他工具。

10. 附录

1, CallerSensitive其实就是实现

1
2
Returns the class of the caller of the method calling this method, ignoring frames
associated with java.lang.reflect.Method.invoke() and its implementation

的功能,这是一个由大神Brian Goetz于1.8提出的JEP-176,StackOverFlow里讲的比较概括些,CallerSensitive的源码链接
2, isVMAnonymousClass
ReflectUtil

1
2
3
4
5
6
7
8
/**
* Checks if {@code Class cls} is a VM-anonymous class
* as defined by {@link sun.misc.Unsafe#defineAnonymousClass}
* (not to be confused with a Java Language anonymous inner class).
*/
public static boolean isVMAnonymousClass(Class<?> cls) {
return cls.getName().indexOf("/") > -1;
}

什么是VM-anonymous class?JSR292的领导者,亦即为JAVA(java7)引入了动态类型语言支持的John Rose在其Anonymous classes in the VM里面有详述,这是一类独立于class loader、system dictionary的而寄生在hostclass的匿名类(但不是必须),这解决了许多基于JVM的动态语言的问题,OpenJDK Multi-Language VM project
定义在Unsafe的一个native实现,具体实现在Unsafe.cpp,源码点击此处TODO!!.
John Rose贡献了MethodHandles等实现,比如常用于JRuby/Jython等语言的invokedynamic指令,即

1
2
Add a new bytecode, invokedynamic, that supports efficient and flexible
execution of method invocations in the absence of static type information.

事实上,JSR292最初分为三部分,AnonymousClassLoader、MethodHandle(s)和invokedynamic, 1.7主要用AnonymousClass来实现动态语言支持,1.8之后则提供了invokedynamic使用,目前看应该是二者混用的,不过AnonymousClassLoader在1.7声称因非标准API会被废弃掉,目前依旧是声明状态, 因为1.8的lambda实现中LambdaMetaFactory正是通过Unsafe.defineAnonymousClass将ASM生成的class插入到original 即host class。
Nashorn项目也在用它loading Nashorn script code, 至少截止20170725是如此。
invokedynamic于1.7引入,但实际上1.8时javac才支持的。
具体目前还未看透,后面可能会再写文章再深入探讨。
3,VM-anonymous class与匿名类(anonymous class)的区别?
最大的区别是,匿名类经过javac之后是一个有名字(XX.$1)的正常的java class,前者不是,可见上文所述。
4,invokeDynamic
Java语言架构师Brian Goetz也在JSR 335提到jAVA Lambda实现Translation of Lambda Expressions…确实太多了,而Reflection与MethodHandle/dynamic区别,简单来讲可以认为Reflection像是java代码层次,MethodHandle是jvm层次,前者局限于java,后者则适用基于JVM的语言。1.8中InvokeDynamic用于lambda对象的创建,实际调用方法还是invokevirtual/inter../speci.., 可以看看MethodHandles and Invokedynamic, Invokedynamic-101, Invokedynamic-Javas-secret-weapon, 或周志明f写的infoq的文章 解析JDK 7的动态类型语言支持, blog签名有趣的JRuby专家first-taste-of-invokedynamic, Oracle一篇tech noteJava Virtual Machine Support for Non-Java Languages, compiling-lambda-expressions-scala-vs-java-8,有趣的perl6贡献者的Using invoke dynamic to teach the JVM a new language 这些文章有内容过时但值得一看.
具体实现如何,可能需要再写文章探讨
5,关于JNI调用耗时
推荐看看what-makes-jni-calls-slow, 其实“慢”,这么说没有意义,要看怎么比,要看具体逻辑。
而实际上,使用jni实现和java原生实现相比,理论上导致差距的地方有,因为jni像个黑盒不会內联不会jit优化,crossing boundaries问题,JAVA和native之间交换数据可能需要拷贝(java访问jni的返回/jni访问java对象)
R神的hotspot intrinsics API
6,privateGetMethodRecursive
代码可见Class.java的privateGetMethodRecursive实现, 如果你仔细看了clazz.newInsstance这块,会发现有 ReflectionData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static class ReflectionData<T> {
volatile Field[] declaredFields;
volatile Field[] publicFields;
volatile Method[] declaredMethods;
volatile Method[] publicMethods;
volatile Constructor<T>[] declaredConstructors;
volatile Constructor<T>[] publicConstructors;
// Intermediate results for getFields and getMethods
volatile Field[] declaredPublicFields;
volatile Method[] declaredPublicMethods;
volatile Class<?>[] interfaces;
// Value of classRedefinedCount when we created this ReflectionData instance
final int redefinedCount;
ReflectionData(int redefinedCount) {
this.redefinedCount = redefinedCount;
}
}

这里面有 Constructor,在newReflectionData,newInstance都是可关注的地方,这里Constructor的构建,其实也是类似本文Method的逻辑,通过ReflectionFactory.newConstructorAccessor提供了一个基于native的NativeConstructorAccessorImpl实现,而另一个就是同样的基于asm bytecode的MethodAccessorGenerator().generateConstructor实现,但是有一个例外,当。

1
2
3
4
5
6
7
// Bootstrapping issue: since we use Class.newInstance() in
// the ConstructorAccessor generation process, we have to
// break the cycle here.
if (Reflection.isSubclassOf(declaringClass,
ConstructorAccessorImpl.class)) {
return new BootstrapConstructorAccessorImpl(c);
}

满足上述条件时,返回的是BootstrapConstructorAccessorImpl,而它然后就直接通过 UnsafeFieldAccessorImpl.unsafe.allocateInstance(constructor.getDeclaringClass())来newInstance的。
7, GeneratedMethodAccessor1.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public java.lang.Object invoke(java.lang.Object, java.lang.Object[]) throws java.lang.reflect.InvocationTargetException;
descriptor: (Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
flags: ACC_PUBLIC
Code:
stack=5, locals=3, args_size=3
0: aload_1
1: ifnonnull 12
4: new #20 // class java/lang/NullPointerException
7: dup
8: invokespecial #28 // Method java/lang/NullPointerException."<init>":()V
11: athrow
12: aload_1
13: checkcast #6 // class X
16: aload_2
17: arraylength
18: sipush 1
21: if_icmpeq 32
24: new #22 // class java/lang/IllegalArgumentException
27: dup
28: invokespecial #29 // Method java/lang/IllegalArgumentException."<init>":()V
31: athrow
32: aload_2
33: sipush 0
36: aaload
37: checkcast #14 // class java/lang/String
40: invokevirtual #10 // Method X.hot:(Ljava/lang/String;)V
43: aconst_null
44: areturn
45: invokespecial #42 // Method java/lang/Object.toString:()Ljava/lang/String;
48: new #22 // class java/lang/IllegalArgumentException
51: dup_x1
52: swap
53: invokespecial #32 // Method java/lang/IllegalArgumentException."<init>":(Ljava/lang/String;)V
56: athrow
57: new #24 // class java/lang/reflect/InvocationTargetException
60: dup_x1
61: swap
62: invokespecial #35 // Method java/lang/reflect/InvocationTargetException."<init>":(Ljava/lang/Throwable;)V
65: athrow

8, JAVA StackTrace方法的性能可以参考 stackoverflow的一个问答how-do-i-find-the-caller-of-a-method-using-stacktrace-or-reflection

11. 引用

  1. 关于反射调用方法的一个log, RednaxelaFX, 也就是国内已知公认JVM专家R大,也即许多ppt作者为 莫枢得,写过一篇文章讨论了这个问题
  2. 关于Java Flame,这里列出几篇参考文章:
  3. Brendan Gregg的站点,记录了些高效的Linux性能工具
  4. 两篇不错的文章 flamegraphs-intro-fire-for-everyone,The Pros and Cons of AsyncGetCallTrace Profilers
  5. 几个常用参考页面:
    Oracle官方的 Java HotSpot VM Options, VM参数中文版,有些已过期
    Oracle官方的 JAVA Options,R大的 JVM调优的”标准参数”的各种陷阱],可以参考HotSpot VM里的各个globals.hpp文件