Object

Object 源码分析

所有类的根父类,定义了 Java 对象的最小契约:身份(getClass / hashCode / equals)、表示(toString)、复制(clone)、以及基于对象监视器的线程协作(wait / notify)。
HashMap 的键、synchronized 的锁对象、对象判等全部落在这几个方法上。

代码块JAVA · 34 行收起展开
// 基于 JDK 25 (本地 JAVA_Source 仓), java.lang.Object
public class Object {

    @IntrinsicCandidate         // JVM 内建实现:JIT 会用固化的快速版本替换调用,下同
    public Object() {}

    // final:运行时类型由 JVM 保证,子类不许伪造。返回的 Class 对象就是 static synchronized 方法锁的那个对象
    @IntrinsicCandidate
    public final native Class<?> getClass();

    // 默认实现是"身份哈希":首次调用时生成并缓存进对象头 mark word,之后终身不变(GC 移动对象也不变)。
    // 契约:equals 相等 => hashCode 必须相等。只重写 equals 不重写 hashCode,对象进 HashMap 会散到不同桶,"存了却查不到"
    @IntrinsicCandidate
    public native int hashCode();

    // 默认语义 = 是不是同一个对象。按业务字段判等必须重写,且满足自反/对称/传递/一致
    public boolean equals(Object obj) {
        return (this == obj);
    }

    // 浅拷贝:逐字段复制,引用字段仍指向同一对象。protected + 要求实现 Cloneable 双重门槛,防止随手拷贝可变状态
    @IntrinsicCandidate
    protected native Object clone() throws CloneNotSupportedException;

    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());   // 重写了 hashCode,这里的默认输出也跟着变
    }

    // ... wait/notify 线程协作部分见下一段 ...

    // 已废弃且待移除(JEP 421):执行时机不可控、能复活对象、异常被吞。清理资源用 Cleaner 或 try-with-resources
    @Deprecated(since="9", forRemoval=true)
    protected void finalize() throws Throwable { }
}

线程协作三件套。调用前提:当前线程必须持有该对象的 monitor,否则抛 IllegalMonitorStateException:

代码块JAVA · 52 行收起展开
// 基于 JDK 25 (本地 JAVA_Source 仓), java.lang.Object 线程协作部分

    // 从该对象的等待集(WaitSet)任选一个线程唤醒,选哪个由 JVM 决定,业务代码不能依赖顺序
    @IntrinsicCandidate
    public final native void notify();

    // 全部唤醒,各自重新抢锁、重查条件。多种线程等不同条件时只能用它,否则可能"唤醒错人"导致信号丢失
    @IntrinsicCandidate
    public final native void notifyAll();

    public final void wait() throws InterruptedException {
        wait(0L);                   // 0 = 不计时,等到被唤醒为止
    }

    // 坑:wait(long) 早已不是 native(JDK 19 起)。Java 层做参数校验和虚拟线程适配,真正的挂起在 native 的 wait0
    public final void wait(long timeoutMillis) throws InterruptedException {
        if (timeoutMillis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (Thread.currentThread() instanceof VirtualThread vthread) {
            try {
                wait0(timeoutMillis);
            } catch (InterruptedException e) {
                // virtual thread's interrupt status needs to be cleared
                vthread.getAndClearInterrupt();     // 虚拟线程的中断位存在 VirtualThread 对象上,native 层清不到,Java 层补清后再抛
                throw e;
            }
        } else {
            wait0(timeoutMillis);
        }
    }

    // final modifier so method not in vtable
    private final native void wait0(long timeoutMillis) throws InterruptedException;

    public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
        if (timeoutMillis < 0) {
            throw new IllegalArgumentException("timeoutMillis value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0 && timeoutMillis < Long.MAX_VALUE) {
            timeoutMillis++;        // "纳秒精度"是假的:nanos 只要 > 0 就把毫秒向上取整 +1,然后被丢弃
        }

        wait(timeoutMillis);
    }

wait 必须放在循环里判断条件,不能用 if:

代码块JAVA · 6 行收起展开
synchronized (lock) {
    while (!conditionMet()) {   // 用 while 不用 if:应对虚假唤醒 + 唤醒后条件可能又被别人消费掉
        lock.wait();            // 释放锁并挂起,被唤醒后重新拿锁、回到 while 再查一遍
    }
    // 走到这里才真正满足条件
}

重写 equals + hashCode 的标准写法(决定对象能否正确用作 Map 的键):

代码块JAVA · 11 行收起展开
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    User user = (User) o;
    return age == user.age && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
    return Objects.hash(name, age);   // 参与 equals 的字段,就要参与 hashCode
}

