1.常量池
在符号解析的过程当中,常量池扮演着非常重要的工作。JVM会在常量池中定义如下信息:
- 字符型数据:utf-8,包括使用常量定义、方法名称、类名称、属性名称等等,这个类型一般用于定义其他类型所关联的字串信息
- 数字型常量:long、integer、double、float,包括使用到的一些常量定义
- String常量:string,包括字串常量定义
- 类和引用信息:包括Class、MethodRef、InterfaceMethodRef、Fieldref、NameAndType信息
关于常量池中的信息如何组织,看下面的例子就会明白
1.public class Test
2.{
3.private String name;
4.
5.private int age = 21;
6.
7.public Test(String name, int age)
8.{
9.super();
10.this.name = name;
11.this.age = age;
12.}
13.
14.public void sayHello(String name)
15.{
16.System.out.println("hello, " + name);
17.}
18.
19.public String getName()
20.{
21.return name;
22.}
23.
24.public int getAge()
25.{
26.return age;
27.}
28.}
|
这么简单的一个例子,符号信息就已经非常地多了
const #1 = class #2; // test/Test
const #2 = Utf-8 test/Test;
const #3 = class #4; // java/lang/Object
const #4 = Utf-8 java/lang/Object;
const #5 = Utf-8 name;
const #6 = Utf-8 Ljava/lang/String;;
const #7 = Utf-8 age;
const #8 = Utf-8 I;
const #9 = Utf-8 <init>;
const #10 = Utf-8 (Ljava/lang/String;I)V;
const #11 = Utf-8 Code;
const #12 = Method #3.#13; // java/lang/Object."<init>":()V
const #13 = NameAndType #9:#14;// "<init>":()V
const #14 = Utf-8 ()V;
const #15 = Field #1.#16; // Test.name:Ljava/lang/String;
const #16 = NameAndType #5:#6;// name:Ljava/lang/String;
const #17 = Field #1.#18; // Test.age:I
const #18 = NameAndType #7:#8;// age:I
const #19 = Utf-8 LineNumberTable;
const #20 = Utf-8 LocalVariableTable;
const #21 = Utf-8 this;
const #22 = Utf-8 LTest;;
const #23 = Utf-8 sayHello;
const #24 = Utf-8 (Ljava/lang/String;)V;
const #25 = Field #26.#28; // java/lang/System.out:Ljava/io/PrintS
tream;
const #26 = class #27; // java/lang/System
const #27 = Utf-8 java/lang/System;
const #28 = NameAndType #29:#30;// out:Ljava/io/PrintStream;
const #29 = Utf-8 out;
const #30 = Utf-8 Ljava/io/PrintStream;;
const #31 = class #32; // java/lang/StringBuilder
const #32 = Utf-8 java/lang/StringBuilder;
const #33 = String #34; // hello,
const #34 = Utf-8 hello, ;
const #35 = Method #31.#36; // java/lang/StringBuilder."<init>":(Lj
ava/lang/String;)V
const #36 = NameAndType #9:#24;// "<init>":(Ljava/lang/String;)V
const #37 = Method #31.#38; // java/lang/StringBuilder.append:(Ljav
a/lang/String;)Ljava/lang/StringBuilder;
const #38 = NameAndType #39:#40;// append:(Ljava/lang/String;)Ljava/lang/String
Builder;
const #39 = Utf-8 append;
const #40 = Utf-8 (Ljava/lang/String;)Ljava/lang/StringBuilder;;
const #41 = Method #31.#42; // java/lang/StringBuilder.toString:()L
java/lang/String;
const #42 = NameAndType #43:#44;// toString:()Ljava/lang/String;
const #43 = Utf-8 toString;
const #44 = Utf-8 ()Ljava/lang/String;;
const #45 = Method #46.#48; // java/io/PrintStream.println:(Ljava/l
ang/String;)V
const #46 = class #47; // java/io/PrintStream
const #47 = Utf-8 java/io/PrintStream;
const #48 = NameAndType #49:#24;// println:(Ljava/lang/String;)V
const #49 = Utf-8 println;
const #50 = Utf-8 getName;
const #51 = Utf-8 getAge;
const #52 = Utf-8 ()I;
const #53 = Utf-8 SourceFile;
const #54 = Utf-8 Test.java;
|
从上面我们可以看出各种类型的大概组织结构
- Utf-8格式:1个字节的tag标志,表明类型、2个字节的长度信息length,表示字串的长度,length个字节的字串,譬如如上的#2
- Integer/float格式:1个字节的tag标志,4个字节的内容,对于在class文件中出现的大于2个字节能够表示的整型数值,会在常量池中出现,并使用ldc指令,小于等于1个字节的值,会使用bipush指令,大于1个字节小于2个字节的值,会使用sipush,在后面例子中我们会看到
- Long/double格式:1个字节的tag标志,8个字节的内容
- String格式:1个字节的tag标志,2个字节的常量池utf-8常量的偏移量
- Class格式:1个字节的tag标志,2个字节的常量池utf-8常量的偏移量,见如上的#1,会引用到#2
- FieldRef/MethodRef(InterfaceMethodRef)格式:1个字节的tag标志,2个字节的常量池class常量偏移量,表明所属的类,2个字节的NameAndType常量偏移量,表明属性名称和类型/方法名称和类型,例如如上的#17/#12
- NameAndType:1个字节的tag标志,2个字节的utf-8常量偏移量,表明名称,2个字节的utf-8常量偏移量,表明类型信息,例如如上的#16
常量池定义了所有在字节码执行过程当中我们需要使用到的所有的符号的信息,实际上对于在加载器解析JVM,只需要获得常量池,就可以知道需要去处理哪些符号的解析。符号解析的过程实际就是将常量池中的符号转换成实际入口地址的过程
2.方法调用
我们这里重点关注方法调用的指令
- invokestatic:调用静态方法
- invokespecial:调用特定的方法,指的是不会根据对象实例的变化而变化的方法,譬如调用父类的方法等
- invokevirtual:调用虚方法,具体调用的方法与调用的对象有关
- invokeinterface:与invokevirtual一样,只是方法是在接口中声明的
我们看下面的例子
Java代码
1.public class BaseTest
2.{
3. public void sayHello2()
4. {
5. System.out.println("sayHello2");
6. }
7.}
8.
9.public class Test extends BaseTest
10.{
11. public void sayHello()
12. {
13. super.sayHello2();//使用invokespecial
14. sayHello3();//使用invokestatic
15. sayHello4();//使用invokevirtual
16. }
17.
18. public static void sayHello3()
19. {
20. System.out.println("sayHello3");
21. }
22.
23. public void sayHello4()
24. {
25. System.out.println("sayHello4");
26. }
27.}
|
看看sayHello的字节码
public void sayHello();
Code:
Stack=1, Locals=1, Args_size=1
0: aload_0 //将方法第一参数的地址推入栈顶,方法第一个参数就是this
1: invokespecial #15; //Method sayHello2:()V,这里字节码由操作码和常量池的
//偏移量组成,这里使用invokespecial是因为可以确定调用的就是BaseTest的sayHello2方法,另外调用的时候会根据方法需要的参数从栈顶弹出相应的参数
4: invokestatic #18; //Method sayHello3:()V,static方法不需要this参数
7: aload_0
8: invokevirtual #21; //Method sayHello4:()V,这里使用invokevirtual是因为Test有可能被继承,如果Test被继承而且继承类重载了sayHello4方法,调用的则会是继承类的sayHello方法
11: return
|
public void sayHello();
Code:
Stack=1, Locals=1, Args_size=1
0: aload_0 //将方法第一参数的地址推入栈顶,方法第一个参数就是this
1: invokespecial #15; //Method sayHello2:()V,这里字节码由操作码和常量池的
//偏移量组成,这里使用invokespecial是因为可以确定调用的就是BaseTest的sayHello2方法,另外调用的时候会根据方法需要的参数从栈顶弹出相应的参数
4: invokestatic #18; //Method sayHello3:()V,static方法不需要this参数
7: aload_0
8: invokevirtual #21; //Method sayHello4:()V,这里使用invokevirtual是因为Test有可能被继承,如果Test被继承而且继承类重载了sayHello4方法,调用的则会是继承类的sayHello方法
11: return
0xd6 invokevirtual_quick
0xd7 invokenonvirtual_quick
0xd8 invokesuper_quick
0xd9 invokestatic_quick
0xda invokeinteface_quick
0xdb invokevirtualobject_quick
|