Java class文件格式之常量池_动力节点Java学院整理

常量池中各数据项类型详解

常量池中的数据项是通过索引来引用的, 常量池中的各个数据项之间也会相互引用。在这11中常量池数据项类型中, 有两种比较基础, 之所以说它们基础, 是因为这两种类型的数据项会被其他类型的数据项引用。 这两种数据类型就是CONSTANT_Utf8 和 CONSTANT_NameAndType , 其中CONSTANT_NameAndType类型的数据项(CONSTANT_NameAndType_info)也会引用CONSTANT_Utf8类型的数据项(CONSTANT_Utf8_info) 。 与其他介绍常量池的书籍或其他资料不同, 本着循序渐进和先后分明的原则, 我们首先对这两种比较基本的类型的数据项进行介绍, 然后再依次介绍其他9中数据项。

(1) CONSTANT_Utf8_info

一个CONSTANT_Utf8_info是一个CONSTANT_Utf8类型的常量池数据项, 它存储的是一个常量字符串。 常量池中的所有字面量几乎都是通过CONSTANT_Utf8_info描述的。下面我们首先讲解CONSTANT_Utf8_info数据项的存储格式。在前面的文章中, 我们提到, 常量池中数据项的类型由一个整型的标志值(tag)决定, 所以所有常量池类型的info中都必须有一个tag信息, 并且这个tag值位于数据项的第一个字节上。 一个11中常量池数据类型, 所以就有11个tag值表示这11中类型。而CONSTANT_Utf8_info的tag值为1, 也就是说如果虚拟机要解析一个常量池数据项, 首先去读这个数据项的第一个字节的tag值, 如果这个tag值为1, 那么就说明这个数据项是一个CONSTANT_Utf8类型的数据项。 紧挨着tag值的两个字节是存储的字符串的长度length, 剩下的字节就存储着字符串。 所以, 它的格式是这样的:

其中tag占一个字节, length占2个字节, bytes代表存储的字符串, 占length字节。所以, 如果这个CONSTANT_Utf8_info存储的是字符串"Hello", 那么他的存储形式是这样的:

现在我们知道了CONSTANT_Utf8_info数据项的存储形式, 那么CONSTANT_Utf8_info数据项都存储了什么字符串呢? CONSTANT_Utf8_info可包括的字符串主要以下这些:

  • 程序中的字符串常量
  • 常量池所在当前类(包括接口和枚举)的全限定名
  • 常量池所在当前类的直接父类的全限定名
  • 常量池所在当前类型所实现或继承的所有接口的全限定名
  • 常量池所在当前类型中所定义的字段的名称和描述符
  • 常量池所在当前类型中所定义的方法的名称和描述符
  • 由当前类所引用的类型的全限定名
  • 由当前类所引用的其他类中的字段的名称和描述符
  • 由当前类所引用的其他类中的方法的名称和描述符
  • 与当前class文件中的属性相关的字符串, 如属性名等

总结一下, 其中有这么五类: 程序中的字符串常量, 类型的全限定名, 方法和字段的名称, 方法和字段的描述符, 属性相关字符串。 程序中的字符串常量不用多说了, 我们经常使用它们创建字符串对象, 属性相关的字符串, 等到讲到class中的属性信息(attibute)时自会提及。 方法和字段的名称也不用多说了 。 剩下的就是类型的全限定名,方法和字段的描述符 。 还有一点需要说明, 类型的全限定名, 方法和字段的名称, 方法和字段的描述符, 可以是本类型中定义的, 也可能是本类中引用的其他类的。

下面我们通过一个例子来进行说明。 示例源码:

package com.jg.zhang;

public class Programer extends Person {

 static String company = "CompanyA";

 static{
 System.out.println("staitc init");
 }

 String position;
 Computer computer;

 public Programer() {
 this.position = "engineer";
 this.computer = new Computer();
 }

 public void working(){
 System.out.println("coding...");
 computer.working();
 }
}

别看这个类简单, 但是反编译后, 它的常量池有53项之多。 在这53项常量池数据项中, 各种类型的数据项都有, 当然也包括不少的CONSTANT_Utf8_info 。 下面只列出反编译后常量池中的CONSTANT_Utf8_info 数据项:

