synchronized 可以锁住一个普通 Java 对象。对象本身并没有声明一个“锁字段”,相关信息存在哪里?这要从 HotSpot 的对象布局说起。
对象由哪些部分组成
这里以 Java 8 的 HotSpot 虚拟机为例。
普通对象主要包含对象头、实例字段和对齐填充。对象头中有 Mark Word 和指向类元数据的指针;数组还需要保存长度。
Mark Word 会根据对象状态采用不同的位布局,用于记录锁相关标记、年龄、身份哈希等信息。不是这些信息永远同时原样放在固定位置,进入某些锁状态时,其中的位会被解释成指针等其他含义。
静态字段属于类的状态,不会在这个类的每个实例里复制一份。字段声明的顺序也不能简单当成实际内存偏移顺序,虚拟机可能为了布局和对齐调整字段位置。
使用 JOL 观察
JOL 可以查看对象布局。Maven 加入依赖:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>import org.openjdk.jol.info.ClassLayout;
public class ObjectLayoutDemo {
private static class Sample {
int number;
Object reference;
}
public static void main(String[] args) {
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
System.out.println(ClassLayout.parseInstance(new Sample()).toPrintable());
System.out.println(ClassLayout.parseInstance(new int[3]).toPrintable());
}
}输出中可以看到字段偏移、字段大小、实例总大小和填充。
在常见的 64 位 HotSpot、开启压缩类指针、对象按 8 字节对齐的配置下,普通对象头通常是 8 字节 Mark Word 加 4 字节类指针。一个没有实例字段的 Object,需要填充到 16 字节。
这不是“任何 new Object 都固定占 16 字节”。关闭压缩指针、改变对齐或更换虚拟机,都可能影响结果。
压缩指针
UseCompressedOops 主要关系到普通对象引用的表示,UseCompressedClassPointers 关系到类指针。它们容易一起出现,但含义不同。
想比较布局,可以在相同 JDK 下改变相关参数后重新运行 JOL,比较引用字段、类指针和总大小。不要只看机器是 64 位,就把所有引用字段都算成 8 字节。
对象头与锁
加锁前后可以用 JOL 查看 Mark Word 的变化。比较输出时,需要同时留意 JVM 参数和线程是否发生竞争。
身份哈希也可能影响对象头状态。普通的 hashCode() 可以被类重写,不等于对象头里的身份哈希;调用 System.identityHashCode() 也可能改变对象头状态,所以调用前后的输出可能不同。