2 Object —— Java 类体系的根节点

35 阅读23分钟

Object —— Java 类体系的根节点

适用版本: JDK 8 难度等级: 进阶 核心方法: getClass、hashCode、equals、clone、toString、wait/notify、finalize 本文不仅覆盖 Object 的全部 API 用法,更深入到 HotSpot JVM 的 C++ 源码层面,帮助有 Java 基础的程序员建立从 Java 到 JVM 的贯通理解。


一、Object 在 Java 类型体系中的位置

Java 是一门单根继承的语言,java.lang.Object 是所有类的终极祖先。即使你定义的类没有显式使用 extends,编译器也会自动将它指向 Object。

// 这两种写法完全等价
public class MyClass {}
public class MyClass extends Object {}

1.1 Object 的 12 个方法 —— 按功能域分类

Object 的 12 个方法可以从功能域角度划分为 6 组,这比简单的 "native / 非 final / final" 分类更能揭示它们的设计意图:

                          Object (12个方法)
                                │
     ┌────────┬────────┬────────┼────────┬────────┐
     │        │        │        │        │        │
  类型信息  相等判定  对象复制  线程协作  生命周期  调试表示
     │        │        │        │        │        │
  getClass  equals   clone    wait    finalize  toString
            hashCode           notify
                               notifyAll
                               wait(timeout)
                               wait(timeout,nanos)

【基础设施】registerNatives (private static native, 类加载时一次性执行)
方法修饰符功能域说明
registerNatives()private static native基础设施注册 JNI 方法映射,类加载时执行
getClass()public final native类型信息返回运行时类对象
hashCode()public native相等判定返回哈希码
equals(Object)public相等判定判断对象逻辑相等
clone()protected native对象复制创建并返回拷贝
toString()public调试表示返回字符串表示
notify()public final native线程协作唤醒单个等待线程
notifyAll()public final native线程协作唤醒所有等待线程
wait(long)public final native线程协作限时等待
wait(long, int)public final线程协作纳秒级限时等待
wait()public final线程协作无限等待
finalize()protected生命周期GC 前回调(JDK 9 已废弃)

二、对象的内存布局 —— 所有 native 方法的物理基础

这一节是理解 Object 所有 native 方法实现的前提。 在 HotSpot JVM 中,每个 Java 对象在堆内存中由三部分组成:

┌──────────────────────────────────────────────────────────┐
│                     Java 对象内存布局                      │
├────────────────────────────┬─────────────────────────────┤
│        Object Header       │                             │
│      (对象头,12/16 字节)    │                             │
├──────────────┬─────────────┤      Instance Data          │
│  Mark Word   │Klass Pointer│       (实例数据)              │
│ (8 bytes)    │ (4/8 bytes) │                             │
├──────────────┴─────────────┼─────────────────────────────┤
│         Padding            │                             │
│       (对齐填充)            │                             │
└────────────────────────────┴─────────────────────────────┘

为什么对象头如此重要?因为 Object 的 7 个 native 方法,全部依赖对象头中的信息工作:

native 方法依赖的对象头字段关系说明
hashCode()Mark WordhashCode 计算后缓存到 Mark Word 中
getClass()Klass Pointer解引用 Klass Pointer 获取 Class 元数据
wait()/notify()/notifyAll()Mark Word(锁状态位)Mark Word 记录了锁状态,指向 ObjectMonitor
clone()Klass Pointer(获取对象大小)根据 Klass 中的对象布局信息做内存拷贝

2.1 Mark Word —— 32 位 JVM 的结构

Mark Word 是对象头中最精妙的设计——它是一块复用空间,根据对象的锁状态不同,存储的内容也不同:

┌──────────────────────────────────────────────────────┬─────┬─────┐
│                   Mark Word (32 bits)                │ 锁标志位 │
├──────────────────────────────────────────────────────┼─────┤
│ hash:25         │ age:4  │ biased_lock:1 │  01       │  01 │ 无锁
├──────────────────────────────────────────────────────┼─────┤
│ thread:23 │ epoch:2 │ age:4  │ biased_lock:1 │  01   │  01 │ 偏向锁
├──────────────────────────────────────────────────────┼─────┤
│          ptr_to_lock_record:30               │  00   │  00 │ 轻量级锁
├──────────────────────────────────────────────────────┼─────┤
│          ptr_to_heavyweight_monitor:30       │  10   │  10 │ 重量级锁
├──────────────────────────────────────────────────────┼─────┤
│          (GC 标记用)                         │  11   │  11 │ GC 标记
└──────────────────────────────────────────────────────┴─────┘

关键发现:在无锁状态下,Mark Word 中 25 bits 用来存储 identity hash code;但当对象进入偏向锁状态后,这 25 bits 被偏向线程 ID 占用——hashCode 无处可存。这就引出了下一节要讨论的核心冲突。

2.2 Klass Pointer —— getClass() 的物理实现

