hotspot源码角度看OOP之类属性的底层实现(一)

hello,大家好,我是江湖人送外号[道格牙]的子牙老师。

最近看hotspo源码有点入迷。hotspot就像一座宝库,等你探索的东西太多了。每次达到一个新的Level回头细看,都有不同的感触。入迷归入迷,分享还是得分享。分享使大家夸我,使我快乐。_

最近报名JVM小班的同学问我问的比较多的是类加载阶段对属性的处理那块。这块知识点还挺多,不局限于加载阶段:

  1. 加载阶段如何存储属性
  2. 准备阶段给属性赋初值,细节是什么
  3. 初始化阶段给静态属性赋值,细节是什么
  4. 创建对象时给非静态属性赋值,细节是什么
  5. 访问属性时,细节又是什么

这么多细节,一篇文章肯定讲不完,就整个系列篇吧。本篇是OOP之类属性的系列篇首篇。开整…

问题分析

在计算机的世界里,一个问题的解决方案永远不止一种。但是取舍过后,最合适的只剩唯一。当然,你能想到的解决方案的多少,与你对这个问题的理解程度是息息相关的。对于一个问题的理解程度,与你的技术视野紧密不可分。你想到而能不能做到,与你的技术实力直接挂钩。好像不止是计算机世界哈,哪个世界都这样!

比如说让你来实现OOP机制。咱们先不说完整的,就聚焦属性继承,你会如何实现。经常看我文章的小伙伴可能比较奇怪,为什么我总是问类型这样的问题?因为研究底层与研究应用层不同,或者说你用研究应用层的思维来研究底层也可以,但是效果一定不会太好。就我自身来说,我研究底层,第一件事情就是让自己身处设计者的角度,以设计者的思维来思考问题,来理解思想,来阅读代码。每次问这样的问题,意在此。设计者思维是一种试图理解思维,学习思维是一种批判思维。

言归正传,我都能猜出来大家会如何实现,上代码
在这里插入图片描述

解释下这段代码:oop是所有Java对象在hotpot中的存在形式,klass是所有Java类在hotspot中的存在形式。现在需要在对象中存储实例数据,毫无疑问需要用到容器,毫无疑问map是最合适的容器。你如果用Java来实现,确实只能这样写。因为Java作为应用层语言,除了Unsafe提供了简单的操作内存的方法外,Java是没有内存处理能力的。而且写Java程序,聚焦业务实现即可。你的代码性能好不好,吃不吃内存,安不安全,本质还是取决于你选择的JVM是否优秀。

如果hotspot这样去实现,也可以哈。只不过不够优秀,可能会听到来自其他编程语言的鄙视。何为优秀的程序,当下能实现的,达到时间与空间完美结合。如果这样实现的话,会存在内存浪费过于严重的问题。这个我就不解释了,我之前的文章有讲过。

那hotshot是如何实现的呢?内存编织。即在一块事先申请好的内存中,按照属性类型,给它分一块相同大小的内存块,织入进去。我之前写的那篇文章,这里没有展开讲。本篇文章,对,展开细讲这里。

这里面有这些问题需要我们来作答:

  1. 这个事先申请好的内存,得申请多大
  2. 属性有可能是bool、char、short、int、long、oop,如何编织能做到既节省内存又内存对齐
  3. 织入的时候是无状态的,即你在访问一个对象的属性的时候,不能通过oop.a这样直接找到,那怎么办呢
  4. 采用内存编织的方式创建对象一定就不会内存浪费了吗

分配多大内存

就如你打算给你素未谋面的男女朋友买双鞋子,谁知道他她多大脚呢?猜一下?合适了说你阅人无数,不合适说你不上心。哎,太难了,还是算吧。

怎么算呢?你脑海中得有一张图?不,是两张图。什么图?对象的内存布局图。
在这里插入图片描述
那第四个问题就有答案了:还是会存在内存浪费,灰色的padding区域就是为了对齐而填充的区域,即浪费的内存。但是这个浪费是很少的了,是可以接受的了,是目前条件下可以做到的最好的了。

