Jvm内存模型分析-class文件结构

硅谷探秘者 3421 0 0


Jvm内存模型分析-class文件结构


以下面的类为例介绍一下class文件的结构

package jvm;
public class MainTest {
         public static int a=0;
         public static String b="time";
         public int name;
         public void test() {
                  int c=0;
         }
}

Javap –v反编译结果为

Classfile /C:/Users/jiajia/eclipse-workspace/jvm/Test/bin/jvm/MainTest.class
  Last modified 2019-5-1; size 506 bytes
  MD5 checksum f5918edd3feb02f16c996163e06aa82f
  Compiled from "MainTest.java"
public class jvm.MainTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Class              #2             // jvm/MainTest
   #2 = Utf8               jvm/MainTest
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               a
   #6 = Utf8               I
   #7 = Utf8               b
   #8 = Utf8               Ljava/lang/String;
   #9 = Utf8               name
  #10 = Utf8               <clinit>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Fieldref           #1.#14         // jvm/MainTest.a:I
  #14 = NameAndType        #5:#6          // a:I
  #15 = String             #16            // time
  #16 = Utf8               time
  #17 = Fieldref           #1.#18         // jvm/MainTest.b:Ljava/lang/String;
  #18 = NameAndType        #7:#8          // b:Ljava/lang/String;
  #19 = Utf8               LineNumberTable
  #20 = Utf8               LocalVariableTable
  #21 = Utf8               <init>
  #22 = Methodref          #3.#23         // java/lang/Object."<init>":()V
  #23 = NameAndType        #21:#11        // "<init>":()V
  #24 = Utf8               this
  #25 = Utf8               Ljvm/MainTest;
  #26 = Utf8               test
  #27 = Utf8               c
  #28 = Utf8               SourceFile
  #29 = Utf8               MainTest.java
{
  public static int a;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC
 
  public static java.lang.String b;
    descriptor: Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_STATIC
 
  public int name;
    descriptor: I
    flags: ACC_PUBLIC
 
  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: iconst_0
         1: putstatic     #13                 // Field a:I
         4: ldc           #15                 // String time
         6: putstatic     #17                 // Field b:Ljava/lang/String;
         9: return
      LineNumberTable:
        line 4: 0
        line 5: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
 
  public jvm.MainTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #22                 // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Ljvm/MainTest;
 
  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: return
      LineNumberTable:
        line 8: 0
        line 9: 2
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       3     0  this   Ljvm/MainTest;
            2       1     1     c   I
}
SourceFile: "MainTest.java"


1.Class文件结构概述

        Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项 时,则会按照高位在前的方式分割成若干个8位字节进行存储。

        根据Java虚拟机规范的规定, Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表,后面的解析都要以这两种数据类型为基础,所以这里要先介绍这两个概念。

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

        表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以 “info”结尾。表用于描述有层次关系的复合结构的数据,整个 Class文本质上就是一张表,它由下图所示的数据项构成


QQ截图20190501224201.png


2.魔数与class版本(Majic,Minor_version,Major_version)

        位置:0-3字节

        java的魔数统一为 0xCAFEBABE (来源于一款咖啡)。它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class文件。很多文件存储标准中都使用数来进行身份识别譬如图片格式,如gif或者jpcg等在文件头中都存有魔数。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。文件格式的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过同时又不会引起混淆即可。