Klass Pointer 指向**元空间(Metaspace)**中的 InstanceKlass 对象(C++ 层),后者存储了该类的完整元数据:

  • 类的名称、修饰符、父类、接口列表
  • 字段的偏移量、类型信息(用于反射和 GC 的对象遍历)
  • 方法表(vtable),用于虚方法分派
  • 常量池引用

这就是为什么 getClass()O(1) 操作——它只是解引用一个指针。java.lang.Class 对象(在 Java 堆中)与 InstanceKlass(在元空间中)是一一对应的:

Java 堆                          元空间 (Metaspace)
┌──────────────────┐            ┌──────────────────┐
│ java.lang.Class  │ ────────→  │  InstanceKlass   │
│   (反射入口)      │  内部指针   │  (类元数据)       │
└──────────────────┘            └──────────────────┘
        ↑                              ↑
        │         ┌──────────┐         │
        └─────────│ 对象头    │─────────┘
                  │ Klass Ptr │
                  └──────────┘

三、registerNatives —— 静态代码块中的秘密

public class Object {
    private static native void registerNatives();
    static {
        registerNatives();
    }
}

这个 static 块在 Object 类首次加载时执行。它的作用是将 Java 层的方法名与 JVM 内部的 C++ 函数地址做绑定映射。

源码位置: src/share/native/java/lang/Object.c

// OpenJDK 中的 JNI 方法映射表
static JNINativeMethod methods[] = {
    {"hashCode",    "()I",   (void *)&JVM_IHashCode},
    {"wait",        "(J)V",  (void *)&JVM_MonitorWait},
    {"notify",      "()V",   (void *)&JVM_MonitorNotify},
    {"notifyAll",   "()V",   (void *)&JVM_MonitorNotifyAll},
    {"clone",       "()Ljava/lang/Object;", (void *)&JVM_Clone},
};

JNIEXPORT void JNICALL
Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls, methods,
        sizeof(methods)/sizeof(methods[0]));
}

这样做的好处是可以提前完成方法绑定,避免每次调用时都要进行耗时的符号查找。这也是整个 JDK native 方法注册的标准模式——并非 Object 类独有。


四、getClass —— 运行时类型信息

public final native Class<?> getClass();

4.1 Java 层行为

getClass() 返回的是引用变量实际指向的对象的运行时 Class 对象,而不是声明类型。

public class GetClassDemo {
    public static void main(String[] args) {
        Number num = Integer.valueOf(42);

        Class<?> clazz = num.getClass();
        System.out.println(clazz.getName());          // java.lang.Integer
        System.out.println(clazz.getSimpleName());     // Integer
        System.out.println(clazz.getSuperclass());     // class java.lang.Number

        // 反射获取所有方法
        for (java.lang.reflect.Method method : clazz.getDeclaredMethods()) {
            System.out.println(" - " + method.getName());
        }
    }
}

getClass()final 修饰,任何子类都无法覆写,保证了运行时类型判断的可靠性。

4.2 JVM 层的实现原理

源码位置: src/share/vm/prims/jvm.cppJVM_GetClass()

getClass() 的 native 实现极其简洁——本质上就是从对象头中读取 Klass Pointer,然后找到对应的 java.lang.Class 对象返回:

        对象实例                        元空间                       Java 堆
    ┌──────────────┐             ┌──────────────┐           ┌──────────────┐
    │ Object Header │             │ InstanceKlass│           │java.lang.Class│
    │ ┌──────────┐ │   解引用     │              │  内部指针  │              │
    │ │Klass Ptr │─┼────────────→│  _java_mirror│──────────→│  反射入口     │
    │ └──────────┘ │             │              │           │              │
    └──────────────┘             └──────────────┘           └──────────────┘

InstanceKlass 中有一个 _java_mirror 字段,指向堆中的 java.lang.Class 对象。所以 getClass() 的操作是:对象头 → Klass Pointer → InstanceKlass → _java_mirror → Class 对象,一共两次指针解引用,真正的 O(1)。


五、hashCode —— 哈希世界的通行证

public native int hashCode();

5.1 哈希码的核心约定

JDK 文档明确规定了 hashCode 的三条铁律:

  1. 一致性:同一对象多次调用 hashCode 必须返回相同值(前提是 equals 比较用到的信息未变)
  2. 等价性a.equals(b) == truea.hashCode() == b.hashCode()
  3. 非必须不等a.equals(b) == false 不要求 hashCode 不同,但不同能提升哈希表性能

5.2 identity hash code 的存储位置 —— Mark Word

这是理解 hashCode 实现的关键:Object 默认的 hashCode() 返回的是 "identity hash code",它与对象内容无关,只与对象身份有关。在 HotSpot 中,identity hash code 首次被调用时计算,然后缓存到 Mark Word 中

源码位置: src/share/vm/runtime/synchronizer.cppObjectSynchronizer::FastHashCode()

