1 类文件数据结构类型

Class文件构造次要有两种数据结构:无符号数和表

无符号数:用来表述数字,索引援用、数量值以及字符串等,比方 图1中类型为u1,u2,u4,u8别离代表1个字节,2个字节,4个字节,8个字节的无符号数

:表是有由多个无符号数以及其它的表组成的复合构造,比方图1中类型以_info结尾的项为表类型。

2 类构造定义

Class类文件是紧凑、程序、无空隙的,魔数(MagicNumber)、Class文件版本(Version)、常量池(Constant\_Pool)、拜访标记(Access\_flag)、本类(This\_class)、父类(Super\_class)、接口(Interfaces)、字段汇合(Fields)、办法汇合(Methods )、属性汇合(Attributes)。其中因为java多继承所以interfaces接口类型为数组;attribute_info则是办法表中定义的code索引,指向具体的办法体字节码。如图1所示。

上面用一段程序做阐明,此类有接口,有办法、类变量和实例变量,机器是如何辨认字节码而后依照下面的规定来定义此class类呢?

package com.jd.crm.Logback;public class TestClass implements Super{    private static final int staticVar = 0;    private int instanceVar=0;    public int instanceMethod(int param) throws  Exception{        return param ++;    }}interface Super{ }

通过javap帮忙解析class文件格式如下:

Classfile /D:/spm-workspace/test/target/classes/com/jd/crm/Logback/TestClass.class  Last modified 2023-4-14; size 597 bytes  MD5 checksum 9d5dd9fc2145ac17393fee7a707d3b9c  Compiled from "TestClass.java"public class com.jd.crm.Logback.TestClass implements com.jd.crm.Logback.Super  minor version: 0  major version: 52  flags: ACC_PUBLIC, ACC_SUPERConstant pool:   #1 = Methodref          #4.#26         // java/lang/Object."<init>":()V   #2 = Fieldref           #3.#27         // com/jd/crm/Logback/TestClass.instanceVar:I   #3 = Class              #28            // com/jd/crm/Logback/TestClass   #4 = Class              #29            // java/lang/Object   #5 = Class              #30            // com/jd/crm/Logback/Super   #6 = Utf8               staticVar   #7 = Utf8               I   #8 = Utf8               ConstantValue   #9 = Integer            0  #10 = Utf8               instanceVar  #11 = Utf8               <init>  #12 = Utf8               ()V  #13 = Utf8               Code  #14 = Utf8               LineNumberTable  #15 = Utf8               LocalVariableTable  #16 = Utf8               this  #17 = Utf8               Lcom/jd/crm/Logback/TestClass;  #18 = Utf8               instanceMethod  #19 = Utf8               (I)I  #20 = Utf8               param  #21 = Utf8               Exceptions  #22 = Class              #31            // java/lang/Exception  #23 = Utf8               MethodParameters  #24 = Utf8               SourceFile  #25 = Utf8               TestClass.java  #26 = NameAndType        #11:#12        // "<init>":()V  #27 = NameAndType        #10:#7         // instanceVar:I  #28 = Utf8               com/jd/crm/Logback/TestClass  #29 = Utf8               java/lang/Object  #30 = Utf8               com/jd/crm/Logback/Super  #31 = Utf8               java/lang/Exception{  public com.jd.crm.Logback.TestClass();    descriptor: ()V    flags: ACC_PUBLIC    Code:      stack=2, locals=1, args_size=1         0: aload_0         1: invokespecial #1                  // Method java/lang/Object."<init>":()V         4: aload_0         5: iconst_0         6: putfield      #2                  // Field instanceVar:I         9: return      LineNumberTable:        line 3: 0        line 7: 4      LocalVariableTable:        Start  Length  Slot  Name   Signature            0      10     0  this   Lcom/jd/crm/Logback/TestClass;  public int instanceMethod(int) throws java.lang.Exception;    descriptor: (I)I    flags: ACC_PUBLIC    Code:      stack=1, locals=2, args_size=2         0: iload_1         1: iinc          1, 1         4: ireturn      LineNumberTable:        line 10: 0      LocalVariableTable:        Start  Length  Slot  Name   Signature            0       5     0  this   Lcom/jd/crm/Logback/TestClass;            0       5     1 param   I    Exceptions:      throws java.lang.Exception    MethodParameters:      Name                           Flags      param}SourceFile: "TestClass.java"

以上是javap帮忙咱们生成的class文件解析后果,只是给人看,而非机器。