这里面每个区域占用内存大小的细节如下:

  1. Mark Word:在32位机器下,占4B。在64位机器下,占8B。本篇文章说的是64位机
  2. 类型指针:又名klass pointer,开启指针压缩占4B,关闭指针压缩是8B。默认是开启的。开启指针压缩占4B是指有效数据占4B,这块区域在内存中还是占8B。这个区域很重要,怎么理解这个重要呢?一、这个区域跟第三个问题的答案有关,后面讲;二、指针压缩的开启或关闭,对内存结构图有影响。对比两幅图就能看出来,会多出一个填充区域。这个细节,后面讲
  3. 数组长度:如果是数组对象,占4B。如果非数组对象,占0B,即不会出现
  4. 实例数据:这块是核心影响区域,等下细讲
  5. 对齐填充:所有的oop必须8B对齐,这个约定。如果一个oop只有12B,比如new object就是12B,无法被8整除,末尾补4B的0

我们说大多数情况:64位机器,开启指针压缩,非数组对象,如果提前分配内存,目前只有实例数据这一块区域的大小是不确定的。这块也是最难确定的。hotspot是怎么做的呢?统计每种数据类型的大小,然后进行统一运算。上代码
在这里插入图片描述
parse_fields就是用来解析字节码文件中的属性信息的。只不过为了配合内存编织的实现,除了解析,还需要统计每种类型的属性的数量。统计到的信息存储到对象FieldAllocationCount中。计算细节如下图:
在这里插入图片描述
就不卖关子了,hotspot中将boolean、byte、char都当成c++层面的byte来处理,即算作占1B。其他的Java类型映射哪个C++类型,看注释就能知晓。静态属性与非静态属性是分开统计的,为什么呢?因为存储的位置不同。静态属性在Class对象对应的oop上,非静态属性在new出来的oop上。

这里面有个细节,Java中的char是2B,这边当成1B处理,不会出问题吗?不会。hotspot底层做了工作,具体怎么做的。后面讲。

统计完以后,就可以知晓即将创建的对象占多少字节了,伪代码如下
在这里插入图片描述
没有容器何谈编织。那现在有了容器,该如何织入呢?

编织细节

同样是64位机。先说关闭指针压缩情况下的编织细节,开启指针压缩的情况有些许特殊。

hotspot支持三种编织规则:

  1. allocation_style=0:属性按由大到小的顺序进行织入,oop优先。编织顺序为oop、long/double、int、short/char、byte。织入所有属性后如果对象大小非8B对齐,尾部增加填充区域。填充字节数是多少?这个公式就交给聪明的大家了。
  2. allocation_style=1:属性还是按由大到小的属性进行织入,不过这种方式,oop最后织入。同样,非8B对齐依然需要补填充区域。
  3. allocation_style=2:这种规则会将子类的oop与父类oop综合起来考虑,略显复杂,后面有空细讲

我想,大家是不是有这个疑惑:为什么不能从小到大进行织入。非不为也,实不能也。自己悟一下咯。

上面有段标红的文字:开启指针压缩占4B是指有效数据占4B,这块区域在内存中还是占8B。这里其实就是开启或关闭指针压缩的核心区别所在。
在这里插入图片描述
不管是否开启指针压缩,这块区域都要吃掉8B内存。那在关闭指针压缩的情况下,这块区域就浪费掉了4B内存。能忍?可忍可不忍。hotspot给了你选择权。通过修改-XX:+/-CompactFields的值,可以选择让hotspot是否往这块间隙中织入属性。默认是开启的。可以通过如下代码测试
在这里插入图片描述
hotspot就是通过这三套规则进行属性织入,达到既节省内存又内存对齐的效果。默认是allocation_style=1的那种。

如何访问属性

到这里就剩最后一个问题了:如何访问。之前有问题提过这里,今天细看源码发现不太对。访问细节是这样子的
oop.类型指针.fields.offset
因为oop只是一块内存,并不知道哪块内存里存储的是什么属性。所以hotspot的方式是通过类型指针找到这个oop对象的klass,klass中有所有的属性信息,存储在数组中。通过属性的名称+签名找到具体访问的属性的所有信息,这个信息中就有这个offset。这个offset还不是对象中的offset,是索引。拿到这个索引再进行运算,才能真正找到属性在oop中的位置。有点抽象,举个例子。

比如Java类中有两个char,是按照代码顺序织入的。如果我想访问c2:

  1. 通过oop.类型指针拿到Test类对应的klass
  2. 通过调用findField,传入属性名+签名拿到c2的完整信息及offset
  3. 再调用注入char_offset_addr(offset)计算得到c2在oop中的内存地址
  4. 进行访问拿到c2的数据
    在这里插入图片描述
    那field.offset是何时计算出来的呢
    在这里插入图片描述

推荐阅读

1、困扰了你大半辈子的STW,今天总算可以毕业了
2、深入剖析Lambda表达式的底层实现原理
3、如何找到native方法对应的Hotspot源码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值