// HotSpot identity hash code 生成的核心逻辑 (简化)
intptr_t ObjectSynchronizer::FastHashCode(Thread * Self, oop obj) {
    // 1. 如果对象处于偏向锁状态,先撤销偏向锁
    if (obj->mark()->has_bias_pattern()) {
        // 偏向锁状态下 Mark Word 被偏向线程 ID 占用
        // 必须先撤销偏向锁,才能腾出空间存储 hash code
        BiasedLocking::revoke_and_rebias(obj, false);
    }

    // 2. 检查 Mark Word 中是否已经缓存了 hash code
    markOop mark = obj->mark();
    if (mark->has_valid_hash()) {
        return mark->hash();  // 已有缓存,直接返回 → O(1)
    }

    // 3. 生成新的 hash code 并写入 Mark Word
    intptr_t hash = obj->identity_hash();  // 调用 get_next_hash()
    obj->set_mark(markOopDesc::prototype()->copy_set_hash(hash));
    return hash;
}

5.3 hashCode 与偏向锁的致命冲突

这是一个生产环境中的真实性能陷阱:当对象处于偏向锁状态时,Mark Word 的 25 bits(32位JVM)被偏向线程 ID 占用,没有空间存储 hash code。此时调用 hashCode() 会触发:

偏向锁 → 撤销偏向锁 → 轻量级锁(或重量级锁)

这意味着——如果先加了 synchronized 再调用 hashCode(),锁会膨胀,并发性能断崖式下降

public class HashCodeBiasLockConflict {
    public static void main(String[] args) {
        Object obj = new Object();

        // 1. 先获取偏向锁
        synchronized (obj) {
            // 此时 Mark Word 中存储的是偏向线程 ID
            System.out.println("对象处于偏向锁状态");

            // 2. 调用 hashCode() → 触发偏向锁撤销!
            int hash = obj.hashCode();
            System.out.println("hashCode = " + hash);
            // 此时锁已膨胀为轻量级锁或重量级锁
        }
        // 之后再进入 synchronized,将走轻量级/重量级锁路径,性能下降
    }
}

规避策略:如果一个对象可能在高并发场景下使用 synchronized在加锁之前先调用一次 hashCode(),让 Mark Word 提前稳定为无锁+hash 状态。

5.4 HotSpot 的六种 hashCode 生成模式

通过 JVM 参数 -XX:hashCode=N 切换:

模式算法特点
0Park-Miller 随机数早期默认值
1基于对象内存地址 + 随机数性能好
2固定值 1仅用于测试
3自增序列简单递增
4内存地址直接转换最直观
5线程状态 + xorshiftJDK 8 默认值
6线程状态 + Marsaglia's xor-shiftJDK 9+ 默认值

源码位置: src/share/vm/runtime/synchronizer.cppget_next_hash()

模式 5(JDK 8 默认)的核心实现是一个 Marsaglia XOR-Shift 随机数生成器,它将线程当前的栈指针、程序计数器等"自然随机源"混合进来,产生了分布良好的哈希值。

5.5 覆写 hashCode 与 identity hash code 的区别

关键区分:当你覆写了 hashCode(),JVM 走的是 Java 方法调用路径,不会操作 Mark Word。identity hash code 的缓存机制仅对 Object 的默认 hashCode() 有效。

public class IdentityHashCodeDemo {
    static class MyObj {
        private int id;
        MyObj(int id) { this.id = id; }

        @Override
        public int hashCode() {
            return id;  // 覆写后,hashCode 不再来自 Mark Word
        }
    }

    public static void main(String[] args) {
        MyObj obj = new MyObj(42);
        System.out.println("覆写的 hashCode: " + obj.hashCode());           // 42
        System.out.println("identity hash code: " + System.identityHashCode(obj)); // 来自 Mark Word
        // identityHashCode() 总会走 native 路径,读取 Mark Word 中的 identity hash
    }
}

5.6 自定义 hashCode 实现示例

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int hashCode() {
        // 经典组合方式:31 作为乘数
        // 31 可被编译器优化为 (h << 5) - h
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Person)) return false;
        Person other = (Person) obj;
        return age == other.age
            && java.util.Objects.equals(name, other.name);
    }

    public static void main(String[] args) {
        Person p1 = new Person("张三", 25);
        Person p2 = new Person("张三", 25);

        System.out.println("equals: " + p1.equals(p2));       // true
        System.out.println("hash相等: " + (p1.hashCode() == p2.hashCode())); // true

        java.util.HashSet<Person> set = new java.util.HashSet<>();
        set.add(p1);
        set.add(p2);
        System.out.println("Set size: " + set.size());        // 1,正确去重
    }
}

六、equals —— 逻辑相等的判定

public boolean equals(Object obj) {
    return (this == obj);
}

Object 的默认实现就是 == —— 内存地址比较。大多数子类需要覆写 equals 实现"内容相等"的比较。

6.1 equals 覆写的五条军规

  1. 自反性x.equals(x) 永远为 true
  2. 对称性x.equals(y) 为 true ⇒ y.equals(x) 也为 true
  3. 传递性x.equals(y)y.equals(z)x.equals(z)
  4. 一致性:信息未变时多次调用结果一致
  5. 非空性x.equals(null) 永远为 false