通过编译后生成class文件格式如下,因为class文件是以8位作为一个字节的二进制流。为了不便计算,用16进制示意二进制(1个字节=2个十六进制的数,故上面每2个数就代表1个字节)

2.1 魔法数

前四个字节cafebabe是固定值,任何语言编译成jvm意识的二进制流,前四位必须是固定的cafebabe字节。

2.2 版本号

紧接着2个字节00示意次版本号为0 ;0034代表主版本为52(jdk版本号对应的jdk版本为1.8)参考jdk版本和class字节版本的对应关系

2.3 常量个数

常量个数const\_pool\_count字节码为00 20对应的阐明常量个数为32,理论为31个,因为首位jvm作为保留位应用。

2.4 常量池

常量池寄存两大常量:字面量和符号引,字面量如文本字符串,被生命的final常量值等,而符号援用则蕴含类、接口的全限名称、字段、办法名称和形容符号等等。参考javap生成的类文件信息。

这里只剖析下其中一个常量,在下面常量个数2个字节前面紧接着一个字节0a十进制为10,参考常量池类型10代表类中办法的符号援用。持续参考办法类型MethodRef_info个格局定义:前两个字节0004代表办法所在类名称的索引,后两个字节0001a代表一个NameAndType类型的索引。

2.5 类拜访标记

紧接常量池定义完后的u2标识拜访标记,本例标识为0x0021和下图标记位按位或计算,如0x0001为真,0x0020也为真,其余为否 最终确认拜访标记位ACC\_PUBLIC、ACC\_SUPER

2.6 本类、父类、接口索引汇合

依据图1的规定,u2两个字节0003标识以后类名的援用到,援用常量池数组下标为#3,依据图3所示子项的类名为com/jd/crm/Logback/TestClass;0004代表父类类名的援用常量池数组下标为#4,依据图4所示援用的父类类名为java/lang/Object;紧接着0001标识接口个数,指明数量为1,0005标识第一个接口数组中接口的名称,指向常量池中下标为5的名称为com/jd/crm/Logback/Super;

比方查找以后类索引如下图

2.7 字段表汇合

字段表以数组的模式定义存储在常量表中

以上图阐明,0002标识域个数为2个域标识,在本类中有两个,一个类的域字段staticVar 一个是实例对象的域字段instanceVar,如字段构造定义(下图)定义,前2个字节001a为拜访标识,和类拜访标识一样,别离用001a的二进制和下图字段域拜访标识类型做位或运算,得出拜访类型为ACC\_PRIVATE类型。name\_index的占用两个字节0006,指向常量表下标为6的援用,descriptor\_index=0007指向常量表下标为7的援用,此处为I标识为数据类型为int,attributes\_count=0001为1个,值为0008指向常量表下标为#8的援用常量ConstantValue,标识为动态变量,最终顺次类推第二个域标识援用

字段构造定义

字段域的拜访标记请参考类拜访标记,逻辑计算统一,只是规定不一样而已 如下图

2.8 办法表汇合

和域字段汇合表定义相似 也是数组形式定义在常量池中 ,其中办法的构造体第四个字段attributes\_count代表办法的属性数量,attribute\_info就是属性的汇合参考属性表汇合

办法表拜访标识类型

通过下面办法的拜访标记、名称索引和形容索引定义方法的根本信息,办法的代码块则寄存于类型为Code的属性表中。

2.9 属性表汇合

类、字段表、办法表自身可蕴含属性表,属性表格构造体如下,属性表构造类型较多,比方有Code类型、Exception类型、MethodParameters类型等等,具体参考属性表类型。所有的属性都是援用常量池中的属性类型名称。而后依据属性的长度指定该属性的内容,依据属性的不同类型解析不同的属性值。格局定义如下

以Code属性举例,Code属性构造如下所示

jvm按属性获取attribute\_name\_index指向常量池一个字符串常量Code,紧接着attribute\_length标识Code类型Info信息长度,这个info内容包含:max\_stack 最大栈深,max\_locals局部变量槽数量,code\_length标识机器字节码长度,往后查问字节码如下图所示,其实就是0/1/4/5/6/9的指令集。Code类型又嵌套异样属性表、行号表LineNumberTable、LocaVariableTable 局部变量表等等信息。如下图javap生成的类定义信息

1.Code1办法执行过程:

构造方法:descriptor ()V标识无参无返回值为Void的办法索引,flags可见性修饰符;

