对一个字段+1

  1. 取到这个字段的内存地址
  2. 把地址对应的数据load到寄存器「先拿着地址在cache里找,如果cache miss,先把其内存中所在的cache line拉到cache中」
  3. 寄存器中作运算
  4. 结果store回去(一般先写入cache,再择机写回内存)

对象的读取

obj.x 为例(obj 是一个引用,x 是字段)

  1. obj 这个“引用值”(可以理解为一个地址或压缩地址)通常在:
    • 栈帧的局部变量里,或
    • 寄存器里(JIT 优化后很常见),或
    • 被优化掉(逃逸分析后甚至不真正分配对象)
  2. 没被优化掉时,访问路径
    1. 先拿到obj的引用值(寄存器/从栈里load)
    2. 根据引用值定位到堆上的对象起始地址
    3. 按偏移值读取字段。对象地址+字段偏移量->该字段的最终内存地址
    4. 先尝试从 L1/L2/L3 cache 命中这条地址所在的 cache line;没命中才去内存
    5. 计算,把结果store回去

栈 vs 堆

  • 局部变量在栈
  • 对象在堆

从CPU性能角度:

  • 栈内存通常更连续、更局部(同一线程栈帧相邻,缓存友好)
  • 堆对象分配与回收导致分布更碎片化(尤其大量短命对象),更容易出现 cache miss
  • 但 HotSpot/JIT 会尽量通过 TLAB 分配、分代收集等机制改善局部性;只是你不能指望它总能把链表节点变成连续存储

慢的根源:慢的根源是访问堆对象时形成的随机访问模式,而不是因为对象在堆上

JIT 会把很多“内存访问”变成“寄存器里的值”

在 Java 里,运行时(HotSpot C2 等)会做大量优化,导致你“以为每次都在堆上读写”的东西,实际可能不是:

  • 标量替换(Scalar Replacement)/ 逃逸分析:对象不逃逸时可能不分配在堆上,字段直接拆成几个寄存器或栈上的标量
  • 循环优化:把重复读取的字段提升到寄存器里(类似 C/C++ 的常见优化)
  • 消除边界检查:ArrayList/数组循环中常见

这也解释了一个现象:同样是“读对象字段”,有时非常快(被优化到寄存器),有时非常慢(真实堆访问 + cache miss)。