6.2 instanceof vs getClass() —— equals 覆写的经典分歧

// 方案 A: 使用 instanceof(允许子类参与比较)
@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof Book)) return false;   // 子类对象也放行
    Book other = (Book) obj;
    return java.util.Objects.equals(isbn, other.isbn);
}

// 方案 B: 使用 getClass()(严格限定同类型)
@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null || this.getClass() != obj.getClass()) return false;
    Book other = (Book) obj;
    return java.util.Objects.equals(isbn, other.isbn);
}
方案优点风险
instanceof支持里氏替换原则可能破坏对称性(如果子类也覆写 equals)
getClass()绝对对称代理对象、Mock 对象无法与原始对象判等

JDK 标准库中的选择:java.util.List(如 ArrayList、LinkedList)使用方案 A,因为不同实现的 List 只要内容相同就应判等;而 java.sql.Timestamp 则因为 instanceof 破坏了对称性(Timestamp 是 Date 的子类,但 date.equals(timestamp) 可能返回 true 而 timestamp.equals(date) 返回 false)被广泛批评。

6.3 标准 equals 覆写模板

public class Book {
    private String isbn;
    private String title;

    public Book(String isbn, String title) {
        this.isbn = isbn;
        this.title = title;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;                 // 1. 同引用快速返回
        if (!(obj instanceof Book)) return false;      // 2. 类型检查
        Book other = (Book) obj;                       // 3. 安全转型
        return java.util.Objects.equals(isbn, other.isbn); // 4. 关键字段比较
    }

    @Override
    public int hashCode() {
        return java.util.Objects.hashCode(isbn);
    }

    public static void main(String[] args) {
        Book b1 = new Book("978-7-111", "设计模式");
        Book b2 = new Book("978-7-111", "设计模式");
        Book b3 = new Book("978-7-112", "Java并发");

        System.out.println(b1.equals(b2)); // true
        System.out.println(b1.equals(b3)); // false
    }
}

七、clone —— 对象拷贝的底层机制

protected native Object clone() throws CloneNotSupportedException;

7.1 Cloneable 标记接口

Cloneable 接口不包含任何方法,它的唯一作用是:告诉 JVM 该类支持 clone() 操作。如果调用了未实现 Cloneable 的类的 clone() 方法,JVM 会抛出 CloneNotSupportedException

7.2 JVM 层的实现 —— JVM_Clone

源码位置: src/share/vm/prims/jvm.cppJVM_Clone()

JVM_ENTRY(jobject, JVM_Clone(JNIEnv* env, jobject obj))
    // 1. 检查类是否实现了 Cloneable
    if (!java_lang_Cloneable::is_instance(obj)) {
        THROW_MSG(vmSymbols::java_lang_CloneNotSupportedException(), ...);
    }

    // 2. 获取对象大小(从 Klass 元数据中获取)
    int size = obj->size();

    // 3. 在堆上分配一块新内存,逐字节拷贝原对象
    oop new_obj = CollectedHeap::obj_allocate(klass, size);
    Copy::conjoint_jbytes(obj, new_obj, size);

    // 4. 返回新对象
    return JNIHandles::make_local(env, new_obj);
JVM_END

JVM_Clone 的本质是 内存层面的浅拷贝(bitwise copy):它在堆上新分配一块内存,将原对象的全部字节原样复制过去。这意味着:

  • 基本类型字段:值被完全独立复制,互不影响
  • 引用类型字段:只复制了引用本身,两个对象指向同一个子对象——这就是"浅拷贝"的根源

数组的 clone 是一个特例:数组的元素如果是基本类型(如 int[]),clone 后每个元素都是独立的;如果是引用类型(如 Object[]),clone 后两个数组中的元素引用了同一组对象。

7.3 浅拷贝 vs 深拷贝

public class CloneDemo {
    // 浅拷贝示例
    static class Address implements Cloneable {
        String city;
        Address(String city) { this.city = city; }

        @Override
        protected Address clone() throws CloneNotSupportedException {
            return (Address) super.clone();
        }
    }

    // 浅拷贝——引用类型字段共享同一个对象
    static class UserShallow implements Cloneable {
        String name;
        Address address;

        UserShallow(String name, Address address) {
            this.name = name;
            this.address = address;
        }

        @Override
        protected UserShallow clone() throws CloneNotSupportedException {
            return (UserShallow) super.clone();
        }
    }

    // 深拷贝——递归克隆所有引用类型字段
    static class UserDeep implements Cloneable {
        String name;
        Address address;

        UserDeep(String name, Address address) {
            this.name = name;
            this.address = address;
        }

        @Override
        protected UserDeep clone() throws CloneNotSupportedException {
            UserDeep cloned = (UserDeep) super.clone();
            cloned.address = this.address.clone();  // 关键:手动克隆引用字段
            return cloned;
        }
    }

