欢迎关注公众号:bin的技术小屋大家好,我是bin,又到了每周我们见面的时刻了,我的公众号在1月10号那天发布了第一篇文章《从内核角度看IO模型的演变》,在这篇文章中我们通过图解的方式以一个C10k的问题为主线,从内核角度详细阐述了5种IO模型的演变过程,以及两种IO线程模型的介绍,最后引出了Netty的网络IO线程模型。读者朋友们后台留言都觉得非常的硬核,在大家的支持下这篇文章的目前阅读量为2038,点赞量为80,在看为32。这对于刚刚诞生一个多月的小号来说,是一种莫大的鼓励。在这里bin再次感谢大家的认可,鼓励和支持~~
由于在对象头中用于记录数组长度大小的属性只占4B的内存,所以Java数组可以申请的最大长度为:2^32。1.2 实例数据(Instance Data)
偏移量是指字段的内存地址与Java对象的起始内存地址之间的差值。比如long类型的字段,它内存占用8个字节,那么它的OFFSET应该是8的倍数8N。不足8N的需要填充字节。
CompactFields选项参数在JDK14中以被标记为过期了,并在将来的版本中很可能被删除。详细细节可查看issue:https://bugs.openjdk.java.net/browse/JDK-8228750上边的三条字段重排列规则非常非常重要,但是读起来比较绕脑,很抽象不容易理解,笔者把它们先列出来的目的是为了让大家先有一个朦朦胧胧的感性认识,下面笔者举一个具体的例子来为大家详细说明下,在阅读这个例子的过程中也方便大家深刻的理解这三条重要的字段重排列规则。
如果JVM开启了-XX +CompactFields时,int型字段是可以插入对象中的第一个long型字段(也就是Parent.l字段)之前的空隙中的。如果JVM设置了-XX -CompactFields则int型字段的这种插入行为是不被允许的。
规则1也规定了int型字段的OFFSET需要对齐至4N,所以Parent.i与Child.i分别存储以OFFSET = 24和OFFSET = 40的位置。因为JVM中的内存对齐除了存在于字段与字段之间还存在于对象与对象之间,Java对象之间的内存地址需要对齐至8N。
这里我们可以看到在开启字段压缩-XX +CompactFields的情况下,Child对象的大小由48字节变成了40字节。2.3 -XX:-UseCompressedOops -XX -CompactFields 关闭压缩指针,关闭字段压缩
默认情况下指针压缩-XX:+UseCompressedOops以及字段压缩-XX +CompactFields都是开启的3. 对齐填充(Padding)
虚拟机中内存对齐的选项为-XX:ObjectAlignmentInBytes,默认为8。也就是说对象与对象之间的内存地址需要对齐至多少倍,是由这个JVM参数控制的。我们还是以上边第一种情况为例说明:图中对象实际占用是44个字节,但是不是8的倍数,那么就需要再填充4个字节,内存对齐至48个字节。
图中的CPU核心指的是物理核心。从图中我们可以看到L1Cache是离CPU核心最近的高速缓存,紧接着就是L2Cache,L3Cache,内存。
CPU逻辑核心共享其所属物理核心的L1Cache和L2CacheL1Cache
笔者机器上的处理器并没有使用超线程技术所以这里其实是4个物理核心。下面我们进入其中一颗CPU核心(cpu0)中去看下L1Cache的情况:
程序局部性原理表现为:时间局部性和空间局部性。时间局部性是指如果程序中的某条指令一旦执行,则不久之后该指令可能再次被执行;如果某块数据被访问,则不久之后该数据可能再次被访问。空间局部性是指一旦程序访问了某个存储单元,则不久之后,其附近的存储单元也将被访问。那么在高速缓存中存取数据的基本单位又是什么呢??
long类型在Java中占用8个字节,一个缓存行可以存放8个long型变量。事实上,你可以非常快速的遍历在连续的内存块中分配的任意数据结构,如果你的数据结构中的项在内存中不是彼此相邻的(比如:链表),这样就无法利用CPU缓存的优势。由于数据在内存中不是连续存放的,所以在这些数据结构中的每一个项都可能会出现缓存行未命中(程序局部性原理)的情况。
还记得我们在《Reactor在Netty中的实现(创建篇)》中介绍Selector的创建时提到,Netty利用数组实现的自定义SelectedSelectionKeySet类型替换掉了JDK利用HashSet类型实现的sun.nio.ch.SelectorImpl#selectedKeys。目的就是利用CPU缓存的优势来提高IO活跃的SelectionKeys集合的遍历性能。4.2 False Sharing(伪共享)
@Contended注解默认只是在JDK内部起作用,如果我们的程序代码中需要使用到@Contended注解,那么需要开启JVM参数-XX:-RestrictContended才会生效。
不过@Contended注解填充字节的大小我们可以通过JVM参数
-XX:ContendedPaddingWidth指定,有效值范围0 - 8192,默认为128。
CPU Adjacent Sector Prefetch:https://www.techarp.com/bios-guide/cpu-adjacent-sector-prefetch/CPU Adjacent Sector Prefetch是Intel处理器特有的BIOS功能特性,默认是enabled。主要作用就是利用程序局部性原理,当CPU从内存中请求数据,并读取当前请求数据所在缓存行时,会进一步预取与当前缓存行相邻的下一个缓存行,这样当我们的程序在顺序处理数据时,会提高CPU处理效率。这一点也体现了程序局部性原理中的空间局部性特征。
i表示二维矩阵中的行地址,在计算机中行地址称为RAS(row access strobe,行访问选通脉冲)。下图中的supercell的RAS = 2,CAS = 2。
j表示二维矩阵中的列地址,在计算机中列地址称为CAS(column access strobe,列访问选通脉冲)。
注意这里只是为了解释地址引脚和数据引脚的概念,实际硬件中的引脚数量是不一定的。5.2 DRAM芯片的访问
DRAM芯片的IO单位为一个supercell,也就是一个字节(8 bit)。5.3 CPU如何读写主存
还记得我们前边讲到的MESI缓存一致性协议吗?当core0修改字段a的值时,其他CPU核心会在总线上嗅探字段a的内存地址,如果嗅探到总线上出现字段a的内存地址,说明有人在修改字段a,这样其他CPU核心就会失效自己缓存字段a所在的cache line。如上图所示,其中系统总线是连接CPU与IO bridge的,存储总线是来连接IO bridge和主存的。
CPU每次会向内存读写一个cache line大小的数据(64个字节),但是内存一次只能吞吐8个字节。所以在内存地址对应的存储器模块中,DRAM0芯片存储第一个低位字节(supercell),DRAM1芯片存储第二个字节,......依次类推DRAM7芯片存储最后一个高位字节。
内存一次读取和写入的单位是8个字节。而且在程序员眼里连续的内存地址实际上在物理上是不连续的。因为这连续的8个字节其实是存储于不同的DRAM芯片上的。每个DRAM芯片存储一个字节(supercell)。
注意:0x0000 - 0x0007内存段中的坐标地址(RAS,CAS)与0x0008 - 0x0015内存段中的坐标地址(RAS,CAS)是不相同的。
还记得笔者在小节开头提出的问题吗 ?原子性
"Java 虚拟机堆中对象的起始地址为什么需要对齐至 8的倍数?为什么不对齐至4的倍数或16的倍数或32的倍数呢?"
现在你能回答了吗???
从Java7开始,当maximum heap size小于32G的时候,压缩指针是默认开启的。但是当maximum heap size大于32G的时候,压缩指针就会关闭。那么我们如何在压缩指针开启的情况下进一步扩大寻址空间呢???
欢迎关注公众号:bin的技术小屋
欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) | Powered by Discuz! X3.4 |