澳门新葡萄京官网注册 18

澳门新葡萄京官网注册Java Class 字节码文件结构详解

Class字节码中有两种数据类型:

  • 字节码查看工具:WinHex

– 深入理解Java虚拟机

-   [[JVM]Java内存区域与垃圾收集 -
    思维导图](https://www.jianshu.com/p/088d71f20a47)
-   [[JVM]类加载机制 -
    思维导图](https://www.jianshu.com/p/5bad514071ab)
-   [[JVM]OOM实例分析](https://www.jianshu.com/p/8eabaf631d15)
-   [[JVM]理解Class文件(1):手动解析常量池](https://www.jianshu.com/p/26f95965320e)
-   [[JVM]理解GC日志](https://www.jianshu.com/p/fd1d4f21733a)
-   [[JVM]理解Class文件(2)](https://www.jianshu.com/p/dbaabe4554b6)
  1. 字节数据直接量:这是基本的数据类型。共细分为u1、u2、u4、u8四种,分别代表连续的1个字节、2个字节、4个字节、8个字节组成的整体数据。
  2. 表:表是由多个基本数据或其他表,按照既定顺序组成的大的数据集合。表是有结构的,它的结构体现在,组成表的成分所在的位置和顺序都是已经严格定义好的。

前言

  • Java虚拟机实现语言无关性的基石就是Class文件
![](https://upload-images.jianshu.io/upload_images/3458176-30ce955fde8883d1.png)

Java虚拟机提供的语言无关性
  • 这篇文章讲Class格式文件的的魔数、版本号和常量池。主要内容是常量池。

引言

class文件是Java虚拟机提供的语言无关性的基石,如果要深入地了解虚拟机,那么必须学习class文件是如何解析的。

澳门新葡萄京官网注册 1

Java虚拟机提供的语言无关性

而常量池是class文件结构中第一个出现的表类型数据项目,也是Class文件中最烦琐的数据,理解了常量池的解析过程,Class文件其它字段的分析也就迎刃而解了。

Class字节码总体结构如下:

Class类文件的结构

Class文件结构

具体详解请参考

全局规范

  • 1.任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)。本章中,只是通俗地将任意一个有效的类或接口所应当满足的格式称为“Class文件格式”,实际上它并不一定以磁盘文件的形式存在。“Class文件”应当是一串二进制的字节流,无论以何种形式存在。

  • 2.Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前(Big-Endian)的方式分割成若干个8位字节进行存储。无符号数据类型最大占8个字节。

  • 3.Class文件中存储数据的类型:无符号数和表。

  • 无符号数(基本数据类型):以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。

  • 表(复合数据类型):是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地“_info”结尾。表用于描述有层次关系的复合结构的数据。表是一个统称,就好比把ArrayList、LinkedList、Set都是称为集合(Collection),但是每个集合的内部结构都是不同的,Class中有很多不同的表。如下图中cp_info类型,是表类型,但是它是一个固定结构的类型吗?不是,它好比Collection集合下的List集合,只是一类集合的统称,实际上cp_info表是14种具体表类型的统称,constant_pool_count-1指出了有多少个cp_info表,那到底是哪些具体的表,就需要具体看了。

  • 4.整个Class文件本质上就是一张表,下表就是Class文件格式。Class中所有内容都在这些类型中定义了。

    • 注:表中的数据项,无论是顺序还是数量,甚至于数据存储的字节序(Byte
      Ordering,Class文件中字节序为Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。

      澳门新葡萄京官网注册 2

      Class文件格式

    ![](https://upload-images.jianshu.io/upload_images/3458176-183170e077348dd9.png)

    class文件结构
  • 5.无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。

  • 如上表的描述常量池数据使用了一个constant_pool_count、多个constant_pool,其中constant_pool是表类型并且数量为constant_pool_count值减去1,把一个constant_pool_count和多个constant_pool数据项称为常量池集合

  • 从Class文件格式中可以看出有:常量池集合、接口索引集合、字段表集合、方法表集合、属性表集合。

  • 6.具体的Class文件案例,以下讲解会通过这个TestClass类的TestClass.class文件来分析。

package com.zlcook.clazz;

public class TestClass{
  private int m;
  public int inc(){
   return m+1;
  }
}

ClassFile结构

Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,包含两种数据类型

  • 无符号数:类型有u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节
  • : 表是多个无符号数或者其他表组合而成的复合数据,如:cp_info
    constant_pool[constant_pool_count-1]就是一个表结构的数据。

每一个Class文件对应于一个如下所示的ClassFile结构体:

ClassFile {
    u4 magic; 
    u2 minor_version; 
    u2 major_version; 
    u2 constant_pool_count; 
    cp_info constant_pool[constant_pool_count-1];
    u2 access_flags; 
    u2 this_class; 
    u2 super_class; 
    u2 interfaces_count; 
    u2 interfaces[interfaces_count]; 
    u2 fields_count; 
    field_info fields[fields_count]; 
    u2 methods_count; 
    method_info methods[methods_count]; 
    u2 attributes_count; 
    attribute_info attributes[attributes_count]; 
}

澳门新葡萄京官网注册 3

1. 魔数与Class文件的版本

  • 由上表得Class文件的前三个数据类型存储了魔数(magic)、次版本号(minor_version)、主版本号(major_version)的值,数据类型分别为u4、u2、u2。共占8个字节。
  • 魔数:0xCAFEBABE
    (16进制),值固定,唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。
  • Class文件版本号:次版本号组成u2+主版本号u2。共占4个字节。
  • 高版本的JDK能向下兼容以前的版本的Class文件,但不能运行高版本的Class文件。
  • JDK1.1的版本号为45.0-45.65535(10进制),之后每个大版本发布主版本号加1,如:JDK1.2:46.0~46.65535。
  • 例如:Class文件中紧接着魔数的4个字节的16进制为:
    ox00000034,那么它代表的十进制版本号为:次版本号为ox0000=0,主版本号为:ox0034
    = 52。所以ox00000034的版本号为52.0,对应的JDK版本为JDK1.8

澳门新葡萄京官网注册 4

TestClass.class文件的前8个字节

示例java代码

将下面这段代码编译生成class,用作后续分析。

public class TestClass {
    private static final String TEST = "test string";
}

我在这里要说明几个细节问题:

2. 常量池

  • 先了解常量池中需要存放哪些内容,再讨论用什么类来存放这些内容。

示例class文件

将生成的class文件用UltraEdit打开,可以清楚地看到Java编译后生成的字节码,我们要解析的内容也就是这些字节码。

00000000h: CA FE BA BE 00 00 00 33 00 12 0A 00 03 00 0E 07 ; 
00000010h: 00 0F 07 00 10 01 00 04 54 45 53 54 01 00 12 4C ; 
00000020h: 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 ; 
00000030h: 3B 01 00 0D 43 6F 6E 73 74 61 6E 74 56 61 6C 75 ; 
00000040h: 65 08 00 11 01 00 06 3C 69 6E 69 74 3E 01 00 03 ; 
00000050h: 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E ; 
00000060h: 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 0A 53 ;
00000070h: 6F 75 72 63 65 46 69 6C 65 01 00 0E 54 65 73 74 ;
00000080h: 43 6C 61 73 73 2E 6A 61 76 61 0C 00 08 00 09 01 ; 
00000090h: 00 09 54 65 73 74 43 6C 61 73 73 01 00 10 6A 61 ; 
000000a0h: 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 01 00 ;  
000000b0h: 0B 74 65 73 74 20 73 74 72 69 6E 67 00 21 00 02 ;  
000000c0h: 00 03 00 00 00 01 00 1A 00 04 00 05 00 01 00 06 ;  
000000d0h: 00 00 00 02 00 07 00 01 00 01 00 08 00 09 00 01 ;  
000000e0h: 00 0A 00 00 00 1D 00 01 00 01 00 00 00 05 2A B7 ; 
000000f0h: 00 01 B1 00 00 00 01 00 0B 00 00 00 06 00 01 00 ; 
00000100h: 00 00 02 00 01 00 0C 00 00 00 02 00 0D          ; 
  1. 为什么说常量表的数量是constant_pool_count-1,且索引从1开始而不是0。其实根本原因在于,索引为0也是一个常量(保留常量),只不过它不存在常量表,这个常量就对应null值。因此加上这个系统保留常量,常量个数共为constant_pool_count个,但是常量表数量要减1。
  2. 在常量池中,如果存在long型或double型字面量,它们会占用两个连续索引。比如:假设一个类中只有一个int型字面量1和一个double型字面量1(当然这种假设是不可能的,因为总会有类名字面量等),则常量池个数为3,而不是2。这正是因为double字面量占用了两个连续的索引。

2.1 常量池中存放的内容

  • Class文件中包含常量池,那么我就需要知道常量池会包含哪些内容,接下来才是关心class格式文件用什么类型来存放这些内容。

  • 字面量(Literal)

  • 字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。

  • 符号引用(Symbolic References)

  • 符号引用则属于编译原理方面的概念,包括了下面三类常量:
    类和接口的全限定名(Fully Qualified Name)
    字段的名称和描述符(Descriptor)
    方法的名称和描述符

  • 其它:常量池中主要内容是上面2项,说明还有其它内容,这部分内容,在下面我们看到用来描述常量池内容的14种常量项的介绍时就发现标志为15、16、18的常量项类型是用来支持动态语言调用的(jdk1.7时才加入的)。

  • 常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用Class文件空间最大的数据项目之一。

魔数和版本号

在解析常量池之前,先来看下Class文件中最开始的两个字段

  • 魔数
    魔数是用来标识这支文件是能被虚拟机所接受的Class文件。魔数值固定为0xCAFEBABE,不会改变。

由于文件扩展名是可以随意修改的,因此,很多文件都会使用魔数作为文件标识,如gif或者jpeg等在头文件中都存在魔数。

  • 版本号
    class文件版本由副版本号+主版本号组成(minor_version +
    major_version)
    ,通过版本号可以知道对应编译器的版本。下图列出了每个JDK版本对应的十六进制版本号

澳门新葡萄京官网注册 5

class文件版本号

  • 示例解析
    头4个字节为魔数CA FE BA BE,紧接着为版本号00 00 00
    33
    ,对照上面的表格,可以知道编译器的版本为JDK 1.7.0

    澳门新葡萄京官网注册 6

    图1.魔数、版本号和常量池计数器


接下来,贴出一个小demo来展示如何读取字节码:

2.2 常量池中为什么要包含这些内容

  • Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。关于类的创建和动态连接的内容,在虚拟机类加载过程时再进行详细讲解。

常量池

ClassParser负责把握Class字节码整体结构的解析。

2.3 Class文件中如何描述常量池中内容

  • 知道Class文件的常量池包含的内容后,我们下面就来看看class格式文件使用了哪些类型数据来存放常量池的内容。

  • 由Class文件格式可得紧接着主版本号的是常量池入口。

类型 名称 数量
u2(无符号数) constant_pool_count 1
cp_info(表) constant_pool constant_pool_count-1
  • 占用的字节数:2+(constant_pool_count-1)个具体表所占字节。

  • 由上表可见,Class文件使用了一个前置的容量计数器(constant_pool_count)加若干个连续的数据项(constant_pool)的形式来描述常量池内容。我们把这一系列连续常量池数据称为常量池集合。

  • 先给看一下TestClass.class文件全局的内容,下面就来分析其中常量池中的内容,其它内容后面的文章在分析。从图片也可以看出常量池内容占据了class文件的很大一部分,当然TestClass类中代码比较少就更显得常量池内容的多了。

![](https://upload-images.jianshu.io/upload_images/3458176-da04f13d9345d534.png)

TestClass.class文件的16进制内容

存放内容

常量池存放的内容如下:

  • 字面量

    • 文本字符串、声明为final的常量值
  • 符号引用

    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符
package com.lixin;

import java.io.IOException;
import java.io.InputStream;

public class ClassParser {

    private InputStream in;

    public ClassParser(InputStream in) {
        this.in = in;
    }

    public void parse() throws IOException {
        // 魔数
        magicNumber();
        // 主次版本号
        version();
        // 常量池
        constantPool();
        // 类或接口修饰符
        accessFlag();
        // 继承关系(当前类、父类、父接口)
        inheritence();
        // 字段集合
        fieldList();
        // 方法集合
        methodList();
        // 属性集合
        attributeList();
    }

    private void attributeList() throws IOException {
        line();
        int attrLength = StreamUtils.read2(in);
        System.out.println("共有"+attrLength+"个属性");
        for (int i=0;i<attrLength;i++) {
            line();
            attribute();
        }
    }
    private void attribute() throws IOException {
        int nameIndex = StreamUtils.read2(in); 
        int length = StreamUtils.read4(in); 
        byte[] info = StreamUtils.read(in, length);
        System.out.println("nameIndex:"+nameIndex);
        System.out.println("length:"+length);
        System.out.println("info:"+info);
    }

    private void methodList() throws IOException {
        int length = StreamUtils.read2(in);
        System.out.println("共有"+length+"个方法");
        for (int i=0;i<length;i++)
            method();
    }

    private void method() throws IOException {
        System.out.println("---------------------");
        int accessFlag = StreamUtils.read2(in);
        int nameIndex = StreamUtils.read2(in);
        int descriptorIndex = StreamUtils.read2(in);
        System.out.println("accessFlag:"+accessFlag);
        System.out.println("nameIndex:"+nameIndex);
        System.out.println("descriptorIndex:"+descriptorIndex);
        attributeList();
    }

    private void fieldList() throws IOException {
        line();
        int length = StreamUtils.read2(in);
        System.out.println("共有"+length+"个字段");
        for (int i=0;i<length;i++) {
            System.out.println("-----------------------------");
            int accessFlag = StreamUtils.read2(in);
            int nameIndex = StreamUtils.read2(in);
            int descriptorIndex = StreamUtils.read2(in);
            System.out.println("accessFlag:"+accessFlag);
            System.out.println("nameIndex:"+nameIndex);
            System.out.println("descriptorIndex:"+descriptorIndex);
            attributeList();
        }
    }

    private void inheritence() throws IOException {
        line();
        int thisClassRef = StreamUtils.read2(in);
        int superClassRef = StreamUtils.read2(in);
        System.out.println("thisClassRef:"+thisClassRef);
        System.out.println("superClassRef:"+superClassRef);
        int interfaceLen = StreamUtils.read2(in);
        System.out.println("接口数量:"+interfaceLen);
        for (int i=0;i<interfaceLen;i++) {
            int interfaceRef = StreamUtils.read2(in);
            System.out.println("interfaceRef:"+interfaceRef);
        }
    }

    private void accessFlag() throws IOException {
        line();
        int accessFlag = StreamUtils.read2(in);
        System.out.println("accessFlag:0x"+Integer.toHexString(accessFlag)+"("+accessFlag+")");
    }

    private void constantPool() throws IOException {
        new ConstantPoolParser(in).constPool();
    }

    private void version() throws IOException {
        line();
        int minorVersion = StreamUtils.read2(in);
        int majorVersion = StreamUtils.read2(in);
        System.out.println("版本:"+majorVersion+"."+minorVersion);
    }

    private void magicNumber() throws IOException {
        line();
        int magic = StreamUtils.read4(in);
        System.out.println("魔数:"+Integer.toHexString(magic).toUpperCase());
    }

    private void line() {
        System.out.println("----------------------");
    }
}

2.3.1 constant_pool_count

  • 常量池容量计数值(u2类型):从1开始,表示常量池中有多少项常量。即constant_pool_count=1表示常量池中有0个常量项。

  • 设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示。Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始的。

  • TestClass.class文件中constant_pool_count的十进制值为19,表示常量池中有18项常量,索引范围1-18。

澳门新葡萄京官网注册 7

TestClass.class文件中constant_pool_count的十进制值为19

常量池数据结构

要解析常量池的数据,先得看下它的数据结构,这些数据结构都是在
Java虚拟机规范(Java SE
7).pdf里面定义的。

  • ##### ClassFile

在class文件中,常量池入口紧接在主版本后之后,

ConstPoolParser负责常量池的解析(因为常量池表较多,且数据量也较大,因此单独拉出来解析)

2.3.2 constant_pool

  • constant_pool_count表明了后面有多少个常量项。

constant_pool_count表示常量的个数,constant_pool是存放的是所有常量信息的表项,存放的个数为**constant_pool_count

1constant_pool**可以将理解为一个数组,其中的每一项代表一个常量。

由“图1.魔数、版本号和常量池计数器”所示constant_pool_count值为18,由于常量池容量计数是从1开始的,0用作表示“不引用任何对象”,因此,常量个数为17个(constant_pool_count

  • 1)

    ClassFile {

      ... ...
      u2 constant_pool_count;
      cp_info constant_pool[constant_pool_count-1];
      ... ...
    

    }


  • ##### cp_info

    要想知道constant_pool里面是怎样存放常量信息的,就需要先看cp_info这个结构体的内容

cp_info { 
    u1 tag; 
    u1 info[]; 
}

其中,tag表示常量的类型info[]代表tag类型所属的表项

tag值对应的类型如下表:

澳门新葡萄京官网注册 8

常量池tag

以开头的class文件为例,紧接着常量池计数器后的“0A”是第一个常量的tag,“0A”对应十进制值为10,结合上表,得出第一个常量类型为CONSTANT_Methodref

澳门新葡萄京官网注册 9

0A.png


  • ##### CONSTANT_Methodref_info

CONSTANT_Methodref对应的数据结构为CONSTANT_Methodref_info,也就是说constant_pool[1]中的u1
info[]
CONSTANT_Methodref_info

  • tag表示当前数据类型CONSTANT_Methodref_info,说明这个常量是一个方法

  • class_index表示引用这个方法的对象在常量池数组的中索引,说明constant_pool[class_index]存放的就是调用该方法的对象名称

  • name_and_type_index指的该方法在常量池数组的中索引,即constant_pool[name_and_type_index]存放着该方法的名称

CONSTANT_Methodref_info { 
    u1 tag; 
    u2 class_index; 
    u2 name_and_type_index; 
}

根据上面的描述,我们接着解析“0A”后面的字段,由CONSTANT_Methodref_info数据结构可知,“0A”往后再取2个u2类型数据的长度就截止了,其中class_index
= 3, name_and_type_index = 14

假设我们已经将后面的常量池字段全部解析完成了(文末附有整个常量池解析结构),可以得出constant_pool[3]
= “java/lang/Object” ,constant_pool[14] = ”
“<init>”:()V

因此,第一个常量constant_pool[1]为:java/lang/Object.”<init>”:()V

澳门新葡萄京官网注册 10

CONSTANT_Methodref_info


  • ##### 解析第二个常量

类型为CONSTANT_Class,对应结构体为CONSTANT_Class_info,由此知解析的第二个常量的数据段为07
00 0F

CONSTANT_Class_info {
     u1 tag; 
     u2 name_index; 
}

最后得到结果:该常量是一个类的名称,类名存放在constant_pool[15] =
“TestClass” , 即constant_pool[2] = TestClass

澳门新葡萄京官网注册 11

解析第二个常量


  • ##### 使用javap命令解析class文件

按照上面的方法,可以手动解析出剩下的15个常量。我们也可以通过javap完成对class文件的解析,javap命令使用方法和输入结果如下:

D:TestClass>javap -verbose TestClass
Classfile /D:/TestClass/TestClass.class
  Last modified 2017-2-7; size 269 bytes
  MD5 checksum b2a3f1078d18eba859aaeac73bd7621d
  Compiled from "TestClass.java"
public class TestClass
  SourceFile: "TestClass.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#14         //  java/lang/Object."<init>":()V
   #2 = Class              #15            //  TestClass
   #3 = Class              #16            //  java/lang/Object
   #4 = Utf8               TEST
   #5 = Utf8               Ljava/lang/String;
   #6 = Utf8               ConstantValue
   #7 = String             #17            //  test string
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               SourceFile
  #13 = Utf8               TestClass.java
  #14 = NameAndType        #8:#9          //  "<init>":()V
  #15 = Utf8               TestClass
  #16 = Utf8               java/lang/Object
  #17 = Utf8               test string
package com.lixin;

import java.io.IOException;
import java.io.InputStream;

public class ConstPoolParser {

    public static final int Utf8_info = 1;
    public static final int Integer_info = 3;
    public static final int Float_info = 4;
    public static final int Long_info = 5;
    public static final int Double_info = 6;
    public static final int Class_info = 7;
    public static final int String_info = 8;
    public static final int Fieldref_info = 9;
    public static final int Methodref_info = 10;
    public static final int InterfaceMethodref_info = 11;
    public static final int NameAndType_info = 12;
    public static final int MethodHandle_info = 15;
    public static final int MethodType_info = 16;
    public static final int InvokeDynamic_info = 18;

    private InputStream in;

    public ConstPoolParser(InputStream in) {
        this.in = in;
    }

    public void constPool() throws IOException {
        line();
        int length = StreamUtils.read2(in);
        System.out.println("共有"+length+"个常量");
        boolean doubleBytes = false;
        for (int i = 1; i < length; i++) {
            if (doubleBytes) {
                doubleBytes = false;
                continue;
            }
            line();
            System.out.println("常量索引:"+i);
            int flag = StreamUtils.read1(in);
//          System.out.println("标志:"+flag);

            switch (flag) {
            case Utf8_info:
                utf8Info();
                continue;
            case Integer_info:
                integerInfo();
                continue;
            case Float_info:
                floatInfo();
                continue;
            case Long_info:
                doubleBytes = true;
                longInfo();
                continue;
            case Double_info:
                doubleBytes = true;
                doubleInfo();
                continue;
            case Class_info:
                classInfo();
                continue;
            case String_info:
                stringInfo();
                continue;
            case Fieldref_info:
                fieldrefInfo();
                continue;
            case Methodref_info:
                methodrefInfo();
                continue;
            case InterfaceMethodref_info:
                interfaceMethodrefInfo();
                continue;
            case NameAndType_info:
                nameAndTypeInfo();
                continue;
            case MethodHandle_info:
                methodHandleInfo();
                continue;
            case MethodType_info:
                methodTypeInfo();
                continue;
            case InvokeDynamic_info:
                invokeDynamicInfo();
                continue;
            default:
                System.err.println(flag);
                throw new RuntimeException("unknown");
            }
        }
    }

    private void line() {
        System.out.println("----------------------");
    }

    private void utf8Info() throws IOException {
        int length = StreamUtils.read2(in);
        byte[] buf = StreamUtils.read(in, length);
        String s = new String(buf,0,buf.length);
        System.out.println("utf8Info表:");
        System.out.println("值:"+s);
    }

    private void integerInfo() throws IOException {
        System.out.println("integerInfo表:");
        int value = StreamUtils.read4(in);
        System.out.println("值:"+value);
    }

    private void floatInfo() throws IOException {
        System.out.println("floatInfo表:");
        int value = StreamUtils.read4(in);
        float f = Float.intBitsToFloat(value);
        System.out.println("值:"+f);
    }

    private void longInfo() throws IOException {
        System.out.println("longInfo表:");
        long value = StreamUtils.read8(in);
        System.out.println("值:"+value);
    }

    private void doubleInfo() throws IOException {
        System.out.println("doubleInfo表:");
        long value = StreamUtils.read8(in);
        double d = Double.longBitsToDouble(value);
        System.out.println("值:"+d);

    }

    private void classInfo() throws IOException {
        System.out.println("classInfo表:");
        int index = StreamUtils.read2(in);
        System.out.println("index:" + index);
    }

    private void stringInfo() throws IOException {
        System.out.println("stringInfo表:");
        int index = StreamUtils.read2(in);
        System.out.println("index:" + index);
    }

    private void fieldrefInfo() throws IOException {
        int classIndex = StreamUtils.read2(in);
        int nameAndTypeIndex = StreamUtils.read2(in);
        System.out.println("fieldrefInfo表:");
        System.out.println("classIndex:" + classIndex);
        System.out.println("nameAndTypeIndex:" + nameAndTypeIndex);
    }

    private void methodrefInfo() throws IOException {
        int classIndex = StreamUtils.read2(in);
        int nameAndTypeIndex = StreamUtils.read2(in);
        System.out.println("methodrefInfo表:");
        System.out.println("classIndex:" + classIndex);
        System.out.println("nameAndTypeIndex:" + nameAndTypeIndex);
    }

    private void interfaceMethodrefInfo() throws IOException {
        int classIndex = StreamUtils.read2(in);
        int nameAndTypeIndex = StreamUtils.read2(in);
        System.out.println("interfaceMethodrefInfo表:");
        System.out.println("classIndex:" + classIndex);
        System.out.println("nameAndTypeIndex:" + nameAndTypeIndex);
    }

    private void nameAndTypeInfo() throws IOException {
        int nameIndex = StreamUtils.read2(in);
        int typeIndex = StreamUtils.read2(in);
        System.out.println("nameAndTypeInfo表:");
        System.out.println("nameIndex:" + nameIndex);
        System.out.println("typeIndex:" + typeIndex);
    }

    private void methodHandleInfo() throws IOException {
        int referenceKind = StreamUtils.read1(in);
        int referenceIndex = StreamUtils.read2(in);
        System.out.println("methodHandleInfo表:");
        System.out.println("referenceKind:"+referenceKind);
        System.out.println("referenceIndex:"+referenceIndex);
    }

    private void methodTypeInfo() throws IOException {
        System.out.println("methodTypeInfo表:");
        int descriptorIndex = StreamUtils.read2(in);
        System.out.println("descriptorIndex:"+descriptorIndex);
    }

    private void invokeDynamicInfo() throws IOException {
        int bootstrapMethodAttrIndex = StreamUtils.read2(in);
        int nameAndTypeIndex = StreamUtils.read2(in);
        System.out.println("bootstrapMethodAttrIndex:"+bootstrapMethodAttrIndex);
        System.out.println("nameAndTypeIndex:"+nameAndTypeIndex);
    }
}
14种常量项结构
  • 常量池中每一项常量都是一个表,JDK1.7之后共有14种不同的表结构数据。一个常量池中的每个常量项都逃不脱这14种结构。根据下图每个类型的描述我们也可以知道每个类型是用来描述常量池中哪些内容(主要是字面量、符号引用)的。比如:CONSTANT_Integer_info是用来描述常量池中字面量信息的,而且只是整型字面量信息。而标志为15、16、18的常量项类型是用来支持动态语言调用的(jdk1.7时才加入的)。

  • 澳门新葡萄京官网注册 12

    常量池中的14种项目类型

  • 澳门新葡萄京官网注册 13

    常量池中的14种常量项的结构总表

![](https://upload-images.jianshu.io/upload_images/3458176-878fa839b1e28cf3.png)

常量池中的14种常量项的结构总表(续)
  • 这14种表(或者常量项结构)的共同点是:表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量项使用的是哪种表结构,即哪种常量类型。

  • 这14种常量项结构还有一个特点是,其中13表占用得字节固定,只有CONSTANT_Utf8_info占用字节不固定,其大小由length决定。为什么呢?因为从常量池存放的内容可知,其存放的是字面量和符号引用,最终这些内容都会是一个字符串,这些字符串的大小是在编写程序时才确定,比如你定义一个类,类名可以取长取短,所以在没编译前,无法确定大小不固定,编译后,通过utf-8编码,就可以知道其长度。

占用字节
CONSTANT_Class_info 3
CONSTANT_Integer_info 5
CONSTANT_Fieldref_info 5
CONSTANT_Methodref_info 5
CONSTANT_Utf8_info 不固定,取决于length大小

参考

  • 深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)
  • Java虚拟机规范(Java SE
    7).pdf

StreamUtils负责从输入字节流中读取数据

2.4 查找testClass.class文件的第一个常量项内容

  • 由上面constant_pool_count得到值为19,因为从1开始计数,所以说明后面有18个常量项,由于每个常量项的表结构都不同但是第一位相同,所以读到第一位就可以确定表结构了。下面我们就来查看第一个常量项包含得内容,至于其它17个常量项内容类似,最后还会介绍java提供得一个工具命令javap来帮我们分析class文件字节码内容。

澳门新葡萄京官网注册 14

第一个表的tag为10

  • 由上图可知常量池中第一项常量标志的16进制值是0x0A=10,查表发现这个常量属于CONSTANT_Methodref_info类型,此类型表示类中方法的符号引用。查看该类型的结构如下:
![](https://upload-images.jianshu.io/upload_images/3458176-18d7609da23c0b93.png)

CONSTANT_Methodref_info类型结构
  • CONSTANT_Methodref_info型常量的第二个数据项为index,类型是u2,index存储的是一个索引值,从class文件中查得该值为oX0004=4,即它指向常量池中第4个常量;第三个数据项也是索引其值为0X000F=15,指向常量池种第15个常量。

澳门新葡萄京官网注册 15

Paste_Image.png

  • 到此为止,第一个常量项是CONSTANT_Methodref_info型常量项,该类型常量项用来表示类中方法的符号引用,其内容为tag=10,index1=4,index2=15,因为其表示的是类中方法的符号引用,所以index中存放的不是一个具体得内容,而是一个索引位置,所以说其具体内容存放在另一个常量项中。下面我们就来看看其索引指向的常量项(即第4个常量项)的内容到底是什么?
  • 找第4个常量项之前需要知道第4个常量项的开始位置,所以需要知道前3个常量项所占字节数。那好就看第2个常量项,由于第一个常量项共占了5个字节,则紧接着的字节就为第二个常量项的tag,如下图可得其值为0X09=9,说明第2个常量项得项目类型为CONSTANT_Fieldref_info。查表得其该类型得字节长度固定占5个字节。

澳门新葡萄京官网注册 16

第二个常量项

  • 依次类推查的第3,4个常量项为CONSTANT_Class_info型。如下图:
![](https://upload-images.jianshu.io/upload_images/3458176-9dedb81d1b044555.png)

前4个常量项
  • 下面就看第四个常量项CONSTANT_Class_info的内容0X070012。
    CONSTANT_Class_info存放的是指向类或接口的符号引用。
![](https://upload-images.jianshu.io/upload_images/3458176-31580b94471eec94.png)

CONSTANT_Class_info型常量项

根据CONSTANT_Class_info项常量项的结构可知其index数据项又是一个索引项,指向全限定名常量项索引,index数据项的值为0X12=18,表示指向第18个常量项,根据constant_pool_count的值为19可得,常量池中一共有18个常量项,巧了正好在最后一个,但是要知道18个常量项必须知道前17个常量项所占字节,这里就不一一找了,最后找到第18个常量项CONSTANT_Utf8_info在class文件中包含的内容如下:

澳门新葡萄京官网注册 17

第18个常量项

  • 澳门新葡萄京官网注册 18

    CONSTANT_Utf8_info型表的结构

  • 根据tag等于1得第18项是CONSTANT_Utf8_info型,该类型存储UTF-8编码的字符串,在TestClass.class文件种该常量项种个数据项的内容如下:

    • length(u2):表示UTF-8编码的字符串占用的字节数,值为0x0010=16.
    • bytes(u1):表示长度为length的UTF-8编码的字符串.
    • 因为length=16,所以
      length后面紧跟的长度为16个字节的连续数据是一个使用UTF-8缩略编码表示的字符串。后面紧跟的第一个字节为0x6A=106,那该编码代表的字符为j,我们发现106其实就是字符j对应的ASCII码。后面16个字节代表的字符就是:
      java/lang/Object

到此为止,我们得到了第一个常量项CONSTANT_Methodref_info的第二个数据项index指向的内容为CONSTANT_Class_info常量项,CONSTANT_Class_info常量的第二个数据项index指向CONSTANT_Utf8_info常量项,CONSTANT_Utf8_info常量项的内容为
java/lang/Object 。
当然CONSTANT_Methodref_info常量项还有第三个数据项index,其存放的也是一个其他常量的索引。

  • 根据上面的找法我们就可以找出常量池中包含的内容:字面量和符号引用。
package com.lixin;

import java.io.IOException;
import java.io.InputStream;

public class StreamUtils {

    public static int read1(InputStream in) throws IOException {
        return in.read() & 0xff;
    }

    public static int read2(InputStream in) throws IOException{
        return (read1(in) << 8) | read1(in);
    }

    public static int read4(InputStream in) throws IOException {
        return (read2(in) <<16) | read2(in);
    }

    public static long read8(InputStream in) throws IOException {
        long high = read4(in) & 0xffffffffl;
        long low  = read4(in) & 0xffffffffl;
        return (high << 32) | (low);
    }

    public static byte[] read(InputStream in,int length) throws IOException {
        byte[] buf = new byte[length];
        in.read(buf, 0, length);
        return buf;
    }
}

2.5 采用javap命令分析class文件

  • 根据上面的找法我们就可以找出常量池中包含的内容:字面量和符号引用。java考虑到这种找法太麻烦了,所以提供了一个命令javap来帮助我们分析class文件的内容。

  • javap分析class文件用法:javap -verbose class文件名

$ javap -verbose TestClass.class
Classfile /E:/studytry/com/zlcook/clazz/TestClass.class
  Last modified 2017-4-7; size 292 bytes
  MD5 checksum 486567c6d4d7432fc359230fed9c92c7
  Compiled from "TestClass.java"
public class com.zlcook.clazz.TestClass
  SourceFile: "TestClass.java"
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#15         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         //  com/zlcook/clazz/TestClass.m:I
   #3 = Class              #17            //  com/zlcook/clazz/TestClass
   #4 = Class              #18            //  java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               inc
  #12 = Utf8               ()I
  #13 = Utf8               SourceFile
  #14 = Utf8               TestClass.java
  #15 = NameAndType        #7:#8          //  "<init>":()V
  #16 = NameAndType        #5:#6          //  m:I
  #17 = Utf8               com/zlcook/clazz/TestClass
  #18 = Utf8               java/lang/Object
{
  public com.zlcook.clazz.TestClass();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 2: 0

  public int inc();
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 6: 0
}
  • 上面通过javap命令得到的结果,该结果显示的很友好,由过上面的理论我们可以很清楚的看到常量池一共18项:其中第一项如下:

 #1 = Methodref          #4.#15       //  java/lang/Object."<init>":()V
  • 和我们通过手动方式查看第一个常量项CONSTANT_Methodref_info对比一下就知道javap显示的内容是多么友好了。
第一个常量项 第几个 tag index index 最终代表的内容
class中16进制值 0X0A 0X004 0X000F
转换成10进制值 10 4 15 查完4和15才知道
javap分析显示的友好值 #1 Methodref #4 #15 java/lang/Object."<init>":()V

TestClass为待解析的目标类,读者可以任意改写此类来多做实验

2.6 class文件中包含的内容

  • 下面我们来看一下class文件中常量池的内容和java源码中的内容。

  • TestClass.java代码内容

package com.zlcook.clazz;

public class TestClass{
  private int m;
  public int inc(){
   return m+1;
  }
}
  • TestClass.class中常量池内容:

Constant pool:
   #1 = Methodref          #4.#15         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         //  com/zlcook/clazz/TestClass.m:I
   #3 = Class              #17            //  com/zlcook/clazz/TestClass
   #4 = Class              #18            //  java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               inc
  #12 = Utf8               ()I
  #13 = Utf8               SourceFile
  #14 = Utf8               TestClass.java
  #15 = NameAndType        #7:#8          //  "<init>":()V
  #16 = NameAndType        #5:#6          //  m:I
  #17 = Utf8               com/zlcook/clazz/TestClass
  #18 = Utf8               java/lang/Object
  • 再复习一下常量池中主要存放字面量:如文本字符串、声明为final的常量值等。和符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
  • 所以出现com/zlcook/clazz/TestClass、java/lang/Object、m、inc都是应该的,那么I、V、<init>、LineNumberTable都是什么?那肯定是字段描述符或者是方法描述符了。这部分是编译时自动生成的,它们会被class文件中其它部分(字段表field_info、方法表method_info、属性表attribute_info)引用到,它们会用来描述一些不方便使用“固定字节”进行表达的内容。譬如描述方法的返回值是什么?有几个参数?每个参数的类型是什么?因为Java中的“类”是无穷无尽的,无法通过简单的无符号字节来描述一个方法用到了什么类,因此在描述方法的这些信息时,需要引用常量表中的符号引用进行表达。
package com.lixin;

public class TestClass {

    private int a = 5;
    protected char c = 'c';
    double x = 1.1;
    long y = 111;

    public void show() {

    }
}

3. 哪些字面量会进入常量池中

  • 我们知道class文件存放字面量:如文本字符串、声明为final的常量值等。这里的“等”就挺烦人。
  • 下面我们来看看哪些字面量会进入常量池。(jdk1.8.0环境)

8种基本类型:

测试案例:

  • final类型 FinalTest.java代码

public class FinalTest{

   private final int int_num =12;
   private final char char_num = 'a';
   private final short short_num =30;
   private final float float_num = 45.3f;
   private final double double_num =39.8;
   private final byte byte_num =121;
   private final long long_num = 2323L;
   private final boolean boolean_flage = true;
}
  • 非final类型 test.java代码

public class test{

   private int int_num =12;
   private char char_num = 'a';
   private short short_num =30;
   private float float_num = 45.3f;
   private double double_num =39.8;
   private byte byte_num =121;
   private long long_num = 2323L;
   private long long_delay_num ;
   private boolean boolean_flage = true;

   public void init(){
     this.long_delay_num = 5555L;
   }
}

上面代码测试结果:

  • final类型的8种基本类型的值会进入常量池。
  • 非final类型的8种基本类型的值double、float、long的值会进入常量池,包括long_delay_num的值。

String类型

  • StringTest.java代码:

public class StringTest{

      private String str1 = "zl"+"cook";
      private String str2 = str1+"hello";
      private String str3 = new String("zlcook here?");
      private String str4 = "everybody "+ new String("here?");

      private final String fin1 = "boy";
      private final String fin2 = fin1+ "is boy";
      private final String fin3 = str1+ "is boy";
}
  • StringTest.class的常量池种包含内容:
![](https://upload-images.jianshu.io/upload_images/3458176-adab40db73749fe0.png)

常量池中包含的字符串类型字面量

所有测试数据github:
测试数据

测试方法入口:

结束

  • 这一节主要讲了Class文件魔数、版本号和常量池,比较详细介绍了常量池包含的内容以及用到的14种常量项结构。记住本节讲的常量池是class文件中的常量池,要记住还有运行时常量池,每个class文件中的常量池内容在类加载侯会进入方法区的运行时常量池中存放。当然运行时常量池的内容不仅包含这些还包含运行期加入的常量,常见的就是String类的intern()方法。
package com.lixin;

import java.io.InputStream;

/**
 * 程序入口
 * @author lixin
 *
 */
public class App {

    public static void main(String[] args) throws Exception {
        InputStream in = Class.class.getResourceAsStream("/com/lixin/TestClass.class");
        ClassParser parser = new ClassParser(in);
        parser.parse();
    }

}

最后,我们可以使用jdk中的javap进行字节码反编译,来对比我们的读取与反编译结果差别,用于查错。

javap -v TestClass.class >./out.txt

发表评论

电子邮件地址不会被公开。 必填项已用*标注