    public static void main(String[] args) throws Exception {
        // 浅拷贝演示
        UserShallow original = new UserShallow("李明", new Address("北京"));
        UserShallow cloned = original.clone();

        cloned.address.city = "上海";  // 修改克隆对象的地址
        System.out.println("浅拷贝: " + original.address.city);  // "上海"——原始对象也被改了

        // 深拷贝演示
        UserDeep originalDeep = new UserDeep("王芳", new Address("深圳"));
        UserDeep clonedDeep = originalDeep.clone();

        clonedDeep.address.city = "广州";
        System.out.println("深拷贝: " + originalDeep.address.city); // "深圳"——互不影响
    }
}

7.4 clone 方法的推荐替代方案

在实际项目中,建议优先使用拷贝构造器或工厂方法,更加显式和安全:

public class Order {
    private String id;
    private double amount;

    // 拷贝构造器——更清晰的语义
    public Order(Order source) {
        this.id = source.id;
        this.amount = source.amount;
    }

    // 静态工厂方法
    public static Order copyOf(Order source) {
        return new Order(source);
    }

    public static void main(String[] args) {
        Order o1 = new Order("ORD-001", 99.90);
        Order o2 = Order.copyOf(o1);
        System.out.println(o2.amount); // 99.90
    }
}
对比维度clone()拷贝构造器
类型安全需要强制转型编译期类型检查
深浅拷贝默认浅拷贝,深拷贝需手写需要在构造器中显式实现
Cloneable 依赖必须实现不需要
final 字段无法赋值(clone 不经过构造器)可以在构造器中正常赋值
异常处理抛出 CloneNotSupportedException无需异常处理

八、toString —— 对象的自我描述

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

默认输出形如 com.example.Book@1a2b3c4d。注意:默认实现调用的是 hashCode(),而不是 System.identityHashCode()——所以如果覆写了 hashCode()toString() 中的哈希值也会随之改变。

日常工作中有几个重要的实践准则:

  • 日志打印前覆写 toString:否则日志中只有无意义的哈希码
  • Lombok 的 @ToString 注解:自动生成易读的 toString
  • 避免在 toString 中暴露敏感字段:如密码、token 等
  • toString 可能被调试器和日志框架频繁调用,注意性能(避免在 toString 中做复杂计算)
public class Product {
    private String sku;
    private String name;
    private double price;

    @Override
    public String toString() {
        return String.format("Product{sku='%s', name='%s', price=%.2f}",
                sku, name, price);
    }
}

九、wait / notify —— 线程间通信的基石(深度源码分析)

9.1 方法签名

public final void wait() throws InterruptedException
public final native void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
public final native void notify()
public final native void notifyAll()

9.2 "为什么 wait/notify 定义在 Object 上而不是 Thread 上?"

这是经典的源码理解题。答案的本质是:Java 的锁(monitor)是与对象绑定的,而不是与线程绑定的。

在 HotSpot 中,每个 Java 对象都有一个与之关联的 ObjectMonitor 结构(需要时创建)。线程是"谁在执行"的概念,而对象是"锁在谁身上"的概念。把 wait/notify 放在 Object 上,意味着任何对象都可以成为同步的锚点。如果定义在 Thread 上,就变成了"线程 A 等待线程 B 的通知"——这会让多线程协作的语义变得混乱(你需要知道具体是哪个线程在持有你关心的锁)。

9.3 ObjectMonitor —— HotSpot 中的重量级锁实现

源码位置: src/share/vm/runtime/objectMonitor.hpp / objectMonitor.cpp

当对象升级为重量级锁时,HotSpot 会为它创建一个 ObjectMonitor 结构:

class ObjectMonitor {
    void * volatile _object;       // 关联的 Java 对象(oop)
    Thread * volatile _owner;     // 当前持有锁的线程(null 表示无锁)

    ObjectWaiter * volatile _EntryList;  // 阻塞等待获取锁的线程队列
    ObjectWaiter * volatile _WaitSet;    // 调用了 wait() 的线程集合

    int _recursions;              // 锁重入次数(支持 synchronized 的可重入性)
    int _count;                   // 等待的线程数
    // ...
};

WaitSet 与 EntryList 是两个不同的队列,这是理解 wait/notify 机制的核心:

线程调用 synchronized(obj) 尝试获取锁:
    │
    ├─ 锁未被占用 → 进入 Owner,_recursions = 1
    │
    ├─ 锁已被占用 → 进入 EntryList 阻塞等待(BLOCKED 状态)
    │
线程在持有锁时调用 obj.wait():
    │
    └─ 从 Owner 退出 → 进入 WaitSet(WAITING/TIMED_WAITING 状态)
       同时释放锁,_recursions 归零

线程调用 obj.notify():
    │
    └─ 从 WaitSet 中随机选一个线程 → 移入 EntryList
       注意:被移入 EntryList 的线程仍然需要竞争锁!不是直接执行!

线程调用 obj.notifyAll():
    │
    └─ 将 WaitSet 中全部线程 → 移入 EntryList

