`
ayufox
  • 浏览: 276988 次
  • 性别: 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,加油鼓励一下...

相关推荐

    HelloWorld的javap -verbose HelloWorld 字节码初探

    在描述中提到的链接是一个博客文章,可能详细解释了如何使用`javap -verbose`选项来解析和解释字节码。`-verbose`选项提供了更详细的输出,包括类加载、主方法、以及每个方法的字节码和局部变量表等信息。 标签...

    初探JVM内存区域

    在类加载过程中,运行时常量池会进行解析,转化为直接引用。 7. **直接内存(Direct Memory)** 不属于JVM规范定义的内存区域,但在高性能应用中常见,如NIO(New Input/Output)库,通过直接内存可以绕过Java堆,...

    初探ASM

    1. **创建类装载器**:ASM使用自定义类装载器来读取和解析.class文件,或者直接生成字节码。 2. **访问框架**:通过`ClassWriter`对象,可以创建新类或修改现有类。ASM提供了一系列的`Visitor`接口,如`...

    JVM初探内存分配GC原理与垃圾收集器共16页.pdf.z

    4. **程序计数器(Program Counter Register)**:每个线程都有一个独立的程序计数器,记录当前线程正在执行的字节码的地址。 5. **本地方法栈(Native Method Stack)**:与Java方法执行对应的栈是虚拟机栈,而...

    基于计算机软件开发的JAVA编程应用初探.pdf

    JVM作为Java程序的运行环境,可以在不同的操作系统上加载和执行字节码,这使得Java编写的程序具备了极高的可移植性。这种特性对开发人员来说无疑减轻了重复劳动和调试的负担,也使得软件开发和部署更加高效。 其次...

    2022年初探Java类加载机制Java教程.docx

    Java类加载机制是Java虚拟机(JVM)中的一种机制,负责将类从字节码文件加载到内存中,并将其转换为可执行的类对象。在Java中,类加载机制是通过ClassLoader来完成的,该机制在JDK 1.2以后变得更加复杂和灵活。 类...

    第一章 初识Java1

    - JVM(Java虚拟机)是Java程序执行的基础,它负责将源代码文件(`.java`文件)编译成字节码文件(`.class`文件),这些字节码文件可以在任何支持JVM的平台上运行,从而实现了Java的“一次编写,到处运行”的特性。...

    初探Java类加载机制

    让我们假设有一个class字节码文件(比如Hello.class文件),那么在应用程序中,他是如何被加载进来,并形成一个类对象的呢?我们这篇文章的目的就是为了解释这个问题。 在java.lang包里有个ClassLoader类,...

    java实现简单投票程序

    它的设计理念是“一次编写,到处运行”,因为Java代码会被编译成字节码,可以在任何支持Java虚拟机(JVM)的平台上运行。 接着,我们来看“投票程序”的概念。在计算机科学中,投票程序通常包含以下功能: 1. 注册...

    HelloWorld

    《HelloWorld——Java编程初探》 在编程世界中,“Hello, World!”是最常见的入门程序,它标志着一段新的编程旅程的开始。对于Java语言来说,"HelloWorld"同样扮演着这样的角色,它帮助初学者理解如何在Java环境中...

    MyFirstApp

    8. **编译与运行**:Java程序需要通过javac编译器编译成字节码(.class文件),然后通过Java虚拟机(JVM)运行。`javac`命令用于编译,`java`命令用于运行。 9. **Maven或Gradle构建工具**:虽然题目中没有明确提及...

    MyFirstCalculator:必须做,因为有时我不做我的工作!

    源代码编写完成后,通过编译器转换成字节码,然后在Java虚拟机(JVM)上运行,实现了“一次编写,到处运行”的理念。 在"MyFirstCalculator"项目中,开发者很可能是使用了Java Swing库来创建图形用户界面(GUI)。...

    DLP_Repository:DLP(编程语言设计)实践的存储库

    这可能涉及到字节码的理解和生成,以及对JVM指令集的熟悉。 4. **类型系统**:编程语言的类型系统是其关键特性之一,DLP_Repository可能会探讨静态类型和动态类型的实现,包括类型检查、类型推断和多态性。 5. **...

    hello-world:实践

    这将会生成一个`HelloWorld.class`文件,这是编译后的字节码文件,可以被Java虚拟机执行。然后,通过以下命令运行程序: ```bash java HelloWorld ``` 你应该能在终端看到"你好,世界"的输出,这意味着你成功地...

    My-First-Java-Program:弓

    - 使用`javac RockPaperScissors.java`命令编译源代码,这将生成一个名为`RockPaperScissors.class`的字节码文件。 - 运行程序使用`java RockPaperScissors`命令,这会启动Java虚拟机(JVM),执行程序。 5. **...

Global site tag (gtag.js) - Google Analytics