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

硅谷探秘者 4039 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) 2333 jvm(1)Java虚拟机在执行Java程序的过程中会把它所管理的为若干个不同的数据区域jvm包括三大子系统:类加载子系统,运行时数据区(),执行引擎详细图示
java虚拟机(jvm) 2697 jvm(5)堆溢出以及1.拟堆溢出代码packagetest;importjava.util.ArrayList;importjava.util.List
java虚拟机(jvm) 3935 jvm(1)已经对进行了一个宏观的概括http://www.jiajiajia.club/weblog/blog/artical/82那么下边具体一下方法执行的过程还是以一个
java虚拟机(jvm) 835 jvm-垃圾收集器和配策略(1)说起垃圾收集(GarbagcCollcction,GC),大部人都把这项技术当做java语言的伴生产物。事实上,GC的历史比Java久远,1960
java虚拟机(jvm) 3083 (StackFrame)用于储局部变量表、操作数栈、动态链接、方法出口等信息。局部变量表是一组变量值储空间,用于放方法参数和方法部定义的局部变量。在Java程序被编译为Class时,就在方法的Code属性
java虚拟机(jvm) 1634 这里以HotSpot为例,且所说的对象指普通的Java对象,不包括数组和Class对象等。参考资料深入理解java虚拟机《周志明》1.对象的布局HotSpot虚拟机中,对象在储的布局可以
java虚拟机(jvm) 3761 块称为“类加载器”。类加载器可以说是Java语言的一项创新,也是Java语言流行的重要原因之一,它最初是为了满足JavaApPlet的需求而开发出来的。虽然目前JavaApplet技术基本上已经“死掉
java虚拟机(jvm) 751 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 2021年04月  4 2021年05月  4 2021年06月  1 2021年07月  7 2021年08月  2 2021年09月  8 2021年10月  9 2021年11月  16 2021年12月  14 2022年01月  7 2022年05月  1 2022年08月  3 2022年09月  2 2022年10月  2
标签
算法基础 linux 前端 c++ 数据结构 框架 数据库 计算机基础 储备知识 java基础 ASM 其他 深入理解java虚拟机 nginx git 消息中间件 搜索 maven redis docker dubbo vue 导入导出 软件使用 idea插件 协议 无聊的知识 jenkins springboot mqtt协议 keepalived minio mysql ensp 网络基础 xxl-job
目录
没有一个冬天不可逾越,没有一个春天不会来临。最慢的步伐不是跬步,而是徘徊,最快的脚步不是冲刺,而是坚持。