重要结论

  1. notify() 唤醒的线程不会立即执行——它只是从 WaitSet 搬到 EntryList,还需要重新竞争锁
  2. 被唤醒的线程从 wait() 返回时,一定是重新获取了锁的——这就是为什么 wait/notify 必须在 synchronized 块中使用
  3. wait()完全释放锁(包括所有重入次数),而 sleep() 不会释放任何锁

9.4 锁的膨胀过程 —— 从偏向锁到重量级锁

理解这个流程对于分析并发性能至关重要:

                        synchronized 竞争加剧
    无锁 ──→ 偏向锁 ──→ 轻量级锁 ──→ 重量级锁
                              │             │
                              │             ├─ 创建 ObjectMonitor
                              │             ├─ EntryList / WaitSet 投入使用
                              │             └─ wait/notify 可用
                              │
                              └─ 基于 CAS 自旋
                                 无 wait/notify 能力

wait() 调用会强制锁膨胀:因为 wait() 依赖于 ObjectMonitor 的 WaitSet,如果对象当前是轻量级锁,调用 wait() 会立即触发膨胀为重量级锁。这是一个不可逆的过程——一旦膨胀,即使之后锁竞争消失,也不会再降级。

9.5 正确使用模式

public class WaitNotifyDemo {
    private boolean dataReady = false;

    public synchronized void produceData() {
        System.out.println("生产者: 开始准备数据...");
        try { Thread.sleep(2000); } catch (InterruptedException e) {}
        dataReady = true;
        System.out.println("生产者: 数据就绪,通知消费者");
        notifyAll();  // 唤醒所有等待线程
    }

    public synchronized void consumeData() throws InterruptedException {
        // 标准等待模式:循环检查条件(while 而非 if)
        while (!dataReady) {
            System.out.println("消费者: 数据未就绪,进入等待...");
            wait();
        }
        System.out.println("消费者: 收到通知,开始消费数据");
    }

    public static void main(String[] args) throws InterruptedException {
        WaitNotifyDemo demo = new WaitNotifyDemo();

        Thread consumer = new Thread(() -> {
            try { demo.consumeData(); }
            catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        });

        Thread producer = new Thread(demo::produceData);

        consumer.start();
        Thread.sleep(500);  // 确保消费者先进入 wait
        producer.start();

        consumer.join();
        producer.join();
    }
}

9.6 关键注意事项

  • 必须在 synchronized 块中调用:否则抛出 IllegalMonitorStateException——因为 JVM 需要检查当前线程是否持有对象的 ObjectMonitor
  • 使用 while 而非 if:防止虚假唤醒(spurious wakeup)。虚假唤醒的根源在于操作系统层面的 pthread 条件变量规范允许 pthread_cond_wait 在没有信号的情况下返回
  • notify 随机唤醒一个 / notifyAll 唤醒全部:多消费者+多生产者场景下,优先使用 notifyAll。只用 notify 可能出现"生产者唤醒了另一个生产者"导致全部线程卡死的情况
  • wait 会释放锁,sleep 不会:这是两者最本质的区别

9.7 wait(long, int nanos) 的微妙细节

public final void wait(long timeout, int nanos) throws InterruptedException {
    if (timeout < 0)
        throw new IllegalArgumentException("timeout value is negative");
    if (nanos < 0 || nanos > 999999)
        throw new IllegalArgumentException("nanosecond timeout value out of range");
    if (nanos >= 500000 || (nanos != 0 && timeout == 0))
        timeout++;
    wait(timeout);
}

注意这里的四舍五入逻辑:当纳秒 >= 500000 时,毫秒部分会加 1。这使得 wait(0, 500000) 等价于 wait(1),而非 wait(0)wait(0) 表示无限等待)。


十、finalize —— 不推荐的资源清理方式

protected void finalize() throws Throwable {}

10.1 为什么 finalize 不可靠

finalize() 的设计初衷是让对象在被 GC 回收前有机会释放外部资源,但实践中不应依赖它

  • 调用时机不确定:对象可被 GC 标记后到实际执行 finalize 可能有任意长延迟
  • 可能永远不被调用:程序正常退出时,GC 不保证执行所有 finalize
  • 性能开销巨大:带有 finalize 的对象回收需要 GC 至少两轮——第一轮 GC 发现对象可回收但重写了 finalize,将其加入 Finalizer 队列;第二轮 GC 在 finalize 执行完毕后才能真正回收
  • 复活风险:在 finalize 中重新引用对象,会导致对象"复活",造成内存管理混乱

10.2 JVM 层面的 Finalizer 机制

源码位置: java.lang.ref.Finalizer + src/share/vm/gc_implementation/shared/markSweep.cpp

对象创建时,如果它的类覆写了 finalize(),JVM 在分配对象时会额外创建一个 java.lang.ref.Finalizer 引用对象,将其注册到一个全局的引用链表中。GC 发现对象不可达时:

GC 标记不可达 → 检查是否有 Finalizer → 如有,加入 Finalizer 队列
                                             │
                                    Finalizer 线程取出执行
                                    finalize() 被调用
                                             │
                                下一轮 GC 才能真正回收对象

