`
ayufox
  • 浏览: 273255 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

[字节码系列]JVM字节码初探——常量池和符号解析

阅读更多

      1.常量池
      在符号解析的过程当中,常量池扮演着非常重要的工作。JVM会在常量池中定义如下信息:

  • 字符型数据:utf-8,包括使用常量定义、方法名称、类名称、属性名称等等,这个类型一般用于定义其他类型所关联的字串信息
  • 数字型常量:long、integer、double、float,包括使用到的一些常量定义
  • String常量:string,包括字串常量定义
  • 类和引用信息:包括Class、MethodRef、InterfaceMethodRef、Fieldref、NameAndType信息

      关于常量池中的信息如何组织,看下面的例子就会明白

public class Test
{
private String name;

private int age = 21;

public Test(String name, int age)
{
super();
this.name = name;
this.age = age;
}

public void sayHello(String name)
{
System.out.println("hello, " + name);
}

public String getName()
{
return name;
}

public int getAge()
{
return age;
}
} 

      这么简单的一个例子,符号信息就已经非常地多了

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一样,只是方法是在接口中声明的

       我们看下面的例子

public class BaseTest
{
    public void sayHello2()
    {
        System.out.println("sayHello2");
    }
}

public class Test extends BaseTest
{
    public void sayHello()
    {
        super.sayHello2();//使用invokespecial
        sayHello3();//使用invokestatic
        sayHello4();//使用invokevirtual
    }

    public static void sayHello3()
    {
        System.out.println("sayHello3");
    }

    public void sayHello4()
    {
        System.out.println("sayHello4");
    }
}

     看看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

      如上,在性能上,invokestatic和invokespecial在加载期的时候就可以确定要调用的方法的入口地址,因此性能上是最高的,invokevirtual和invokeinterface必须在运行期根据调用的对象,才能确定要调用的方法。
      实际上我们无法获知虚拟机会如何去实现,我们可以选择使用如下的方式实现:
      每个类型会定义一个方法表,按声明的顺序+父声明在前,我们可以得到如下的表,我们可以看到,对于toString方法,无论调用的对象是哪一个,其偏移数量总是7,在进行方法解析的时候,我们只需要知道每个类方法表的入口和该方法偏移量,则可以知道invokevirtual下一步该进入到哪里。但这种方法无法处理接口的问题,实际上接口的顺序是不确定的,所以对于invokeinterface,可能就需要维持一个map<string,int>,来映射方法声明和偏移量的对应关系了。从这一点上看,大概可以猜测invokevirtual性能上会比invokeinterface要高。
      另外,出于性能的考虑,Sun JVM不希望每次都需要去判断引用的常量池是否已经解析成对应的入口指针,因此在解析过之后,会把相应的方法调用置换成对应的quick指令,这些quick指令不需要判断引用的常量池是否已经解析成对应的入口指针。需要注意的是,这些quick指令是Sun JVM在在运行期替换成的指令,编译的class是不会出现这些指令的

0xd6 invokevirtual_quick
0xd7 invokenonvirtual_quick
0xd8 invokesuper_quick
0xd9 invokestatic_quick
0xda invokeinteface_quick
0xdb invokevirtualobject_quick

 

 

2
2
分享到:
评论
2 楼 ayufox 2010-05-20  
melin 写道
要深入讲讲constant_class_info就好了....
一直关注你的blog,加油鼓励一下...

感谢支持,后续关于解析部分在细节方面再补充一下
1 楼 melin 2010-05-20  
要深入讲讲constant_class_info就好了....
一直关注你的blog,加油鼓励一下...

相关推荐

Global site tag (gtag.js) - Google Analytics