来源:京东技术 导读
一个java程序类文件由程序员编写并且编译二进制后,如何能更节约空间、更安全的、更高效寻址等等,可以从本文略知一二。
本文将从以下几个点说明:
1、类、方法、普通字段域、静态字段域等等字节码存储
2、类对象如何实例化,内存分配如何,分配的过程中存在什么问题等等
3、方法调用的当前线程栈的运行情况
导读
一个java程序类文件由程序员编写并且编译二进制后,如何能更节约空间、更安全的、更高效寻址等等,可以从本文略知一二。
本文将从以下几个点说明:
1、类、方法、普通字段域、静态字段域等等字节码存储
2、类对象如何实例化,内存分配如何,分配的过程中存在什么问题等等
3、方法调用的当前线程栈的运行情况
Class文件结构主要有两种数据结构:无符号数和表
无符号数:用来表述数字,索引引用、数量值以及字符串等,比如图1中类型为u1,u2,u4,u8分别代表1个字节,2个字节,4个字节,8个字节的无符号数。
表:表是有由多个无符号数以及其它的表组成的复合结构,比如图1中类型以_info结尾的项为表类型。
类结构定义
图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.classLast modified 2023-4-14; size 597 bytesMD5 checksum 9d5dd9fc2145ac17393fee7a707d3b9cCompiled from "TestClass.java"public class com.jd.crm.Logback.TestClass implements com.jd.crm.Logback.Superminor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPERConstant pool:#1 = Methodref #4.#26 // java/lang/Object."":()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#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 // "":()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: ()Vflags: ACC_PUBLICCode:stack=2, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."":()V 4: aload_05: iconst_06: putfield #2 // Field instanceVar:I9: returnLineNumberTable:line 3: 0line 7: 4LocalVariableTable:Start Length Slot Name Signature0 10 0 this Lcom/jd/crm/Logback/TestClass;public int instanceMethod(int) throws java.lang.Exception;descriptor: (I)Iflags: ACC_PUBLICCode:stack=1, locals=2, args_size=20: iload_11: iinc 1, 14: ireturnLineNumberTable:line 10: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcom/jd/crm/Logback/TestClass;0 5 1 param IExceptions:throws java.lang.ExceptionMethodParameters:Name Flagsparam}SourceFile: "TestClass.java"
以上是javap帮助生成的class文件解析结果,只是给人看,而非机器。
通过编译后生成class文件格式如下,因为class文件是以8位作为一个字节的二进制流。为了方便计算,用16进制表示二进制(1个字节=2个十六进制的数,故下面每2个数就代表1个字节)
图2
2.1 魔法数
2.2 版本号
2.3 常量个数
2.4 常量池

2.5 类访问标志
图4
2.6 本类、父类、接口索引集合
比如查找当前类索引如下图
图5
2.7 字段表集合
图6
图7字段域的访问标志请参考类访问标志,逻辑计算一致,只是规则不一样而已 如下图
图8
2.8 方法表集合


方法表访问标识类型
2.9 属性表集合
类、字段表、方法表本身可包含属性表,属性表格结构体如下,属性表结构类型较多,比如有Code类型、Exception类型、MethodParameters类型等等,具体参考属性表类型。所有的属性都是引用常量池中的属性类型名称。然后根据属性的长度指定该属性的内容,根据属性的不同类型解析不同的属性值。格式定义如下
图11
图12
图13
2.10 字节码简介指令
加载和存储指令
运算指令
类型转换指令
对象创建和访问指令
操作数栈管理指令
控制转移指令
异常处理指令
同步指令
方法调用和返回执行
invokervirtual:调用对象的实例方法 invokerinterface 调用接口方法,自动运行期搜索一个实现接口的对象进行方法调用;invokerspeical:调用init、私有和父类调用的特殊方法调用;invokedynamic:运行时动态解析。
类文件加载
图14
3.1 加载
3.2 验证
1、文件格式验证
2、元数据验证
3、字节码验证
3.3 准备
3.4 解析
类和接口的解析
字段解析根据常量池字段filedrf_info中的符号进行解析,首先在符号引用的类中根据简单名称和字段描述符查找,如果查到则返回这个字段的直接引用并结束,否则从下往上地柜各个父类查找,如果还未查到则抛出NoSuckFieldError异常
方法解析
接口方法解析
对象初始化
图154.1 初始化对象前检查
jvm碰到一个new指令,首先判断改指令指向的常量池的类全名是否被加载、解析初始化过,如果没有则进行类加载,参考类文件加载。
4.2 内存分配
CAS:CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。 TLAB(本地现成缓冲区):为每一个线程预先分配一块堆内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配。
4.3 初始化0值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4.4 对象头设置
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
4.5 实例构造器初始化
略
4.6 对象的内存布局
对象在对中的存储布局主要分为三部分,对象头、实例数据、对齐填充
图16
主要两类:其主要包括两部分数据:Mark Word、Class对象指针。特别地对于数组对象而言,其还包括了数组长度数据。在64位的HotSpot虚拟机下,Mark Word占8个字节,其记录了Hash Code、GC信息、锁信息等相关信息;而Class对象指针则指向该实例的Class对象。
图17
HotSpot对象头
实例数据:对象定义的实例变量,这部分数据存储受到虚拟机分配策略参数(-XX:FieldsAllocationStype)和字段定义的顺序影响。HotSpot默认分配的策略是将相同宽度字段一起存放,父类的变量会出现在子类变量之前。
图18
图19
Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
5.2 直接访问
这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。就本文讨论的主要虚拟机Sun HotSpot而言,它是使用第二种方式进行对象访问的,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。
6.1 运行时栈帧结构
6.2 方法调用
图20
6.3 基于栈的字节码解释执行引擎
下面是一个基于栈来展示在虚拟机中字节码是如何执行的。
图21
图227.1 文件常量池
7.2 运行时常量池
图23
7.3 字符串常量池
字符串常量池存放在堆内存(>=1.8)中,堆里边的字符串常量池存放的是字符串的引用或者字符串(两者都有),如下图描述字符串创建的堆分布。
图24
字符串相加:先创建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);//返回trueString s1 ="a";String s = new String ("a");System.out.println(s == s1);//返回false附件
jvm常量池类型和结构体定义

图25

图26
常量池类型结构定义

图27
常见的属性类型


图29