Java 中有一个独立的 Finalizer 守护线程java.lang.ref.Finalizer$FinalizerThread),它专门从队列中取出待终结的对象并调用其 finalize()。如果这个线程因为某个 finalize() 方法阻塞或被高负载压垮,会导致大量待回收对象堆积,引发 OOM。

10.3 推荐替代方案

public class ResourceHolder implements AutoCloseable {
    private java.io.FileInputStream stream;

    public ResourceHolder(String path) throws java.io.FileNotFoundException {
        this.stream = new java.io.FileInputStream(path);
    }

    @Override
    public void close() throws java.io.IOException {
        if (stream != null) {
            stream.close();
            stream = null;
        }
    }

    public static void main(String[] args) throws Exception {
        // try-with-resources —— JDK 7+ 的最佳实践
        try (ResourceHolder holder = new ResourceHolder("data.txt")) {
            // 使用资源
        }
        // 自动调用 close(),无需 finally 块
    }
}

JDK 9 中 finalize() 已被标记为 @Deprecated(since="9"),推荐的替代品是 java.lang.ref.Cleaner(基于 PhantomReference,性能更好且不会阻塞 GC)。


十一、综合实战:手写一个安全的缓存类

import java.util.concurrent.TimeUnit;

/**
 * 基于 Object 核心方法实现的线程安全 LRU 缓存
 * 综合运用 hashCode、equals、clone、wait/notify 等
 */
public class ObjectBasedCache {

    static class CacheEntry implements Cloneable {
        String key;
        String value;
        long expireAt;

        CacheEntry(String key, String value, long ttlMillis) {
            this.key = key;
            this.value = value;
            this.expireAt = System.currentTimeMillis() + ttlMillis;
        }

