hotspot源码角度看OOP之类属性的底层实现(一)
hello,大家好,我是江湖人送外号[道格牙]的子牙老师。
最近看hotspo源码有点入迷。hotspot就像一座宝库,等你探索的东西太多了。每次达到一个新的Level回头细看,都有不同的感触。入迷归入迷,分享还是得分享。分享使大家夸我,使我快乐。_
最近报名JVM小班的同学问我问的比较多的是类加载阶段对属性的处理那块。这块知识点还挺多,不局限于加载阶段:
- 加载阶段如何存储属性
- 准备阶段给属性赋初值,细节是什么
- 初始化阶段给静态属性赋值,细节是什么
- 创建对象时给非静态属性赋值,细节是什么
- 访问属性时,细节又是什么
这么多细节,一篇文章肯定讲不完,就整个系列篇吧。本篇是OOP之类属性的系列篇首篇。开整…
问题分析
在计算机的世界里,一个问题的解决方案永远不止一种。但是取舍过后,最合适的只剩唯一。当然,你能想到的解决方案的多少,与你对这个问题的理解程度是息息相关的。对于一个问题的理解程度,与你的技术视野紧密不可分。你想到而能不能做到,与你的技术实力直接挂钩。好像不止是计算机世界哈,哪个世界都这样!
比如说让你来实现OOP机制。咱们先不说完整的,就聚焦属性继承,你会如何实现。经常看我文章的小伙伴可能比较奇怪,为什么我总是问类型这样的问题?因为研究底层与研究应用层不同,或者说你用研究应用层的思维来研究底层也可以,但是效果一定不会太好。就我自身来说,我研究底层,第一件事情就是让自己身处设计者的角度,以设计者的思维来思考问题,来理解思想,来阅读代码。每次问这样的问题,意在此。设计者思维是一种试图理解思维,学习思维是一种批判思维。
言归正传,我都能猜出来大家会如何实现,上代码
解释下这段代码:oop是所有Java对象在hotpot中的存在形式,klass是所有Java类在hotspot中的存在形式。现在需要在对象中存储实例数据,毫无疑问需要用到容器,毫无疑问map是最合适的容器。你如果用Java来实现,确实只能这样写。因为Java作为应用层语言,除了Unsafe提供了简单的操作内存的方法外,Java是没有内存处理能力的。而且写Java程序,聚焦业务实现即可。你的代码性能好不好,吃不吃内存,安不安全,本质还是取决于你选择的JVM是否优秀。
如果hotspot这样去实现,也可以哈。只不过不够优秀,可能会听到来自其他编程语言的鄙视。何为优秀的程序,当下能实现的,达到时间与空间完美结合。如果这样实现的话,会存在内存浪费过于严重的问题。这个我就不解释了,我之前的文章有讲过。
那hotshot是如何实现的呢?内存编织。即在一块事先申请好的内存中,按照属性类型,给它分一块相同大小的内存块,织入进去。我之前写的那篇文章,这里没有展开讲。本篇文章,对,展开细讲这里。
这里面有这些问题需要我们来作答:
- 这个事先申请好的内存,得申请多大
- 属性有可能是bool、char、short、int、long、oop,如何编织能做到既节省内存又内存对齐
- 织入的时候是无状态的,即你在访问一个对象的属性的时候,不能通过oop.a这样直接找到,那怎么办呢
- 采用内存编织的方式创建对象一定就不会内存浪费了吗
分配多大内存
就如你打算给你素未谋面的男女朋友买双鞋子,谁知道他她多大脚呢?猜一下?合适了说你阅人无数,不合适说你不上心。哎,太难了,还是算吧。
怎么算呢?你脑海中得有一张图?不,是两张图。什么图?对象的内存布局图。
那第四个问题就有答案了:还是会存在内存浪费,灰色的padding区域就是为了对齐而填充的区域,即浪费的内存。但是这个浪费是很少的了,是可以接受的了,是目前条件下可以做到的最好的了。
这里面每个区域占用内存大小的细节如下:
- Mark Word:在32位机器下,占4B。在64位机器下,占8B。本篇文章说的是64位机
- 类型指针:又名klass pointer,开启指针压缩占4B,关闭指针压缩是8B。默认是开启的。开启指针压缩占4B是指有效数据占4B,这块区域在内存中还是占8B。这个区域很重要,怎么理解这个重要呢?一、这个区域跟第三个问题的答案有关,后面讲;二、指针压缩的开启或关闭,对内存结构图有影响。对比两幅图就能看出来,会多出一个填充区域。这个细节,后面讲
- 数组长度:如果是数组对象,占4B。如果非数组对象,占0B,即不会出现
- 实例数据:这块是核心影响区域,等下细讲
- 对齐填充:所有的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支持三种编织规则:
- allocation_style=0:属性按由大到小的顺序进行织入,oop优先。编织顺序为oop、long/double、int、short/char、byte。织入所有属性后如果对象大小非8B对齐,尾部增加填充区域。填充字节数是多少?这个公式就交给聪明的大家了。
- allocation_style=1:属性还是按由大到小的属性进行织入,不过这种方式,oop最后织入。同样,非8B对齐依然需要补填充区域。
- allocation_style=2:这种规则会将子类的oop与父类oop综合起来考虑,略显复杂,后面有空细讲
我想,大家是不是有这个疑惑:为什么不能从小到大进行织入。非不为也,实不能也。自己悟一下咯。
上面有段标红的文字:开启指针压缩占4B是指有效数据占4B,这块区域在内存中还是占8B。这里其实就是开启或关闭指针压缩的核心区别所在。
不管是否开启指针压缩,这块区域都要吃掉8B内存。那在关闭指针压缩的情况下,这块区域就浪费掉了4B内存。能忍?可忍可不忍。hotspot给了你选择权。通过修改-XX:+/-CompactFields的值,可以选择让hotspot是否往这块间隙中织入属性。默认是开启的。可以通过如下代码测试
hotspot就是通过这三套规则进行属性织入,达到既节省内存又内存对齐的效果。默认是allocation_style=1的那种。
如何访问属性
到这里就剩最后一个问题了:如何访问。之前有问题提过这里,今天细看源码发现不太对。访问细节是这样子的
因为oop只是一块内存,并不知道哪块内存里存储的是什么属性。所以hotspot的方式是通过类型指针找到这个oop对象的klass,klass中有所有的属性信息,存储在数组中。通过属性的名称+签名找到具体访问的属性的所有信息,这个信息中就有这个offset。这个offset还不是对象中的offset,是索引。拿到这个索引再进行运算,才能真正找到属性在oop中的位置。有点抽象,举个例子。
比如Java类中有两个char,是按照代码顺序织入的。如果我想访问c2:
- 通过oop.类型指针拿到Test类对应的klass
- 通过调用findField,传入属性名+签名拿到c2的完整信息及offset
- 再调用注入char_offset_addr(offset)计算得到c2在oop中的内存地址
- 进行访问拿到c2的数据
那field.offset是何时计算出来的呢
推荐阅读
1、困扰了你大半辈子的STW,今天总算可以毕业了
2、深入剖析Lambda表达式的底层实现原理
3、如何找到native方法对应的Hotspot源码
hotspot源码角度看OOP之类属性的底层实现(一)相关推荐
- 从源码角度看Android系统SystemServer进程启动过程
SystemServer进程是由Zygote进程fork生成,进程名为system_server,主要用于创建系统服务. 备注:本文将结合Android8.0的源码看SystemServer进程的启动 ...
- 从源码角度看Android系统Zygote进程启动过程
在Android系统中,DVM.ART.应用程序进程和SystemServer进程都是由Zygote进程创建的,因此Zygote又称为"孵化器".它是通过fork的形式来创建应用程 ...
- 从JDK源码角度看Long
概况 Java的Long类主要的作用就是对基本类型long进行封装,提供了一些处理long类型的方法,比如long到String类型的转换方法或String类型到long类型的转换方法,当然也包含与其 ...
- 从源码角度看Android系统Launcher在开机时的启动过程
Launcher是Android所有应用的入口,用来显示系统中已经安装的应用程序图标. Launcher本身也是一个App,一个提供桌面显示的App,但它与普通App有如下不同: Launcher是所 ...
- 从JDK源码角度看Short
概况 Java的Short类主要的作用就是对基本类型short进行封装,提供了一些处理short类型的方法,比如short到String类型的转换方法或String类型到short类型的转换方法,当然 ...
- 从源码角度看CPU相关日志
简介 (本文原地址在我的博客CheapTalks, 欢迎大家来看看~) 安卓系统中,普通开发者常常遇到的是ANR(Application Not Responding)问题,即应用主线程没有相应.根本 ...
- 从template到DOM(Vue.js源码角度看内部运行机制)
写在前面 这篇文章算是对最近写的一系列Vue.js源码的文章(github.com/answershuto-)的总结吧,在阅读源码的过程中也确实受益匪浅,希望自己的这些产出也会对同样想要学习Vue.j ...
- 从源码角度看Android系统init进程启动过程
init进程是Linux系统中用户空间的第一个进程,进程号为1.Kernel启动后,在用户空间启动init进程,并调用/system/core/init.cpp中的main方法执行一些重要的工作. 备 ...
- 从源码角度看Spark on yarn client cluster模式的本质区别
首先区分下AppMaster和Driver,任何一个yarn上运行的任务都必须有一个AppMaster,而任何一个Spark任务都会有一个Driver,Driver就是运行SparkContext(它 ...
最新文章
- jquery 悬浮验证框架 jQuery Validation Engine
- 虚拟主机评测网已经崭露头角
- VS2010 使用GDI+创建图片水印的MFC程序
- PCM音频文件的制作
- 计算机管理 没有适当的权限,提示没有合适的权限访问怎么办
- idea 添加配置文件 绿叶子
- 微软3月补丁星期二最值得注意的是CVE-2020-0684和神秘0day CVE-2020-0796
- 洛谷——P1909 [NOIP2016 普及组] 买铅笔
- java三角函数计算器_java 计算器代码能实现三角函数和阶乘功能
- 谷歌出品!机器学习中英文术语对照表
- dll文件保存到服务器,dll是什么文件?dll文件怎么打开?
- 【yolov3详解】一文让你读懂yolov3目标检测原理
- 新加坡政府企业架构:问题、实践和趋势(2008)
- AOP应用(Transactions 事务)
- Altium Designer——PCB中更改线宽的技巧总结
- notepad++格式化xml文件
- 建行u盾单片机可以再次使用吗_Si7021建行U盾19264液晶制作温湿度显示,实物单片机代码开源...
- UE在.CS文件中打印Log(日志)
- CToolBar的使用总结
- 博客园申请js权限方式