共计 10403 个字符,预计需要花费 27 分钟才能阅读完成。
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_SUPER
Constant 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 字节版本号对应关系
属性表类型
作者:京东物流 王北永
起源:京东云开发者社区