Jvm 常量池
从 Java 字节码文件看,其实主要包含三部分:常量池、字段信息、方法信息。其中常量池存储了字段和方法的相关符号信息,也是 Java 字节码文件的核心。这里说的常量池不同于 JVM 内存模型中的常量区,这里的常量池仅仅是字节码中的一部分信息,是 Java 编译器对 Java 源代码进行语法解析后的产物 ,比较粗糙,包含了各种引用,信息不够直观。而 JVM 最终会将字节码文件中的 常量池信息进行二次解析 ,还原出所有常量元素的全部信息,并存储到 JVM 内存模型中的 常量区 ,让内存与编写的 Java 源代码保持一致。
JVM 解析 Java 类字节码文件的接口是 ClassFileParser::parseClassFile(),其步骤为: 解析魔数 - 解析版本号 - 解析常量池 - 解析父类 - 解析接口 - 解析类变量 - 解析类方法 - 构建类结构 其中在解析完魔数、版本号后对常量池进行解析,主要链路为: ClassFileParser::parseClassFile() - > ClassFileParser::parse_constant_pool() - > oopFactory::new_constantPool()
分配常量池内存 / ClassFileParser::parse_constant_pool_entries() 解析常量池信息
内存分配
JVM 想要解析常量池信息,就必须先划出一块内存空间,将常量池的结构信息加载进来,然后才能进一步解析。内存空间的划分主要考虑两点:分配多大的内存、分配在哪里。JVM 使用一个专门的 C++类 constantPoolOop 来保存常量池的结构信息,而该类里实际保存数据的是属于 typeArrayOop 类型的_tags 实例对象。typeArrayOop 类是继承与 oopDesc 顶级结构,且没有新增字段,也就是这种类型仅仅只有标记和元数据
oopFactory::new_constantPool()
链路比较长,从宏观层面看,大体可以分为 3 个步骤:
- 在堆区分配内存空间 :最终在 psOldGen.hpp 中通过
object_space()->allocate(word_size)
实现 - 初始化对象 :主要通过 collectedHeap.inline.hpp 中调用
init_obj()
进行对象初始化,其实就仅仅是清零 - 初始化 oop :collectedHeap.inline.hpp 中调用
post_allocation_install_obj_klass()
完成 oop 初始化并赋值。
JVM 内部通过 oop-klass 来描述一个 Java 类。一个 Java 类的 实例数据 会被存放在堆中,而为了支持运行时反射、虚函数分发等高级操作,Java 类 实例指针 oop(堆中) 会保存一个指针,用于指向 Java 类的 描述对象 (方法区中),类描述对象中保存一个 Java 类中所包含的全部成员变量和全部方法信息,本步骤就是为了这一目标设计的
涉及到的类:
1、 oopFactory :即 oop 的工厂类。Java 是面向对象的语言,在 JVM 内部实现层面也得到彻底实现。在 JVM 内部,常量池、字段、符号、方法等一切都被对象包装起来,所有 这一切对象在内存中都通过 oop 这种指针进行跟踪(指向) ,JVM 根据 oop 所指向的实际内存位置便可获取到对象的具体信息。而在 JVM 内部,所有这些对象的内存分配、对象创建与初始化(清零) 工作都通过 oopFactory 这个入口得以统一实现。oopFactory 为各种 JVM 内建对象分配内存空间并初始化类型实例的机制在本质上是一样的, 都先获取对应的 klass 类描述信息,然后为 oop 分配内存空间
2、 constantPoolKlass :常量池内存对象。JVM 内部都通过 oop 指向某个内存位置,这个内存位置往往便是与该 oop 相对应的 klass 类型。 klass 用于描述 JVM 内部一个具体对象的结构信息(“元”信息) ,例如一个 Java 类中包含哪些字段、哪些方法、哪些常量,相当于 JVM 内部对数据结构的实现 。同理常量池在 JVM 内部也被表示为一种对象,不同 Java 类被编译后产生的字节码文件中的常量池大小、元素顺序和结构等都不同,因此 JVM 内部必须要预留一段内存区块来描述常量池的结构信息,这便是 constantPoolKlass 的意义所在
3、 collectedHeap :表示 JVM 内部的堆内存区,可被垃圾收集器回收并反复利用,代表了 JVM 内部广义的堆内存区域。在 JVM 内部,除了堆栈变量之外的一切内存分配,都需要经过该区域。如果 JVM 内部的一个实例对象不在这一区域申请内存空间,那么只能跑到 JVM 堆外内存去申请了
4、 psPermGen :表示 perm 区内存,在 JDK6 时代 Java 类的字节码信息会保存到该区域,而在 JDK8 时代, perm 区的概念被 metaSpace 取代 。
perm 是指内存的永久保存区,用于 存放 Class 和 Meta 的信息 ,Class 被加载的时候放入 permGen space 区域,它和存放 Java 类实例对象的堆内存区域不同,如果 Java 程序加载了太多 Java 类,就很可能出现 PermGen space 错误;而在 metaSpace 时代,这块区域属于”本地内存”区域,就是指相对于 JVM 虚拟机内存而言的操作系统内存,由 JVM 直接向操作系统申请内存存储 Java 类的元信息,因此类元数据空间的申请只受可用本地内存的限制(32 位/64 位操作系统),理论上只要物理机器尚有可用的虚拟内存,JVM 便能加载新的类元数据。
而当 JVM 所加载的类元信息所占内存空间达到 MaxMetaspaceSize 设定值时,会触发对僵死的类及类加载器的垃圾回收,而且相比 permSpace 的性能上也做了优化 PS.虽然 meatSpace 相比 permSpace 有重大改进,但是从 JVM 角度而言,并没有本质改变,JVM 的类结构在运行期的描述机制并没有改变,JVM 仍然从字节码文件中解析出常量池、字段、方法等类元信息,然后保存到内存的某个位置, 所以后续以 JDK6 为主分析
由于 JVM 的堆在 JVM 初始化过程中便完成了指定大小的空间分配,因此完成当前被加载的类所对应的常量池内存分配没有真正向操作系统申请,仅从 JVM 已申请的堆内存中划拨了一块空间用于存储常量池结构信息。内存的申请最终通过 object_space->allocate()
实现,该函数定义在 mutableSpace.cpp 中,会先执行 HeapWord* obj = top()
,再执行 HeapWord* new_top = obj + size
,将 permSpace 内存区域的 top 指针往高地址方向移动 size 大小的字节数,而最终返回的依旧是 obj 指针,该指针指向的是原来堆的最顶端,这样调用方通过指针可以还原从原堆顶到当前堆顶之间的内存空间,将其强制转换为常量池对象。在 JVM 启动过程中完成 permSpace 的内存申请和初始化,但是这块区域一开始是空的,随着不断有常量池信息写入到这块区域,top() 指针也会不断往高内存地址方向移动,每次新写入一个对象,该对象的内存首地址便是写入前 top() 指针所指的位置
constantPool 的内存有多大
为一个 Java 类创建对应的常量池,需要在 JVM 堆区为常量池先申请一块连续的内存空间,其大小取决于一个 Java 类在编译时所确定的常量池大小
在常量池初始化链路中会调用 constantPoolKlass::allocate() 方法,该方法会调用 constatnPoolOopDesc::object_size(length) 方法来获取常量池大小:
// src/share/vm/oops/constantPoolOop.hpp
// length 为 Java 类在编译期间由编译器所计算出来的常量池的大小
static int object_size(int length) {
// 将 header_size 与 length 相加后再进行内存对齐,便于 GC 进行工作时能高效回收垃圾,但会造成一定的空间浪费
return align_object_size(header_size() + length);
}
// 对象头大小
static int header_size() {
return sizeof(constantPoolOopDesc)/HeapWordSize;
}
sizeof() 函数在计算 C++类时,该函数返回的是其所有变量的大小加上虚函数指针的大小。对于 constantPoolOopDesc 本身包含 8 个字段,由于继承 oopDesc 父类,还会包含父类的 2 个成员变量:
typeArrayOop _tags;
constantPoolCacheOop _cache;
klassOop _pool_holder;
typeArrayOop _operands;
int _flags;
int _length;
volatile bool _is_conc_safe;
int _orig_length;
volatile markOop _mark; // 类型是指针
union _metadata{..} _metadata; // 联合体内部是指针类型
而对于 oopDesc 包含的 static BarrierSet bs 这个静态类型变量,在 JVM 启动之初会被操作系统直接分配到 JVM 程序的数据段内存区,因此 10 个字段在 32 位平台上将返回 40(字节)。而对于 HeapWordSize 是 HeapWord 类的大小,而该类只包含一个 char指针型变量,即返回指针宽度,因此 32 位平台上返回 4(字节),64 位平台上返回 8(字节),这样 header_size() 函数就是计算出 constantPoolOopDesc 这个类型实例在内存汇总所占用的双字数(32 位平台一个指针宽度为 4 字节,即双字) 因此最终 constantPoolKlass::allocate() 从 JVM 堆内存中所申请的内存空间大小包含两个部分: constantPoolOopDesc 大小 + Java 类常量池元素数量(length) ,即 JVM 最终在 32 位平台分配出(40 + length)*4 字节的内存大小
内存空间布局
在为 constantPoolOop 常量池对象分配内存时,需要分析 JVM 为常量池所申请的内存空间布局模型,前面说过 JVM 为 constantPoolOop 实例分配的内存大小是(headSize + length) 个指针宽度。在 JDK6 中 JVM 为常量池申请的内存位于 perm 区的一片连续空间,而 JVM 为常量池申请内存时也是整片区域 连续划分,不会存在碎片化
JVM 内部几乎所有的对象都是这种布局。总体而言 JVM 内部为对象分配内存时,会先分配 对象头 ,然后分配对象的 实例数据 ,不管字段对象还是方法对象还是数组,JVM 内部为对象实例分配内存空间的模型都是 对象头+实例数据 的结构。对于常量池而言,其对象头就是 constantPoolOop 对象本身,而实例数据所占空间的大小等于 Java 字节码文件中所有常量池元素所占空间的大小,那么应该就是保存 Java 字节码文件中的常量池元素的某些特殊信息,后面再细说
初始化内存
JVM 为 Java 类对应的常量池分配好空间后,就需要执行这段内存空间的初始化,所谓的初始化就是清零操作。由于在申请内存空间时执行了内存对齐,同时由于 JVM 堆区会反复加载新类和擦除旧类,因此如果不执行清零,则会影响后续对 Java 类的解析
在 CollectedHeap::common_permanent_mem_allowcate_init() 中调用 init_obj(obj, size),其内部调用其他函数对 JVM 内部对象清零,会将制定内存区的内存数据全部清空为零值
opp-klass 模型
上一篇文章和这篇之前也讲到过 opp-klass 模型 —— 一分为二的内存模型。为了消灭指针的目的,JVM 本身的类模型非常更复杂,而且这种复杂性从 JVM 发布以来,即使从 JDK6 到 JDK8 也仅仅是对 JVM 内部的几种具体类型进行了重组和去繁就简,并没有根本上的变革,只要 Java 语言对外提供的功能特性不发生巨大变化,则 JVM 内部的类模型就不会发生巨大的质变
前面讲过,JVM 内部基于 oop-klass 模型描述一个 Java 类,将一个 Java 类一拆为二分别描述,第一个模型是 oop,第二个模型是 klass。 oop 是普通对象指针,用来表示对象的实例信息 ,看起来像个指针,而实际上对象实例数据都藏在 指针所指向的内存首地址后面的一片内存区域中 。而 klass 包含元数据和方法信息 ,用来描述 Java 类或 JVM 内部自带的 C++类型信息,便是前面讲的数据结构,Java 类的基层信息、成员变量、静态变量、成员方法、构造函数等信息都在 klass 中保存,JVM 据此可以在运行期反射出 Java 类的全部结构信息,当然 JVM 本身所定义的用于描述 Java 类的 C++类也使用 klass 去描述
JVM 使用 oop-klass 这种一分为二的模型描述一个 Java 类,虽然模型只有两种,但是其实从 3 个不同的维度对一个 Java 类进行了描述。侧重于描述 Java 类的实例数据的第一种模型 oop,主要为 Java 类生成一张 实例数据视图 , 从数据维度描述一个 Java 类实例对象中各个属性在运行期的值 。而第二种模型 klass 则分别从两个维度去描述一个 Java 类,第一个维度是 Java 类的 元信息视图 ,另一个维度则是 虚函数列表,或者叫做方法分发规则 。元信息视图为 JVM 在运行期呈现 Java 类的”全息” 数据结构信息 ,这是 JVM 在运行期以动态反射出类信息的基础。
在 Java 语言中并没有 虚函数 这个概念,就是不能使用 virtual 这个关键字去修饰一个 Java 方法。而 C++实现面向对象多态性的关键字主要就是 virtual,而 JVM 在内部使用 C++类定义的一套对象机制去表达 Java 类的面向对象机制,这样 Java 类最终被表达成为 JVM 内部的 C++类,并且 Java 类方法的调用最终要通过对应的 C++,而 Java 方法本身不支持 virtual 这个关键字修饰,那么面对一个多重继承的 Java 类体系,C++如何知道类中哪个方法是虚函数,哪个不是呢?
而 Java 的做法是 将 Java 类的所有函数都视为是 virtual 的 ,这样 Java 类中的每个方法都可以直接被其子类、子子类覆盖而不需要增加任何关键字作为修饰。正因如此,Java 类中的每个方法都可以晚绑定,只不过对于一些确定的调用,在编译器就可以实现早绑定。正因为 JVM 将 Java 类中每个函数都视为虚函数,所以最终在 JVM 内部的 C++层面就必须维护一套函数分发表
体系总览
在 JVM 内部定义了 3 种结构去描述一种类型:oop、klass、handle 类,这 3 种数据结构不仅能够描述外在的 Java 类,也能够描述 JVM 内在的 C++类型对象
前面讲过 klass 主要描述 Java 和 JVM 内部 C++类的元信息和虚函数,这些元信息的实际值就保存在 oop 里面。oop 中保存一个指针指向 klass,这样在运行期 JVM 便能知道每一个实例的数据结构和实际类型。handle 则是对 oop 的行为的封装,在访问 Java 类时一定是通过 handle 内部指针得到 oop 实例的,再通过 oop 就能拿到 klass,如此 handle 最终便能操纵 oop 的行为了(特别的,如果是调用 JVM 内部 C++类型所对应的 oop 的函数,就不需要通过 handle 来中转,直接通过 oop 拿到指定 klass 便能实现)。klass 不仅包含自己所固有的行为接口,而且也能够操作 Java 类的函数。由于 Java 函数在 JVM 内部都被表示为虚函数,因此 handle 模型其实就是 Java 类行为的表达
它们三者的关系如下,可以看到 Handle 类内部只有一个成员变量_handle,类型为 oop*,最终指向一个 oop 的首地址,因此只要拿到 handle,就能进一步获取 oop 和关联的 klass 实例,从而取到 klass 对象实例后,便能实现对 oop 对象方法的调用前面讲到的 constantPool 常量池对象,其 oop 对象就是 constantPoolOop,对象结构与模型完全符合。JVM 内部定义了若干 oop 类型,每一种 oop 类型都有自己特有的数据结构,oop 的专有属性区就是用于存放各个 oop 所特有的数据结构的地方
oop 体系
Hotspot 里的 oop 其实就是 GC 所托管的指针,每一个 oop 都是一种 xxxOopDesc*类型的指针。所有 oopDesc 及其子类(除 markOopDesc 外) 的实例都由 GC 所管理,这才是最重要的,是 oop 区分 Hotspot 里所使用的其他指针类型的地方
对象指针本质而言就是个指针,由于 OOP 的鼻祖 SmallTalk 语言,它的对象也由 GC 管理,但是其一些简单的值类型对象会使用”直接对象”的机制实现,例如整数类型就并不是在 GC 堆上分配的对象实例,而是直接将实例内容存在了对象指针里的对象,这种指针也叫做带标记的指针,这一点与 markOopDesc 类型如出一辙,因为 markOopDesc 也是将整数值直接存储在指针里面,这个指针其实没有指向内存的功能。
所以在 SmallTalk 运行期,每拿到一个指针需要判断是直接对象还是真的指针,如果是真的指针,那么就是普通的对象指针了。所以在 Hotspot 里,oop 就是指一个真的指针,而 markOop 则是一个看起来像指针但实际上是藏在指针里的对象(数据),这也是 markOop 实例不受 GC 托管的原因,因为只要出了函数作用域,指针变量就直接从堆栈中释放掉了,不需要垃圾回收
在 oopsHierarchy.hpp 中定义了 oop 体系的所有成员,有十多种不同的 oop,其中最常用的是 constantPoolOop 和 instanceOop,前者前面讲过,后者作为 Java 程序的解释器和虚拟运行介质,JVM 将 Java 实例映射为 instanceOop
klass 体系
klass 提供了 2 种能力: 1、提供一个与 Java 类对等的 C++类型描述 2、提供虚拟机内部的函数分发机制 即 klass 分别从类结构与类行为这两方面去描述一个 Java 类(也包含 JVM 内部非开放的 C++类)
与 oop 相同,在 klassHierarchy.hpp 中也定义了 klass 体系的所有成员,除去 constantPoolOop 对应的 constantPoolKlass 外,最重要的就是 instanceKlass 和 klassKlass(在 JDK8 中已被去除) 了
字段名 | 含义/作用 |
---|---|
_layout_helper | 对象布局的综合描述符 |
_name | 类名,例如 java.lang.String 的该属性值是 java/lang/String |
_java_mirror | 类的镜像类 |
_super | 父类 |
_subklass | 指向第一个子类,若无则为 NULL |
_next_sibling | 指向下一个兄弟节点,若无则为 NULL |
_modifier_flags | 修饰符标识,例如 static |
_access_flags | 访问权限标识,例如 public |
如果一个 Klass 既不是 instance 也不是 array,则其_layout_helper 为 0;如果是 instance 则值为正数,表示 instance 的大小;如果是一个数组,则值为负数
handle 体系
handle 封装了 oop,通过 oop 拿到 klass,因此 handle 间接封装了 klass,JVM 内部使用一个 table 来存储 oop 指针
如果 oop 是对普通对象的直接引用,那么 handle 就是对普通对象的一种间接引用,是因为 GC 考虑使用这种方式的: 1、通过 handle,能够让 GC 知道其内部代码都有哪些地方持有 GC 所管理的对象的引用,这只要扫描 handle 所对应的 table,这样 JVM 便无须关注其内部到底那些地方持有对普通对象的引用 2、在 GC 过程中,如果发生了对象移动(比如从新生代移到老一代),那么 JVM 的内部引用无须跟着更改为被移动对象的新地址,JVM 只需要更改 handle table 里对应的指针即可
在涉及 Java 类的继承和接口继承,C++领域类的继承和多态性最终通过 vptr(虚函数表) 来实现,在 klass 内部记录了每一个类的 vptr 信息
vtable 虚函数表 :vtable 中存放 Java 类中非静态和非 private 的方法入口,JVM 调用 Java 类的方法(非 static/private) 时,最终会访问 vtable,找到对应的方法入口
itable 接口函数表 :Itable 中存放 Java 类所实现的接口类方法,同样 JVM 调用接口方法时,最终会访问 itable,找到对应的接口方法入口
不过要注意,vtable 和 itable 里存放的并不是 Java 类方法和接口方法的直接入口,而是指向了 Method 对象入口,JVM 会通过 Method 最终拿到真正的 Java 类方法入口,得到方法所对应的字节码/二进制机器码并执行。当然对于被 JIT 进行动态编译后的方法,JVM 最终拿到的是其对应的被编译后的本地方法入口
同样在 handles.hpp 里定义了 handle 体系的成员,还通过宏分别批量声明了 oop 和 klass 家族的各个类所对应的 handle 类型,在编译器宏被替换后,便有 opp 和 klass 其对应的 handle 体系。但是为啥定义了 2 套不同的 handle 体系,那是因为 JVM 使用 oop-klass 这种一分为二的模型去描述 Java 类以及 JVM 内部的特殊类群体,为此 JVM 内部特定义了各种 oop 和 klass 类型。但是对于每一个 oop 都是一个 C++类型,即 klass;而对于每个 klass 所对应的 class,在 JVM 内部又被封装为 oop。JVM 在具体描述一个类型时,会用 oop 去存储这个类型的实例数据,并用 klass 去存储这个类型的元数据和虚方法表。
当一个类型完成生命周期后 JVM 触发 GC 去回收,回收时既要回收一个类实例对应的实例数据 oop 也要回收对应的元数据和虚方法表(不是同时回收,一个在堆垃圾回收,一个在方法区垃圾回收)。为了让 GC 既能回收 oop 又能回收 klass, 因此 oop 本身被封装成 oop,而 klass 也被封装为 oop ,只是 JVM 内部恰好将描述实例的 oop 全部定义为以 oop 结尾的类,并将描述类结构和方法的 klass 全部定义为以 klass 结尾的类, 正好与 JVM 内部描述类信息的模型 oop-klass 重名了 ,所以产生了误解。
互相转换
1、oop 和 klass 到 handle : handle 主要用于封装 oop 和 klass,因此往往在声明 handle 类实例时,直接将 oop 或者 klass 传递进去,便完成了这种封装。同时当 JVM 执行 Java 类的方法时,最终也是通过 handle 拿到对应的 oop 和 klass
2、klass 与 oop 相互转化 : 为了便于 GC 回收,每一种 klass 实例最终都被封装成对应的 oop,具体操作时先分配对应的 oop 实例,接着 将 klass 实例分配到 oop 对象头的后面 ,从而实现 oop+klass 这种内存布局结构。对于任何一种给定的 oop 和其对应的 klass,oop 对象首地址到其对应的 klass 对象的 首地址距离是固定的 ,因此只要得到 oop 对象首地址,便能通过 偏移 固定的距离得到 klass 对象的首地址,相反得到 klass 的首地址后,也能通过偏移固定的距离得到 oop 的首地址。通过内存偏移,便能实现 oop 和 klass 的相互转换。对于每一种 oop,都提供了 klass_part() 这样的函数,通过该函数可以直接由 oop 得到对应的 klass 实例
klass 模型创建
在 JVM 启动过程中,先创建了 klassKlass 实例,再根据该实例,创建了常量池所对应的 Klass 类 —— constantPoolKlass。因此要分析 constantPoolKlass 需要先了解 klassKlass 实例的构建
klassKlass 创建大体上分为 6 步: 1、 为 klassOop 申请内存 (内存申请,标识初始化) 2、 klassOop 内存清理 3、 初始化 klassOop._mark 标识 (markOopDesc 不是真正的 oop,仅用于存储 JVM 内部对象的哈希值、锁状态标识等信息) 4、 初始化 klassOop._metadata (设置为 NULL) 5、 初始化 klass (klassOop 也是一个 oop,因此对象头的内存后面也会接一段数据区,这段数据区正是 klassKlass 类型实例存放的地方) 6、 自指 (_metadata 指向自己)
同样,constantPoolKlass 模型构建的步骤也是这样,只不过不需要第 6 步。JVM 最终在方法区创建的对象是 constantPoolKlass,但由于每个 klass 最终都被包装为 oop,因此在内存中构建的模型为:
在 JVM 构建 constantPoolOop 的过程中,由于其本身大小不确定,这种不确定体现在 length 个指针宽度 这块区域,因为不同的 Java class 被编译后,常量池元素的数量不同。因此 JVM 需要 使用 constantPoolKlass 来描述这些不固定的信息 ,这样最终 GC 在回收垃圾的时候才能准确地知道到底要回收多大的内存空间,这便是 constantPoolKlass 的意义所在。
而 constantPoolKlass 的实例大小也是不确定的,因此 constantPoolKlass 本身也需要其他 klass 来描述,这便是 JVM 在构建 constantPoolKlass 的过程中会引用 klassKlass 的原因。但是不能一个 klass 一直由另一个 klass 来描述,所以 JVM 将 klassKlass 作为整个引用链的终结符 ,并且让 klassKlass 指向自己,这便是 klassKlass 自指的原因
常量池解析
Java 类源代码被编译成字节码,字节码中使用常量池来描述 Java 类中的结构信息,JVM 加载某个类时需要解析字节码中的常量池信息,从字节码文件中还原出 Java 源代码中定义的全部变量和方法。而 JVM 运行时的对象 constantPoolOop 便是用来保存 JVM 对字节码常量池信息分析结果的
constantPoolOop 在创建的过程中,会执行 constantPoolKlass::allocate() 函数,该函数主要干了 3 件事情: 1、 创建 constantPoolOop 对象实例 (前文已经讲了) 2、 初始化 constantPoolOop 实例域变量 (将自己的字段设置默认值 0/null/true) 3、 初始化 tag (_tag 实际上会存放字节码常量池中的所有元素的标记,前一步被设为 null,会先为_tags 申请内存空间,大小也为 length 个指针宽度)
完成 constantPoolOop 的基本构建工作,然后 JVM 就是一步一步将字节码信息翻译成可被物理机器识别的动态的数据结构,即本文顶端步骤里写到的 ClassFileParser::parse_constant_pool_entries() 解析常量池信息,这个函数中通过一个 for 循环 处理所有的常量池元素,每次循环开始先执行 u1 tag = cfs->get_u1_fast()
从字节码文件中读取占 1 字节宽度的字节流,这时因为每个常量池元素起始的 1 字节都用于描述常量池元素类型,这在 上一篇文章 中有说过,JVM 解析常量池的第一步就是需要知道这是哪个元素类型。
在获取常量池元素类型后,通过 swtich 对不同元素进行处理,由于不同类型的组成结构不同,例如 JVM_CONSTANT_Class 类型的结构是 u1 标识+u2 索引,因此 JVM 只需要再调用 cfs->get_u2_fast()
获取索引即可,在获取到索引后将当前信息保存到 constantPoolOop 中,将当前常量池元素的 类型 保存到 constantPoolOop 所指的 tag 对应位置的数组中,然后将 名称索引 保存到 constantPoolOop 的数据区中对应的位置
对于像 JVM_CONSTANT_Class,由于其在常量区中的位置是 1,因此最终在 tag 和 constantPoolOop 数据区中的位置也是 1,其中在 constantPoolOop 数据区存储的值为 2,因为当前常量池元素的名称索引为 2,而 tag 的存储值为 7,即当前常量池元素类型是 JVM_CONSTANT_Class 枚举值 7。
然而并非所有元素类型在标识后面只有 1 个属性,方法元素就是其中之一,那么类型存在 tag 中,而因为有 2 个索引,JVM 给出的方案就是将这两个索引进行 拼接 ,变成一个值,然后再保存。对于常量池来说字符串的概念比较广泛,并不单指字符串变量,类名、方法名、类型、this 指针名等等,都可以看做字符串,最终都会被 JVM 当做字符串处理,存储到符号区。由于无论是 tag 还是 constantPoolOop 的数据区,一个存储位置只能存放一个指针宽度的数据,而字符串往往很大,因此 JVM 专门设计了一个 符号表 的内存区(Symbol Table),tag 和 constantPoolOop 数据区内仅保存指针指向符号区。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: JVM 概述
下一篇: Mybatis select 查询
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论