#2 = Utf8    com/jg/zhang/Programer   //当前类的全限定名
#4 = Utf8    com/jg/zhang/Person    //父类的全限定名
#5 = Utf8    company       //company字段的名称
#6 = Utf8    Ljava/lang/String;    //company和position字段的描述符
#7 = Utf8    position      //position字段的名称
#8 = Utf8    computer      //computer字段的名称
#9 = Utf8    Lcom/jg/zhang/Computer;   //computer字段的描述符
#10 = Utf8    <clinit>      //类初始化方法(即静态初始化块)的方法名
#11 = Utf8    ()V        //working方法的描述符
#12 = Utf8    Code       //Code属性的属性名
#14 = Utf8    CompanyA      //程序中的常量字符串
#19 = Utf8    java/lang/System    //所引用的System类的全限定名
#21 = Utf8    out        //所引用的out字段的字段名
#22 = Utf8    Ljava/io/PrintStream;   //所引用的out字段的描述符
#24 = Utf8    staitc init      //程序中的常量字符串
#27 = Utf8    java/io/PrintStream    //所引用的PrintStream类的全限定名
#29 = Utf8    println       //所引用的println方法的方法名
#30 = Utf8    (Ljava/lang/String;)V   //所引用的println方法的描述符
#31 = Utf8    LineNumberTable     //LineNumberTable属性的属性名
#32 = Utf8    LocalVariableTable    //LocalVariableTable属性的属性名
#33 = Utf8    <init>       //当前类的构造方法的方法名
#41 = Utf8    com/jg/zhang/Computer   //所引用的Computer类的全限定名
#45 = Utf8    this       //局部变量this的变量名
#46 = Utf8    Lcom/jg/zhang/Programer;  //局部变量this的描述符
#47 = Utf8    working       //woking方法的方法名
#49 = Utf8    coding...      //程序中的字符串常量
#52 = Utf8    SourceFile      //SourceFile属性的属性名
#53 = Utf8    Programer.java     //当前类所在的源文件的文件名

上面只列出了反编译结果中常量池中的CONSTANT_Utf8_info数据项。 其中第三列不是javap反编译的输出结果, 而是我加上的注释。 读者可以对比上面的程序源码来看一下, 这样的话, 就可以清楚的看出, 源文件中的各种字符串, 是如何和存放到CONSTANT_Utf8_info中的。

这里要强调一下, 源文件中的几乎所有可见的字符串都存放在CONSTANT_Utf8_info中, 其他类型的常量池项只不过是对CONSTANT_Utf8_info的引用。 其他常量池项, 把引用的CONSTANT_Utf8_info组合起来, 进而可以描述更多的信息。 下面将要介绍的CONSTANT_NameAndType_info就可以验证这个结论。

(2) CONSTANT_NameAndType类型的数据项

常量池中的一个CONSTANT_NameAndType_info数据项, 可以看做CONSTANT_NameAndType类型的一个实例 。 从这个数据项的名称可以看出, 它描述了两种信息,第一种信息是名称(Name), 第二种信息是类型(Type) 。 这里的名称是指方法的名称或者字段的名称, 而Type是广义上的类型, 它其实描述的是字段的描述符或方法的描述符。 也就是说, 如果Name部分是一个字段名称, 那么Type部分就是相应字段的描述符; 如果Name部分描述的是一个方法的名称, 那么Type部分就是对应的方法的描述符。 也就是说, 一个CONSTANT_NameAndType_info就表示了一个方法或一个字段。

下面先看一下CONSTANT_NameAndType_info数据项的存储格式。 既然是常量池中的一种数据项类型, 那么它的第一个字节也是tag, 它的tag值是12, 也就是说, 当虚拟机读到一个tag为12的常量池数据项, 就可以确定这个数据项是一个CONSTANT_NameAndType_info 。 tag值一下的两个字节叫做name_index, 它指向常量池中的一个CONSTANT_Utf8_info, 这个CONSTANT_Utf8_info中存储的就是方法或字段的名称。 name_index以后的两个字节叫做descriptor_index, 它指向常量池中的一个CONSTANT_Utf8_info, 这个CONSTANT_Utf8_info中存储的就是方法或字段的描述符。 下图表示它的存储布局:

