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只是一块内存,并不知道哪块内存里存储的是什么属性。所以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源码

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

  1. 从源码角度看Android系统SystemServer进程启动过程

    SystemServer进程是由Zygote进程fork生成,进程名为system_server,主要用于创建系统服务. 备注:本文将结合Android8.0的源码看SystemServer进程的启动 ...

  2. 从源码角度看Android系统Zygote进程启动过程

    在Android系统中,DVM.ART.应用程序进程和SystemServer进程都是由Zygote进程创建的,因此Zygote又称为"孵化器".它是通过fork的形式来创建应用程 ...

  3. 从JDK源码角度看Long

    概况 Java的Long类主要的作用就是对基本类型long进行封装,提供了一些处理long类型的方法,比如long到String类型的转换方法或String类型到long类型的转换方法,当然也包含与其 ...

  4. 从源码角度看Android系统Launcher在开机时的启动过程

    Launcher是Android所有应用的入口,用来显示系统中已经安装的应用程序图标. Launcher本身也是一个App,一个提供桌面显示的App,但它与普通App有如下不同: Launcher是所 ...

  5. 从JDK源码角度看Short

    概况 Java的Short类主要的作用就是对基本类型short进行封装,提供了一些处理short类型的方法,比如short到String类型的转换方法或String类型到short类型的转换方法,当然 ...

  6. 从源码角度看CPU相关日志

    简介 (本文原地址在我的博客CheapTalks, 欢迎大家来看看~) 安卓系统中,普通开发者常常遇到的是ANR(Application Not Responding)问题,即应用主线程没有相应.根本 ...

  7. 从template到DOM(Vue.js源码角度看内部运行机制)

    写在前面 这篇文章算是对最近写的一系列Vue.js源码的文章(github.com/answershuto-)的总结吧,在阅读源码的过程中也确实受益匪浅,希望自己的这些产出也会对同样想要学习Vue.j ...

  8. 从源码角度看Android系统init进程启动过程

    init进程是Linux系统中用户空间的第一个进程,进程号为1.Kernel启动后,在用户空间启动init进程,并调用/system/core/init.cpp中的main方法执行一些重要的工作. 备 ...

  9. 从源码角度看Spark on yarn client cluster模式的本质区别

    首先区分下AppMaster和Driver,任何一个yarn上运行的任务都必须有一个AppMaster,而任何一个Spark任务都会有一个Driver,Driver就是运行SparkContext(它 ...

最新文章

  1. jquery 悬浮验证框架 jQuery Validation Engine
  2. 虚拟主机评测网已经崭露头角
  3. VS2010 使用GDI+创建图片水印的MFC程序
  4. PCM音频文件的制作
  5. 计算机管理 没有适当的权限,提示没有合适的权限访问怎么办
  6. idea 添加配置文件 绿叶子
  7. 微软3月补丁星期二最值得注意的是CVE-2020-0684和神秘0day CVE-2020-0796
  8. 洛谷——P1909 [NOIP2016 普及组] 买铅笔
  9. java三角函数计算器_java 计算器代码能实现三角函数和阶乘功能
  10. 谷歌出品!机器学习中英文术语对照表
  11. dll文件保存到服务器,dll是什么文件?dll文件怎么打开?
  12. 【yolov3详解】一文让你读懂yolov3目标检测原理
  13. 新加坡政府企业架构:问题、实践和趋势(2008)
  14. AOP应用(Transactions 事务)
  15. Altium Designer——PCB中更改线宽的技巧总结
  16. notepad++格式化xml文件
  17. 建行u盾单片机可以再次使用吗_Si7021建行U盾19264液晶制作温湿度显示,实物单片机代码开源...
  18. UE在.CS文件中打印Log(日志)
  19. CToolBar的使用总结
  20. 博客园申请js权限方式

热门文章

  1. 哪种不是计算机的颜色,计算机调色与人工调色如何选择?
  2. workflow 添加html,为alfred编写workflow
  3. 基本面分析:原理、类型和使用方法
  4. Excel在统计分析中的应用—第二章—描述性统计-未分组数据的四分位偏差的求解方法
  5. 破解创维酷开电视安装第三方应用限制以及替换默认桌面应用突破笔记
  6. 如何提交一份高质量的缺陷报告
  7. 如何从iCloud中下载元气骑士存档
  8. 流年做戏,我不会再爱你
  9. 知识汇总二(简单光照模型)
  10. NSIS打包软件,初步使用心得