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() 也可能改变对象头状态,所以调用前后的输出可能不同。

最后修改:2018 年 05 月 06 日
如果觉得我的文章对你有用,请随意赞赏