下面举一个实例进行说明, 实例的源码为:

package com.jg.zhang;

public class Person {

 int age;

 int getAge(){
 return age;
 }
}

这个Person类很简单, 只有一个字段age, 和一个方法getAge 。 将这段代码使用javap工具反编译之后, 常量池信息如下:

#1 = Class    #2    // com/jg/zhang/Person
 #2 = Utf8    com/jg/zhang/Person
 #3 = Class    #4    // java/lang/Object
 #4 = Utf8    java/lang/Object
 #5 = Utf8    age
 #6 = Utf8    I
 #7 = Utf8    <init>
 #8 = Utf8    ()V
 #9 = Utf8    Code
 #10 = Methodref   #3.#11   // java/lang/Object."<init>":()V
 #11 = NameAndType  #7:#8   // "<init>":()V
 #12 = Utf8    LineNumberTable
 #13 = Utf8    LocalVariableTable
 #14 = Utf8    this
 #15 = Utf8    Lcom/jg/zhang/Person;
 #16 = Utf8    getAge
 #17 = Utf8    ()I
 #18 = Fieldref   #1.#19   // com/jg/zhang/Person.age:I
 #19 = NameAndType  #5:#6   // age:I
 #20 = Utf8    SourceFile
 #21 = Utf8    Person.java

常量池一共有21项, 我们可以看到, 一共有两个CONSTANT_NameAndType_info 数据项, 分别是第#11项和第#19项, 其中第#11项的CONSTANT_NameAndType_info又引用了常量池中的第#7项和第#8项, 被引用的这两项都是CONSTANT_Utf8_info , 它们中存储的字符串常量值分别是 <init> 和 ()V。 其实他们加起来表示的就是父类Object的构造方法。 那么这里为什么会是父类Object的构造方法而不是本类的构造方法呢? 这是因为类中定义的方法如果不被引用(也就是说在当前类中不被调用), 那么常量池中是不会有相应的 CONSTANT_NameAndType_info 与之对应的, 只有引用了一个方法, 才有相应的CONSTANT_NameAndType_info 与之对应。 这也是为什么说CONSTANT_NameAndType_info 是方法的符号引用的一部分的原因。 (这里提到一个新的概念, 叫做方法的符号引用, 这个概念会在后面的博客中进行讲解) 可以看到, 在源码存在两个方法, 分别是编译器默认添加的构造方法和我们自己定义的getAge方法, 因为并没有在源码中显示的调用这两个方法,所以在常量池中并不存在和这两个方法相对应的CONSTANT_NameAndType_info 。  之所以会存在父类Object的构造方法对应的CONSTANT_NameAndType_info , 是因为子类构造方法中会默认调用父类的无参数构造方法。 我们将常量中的其他信息去掉, 可以看得更直观:

下面讲解常量池第#19项的CONSTANT_NameAndType_info , 它引用了常量池第#5项和第#6项, 这两项也是CONSTANT_Utf8_info 项, 其中存储的字符串分别是age和I, 其中age是源码中字段age的字段名, I是age字段的描述符。 所以这个CONSTANT_NameAndType_info 就表示对本类中的字段age的引用。 除去常量池中的其他信息, 可以看得更直观:

和方法相同, 只定义一个字段而不引用它(在源码中表现为不访问这个变量), 那么在常量池中也不会存在和该字段相对应的CONSTANT_NameAndType_info 项。这也是为什么说CONSTANT_NameAndType_info作为字段符号引用的一部分的原因。 (这里提到一个新的概念, 叫做字段的符号引用, 这个概念会在后面的博客中进行讲解) 在本例中之所以会出现这个CONSTANT_NameAndType_info , 是因为在源码的getAge方法中访问了这个字段:

int getAge(){
 return age;
} 

下面给出这两个CONSTANT_NameAndType_info真实的内存布局图:

和Object构造方法相关的CONSTANT_NameAndType_info的示意图:

和age字段相关的CONSTANT_NameAndType_info示意图:

这两张图能够很好的反映出CONSTANT_NameAndType_info和CONSTANT_Utf8_info 这两种常量池数据项的数据存储方式, 也能够真实的反应CONSTANT_NameAndType_info和CONSTANT_Utf8_info 的引用关系。

总结