        boolean isExpired() {
            return System.currentTimeMillis() > expireAt;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof CacheEntry)) return false;
            CacheEntry that = (CacheEntry) o;
            return java.util.Objects.equals(key, that.key);
        }

        @Override
        public int hashCode() {
            return java.util.Objects.hash(key);
        }

        @Override
        protected CacheEntry clone() {
            try {
                return (CacheEntry) super.clone();
            } catch (CloneNotSupportedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private final java.util.LinkedHashMap<String, CacheEntry> store;
    private final int maxSize;

    public ObjectBasedCache(int maxSize) {
        this.maxSize = maxSize;
        this.store = new java.util.LinkedHashMap<String, CacheEntry>(16, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(
                    java.util.Map.Entry<String, CacheEntry> eldest) {
                return size() > ObjectBasedCache.this.maxSize;
            }
        };
    }

    public synchronized void put(String key, String value, long ttl, TimeUnit unit) {
        store.put(key, new CacheEntry(key, value, unit.toMillis(ttl)));
        notifyAll();  // 唤醒可能在 get 中等待的线程
    }

    public synchronized String get(String key) throws InterruptedException {
        CacheEntry entry = store.get(key);
        if (entry == null || entry.isExpired()) {
            return null;
        }
        return entry.value;
    }

    public synchronized CacheEntry getClone(String key) throws InterruptedException {
        CacheEntry entry = store.get(key);
        if (entry == null || entry.isExpired()) {
            return null;
        }
        return entry.clone();  // 返回克隆副本,防止外部修改
    }

    @Override
    public String toString() {
        return "Cache[size=" + store.size() + "/" + maxSize
                + ", keys=" + store.keySet() + "]";
    }

    public static void main(String[] args) throws InterruptedException {
        ObjectBasedCache cache = new ObjectBasedCache(3);

        cache.put("user:1", "Alice", 5, TimeUnit.SECONDS);
        cache.put("user:2", "Bob", 5, TimeUnit.SECONDS);
        cache.put("user:3", "Charlie", 5, TimeUnit.SECONDS);

        System.out.println(cache);  // size=3

        // 触发淘汰:添加第4个元素,最老的被移除
        cache.put("user:4", "David", 5, TimeUnit.SECONDS);
        System.out.println(cache);  // size=3

        // 验证深拷贝不影响原始缓存
        CacheEntry cloned = cache.getClone("user:4");
        cloned.value = "被篡改的值";
        System.out.println("原始缓存: " + cache.get("user:4"));  // David
    }
}

十二、陷阱与性能 —— 生产者容易踩的坑

12.1 hashCode 与偏向锁互斥

如上文 5.3 节详述,在偏向锁状态下调用 hashCode() 会导致锁膨胀,在高并发场景下性能骤降。教训:如果一个对象需要频繁进出 synchronized 块,应提前(在锁外)初始化其 hashCode。

12.2 clone 的循环引用

如果对象图中存在循环引用,递归深拷贝会导致 StackOverflowError 或无限循环:

class Node implements Cloneable {
    Node next;
    @Override
    protected Node clone() throws CloneNotSupportedException {
        Node cloned = (Node) super.clone();
        if (next != null) cloned.next = next.clone();  // 循环引用 → 无限递归!
        return cloned;
    }
}

12.3 notify 导致的生产者-生产者死锁

// 危险:只用 notify,且只有一个条件队列
synchronized (queue) {
    while (queue.isEmpty()) {
        queue.wait();  // 生产者 A 从 wait 返回后可能发现队列仍满
    }
}

// 安全:使用 notifyAll 或区分条件谓词
synchronized (queue) {
    while (queue.isEmpty()) {
        queue.wait();
    }
}

12.4 未覆写 hashCode 导致 HashMap 内存泄漏

class BadKey {
    String id;
    BadKey(String id) { this.id = id; }
    @Override
    public boolean equals(Object o) {
        return o instanceof BadKey && ((BadKey) o).id.equals(this.id);
    }
    // 没有覆写 hashCode!同一逻辑 key 被分散到不同桶中
}

Map<BadKey, String> map = new HashMap<>();
BadKey k1 = new BadKey("test");
map.put(k1, "value");
// map.get(new BadKey("test")) → null!因为 hashCode 不同,没找到桶
// 但 k1 已在 map 中,造成隐式的"内存泄漏"

十三、面试高频考点

问题关键要点涉及源码层知识
hashCode 与 equals 的关系equals 相等则 hashCode 必等,反之不必然Mark Word 缓存、identity hash code
为什么要同时覆写HashMap 先用 hashCode 找桶,再用 equals 判等桶数组索引 = (n-1) & hash
wait 与 sleep 的区别wait 释放锁 + 需在 synchronized 中调用;sleep 不释放锁ObjectMonitor::wait 释放 Owner + 进入 WaitSet
wait/notify 为什么在 Object 上锁是对象级别的,monitor 与对象绑定每个对象可关联一个 ObjectMonitor
浅拷贝 vs 深拷贝浅拷贝共享引用对象,深拷贝递归克隆JVM_Clone 做 bitwise copy
为什么 clone 是 protected防止任意类随意调用,仅开放给有 Cloneable 声明的子类JVM_Clone 在 native 层检查 Cloneable
finalize 为什么废弃(JDK 9)不保证执行时机、性能损耗大、对象复活风险Finalizer 线程、FinalReference 机制
偏向锁与 hashCode 冲突偏向锁状态下 Mark Word 无法存储 hashCode,调用会触发锁膨胀Mark Word 复用机制

十四、源码阅读指南 —— 深入学习路径

14.1 Object 相关核心源码文件

方法OpenJDK 8 源码位置关键函数
registerNativessrc/share/native/java/lang/Object.cJava_java_lang_Object_registerNatives
hashCodesrc/share/vm/runtime/synchronizer.cppObjectSynchronizer::FastHashCode()
getClasssrc/share/vm/prims/jvm.cppJVM_GetClass()
clonesrc/share/vm/prims/jvm.cppJVM_Clone()
waitsrc/share/vm/runtime/objectMonitor.cppObjectMonitor::wait()
notify/notifyAllsrc/share/vm/runtime/objectMonitor.cppObjectMonitor::notify()
finalizesrc/share/vm/gc_implementation/shared/markSweep.cppGC 标记阶段的 Finalizer 处理
对象头/Mark Wordsrc/share/vm/oops/markOop.hppMark Word 的位布局定义
ObjectMonitorsrc/share/vm/runtime/objectMonitor.hppObjectMonitor 结构体定义

14.2 推荐后续学习路径

学习完 Object 的源码后,建议按以下路径深入:

  1. java.lang.String — 理解 hashCode 的 31 乘数选择、不可变性的意义、字符串常量池(StringTable)
  2. java.lang.Class + 反射机制 — getClass 返回的 Class 对象如何与 InstanceKlass 协作,Method.invoke() 的 native 实现
  3. java.lang.Thread — Thread 如何与 Object 的 wait/notify 协作,Thread.join() 内部实际调用的就是 wait()
  4. synchronized 字节码monitorenter / monitorexit 指令如何对应 ObjectMonitor 的 enter/exit
  5. java.util.HashMap — hashCode 和 equals 的实际消费者,理解 tableSizeFor 和 (n-1) & hash 的取模优化
  6. java.util.concurrent.locks.AbstractQueuedSynchronizer (AQS) — JUC 锁如何取代 ObjectMonitor 提供更灵活的同步控制

十五、小结

Object 类是 Java 中最简短却又最深刻的类——仅 12 个方法、Java 层源码不到 100 行,但其 native 实现在 HotSpot 中对应着数千行 C++ 代码,横跨对象内存布局(Mark Word / Klass Pointer)、同步锁机制(ObjectMonitor / 锁膨胀)、GC(finalize / Finalizer 线程)三大核心子系统。

掌握 Object 的源码级实现,意味着你真正理解了 Java 对象在 JVM 中"长什么样"、锁"是如何工作的"、以及为什么某些看似简单的 API 调用会触发连锁的底层反应。这正是从"会用 Java"到"理解 JVM"的关键一步。