Java虚拟机之方法引用实现原理(上)

本篇主要涉及四种方法调用指令:invokestatic、invokespecial、invokevirtual以及invokeinterface。invokestatic和invokespecial指令调用的方法,都可以在解析阶段确定唯一调用版本,在类加载的时候就把符号引用解析为该方法的直接引用,剩下两种需要在运行时再确定。此外,final方法是非虚方法。

解析方法的符号引用

如无特别强调,下文中提到的参数个数表示参数所占Slot_实例数。

节约篇幅这里只列举解析非接口方法符号引用的例子(接口方法类似),首先放出代码:

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 static Method_ resolveMethodRef(MethodRef methodRef) {
// 当前代码所处的类d
Class_ d = methodRef.getRuntimeConstantPool().getClass_();
// 要解析的非接口方法所属的类或接口c
Class_ c = methodRef.getClass_();
if (c == null) {
c = ResolvedRef.resolvedClassRef(methodRef.getClassName(), methodRef.getRuntimeConstantPool());
}
// 判断c是否是接口,如果是则抛出IncompatibleClassChangeError异常
if (c.isInterface()) {
throw new IncompatibleClassChangeError();
}
// 注释见lookupMethod实现
Method_ method = methodRef.lookupMethod(c, methodRef.getName(), methodRef.getDescriptor());
// 执行到这一步如果仍然没有查找到符合条件的方法,则抛出NoSuchMethodError异常
if (method == null) {
throw new NoSuchMethodError();
}
// 查找到了方法,返回了直接引用,还要在这一步进行权限验证,当前类应该能访问查找到的方法,
// 否则抛出IllegalAccessError异常
if (!method.isAccessibleTo(d)) {
throw new IllegalAccessError();
}
return method;
}

解析的步骤:

  1. 解析方法出所属的类,判断是否是接口;
  2. 在继承中查找方法;
  3. 判断方法是否查找到;
  4. 判断当前类是否对解析的方法有访问权限;

由于Java 8之后接口可以拥有默认方法,所以在继承中查找方法未遂将继续在接口中查找。具体查找方式是通过比较简单名和描述符来实现的:

1
if (method.getName().equals(name) && method.getDescriptor().equals(descriptor))

方法参数计数

一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ClassFileDemo {
int a;
int b;
public void add01() {
a = a + 1;
}
public static String add02(int x, int y, String s, ClassFileDemo a) {
return "hehe";
}
public static void main(String[] args) {
add02(11, 13, "nihao", new ClassFileDemo());
ClassFileDemo aa = new ClassFileDemo();
ClassFileDemo bb = aa;
aa = null;
System.out.println(aa == null);
}
}

将其编译后用javap反编译字节码:

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
Compiled from "ClassFileDemo.java"
public class ClassFileDemo {
int a;
int b;
public ClassFileDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void add01();
Code:
0: aload_0
1: aload_0
2: getfield #2 // Field a:I
5: iconst_1
6: iadd
7: putfield #2 // Field a:I
10: return
public static java.lang.String add02(int, int, java.lang.String, ClassFileDemo);
Code:
0: ldc #3 // String hehe
2: areturn
public static void main(java.lang.String[]);
Code:
0: bipush 11
2: bipush 13
4: ldc #4 // String nihao
6: new #5 // class ClassFileDemo
9: dup
10: invokespecial #6 // Method "<init>":()V
13: invokestatic #7 // Method add02:(IILjava/lang/String;LClassFileDemo;)Ljava/lang/String
;
16: pop
17: new #5 // class ClassFileDemo
20: dup
21: invokespecial #6 // Method "<init>":()V
24: astore_1
25: aload_1
26: astore_2
27: aconst_null
28: astore_1
29: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
32: aload_1
33: ifnonnull 40
36: iconst_1
37: goto 41
40: iconst_0
41: invokevirtual #9 // Method java/io/PrintStream.println:(Z)V
44: return
}

add02方法的描述符是(IILjava/lang/String;LClassFileDemo;)Ljava/lang/String ;,下面给出本人计算传入参数个数的思路:

  • 用正则表达式计算出括号内的字符串;
  • 从上一步计算出的字符串中以非贪婪的方式解析出L开头;结尾的字符串,求出此类字符串出现的次数r;
  • 从第一步计算出的字符串中剔除第二步算出的字符串部分,计算非DJ的个数m以及DJ的个数n;
  • 参数个数即为r + m + 2 * n(如果方法不是静态还需要在此基础上加1);

用代码实现即:

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
public static int countMethod(String descriptor) {
String paramsString = null;
// test:String descriptor = "(IILjava/lang/String;LClassFileDemo;)Ljava/lang/String;";
// 贪婪式
Pattern pattern = Pattern.compile("(?<=\\().*(?=\\))");
Matcher matcher = pattern.matcher(descriptor);
if (matcher.find()) {
// 计算出传入描述符字符串括号内的子串
paramsString = matcher.group();
}
if (paramsString == null || paramsString.equals("")) {
return 0;
}
// 非贪婪式
Pattern pattern1 = Pattern.compile("L.*?;");
Matcher matcher1 = pattern1.matcher(paramsString);
// 引用类参数的个数
int refCount = 0;
while (matcher1.find()) {
refCount++;
}
// 非引用类型部分的参数
String nonRefPart = matcher1.replaceAll("");
int nonRefCount = 0;
char[] chars = nonRefPart.toCharArray();
for (char c : chars) {
switch (c) {
case 'B':
case 'C':
case 'F':
case 'I':
case 'S':
case 'Z':
nonRefCount++;
break;
case 'D':
case 'J':
nonRefCount += 2;
break;
default:
break;
}
}
return refCount + nonRefCount;
}

操作数栈到局部变量表的映射逻辑

在main方法中观察add02()被调用前后的字节码指令,可知将传入被调用方法的参数首先会被压入当前帧的操作数栈,而在Java中,一个方法如果是静态的,那么参数会从方法局部变量表的0号位置开始依次保存。所以当前帧的操作数栈中的元素和被调用方法的参数的对应关系如下:

虚拟机方法调用

根据对应关系给出核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
int argSlots = method.getArgSlotCount();
if (argSlots > 0) {
for (int i = argSlots - 1; i >= 0; i--) {
// 方法参数首先是存储在调用方法的栈帧的操作数栈中
Slot_ slot = invokerFrame.getOperandStack().popSlot();
// 将弹出的参数存储在新创建的局部变量表中
newFrame.getLocalVars().setSlot(i, slot);
}
}

总结

本篇讲解方法符号引用的解析,方法参数的计数和参数的映射,下篇将完成四种指令的实现。

参考

正则表达式全集

正则表达式在线测试