对一个字段+1
- 取到这个字段的内存地址
- 把地址对应的数据load到寄存器「先拿着地址在cache里找,如果cache miss,先把其内存中所在的cache line拉到cache中」
- 寄存器中作运算
- 结果store回去(一般先写入cache,再择机写回内存)
对象的读取
以 obj.x 为例(obj 是一个引用,x 是字段)
- obj 这个“引用值”(可以理解为一个地址或压缩地址)通常在:
- 栈帧的局部变量里,或
- 寄存器里(JIT 优化后很常见),或
- 被优化掉(逃逸分析后甚至不真正分配对象)
- 没被优化掉时,访问路径
- 先拿到obj的引用值(寄存器/从栈里load)
- 根据引用值定位到堆上的对象起始地址
- 按偏移值读取字段。对象地址+字段偏移量->该字段的最终内存地址
- 先尝试从 L1/L2/L3 cache 命中这条地址所在的 cache line;没命中才去内存
- 计算,把结果store回去
栈 vs 堆
- 局部变量在栈
- 对象在堆
从CPU性能角度:
- 栈内存通常更连续、更局部(同一线程栈帧相邻,缓存友好)
- 堆对象分配与回收导致分布更碎片化(尤其大量短命对象),更容易出现 cache miss
- 但 HotSpot/JIT 会尽量通过 TLAB 分配、分代收集等机制改善局部性;只是你不能指望它总能把链表节点变成连续存储
慢的根源:慢的根源是访问堆对象时形成的随机访问模式,而不是因为对象在堆上
JIT 会把很多“内存访问”变成“寄存器里的值”
在 Java 里,运行时(HotSpot C2 等)会做大量优化,导致你“以为每次都在堆上读写”的东西,实际可能不是:
- 标量替换(Scalar Replacement)/ 逃逸分析:对象不逃逸时可能不分配在堆上,字段直接拆成几个寄存器或栈上的标量
- 循环优化:把重复读取的字段提升到寄存器里(类似 C/C++ 的常见优化)
- 消除边界检查:ArrayList/数组循环中常见
这也解释了一个现象:同样是“读对象字段”,有时非常快(被优化到寄存器),有时非常慢(真实堆访问 + cache miss)。