程序运行时,先将常量池、办法字节码、字符串常量池,动态变量加载到元数据区(1.8后字符串常量池,动态变量放入了堆);main线程开始运行,调配栈帧内存,其中操作数栈stack=2示意运行该办法所须要的最大操作数栈的深度是2;locals=1示意该运行办法所须要的最大部分办法表的最大slot数据是1;args\_size是该办法的形参个数,如果是实例办法 第一个形参是this援用。此例正是this援用。所以args\_size=1+理论的参数

aload_0: 加载 slot0的局部变量,即this,作为上面的invokespecial 构造方法调用的参数

invokespecial: 调用构造方法,常量池第#1项,即【Method java/lang/Object."<init>":()V】

aload_0 :再次加载 slot0的局部变量,即this

iconst0: 将int类型为0的数值压入栈顶(为什么要再放入栈顶,我集体人为可能是上面初始化实例会须要指定到以后的实例对象)

putfileld: 将常量池中#2 也就是com/jd/crm/Logback/TestClass.instanceVar 实例变量赋值为0,并弹出栈。

通过以上指令操作,对象曾经初始化,可发现在实例变量初始化之前是先调用的结构器办法,后才初始化实例变量。

1.Code2办法instanceMethod执行过程:

descriptor标识为int类型入参、int类型出参

flags标识办法问public类型

statck=2代表栈深度为2,locals=2标识预留两个局部变量槽;args_size=2标识两个参数,别离为暗藏的this和办法的形式参数,下标[0]=this、 [1]=param 如下所示

LocalVariableTable:

Start Length Slot Name Signature

0 4 0 this Lcom/jd/crm/Logback/TestClass;

0 4 1 param I

0:iload_1 标识将下面局部变量槽LocalVariableTable下标为1的param参数压入栈

1:iconst_1 将int类型为1的常量数字压入栈

2: iadd 将以后栈顶的两个元素 param和1相加

3: ireturn 返回

LineNumberTable:

line 10: 0

标识理论java源代码的行数

2.10 字节码指令简介

•加载和存储指令:

•运算指令

•类型转换指令

•对象创立和拜访指令

•操作数栈治理指令

•管制转移指令

•异样解决指令

•同步指令

•办法调用和返回执行

invokervirtual:调用对象的实例办法 invokerinterface 调用接口办法,主动运行期搜寻一个实现接口的对象进行办法调用;invokerspeical:调用init、公有和父类调用的非凡办法调用;invokedynamic:运行时动静解析

3 类文件加载

3.1 加载

jvm通过classLoader(双亲委派)将class类文件二进制流加载到元数据区内存,

将字节流所标识的动态存储构造转换为元数据区的动静存储

在堆内存创立一个Class对象,堆中的Class并不存储动态变量、常量、办法等理论信息(理论存储元空间),能够看做只是一个句柄,通过对象头的类指针指向元空间类信息。这样在强制转换或者InstanceOf判断时,会依据对象中的类指针指向元空间的类常量池进行判断是否为同一个类。

3.2 验证

1、文件格式验证

2、元数据验证

3、字节码验证

4、符号援用验证

3.3 筹备

筹备阶段是为类变量(动态变量)分配内存并设置类变量初始值的阶段,调配这些内存是在元数据区外面进行的,然而类变量(无final润饰的动态变量)、字符串常量在1.8及当前都放入了堆区间。这个阶段有两点须要重点介绍以下的:

1、只有类变量(被static润饰的变量赋值初始值,static final润饰的赋值为程序指定值)会分配内存,不包含实例变量,实例变量是在对象实例化的时候在堆中分配内存的。

2、设置类变量的初始值是数量类型对应的默认值,而不是代码中设置的默认值。例如public static int number=111,这类变量number在筹备阶段之后的初始值是0而不是111。而给number赋值为111是在类的初始化阶段。

3.4 解析

解析阶段是虚拟机将常量池内的符号援用替换为间接援用的过程,解析动作次要针对类或接口、字段、类办法、接口办法、办法类型、办法句柄和调用点限定符7类符号援用进行。

符号援用:常量池中类、字段的常量字符串示意形式

类和接口的解析举例:如果类A援用了类B,加载阶段是动态解析,这时候B还没有被放到JVM内存中,这时候A援用的只是代表B的符号,这是符号援用。

间接援用: 指向指标的指针或者绝对偏移量

类和接口的解析举例:类A在解析阶段发现自己符号援用了B,如果这个时候B还没被加载。就是间接触发B的类加载,加载后会在运行常量池存储B的无效类信息地址,并且间接援用。

•类和接口的解析

•字段解析依据常量池字段filedrf_info中的符号进行解析,首先在符号援用的类中依据简略名称和字段描述符查找,如果查到则返回这个字段的间接援用并完结,否则从下往上地柜各个父类查找,如果还未查到则抛出NoSuckFieldError异样