在本文中我们主要介绍了常量池中的两种数据项: CONSTANT_NameAndType_info 和 CONSTANT_Utf8_info  。 其中CONSTANT_Utf8_info存储的是源文件中的各种字符串, 而CONSTANT_NameAndType_info表述的是源文件中对一个字段或方法的符号引用的一部分(即 方法名加方法描述符, 或者是 字段名加字段描述符)。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 浅谈java常量池

    java常量池技术 java中常量池技术说的通俗点就是java级别的缓存技术,方便快捷的创建一个对象.当需要一个对象时,从池中去获取(如果池中没有,就创建一个并放入池中),当下次需要相同变量的时候,不用重新创建,从而节省空间. java八种基本类型的包装类和对象池 java中的基本类型的包装类.其中Byte.Boolean.Short.Character.Integer.Long实现了常量池技术,(除了Boolean,都只对小于128的值才支持) 比如,Integer对象 Integer i1

  • Java 常量池的实例详解

    Java 常量池的实例详解 Java的常量池中包含了类.接口.方法.字符串等一系列常量值.常量池在编译期间就已经确定,并保存在*.class文件中 一.对于相同的常量值,常量池中只保存一份拷贝. 而且,当一个字符串由多个字符串常量链接而成时,多个字符串被组成一个字符串常量. 例如: package lxg; public class main { public static void main(String[] args) { String name = "lengxuegang";

  • java String类常量池分析及"equals"和"==”区别详细介绍

    java "equals"和"=="异同 首先简单说一下"equal"和"==" ==操作对于基本数据类型比较的是两个变量的值是否相等, 对于引用型变量表示的是两个变量在堆中存储的地址是否相同, 即栈中的内容是否相同 equals操作表示的两个变量是否是对同一个对象的引用, 即堆中的内容是否相同. 综上,==比较的是2个对象的地址,而equals比较的是2个对象的内容. 再简单介绍一下String类 String类 又称作不可

  • Java 中的字符串常量池详解

    Java中的字符串常量池 Java中字符串对象创建有两种形式,一种为字面量形式,如String str = "droid";,另一种就是使用new这种标准的构造对象的方法,如String str = new String("droid");,这两种方式我们在代码编写时都经常使用,尤其是字面量的方式.然而这两种实现其实存在着一些性能和内存占用的差别.这一切都是源于JVM为了减少字符串对象的重复创建,其维护了一个特殊的内存,这段内存被成为字符串常量池或者字符串字面量池.

  • Java中的字符串常量池详细介绍

    Java中字符串对象创建有两种形式,一种为字面量形式,如String str = "droid";,另一种就是使用new这种标准的构造对象的方法,如String str = new String("droid");,这两种方式我们在代码编写时都经常使用,尤其是字面量的方式.然而这两种实现其实存在着一些性能和内存占用的差别.这一切都是源于JVM为了减少字符串对象的重复创建,其维护了一个特殊的内存,这段内存被成为字符串常量池或者字符串字面量池. 工作原理 当代码中出现字

  • Java class文件格式之常量池_动力节点Java学院整理

    常量池中各数据项类型详解 常量池中的数据项是通过索引来引用的, 常量池中的各个数据项之间也会相互引用.在这11中常量池数据项类型中, 有两种比较基础, 之所以说它们基础, 是因为这两种类型的数据项会被其他类型的数据项引用. 这两种数据类型就是CONSTANT_Utf8 和 CONSTANT_NameAndType , 其中CONSTANT_NameAndType类型的数据项(CONSTANT_NameAndType_info)也会引用CONSTANT_Utf8类型的数据项(CONSTANT_Ut

  • Java class文件格式之特殊字符串_动力节点Java学院整理

    class文件中的特殊字符串 首先说明一下, 所谓的特殊字符串出现在class文件中的常量池中,本着循序渐进和减少跨度的原则, 首先把class文件中的特殊字符串做一个详细的介绍, 然后再回过头来继续讲解常量池. 现在我们将重点放在特殊字符串上. 特殊字符串包括三种: 类的全限定名, 字段和方法的描述符, 特殊方法的方法名. 下面我们就分别介绍这三种特殊字符串. (1) 类的全限定名 在常量池中, 一个类型的名字并不是我们在源文件中看到的那样, 也不是我们在源文件中使用的包名加类名的形式. 源文

  • Java中常用的数据库连接池_动力节点Java学院整理

    定义 数据库连接是一种关键的有限的昂贵的资源,这一点在多用户的网页应用程序中体现得尤为突出.对数据库连接的管理能显著影响到整个应用程序的伸缩性和健壮性,影响到程序的性能指标.数据库连接池正是针对这个问题提出来的. 数据库连接池负责分配.管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个:释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏.这项技术能明显提高对数据库操作的性能. 参考资料 DBCP 下载地址:http://

  • Java JVM原理与调优_动力节点Java学院整理

    JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的.Java虚拟机包括一套字节码指令集.一组寄存器.一个栈.一个垃圾回收堆和一个存储方法域. JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行.是运行Java应用最底层部分. JDK(Java Development kit) 整个Java的核心,包括了Java运行环境(Java Runtime E

  • Java线程安全的常用类_动力节点Java学院整理

    线程安全类 在集合框架中,有些类是线程安全的,这些都是jdk1.1中的出现的.在jdk1.2之后,就出现许许多多非线程安全的类. 下面是这些线程安全的同步的类: vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用.在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的. statck:堆栈类,先进后出 hashtable:就比hashmap多了个线程安全 除了这些之外,其他的集合大都是非线程安全的类和接口. 线程安全的类其方法是同步

  • Java中Object toString方法简介_动力节点Java学院整理

    一.Object类介绍  Object类在Java里面是一个比较特殊的类,JAVA只支持单继承,子类只能从一个父类来继承,如果父类又是从另外一个父类继承过来,那他也只能有一个父类,父类再有父类,那也只能有一个,JAVA为了组织这个类组织得比较方便,它提供了一个最根上的类,相当于所有的类都是从这个类继承,这个类就叫Object.所以Object类是所有JAVA类的根基类,是所有JAVA类的老祖宗.所有的类,不管是谁,都是从它继承下来的. 二.toString方法介绍  一个字符串和另外一种类型连接

  • Java IO流体系继承结构图_动力节点Java学院整理

    Java IO体系结构看似庞大复杂,其实有规律可循,要弄清楚其结构,需要明白两点: 1. 其对称性质:InputStream 与 OutputStream, Reader 与 Writer,他们分别是一套字节输入-输出,字符输入-输出体系 2. 原始处理器(适配器)与链接流处理器(装饰器) 其结构图如下: Reader-Writer体系 1. 基类 InputStream与OutputStream是所有字节型输入输出流的基抽象类,同时也是适配器(原始流处理器)需要适配的对象,也是装饰器(链接流处

  • Java 线程池_动力节点Java学院整理

    线程池 系统启动一个新线程的成本是比较高的,因为它涉及到与操作系统的交互.在这种情况下,使用线程池可以很好的提供性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池. 与数据库连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象传给线程池,线程池就会启动一条线程来执行该对象的run方法,当run方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run方法. 除此之外,使用线程池可以有效地

  • Java动态代理机制详解_动力节点Java学院整理

    class文件简介及加载 Java编译器编译好Java文件之后,产生.class 文件在磁盘中.这种class文件是二进制文件,内容是只有JVM虚拟机能够识别的机器码.JVM虚拟机读取字节码文件,取出二进制数据,加载到内存中,解析.class 文件内的信息,生成对应的 Class对象: class字节码文件是根据JVM虚拟机规范中规定的字节码组织规则生成的.具体class文件是怎样组织类信息的,可以参考 此博文:深入理解Java Class文件格式系列.或者是Java虚拟机规范. 下面通过一段代

  • Java的几个重要版本_动力节点Java学院整理

    java几个重大版本 java从1995年发布到现在,也走过18年了,个人认为,其中几个java版本都肩负着重大使命,影响甚远: jdk1.0 1995年5月23日诞生,Oak语言改名为Java,并提出"Write Once ,Run anywhere": jdk1.2  1999年6月发布,将java划分为J2SE,J2ME,J2EE三大平台: jdk1.4 主要是性能提升,在2000年时候JAVA成为世界上最流行的电脑语言,跟这个版本离不开关系,估计国内还有大量的java应用是运行

随机推荐