原理串讲

以生产者-消费者为例走一遍完整链路。消费者线程进入 synchronized (queue),发现队列为空,调用 queue.wait()
Java 层依次经过 wait()wait(0L)wait(long) 的负数校验,平台线程直接落到 native 的 wait0(0L)
JVM 在 wait0 里操作这个对象关联的 ObjectMonitor:把当前线程挂进 monitor 的 WaitSet,然后完全释放锁——重入了几层就退几层,同时记下重入次数——最后把线程 park 挂起。
释放锁这一步是 wait 与 Thread.sleep 的本质区别:不释放锁,生产者就永远进不了临界区,条件永远不会变,等待就成了死等。

生产者随后拿到锁、放入元素、调用 queue.notify()。JVM 从 WaitSet 里取一个线程移入 EntryList(抢锁队列)。
注意 notify 本身不交出锁:被唤醒的线程要等生产者退出 synchronized 块后,和其他线程一起公平竞争 monitor;抢到后 JVM 恢复它之前记下的重入次数,wait0 返回,线程回到 Java 层 while 的条件判断——从被 notify 到真正重新持锁之间存在窗口,条件完全可能已被别的线程消费掉,这是必须用 while 的第二个理由(第一个是规范明确允许的虚假唤醒:底层条件变量原语本身就可能无故返回,JVM 不为屏蔽它付出代价,把复查责任交给调用方)。

为什么 wait/notify 定义在 Object 上而不是 Thread 上?因为 Java 把 monitor 做进了每个对象的对象头,任何对象都能当锁;而条件等待必须和某把锁绑定——只有先持锁,“检查条件”和”进入等待”才是原子的,否则两步之间别的线程完成”改条件 + notify”,唤醒信号就永久丢失(lost wakeup)。
等待集合跟着锁走,API 自然就挂在锁对象上,这也是三件套都要求先持有该对象锁的根源。

为什么 JDK 19 起把 wait(long) 从 native 拆成 Java 壳 + wait0?为虚拟线程留钩子:虚拟线程的中断状态记录在 VirtualThread 对象里,native 代码处理不了这种 Java 层的类型分支,于是把校验、instanceof VirtualThread 判断和中断位清理都放在 Java 层,native 只保留最小的挂起原语。
wait0 标成 private final 是为了不在虚方法表里占条目(源码注释原话:final modifier so method not in vtable)。

设计取舍

  • notify 只省了无效唤醒的开销,却要求”所有等待者都在等同一个条件”;拿不准就用 notifyAll,正确性优先于性能。
  • 默认 hashCode 与内存地址没有必然关系(HotSpot 默认用随机数生成后缓存进 mark word),“返回内存地址”是流传很广的口误。
  • clone 的设计公认失败:Cloneable 是个空标记接口,clone 却定义在 Object 上还是 protected。工程上优先拷贝构造器。
  • wait(timeoutMillis, nanos) 的纳秒参数不提供纳秒精度,只做向上取整;要高精度定时等待用 LockSupport.parkNanos 或 Condition。
  • 业务代码优先 BlockingQueue / Condition,但 synchronized + wait/notify 是所有条件队列的心智原型,值得吃透。

延伸阅读