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

2019 精帖
0 422


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

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


未完待续.......




留言(0)
加载更多
猜你喜欢
  • blog 程序员必须掌握的数据和算法

    链接:https://www.zhihu.com/question/23148377?sort=created  算法基础 时间复杂度 空间复杂度  基础数据 线性表 列表(必学)
  • blog java异常捕获

    java异常捕获 思考问题: 调用下面的方法别会返和输出回什么?package com.itdragon.controller;import org.junit.Test;public class TestMain { p
  • blog java类是如何加载的

    首先要知道类的加载的过程包括了加载、验证、准备、解、初始化五个阶段。 java虚拟机加载class的过程         虚拟机把Class加载到,然后进行校验,解和初始化,最终形
  • blog java使用dom4j解xml

    引入pom<dependency> <groupId>org.dom4j</groupId> <artifactId>dom4j</artifactId> <version>2.0.0</version></depen
  • blog linux的目录

    linux的目录/bin: /usr/bin: 可执行二进制的目录,如常用的命令ls、tar、mv、cat等。/boot:放置linux系统启动时用到的一些。/boot/vmlinuz 为 linux 的,以及 /boo
  • blog SpringBoot打包离资源

    SpringBoot打包离资源         springboot项目打包时,把资源,如配置,静态资源离出来,避免为了修改资源时重
  • blog java工具 jmap 命令的使用方法以及堆快照的创建及(1)

            jmap是java虚拟机自带的一种映像工具,我们可以通过该工具配合不同的参数来查看java虚拟机的详细信息(如程序中出现的所有对象的数量以及占用大小等),以及通过虚拟机
  • blog 并发编程之-java线程(JMM)和volatile关键字理解

    的效率与一致性         在正式讲解Java虚拟机并发相关的知识之前,我们先花费一点时间去了解一下物理计算机中的并发问题,物理机遇到的并发问题与虚拟机中的情况有不少相似之处,物理机对并发的
  • blog java-java虚拟机栈最大深度问题与优化

    Java虚拟机栈都包含那些东西         在阅读过深入理解java虚拟机以后了解到java虚拟机栈包括栈帧、局部变量表、操作数栈、动态链接、方法返回等。 Java虚拟机栈都储那些容呢
  • blog java-hotspot虚拟机对象探秘

    Java对象的创建 Java是一门面向对象的编程语言,在Java程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象(例如克隆、反序列化)通常仅仅是一个new关键字而已,而在虚拟机中,对象(中讨论的对象限于普通Java对象,不