•办法解析

•接口办法解析

4 类实例初始化

初始化,为类的动态变量赋予正确的初始值,JVM负责对类进行初始化,次要对类变量进行初始化clinit办法。在Java中对类变量进行初始值设定有两种形式:定义动态变量并指定值、应用动态代码块

对象初始化

4.1 初始化对象前查看

jvm碰到一个new指令,首先判断改指令指向的常量池的类全名是否被加载、解析初始化过,如果没有则进行类加载,参考类文件加载

4.2 内存调配

通过jvm内存分配机制,此分配机制取决回收机制,通过指针碰撞办法或者闲暇列表形式进行堆内存调配;

1.指针碰撞法 假如Java堆中内存是残缺的,已调配的内存和闲暇内存别离在不同的一侧,通过一个指针作为分界点,须要分配内存时,仅仅须要把指针往闲暇的一端挪动与对象大小相等的间隔。应用的GC收集器:Serial、ParNew,实用堆内存规整(即没有内存碎片)的状况下。这两种都是新生代垃圾收集器,因而都是应用复制算法,能够失去比拟残缺的内存区域。

2.闲暇列表法 事实上,Java堆的内存并不是残缺的,已调配的内存和闲暇内存互相交织,JVM通过保护一个列表,记录可用的内存块信息,当调配操作产生时,从列表中找到一个足够大的内存块调配给对象实例,并更新列表上的记录。应用的GC收集器:CMS,实用堆内存不规整的状况下。从名字中的Mark Sweep这两个词能够看出,CMS 收集器是一种“标记-革除”算法实现的,因而会失去很多碎片因而和闲暇列表配合应用。

内存调配并发问题

在创建对象的时候有一个很重要的问题,就是线程平安,因为在理论开发过程中,创建对象是很频繁的事件,作为虚拟机来说,必须要保障线程是平安的,通常来讲,虚拟机采纳两种形式来保障线程平安:

•CAS: CAS 是乐观锁的一种实现形式。所谓乐观锁就是,每次不加锁而是假如没有抵触而去实现某项操作,如果因为抵触失败就重试,直到胜利为止。虚拟机采纳 CAS 配上失败重试的形式保障更新操作的原子性。

•TLAB(本地现成缓冲区): 为每一个线程事后调配一块堆内存,JVM在给线程中的对象分配内存时,首先在TLAB调配,当对象大于TLAB中的残余内存或TLAB的内存已用尽时,再采纳上述的CAS进行内存调配。

4.3 初始化0值

内存调配实现后,虚拟机须要将调配到的内存空间都初始化为零值(不包含对象头),这一步操作保障了对象的实例字段在 Java 代码中能够不赋初始值就间接应用,程序能拜访到这些字段的数据类型所对应的零值。

4.4 对象头设置

初始化零值实现之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何能力找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。这些信息寄存在对象头中。另外,依据虚拟机以后运行状态的不同,如是否启用偏差锁等,对象头会有不同的设置形式。

4.5 实例结构器初始化

4.6 对象的内存布局

对象在对中的存储布局次要分为三局部,对象头、实例数据、对齐填充

对象头:

次要两类:其次要包含两局部数据:Mark Word、Class对象指针。特地地对于数组对象而言,其还包含了数组长度数据。在64位的HotSpot虚拟机下,Mark Word占8个字节,其记录了Hash Code、GC信息、锁信息等相干信息;而Class对象指针则指向该实例的Class对象。

HotSpot对象头

实例数据:对象定义的实例变量,这部分数据存储受到虚拟机调配策略参数(-XX:FieldsAllocationStype)和字段定义的程序影响。HotSpot默认调配的策略是将雷同宽度字段一起寄存,父类的变量会呈现在子类变量之前。

对齐填充:jvm存储任何大小必须是8个字节的整数倍,不够补齐。这个和类二级制字节流统一。上面是个无锁状态的对象实例化后的数据结构,应用jol工具打印出的实例布局如下

5 对象的拜访

5.1 句柄拜访

Java堆中将会划分出一块内存来作为句柄池,reference中 存储的就是对象

的句柄地址,而句柄中蕴含了对象实例数据与类型数据各自的具体地址信 息

5.2 间接拜访

间接拜访是reference中间接存储的实例对象的地址,实例对象中蕴含了类对象的拜访指针,也就是如果拜访类对象须要多一层援用

优缺点