QQ截图20190501224201.png


        紧接着魔数的4个字节存储的是 Class文件的版本号:第5和第6个字节是次版本号,第七第八字节是主版本号。

        java的版本号是从45开始的,JDK1之后的每个JDK大版本发布主版本号向上加1(JDK1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能向下兼容以前版本的 Class文件,但是不能运行以后的class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的 Class文件。


QQ截图20190501224201.png


QQ截图20190501224507.png


3.常量池(Constant_pool)

        紧接着主次版本号之后的就是常量池入口。常量池可以理解为class文件的资源仓库。

        常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于java语言层面的常量的概念,比如文本字符串,声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:

        1.      类和接口的全限定名

        2.      字段的名称和描述符

        3.      方法的名称和描述符

 

        常量池中常量的数量是不固定的,所以常量池的入口处有一个u2类型的数据,表示常量池中常量的数值大小

sd.png


        0x001E转换为10进制为30,意为常量池中有29个常量。(注意常量池计数是从1而不是0开始)

        可以用javap -v 命令反编译查看Class文件的信息

        从下图中也可以看出常量池中有29个常量

sdsds.png


        常量池中的每个常量都是一个表,共有11种不同的表结构,它们有一个共同的特点,就是表开始的第一位都是一个u1类型的标志位(tag,取值为1到12,缺少标志为2的数据类型)。


常量池的项目类型

QQ截图20190501224724.png

        常量池的数据结构总表(图片来自于《深入理解java虚拟机》)


1212121.jpg



以上一个类文件为例,简单分析一下常量池的信息

a.png


        常量池的入口从第9和第10个字节开始,ox001E代表有29个常量项,接下来0x07代表第一项的tag值为7(CONSTANT_Class_info),接下来两个字节0x0002代表指向全限定名常量项的索引索引值为2,接下来ox01代表第二项的tag值为7(CONSTANT_Utf8_info)接下来两个字节0x000c代表字符串占用的字节数length为12,接下来有length(12)个长度为u1(即一个字节)的字符串,即0x6A 74 6D 2F 4D 61 69 6E 54 65 73 74就表示了字符串jvm/MainTest。

        和javap –v命令分析的结构一样

b.png


4.访问标志

        常量池之后,紧跟着2个字节来表示访问标志,用于识别一些类或者接口层次的访问信息,包括:这个class是类还是接口,是否定义为public类型,是否定义为abstract类型,如果是的话是否被声明为final等。具体的标志,以及标志的含义如下:

QQ截图20190501225116.png


        针对MainTest这个类来说,其访问标志应该是ACC_PUBLIC、ACC_SUPER这2个标志为真,所以其值为 0x0001 | 0x0020 = 0x0021

c.png


5.类索引、父类索引、以及接口索引集合

        类索引(thiss_classsuper class)都是一个u2类型的数据,而接口索引集合(interfaces Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang. Object外,所有Java类的父类索引都不为0接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按 implements语句(如果这个类本身是一个接口,则应当是 extends语句)后的接口顺序从左到右排列在接口索引集合中。

        类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为 CONSTANTClass info的类描述符常量,通过 CONSTANT Class_ iinfo类型的常量中的索引值可以找到定义在 CONSTANT_utf8 info类型的常量中的全限定名字符串。

        对于接口索引集合,入口的第一项——u2类型的数据为接口计数器(interfacescount), 表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。

d.png


        如上图:0x0021为访问标志,0x0001为类索引,0x0003为父类索引。由于没有实现接口,所以Interfaces_count为0 。



6.字段表集合

        字段表(feld_info)用于描述接口或者类中声明的变量。字段(feld)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。我们可以想一想在Java中描述一个字段可以包含什么信息?可以包括的信息有:字段的作用域(public private、 protected修饰符)、是实例变量还是类变量,可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。



字段信息结构表

QQ截图20190501225432.png


字段访问标志:

sssss.png


描述符标识字符含义

        描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值

tytytytyty.png


        接口索引后面紧跟着的是字段表信息,字段表的入口前2个字节表示字段的个数,在本例子中只定义了个三个字段,所以其值为0x0003,后面紧跟着的是该字段的描述表。

        第一个变量为public static int a=0;所以access_flags访问标志应该为0x0001|0x0008=0x0009

        0x0005指向了常量池中的第五个索引(变量名name_index),0x006指向了常量池中的第六个索引(字段名简单描述descriptor_index)。0x0000表示attribute_count = 0,说明本字段没有额外的描述信息。

可参考下图理解

sdsdsdsdsd.png


7.方法表集合

        方法表的结构与字段表的结构是一样的

rrrrr9.png


        从MainTest 类的class文件的信息中可以发现,类中有三个方法如下:

dfdfdgghghgjhjhjhjhjh.png


lll.png


        方法表的入口0x0003代表有三个方法

        0x0008代表第一个方法的访问标志access_flags为static

        0x000A代表第一个方法的方法名指向常量池中的第10个索引

        0x000B代表第一个方法的方法描述符指向常量池中的第11个索引

        0x0001代表第一个方法的属性集合有一个属性

        0x000C代表第一个属性的索引为常量表中的12,对应常量Code.说明此属性是方法字节码描述,这个属性就存储了方法里的java代码编译后的字节码指令。


8.属性表


    在Class文件、字段表、方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

QQ截图20190502141331.png


Code属性

        Java方法里的代码被编译处理后,变为字节码指令存储在方法表的Code属性里,但并不是所有的方法表里都有Code属性,例如接口或抽象类中的方法就可能没有该属性。

Code属性数据结构:

QQ截图20190502141409.png

     上边已经说到0x0001代表有一个属性,并且0x000c代表是code属性

    那么根据code属性的数据结构可知0x000c代表属性名,随后四个字节0x00002E代表code属性的长度为46个字节。

sdf.png


    紧随attribute_length属性后的两个字节0x0001代表操作数栈的数量为1,随后0x0000代表本地变量表的数量为0.再往后四个字节0x0000000a代表字节码指令的长度为10,接下来10个字节为单字节具体的的字节码指令。

例:0x03  iconst_0   int0推送至栈顶

         0xb3  putstatic   为指定的类的静态域赋值 0x000d指向了常量池中的第13个索引i.

         0x12  ldc      int, floatString型常量值从常量池中推送至栈顶 0x0f指向了常量池中的第15个索引。

         0xb3  putstatic   为指定的类的静态域赋值 0x0011代表了常量池中第17个索引

         0xb1  return    从当前方法返回void

方法结束

对应的代码如下:

sdfsdfsdfsdfsdf.png

sdsdf.png

    再往后还有异常信息和其他属性,暂时忽略


未完待续.......




猜你喜欢
java虚拟机(jvm) 1842 jvm(1)Java虚拟机在执行Java程序的过程中会把它所管理的为若干个不同的数据区域jvm包括三大子系统:类加载子系统,运行时数据区(),执行引擎详细图示
java虚拟机(jvm) 2217 jvm(5)堆溢出以及1.拟堆溢出代码packagetest;importjava.util.ArrayList;importjava.util.List
java虚拟机(jvm) 3478 jvm(1)已经对进行了一个宏观的概括http://www.jiajiajia.club/weblog/blog/artical/82那么下边具体一下方法执行的过程还是以一个
java虚拟机(jvm) 430 jvm-垃圾收集器和配策略(1)说起垃圾收集(GarbagcCollcction,GC),大部人都把这项技术当做java语言的伴生产物。事实上,GC的历史比Java久远,1960
java虚拟机(jvm) 2645 (StackFrame)用于储局部变量表、操作数栈、动态链接、方法出口等信息。局部变量表是一组变量值储空间,用于放方法参数和方法部定义的局部变量。在Java程序被编译为Class时,就在方法的Code属性
java虚拟机(jvm) 1142 这里以HotSpot为例,且所说的对象指普通的Java对象,不包括数组和Class对象等。参考资料深入理解java虚拟机《周志明》1.对象的布局HotSpot虚拟机中,对象在储的布局可以
java虚拟机(jvm) 3082 块称为“类加载器”。类加载器可以说是Java语言的一项创新,也是Java语言流行的重要原因之一,它最初是为了满足JavaApPlet的需求而开发出来的。虽然目前JavaApplet技术基本上已经“死掉
java虚拟机(jvm) 337 1.标记-清除算法最基础的收集算法是“标记-清除”(Mark-Sweep)算法,如同它的名字一样,算法为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对
归档
2018年11月  12 2018年12月  33 2019年01月  28 2019年02月  28 2019年03月  32 2019年04月  27 2019年05月  33 2019年06月  6 2019年07月  12 2019年08月  12 2019年09月  21 2019年10月  8 2019年11月  15 2019年12月  25 2020年01月  9 2020年02月  5 2020年03月  16 2020年04月  4 2020年06月  1 2020年07月  7 2020年08月  13 2020年09月  9 2020年10月  5 2020年12月  3 2021年01月  1 2021年02月  5 2021年03月  7
标签
算法基础 linux 前端 c++ 数据结构 框架 数据库 计算机基础 储备知识 java基础 ASM 其他 深入理解java虚拟机 nginx git 消息中间件 搜索 maven redis docker dubbo
目录