作业3:java对象模型
一 对象表示机制
1 Hotsplot JVM内部对象表示系统
(1)OOP-Klass二分模型
- OOP:Ordinary Object Pointer 或者OOPS。即普通对象指针,描述对象实例信息。
- 职能:表示对象的实例数据,没必要持有任何虚函数(java的重写方法的实现)。
- Klass:Java类的C++对等体,用来描述
- 职能:Klass对象中有VTBL(继承自Klass父类 Klass_ktbl),Klass能根据java对象的实际类型进行C++分发,即OOPS对象只需要通过Klass就能找到所有的虚函数。
(2)Klass向JVM提供两个功能
- 实现语言层面的Java类,Klass_ktbl中实现
- 实现Java对象的分发功能(注意:虚函数是C++实现多态的工具,在运行时根据虚表决定调用合适的函数。这被称作动态分发。java的运行时多态的实现。),Klass子类提供的虚函数实现。
2 Oops模块:OOP框架和Klass框架
(1)Oops模块示意图
(2)OOP框架框架类层次
3 OOP框架以及对象访问机制
(1)OOP框架类的基类
class oopDesc{
private:
volatile markOop _mark;
union _metadata{
wideKlassOop _klass; // 32位
narrowOop _compressed_klass; // 开启-XX:UseCompressedOops才选用,32位指针
} _metadata; //元数据指针
}
(2)instacneOopDecs或arrayOopDesc又称为对象头,instacneOopDecs对象头包含
- Mark Word:_mark成员
注:互斥量就是Monitor。
参考:《Java并发编程的艺术》 第二章节
- 元数据指针:指向描述类型的Klass对象的指针,Klass对象又包含了实例对象所属类型的元数据。
(3)OOPS对象优化措施:C++内联函数!
-
普通函数调用原理:执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元,执行函数代码(也许还需将返回值放入寄存器中),然后跳回到地址被保存的指令。
-
简言之,普通函数调用需要保护现场(main函数运行到第几行指令进栈)-> 调用函数(复制main函数中的参数副本) -> 恢复现场(出栈返回结果至main函数的第几行指令)。这是存在系统资源开销的,如何将调用函数的代码直接嵌入到主函数就没了这个开销。(参考:https://blog.csdn.net/buptzhengchaojie/article/details/50568789)
-
内联函数:短小精炼的代码,在编译阶段将函数体嵌入到每一个调用该函数的语句块中,最大程度降低调用开销。
-
OopDesc中的部分内联函数
// 初始化mark word
inline void oopDesc::init_mark()
// 是否是类实例
omit ...
// 是否是数组
omit ...
// 原子操作设置mark word
inline markOop oopDesc::cas_set_mark(markOop new_mark,markOop old_mard){
// cmpxchg加Lock前缀能够在多核下锁住总线,使得只有单个CPU操作共享内存中的变量.
return (markOop) Atomic::cmpxchg_ptr(new_mark,&_mark,old_mark)
}
// 获取对象对应的类型klass
inline klassOop oopDesc::klass() const{
if(UseCompressedOops){
return (klassOop)decode_heap_oop_not_null(metadata.compressed_klass);
}else{
return metadata.klass;
}
}
参考1:https://www.jianshu.com/p/8c3c0426e4f7
参考2:http://www.lenky.info/archives/2012/11/2028
(4)对象访问定位
4 Klass和instanceKlass
(1)Klass类层次结构
(2)核心数据结构:Klass
-
对于instance而言,_layout_helper值为正,代表instance的大小
-
对于数组Klass,_layout_helper值为负,如下图所示:
(3)核心数据结构:instanceKlass
HotSpot 为每一个加载的Java类创建一个instanceKlass对象,用于在JVM层表示Java类。
(4)实例数据的存储顺序
-
默认为longs/doubles、ints、shorts/chars、bytes/booleans、OOPS的顺序分配。
-
相同宽度的字段总是分配在一起
-
父类中定义的变量可能出现在子类之前
5 使用HSDB调试HotSpot
(1)如何启动HSDB
jdk目录下输入:java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
注:如果遇到sawindbg.dll不存在,参考该地址解决:https://blog.csdn.net/fl_zxf/article/details/42689569
(2)HSDB工具
(3)CLHSDB命令行
二 类的状态转换
1 Class文件
jvm规范(JavaSE 10):https://docs.oracle.com/javase/specs/jvms/se10/jvms10.pdf
ClassFile {
u4 magic; // value 常数: 0xCAFEBABE
u2 minor_version;
u2 major_version; // java8为例 45.0<=version<=52.0
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags; // 访问标识,如public final abstract super interface 等
u2 this_class; // 当前类:记录一个有效的常量池下标
u2 super_class; // 超类,同上
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
(1)常量池
// 常量池项
cp_info{
u1 tag; // 不同的值代表不同的类型,参照下图
u1 info[];
}
(2)字段表
field_info{
u2 access_flags;
u2 name_index; // 常量池索引,该字段的全限定名
u2 descriptor_index; // 常量池索引,字段的描述符
u2 attributes_count; // 字段附加属性数量
attribute_info attributes[attributes_count];
}
(3)方法表
method_info{
u2 access_flags; // 与字段表类似,具体参考jvm规范手册
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
(4)属性表
- 类文件、字段和方法都能有属性列表,用于标识它们的一些属性,如:表示内部类、表示过期的、表示运行时注解可见等。其中ConstantValue表示常量,常量的类型由ConstantValue中constantvalue_index指向的类型表示。
attribute_info{
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length]; // 属性
}
2 JVM内部定义的类状态
- unparsable_by_gc:初始值,未解析
- allocated:已分配,未链接
- loaded:已加载,并插入到JVM内部类的层次体系(class hierarchy)
- linked:已连接,未初始化
- being_initialized:正在初始化
- fully_initialized:完成初始化
- initialization_error:初始化过程中出错
3 类加载、连接和初始化过程
- 加载:从*.class读取字节流,并根据JVM规范解析处类或接口类型的二进制描述格式,并创建相应的类或接口
- 链接:JVM运行时识别有效的类或接口的过程
-
- 验证:确保类或接口的二进制表示结构的
- 准备:为静态字段分配空间,并初始化这些字段
- 解析:将符号引用转化为直接引用的过程
- 初始化:执行类或接口初始化方法的过程
注:visualvm能进行性能统计:类加载时间等
4 加载
(1)初始化类加载器
- 初始化一些Perf Data计数器()
- 搜索lib库:libverify、libjava和ibzip库
- sun.boot.class.path表示的路径下初始化启动类加载路径 日志输出=> Bootstrap loader class path
- 如果VM选项LazyBootClassLoader,则需要设置meta,日志输出 => Meta index for
调试参数:-XX:TraceClassLoading -XX:TraceClassLoadingPreorder
(2)加载(Classfile模块):从Class文件字节流中提取类型信息。
- ClassFileParser:类解析器,解析.class文件,利用ClassFileStream读取.class文件作为输入流,作为ClassFileParser输入
- Verifier:验证器,验证*.class文件中的字节码。每个类都需要创建一个ClassVerifier来验证
- ClassLoader:类加载器
- SystemDictionary:系统字典,记录已加载的类(类、类加载器、公共类klass)
- SymboleTable:字符表,用于快速查找字符串
- 下图为类加载流程图:
5 链接
Class文件中有个静态常量池,加载成功后,静态常量池中的符号引用转化为直接引用并对接运行时常量池的过程称为解析。当符号引用首次被访问时才去解析(Hotspot,不同JVM有不同实现)。
下图是instanceKlass类中定义了链接过程link_class_impl(),主要步骤如下:
(1) 验证:确保类或接口的二进制信息有效,主要验证method。
- 方法的访问控制
- 参数和静态变量
- 变量是否初始化
- 变量类型检查
- 验证局部变量表
- 堆栈是否被滥用
(2) 准备:分配内存空间并准备好初始化类中的静态变量
- char类型默认为'\u0000'
- byte默认为(byte)0
- boolean默认为0
- float默认为0.0f
- double默认为0.0d
- long默认为0L
(3) 解析:将常量池中的符号引用转换为直接引用,包括类、接口、字段和类方法和接口方法。
最重要的方法解析:
- 方法字节码重写是为了解释器运行性能,向常量池添加缓存(constantPoolCache),并调整相应的字节码的常量池索引重新指向常量池Cache索引。
- 重定位和方法链接:为Java的每个方法配置编译器和解释器的内部入口。
6 初始化:执行初始化方法
(1)触发类初始化的几种情况
- JVM需要引用类或接口的指令时:new getstatic putstatic invokestatic
- 调用java.lang.invoke.MethodHandle实例时,返回结果为REF_getStatic REF_putStatic REF_invokeStatic
- 调用类库中的反射方法时,如Class类或java.lang.reflect包
- 初始化类的子类时
- 类是JVM启动时的初始类
(2)(instanceKlass)类初始化
三 类的创建
1 概述
字节码new表示创建对象,JVM遇到这个指令,从栈顶取得目标对象在常量池中的索引,接着定位目标对象的类型,根据JVM中该类的状态,采取相应的内存分配技术(根据类是否已经初始化),在内存中分配实例空间,并完成实例数据和对象头的初始化。
2 实例化流程
- 快速分配:栈上分配
- 慢速分配:堆上分配