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)。interfaces接口类型是数组,因为java多继承;attribute_info是方法表中定义的code索引,指向具体的方法体字节码。interfaces接口类型是数组,因为java多继承;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{ }
class文件格式通过javap帮助分析如下:
Classfile /D:/spm-workspace/test/target/classes/com/jd/crm/Logback/TestClass.class Last modified 2023-4-14; size 597 bytes MD5 checksum 9d5d9fc2145ac17393fee7a707d3b9c 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个16进制,所以下面每2个代表一个字节)
2.1 魔法数前四个字节cafebabe是固定值,任何语言编译成jvm知道的二进制流,前四个字节必须是固定cafebabe字节。
2.2 版本号然后2个字节00表示次版本号为0 ;0034代表主版本为52(jdk版本号对应的jdk版本为1.8)参考jdk版本与class字节版本的对应关系
2.3 常量个数const____pool_count字节码000 20对应的说明常数为32,实际上为31,因为第一个jvm用作保留位。
2.4 常量池常量池存储两个常量:字面量和符号引用,如文本字符串、生命final常量值等,符号引用包括类别、界面的全限名称、字段、方法名称和描述符号等。参考javap生成的类别文件信息。
在这里,我们只分析其中一个常数。在上面的常数2字节后面,一个字节0a10进制为10,参考常量池类型10代表类中方法的符号。继续参考Methodref_info格式定义:前两个字节004代表方法所在类名称的索引,后两个字节0001a代表NameandType类型的索引。
2.5 类访问标志常量池定义后的U2标志访问标志,本例标志为0x0021和下图标志位按位或计算,如0x001为真,0x0020为真,其他是否为真 ACC最终确认访问标志位PUBLIC、ACC\_SUPER
2.6 这类、父类、接口索引集合根据图1的规则,引用u2字节0003识别当前类名,引用常量池数组下标为#3,根据图3所示子项的类名为com/jd/crm/Logback/TestClass;代表父类类名的引用常量池数组下标为#4,根据图4引用的父类名称为java//lang/Object;然后0001识别接口的数量,指出数量为10005识别第一个接口数组中接口的名称,指向常量池中下标记为5的名称为com/jd/crm/Logback/Super;
例如,找到当前的类索引如下图所示
2.7 字段表集合字段表以数组的形式存储在常量表中
上图显示,0002标识域的数量为2个域标识,其中有两个类别,一个类别的域字段staticVar 一是实例对象的域字段instanceVar,如字段结构定义(下图)定义,前两个字节001a为访问标志,与类访问标志一样,分别采用001a二进制和下图字段域访问标志类型进行定位或操作,访问类型为ACC\_PRIVATE类型。name\_index占用两个字节006,指向常量表下标为6的引用,descriptor\_index=007指向常量表下标记为7的引用,I标记为int的数据类型,attributes\_count=001为1,值为0008指向常量表下标有#8的引用常量Constantvalue,标识为静态变量,最后依次类推第二个域标识
定义字段结构
请参考字段域的访问标志,逻辑计算一致,但规则不同 如下图
2.8 方法表集合类似于域字段集合表的定义 常量池中也定义了数组模式 ,attributes\count代表方法的属性数量,attribute_info是属性集合参考属性表集合
访问标识类型的方法表
该方法的代码块存储在类型为Code的属性表中,通过上述方法的访问标志、名称索引和描述索引定义方法的基本信息。
2.9 属性表集合类别、字段表、方法表本身可以包含属性表,属性表结构如下,属性表结构类型较多,如Code类型、Exception类型、MethodParameters类型等,具体参考属性表类型。所有属性都引用常量池中的属性类型名称。然后根据属性的长度指定属性的内容,根据属性的不同类型分析不同的属性值。格式定义如下
以Code属性为例,Code属性结构如下所示
attribute__jvm按属性获取atributename\_index指向常量池中的字符串常量Code,然后attribute_length标识Code类型Info信息长度,包括:max\_stack 最大栈深,max_locals局部变量槽数量,code\_length识别机器字节码的长度,以后查询字节码如下图所示,实际上是0/1/4/5/6/9的指令集。Code类型,嵌套异常属性表,Linenumbertable,行号表、LocaVariableTable 局部变量表等信息。Code类型,嵌套异常属性表,Linenumbertable,行号表、LocaVariableTable 局部变量表等信息。javap生成的类定义信息如下图所示
1.Code1方法的执行过程:
构造方法:descriptor ()V标识无参无返回值为Void方法索引,flags可见性修饰符;
程序运行时,先将常量池、方法字节码、字符串常量池、静态变量加载到元数据区(1.8后字符串常量池、静态变量堆放);main线程开始运行,分配栈帧内存,操作数栈Stack=2表示操作方法所需的最大操作数栈深度为2;locals=1表示操作方法所需的最大局部方法表的最大slot数据是1;args_size是该方法的形参数,如果是实例方法 this引用了第一个形参。这个例子是this引用的。这个例子是this引用的。所以args\_size=1+实际参数
aload_0: 加载 slot0的局部变量,即this,作为下面的invokespeciall 调用结构方法的参数
invokespecial: 调用结构方法,常量池第#1项,即【Method java/lang/Object."":()V】
aload_0 :再次加载 slot0的局部变量,即thiss
iconst0: 将int类型为0的数值压入栈顶(为什么要再次放入栈顶,我个人可能是以下初始化实例需要指定到当前的实例对象)
putfileld: 常量池#2 也就是com//jd/crm/Logback/TestClass.instanceVar 实例变量赋值为0,并弹出栈。
通过上述指令操作,对象已初始化。可以发现,在实例变量初始化之前,先调用构造器方法,再初始化实例变量。
1.instancemethod执行Code2方法:
descriptor标识为int类型参与,int类型参与
flags标识方法问public类型
statck=2代表栈深度为2,locals=2.标志预留两个局部变量槽;args_size=标识两个参数,分别是隐藏的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中的符号进行分析的。首先,根据简单的名称和字段描述符在符号引用类别中搜索。如果发现,则返回该字段的直接引用并结束。否则,每个家庭都会从下到上搜索地柜。如果未发现,则抛出nosuckfielderor异常
•方法解析
•接口方法分析
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对象指针指向Clas对象。
Hotspot对象头头
实例数据:由虚拟机分配策略参数存储的对象定义的实例变量(-XX:FieldsAllocationStype)以及字段定义的顺序。HotSpot的默认分配策略是将相同宽度的字段存储在一起,在子类变量之前会出现父类变量。
对齐填充:jvm存储的任何尺寸都必须是8个字节的整数倍,这是不够的。这与二级字节流一致。以下是无锁对象实例化后的数据结构。用jol工具打印的实例布局如下
5 对象的访问 5.1 句柄访问Java堆将一块内存划分为句柄池,在reference中 存储是对象
句柄地址包含对象实例数据和类型数据的具体地址 息
5.2 直接访问直接访问是直接存储在reference中的实例对象的地址。实例对象包含类对象的访问指针,即如果访问对象需要更多的参考层
优缺点
这两种对象的访问方式各有优势。使用句柄访问的最大优点是,稳定的句柄地址存储在reference中。当对象被移动(垃圾收集时移动对象是一种非常常见的行为)时,只会改变句柄中的实例数据指针,而reference本身不需要修改。 使用直接指针访问的最大好处是速度更快,节省了一次指针定位的时间开支, 由于对象访问在Java中非常频繁,这种费用也是一个非常可观的执行成本。本书讨论的主要虚拟机Sunn 就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定义的文件常量合并后放入运行常量池,新常量也可以放入运行过程中的池中,如stringintern方法
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();///强制生成字符串常量池引用Systemm.out.println(s == s1);//返回true
String s1 ="a";String s = new String ("a");System.out.println(s == s1);//返回false
8 附件
jvm常量池类型和结构体定义
常量池类型
定义常量池类型结构
常见的属性类型
jdk版本好clas字节版本号对应关系
属性表类型