这两种对象拜访形式各有劣势,应用句柄来拜访的最大益处就是reference中存储的是稳固的句柄地址,在对象被挪动(垃圾收集时挪动对象是十分广泛的行为)时只会扭转句柄中的实例数据指针,而reference自身不须要批改。 应用间接指针拜访形式的最大益处就是速度更快,它节俭了一次指针定位的工夫开销, 因为对象的拜访在Java中十分频繁,因而这类开销千里之行;始于足下后也是一项十分可观的执行老本。就本书探讨的次要虚拟机Sun HotSpot而言,它是应用第二种形式进行对象拜访的,但从整个软件开发的范畴来看,各种语言和框架应用句柄来拜访的状况也非常常见

6 虚拟机字节码执行引擎

6.1 运行时栈帧构造

1.局部变量表:在class文件被编译时,就已知某个办法的局部变量槽有几个,次要寄存办法参数和办法外部定义的局部变量

2.操作数栈:和局部变量表类似,编译时就明确了操作数栈的深度

3.动静链接:大部分类在类加载解析过程中,会将符号援用转为间接援用,也就是在类加载阶段分明调用哪个类的哪个办法(这些办法调用参考字节码指令简介中invoke*指令),然而有一部分必须在运行期间能力确定指标的办法的间接援用。

4.办法返回地址

6.2 办法调用

1.解析:在内解析阶段,会将符号援用转换为间接援用,这种在解析阶段就能确定的调用办法版本称为解析,比方invokesatic invokespecial invokevirtual等等指令批示的办法调用

2.动态分派:办法的重载,虚拟机须要依据办法的入参个数和类型方能定位到某个具体方法,产生在编译阶段,故也属于一种解析形式

3.重载办法匹配优先级:办法重载过程中,波及办法的入参和个数,而入参存在主动类型转换,比方重载办法入参为char类型,如果不存在入参为char类型的办法匹配,则char进行主动类型转换为int类型,在最终匹配了Int入参类型的办法。办法重载的实质

4.动态分配:如下图所示,man和women和从新man援用指向women而后办法调用sayHello,此时字节码显示的符号援用都是Human#sayHello,然而理论执行后果和指令码不统一,这是因为invokevirtual指令,在指令调用之前都会aload_x来加载理论的数据类型,这就是办法重写的实质

5.invokedynamic指令:为了解决其余invok*指令办法调配规定齐全固化在虚拟机中的问题,jvm反对设计者更高的灵便度,将动静调用能够以api的形式间接应用。参考java.lang.invoke包的应用形式。

6.3 基于栈的字节码解释执行引擎

jvm是基于栈的指令汇合,这种指令本身不带参数,应用操作数栈的输入输出作为指令自身的参数。物理机个别是基于寄存器的指令集,指令自身携带参数并存放在寄存器。

上面是一个基于栈来展现在虚拟机中字节码是如何执行的。

以上字节码执行过程如下

7 容易混同点

7.1 文件常量池

类加载后,类的域字段、办法和类形容信息会加载到元数据区,既属于类的动态常量池

7.2 运行时常量池

咱们下面说的class文件中的常量池,它会在类加载后进入办法区中的运行时常量池。并非只有Class定义的文件常量合并解决后放入运行时常量池,在运行期间也能够将新的常量放入池中,比方String类的intern办法

7.3 字符串常量池

字符串常量池寄存在堆内存(>=1.8)中,堆里边的字符串常量池寄存的是字符串的援用或者字符串(两者都有),如下图形容字符串创立的堆散布

上图阐明:

援用初始化初始化s、s2是先看常量池,有就返回对象援用,否则创立abc对象,而后创立s1/s2Ref常量援用返回

字符串相加:先创立StringBuilder对象,而后apend字符串a、apend字符串b 而后toString(new办法)生成字符串ab对象并在字符串常量池生成援用返回,为什么不要字符串相加,就是因为会生成大量StringBuilder对象

String s = "a"+"b";//返回的是常量池的ab字符串的援用String s1 ="ab";System.out.println(s == s1);//因两个最终都指向字符串常量池,所以为true

new 字符串相当于堆创立两个对象,一个String对象,而后创立字符串堆存储,而后String对象援用到字符串的堆存储,

String s1 ="a";String s = new String ("a").intern();//强制生成字符串常量池援用System.out.println(s == s1);//返回true
String s1 ="a";String s = new String ("a");System.out.println(s == s1);//返回false

8 附件

jvm常量池类型和构造体定义

常量池类型

常量池类型构造定义

常见的属性类型

jdk版本好class字节版本号对应关系

属性表类型

作者:京东物流 王北永

起源:京东云开发者社区