基础回顾之 Object 类

Object 类是所有类的基类,完整路径为 java.lang.Object ,包含以下几种方法:

registerNatives()

1
2
3
4
private static native void registerNatives();
static {
registerNatives();
}

Native 方法,在 JNI 中又调用了 RegisterNatives 来动态注册 Java 方法和 JNI 函数的对应关系,代码位于 jdk 的 native 目录 src/share/native/java/lang/Object.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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]));
}

JNIEXPORT jclass JNICALL
Java_java_lang_Object_getClass(JNIEnv *env, jobject this)
{
if (this == NULL) {
JNU_ThrowNullPointerException(env, NULL);
return 0;
} else {
return (*env)->GetObjectClass(env, this);
}
}

getClass()

1
public final native Class<?> getClass();

也是一个本地方法,实现实在 Native 层,但并没有采用动态注册的方式,而是采用静态注册的方式,在 Object.c 中对应的 Native 方法为:Java_java_lang_Object_getClass()

返回此对象的运行时类,返回的Class对象是被表示的类的静态同步方法锁定的对象。(应该就是表示这个方法的本地实现应该是用了类锁吧。。)

hashCode()

1
public native int hashCode();

Native 方法,在 JVM 中实现。

返回对象的 Hash 值,该方法是为了使哈希表(例如HashMap提供的哈希表)受益。Hash code 一般又以下约定:

  • 在Java应用程序一次执行期间,hashcode() 必须返回相同的整数值,不同的执行期间则不做要求
  • 如果调用 equals() 判断两个对象相等,则这两个对象的 hashCode 方法返回值必须相同
  • 如果调用 equals() 判断两个对象不相等,则这两个对象的 hashCode 方法返回值可以相同也可以不同,这个并不是必须的,但不同对象还是建议返回不同的hashcode,这样可以降低hash碰撞的概率

一般情况下,Obecjt 中的 hashCode() 可以保证不同的对象返回不同的值。

equals()

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

表示两个对象是否相等,具有以下特性:

  • 自反性x.equals(x) 必须返回 ture
  • 对称性x.equals(y)y.equals(x) 的值必须相同
  • 传递性:如果 x.equals(y) 为 true,y.equals(z) 为 true,则 x.equals(z) 也为 true
  • 一致性:对于任何非空对象, x.equals(null) 均为 false

equals 方法默认实现是直接通过 == 来判断两个对象是否指向同一个引用,equals() 更多的是判断对象的内容是否相等,而 == 则用来判断是否是同一个对象。

如果需要重写该方法,务必同时也要重写 hashCode方法,以保证 hashCode方法的常规约定:相等的对象必须具有相等的 hash 值。

clone()

1
2
3
public class Object {
protected native Object clone() throws CloneNotSupportedException;
}

native 方法,其遵守如下约定:

  • x.clone() != x 为 true,即二者为不同的对象
  • x.clone().getClass() == x.getClass() 为 true,即二者的类是相同的
  • x.clone().equals(x) 为 true,因为 equals() 是用来比较两个对象的值是否相同,那么理应克隆后的对象与愿对象的值应该是相同的

实现克隆需要实现 Cloneable 接口并重写 Object 中的 clone() 方法:

1
2
//java.lang.Cloneable
public interface Cloneable {}

Cloneable 是一个空接口,表明可以调用类的 clone() 方法来进行克隆,如果对一个没有实现接口的对象调用 clone() 将会抛出 CloneNotSupportedException 异常。如果实现了该接口,按照惯例,必须重写 clone() 方法,并将可见效改为 public,其在 Object 中默认为 protected,这也是为什么如果一个类不重写则调用不到,因为是在 Object 类所处的包下。

toString()

1
2
3
4
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
//例如 HelloWord.java 返回 HelloWord@15db9742

返回对象的信息,当将对象直接和字符串相加时,隐式调用该对象的 toString 方法。

notify() & notifyAll()

1
2
public final native void notify();
public final native void notifyAll();

这两个方法都是用于唤醒其他等待该对象 monitor 锁的线程(通过调用了 wait 方法)

  • notify 方法会任意选择一个线程唤醒,这个选择是随机并且由具体的虚拟机自行实现。
  • notifyAll 则会唤醒所有等待锁的线程

被唤醒的线程不会立即执行,而是需要等到当前持有锁的线程释放后,如果此时存在其他线程也获取锁,则需要进行竞争。

该方法只能在已经获取到对象锁的线程中调用,否则会抛出 IllegalMonitorStateException 异常。

wait()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public final native void wait(long timeout) throws InterruptedException;

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 > 0) {
timeout++;
}
wait(timeout);
}

public final void wait() throws InterruptedException {
wait(0);
}

wait 有 3 个重载方法,最终调用的都是本地方法 wait(long timeout),其中 timeout 的单位为毫秒,

wait 方法的调用必须要求当前线程已经持有了对象锁,否则也会会抛出 IllegalMonitorStateException 异常。当线程调用 wait 方法后,会被加入到该对象的锁等待集合中,然后进入 Waitng 或 Timed Waiting 等待状态,直到以下四种情况发生才会被唤醒:

  • 其他线程调用对象的 notify() 方法,并且正好该线程被任意选择为唤醒线程
  • 其他线程调用对象的 notifyAll() 方法
  • 其他线程通过 interrupt 方法中断该线程
  • 超过指定等待超时时间

被唤醒后该线程会从对象的等待集合中删除,并重新启用线程调度,然后和其他线程一起竞争。一旦重新获取到锁,它对对象的所有同步声明都将恢复为原状态,即调用 wait方法时的情况,接着从调用 wait() 的地方继续执行。

通过上边的代码可以看到,wait() 会抛出 InterruptedException 异常,因此在调用 wait() 时必须捕获这个异常,如果是被其他线程通过 interrup() 来打断,就会捕获到这个异常,因此也就等于被唤醒。

虚假唤醒

除了上边的三种被唤醒的情况外,还存在一种虚假唤醒的情况。它发生的概率很小,但如果发生了也会导致一些异常的情况发生,因此为了保证正确性,可以将 wait() 的调用放在 while 循环中:

1
2
3
4
5
synchronized (obj) {
while (<condition does not hold>) //如果没有持有锁则继续 wait
obj.wait(timeout);
... // Perform action appropriate to condition
}

finalize()

1
protected void finalize() throws Throwable { }

当垃圾回收确定不再有对该对象的引用时,由垃圾回收器在对象上调用,该方法在 Object 中为空实现,子类可以通过覆写来执行对应场景的逻辑。

以下内容为《深入理解 Java 虚拟机》书中的内容:

当一个对象不可达时,至少需要经历两次标记过程才会被真正回收:当对象在进行可达性分析后发现没有和 GC Root 连接的引用链,则会被第一次标记并进行筛选,此时会判断是否有必要执行 finalize(),如果对象没有覆写finalize() 或是已经执行过了,则不会去执行。而如果需要执行,那么对象会被放置在 F-Queue 队列中,并在稍后由 JVM 自动创建的、低优先级的 Finalizer 线程来执行该对象的 finalize() 方法,但并不保证会等待 finalize() 方法执行完成,因为如果一个对象的 finalize() 方法执行缓慢或发生了死循环将会导致队列中其他对象永远处于等待,甚至导致回收系统奔溃。稍后 GC 会对 F-Queue 中的对象进行第二次小规模的标记,此时如果对象在finalize() 方法中重新和其他对象关联,那么会被从”即将回收“的集合中被移除,否则就会被真正回收了。

如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class FinalizeDemo {

private static FinalizeDemo sInstance = null;

public void isAlive() {
System.out.println("isAlive.");
}

@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize run.");
sInstance = this;
}

public static void main(String[] args) throws InterruptedException {
sInstance = new FinalizeDemo();
sInstance = null; //将对象的引用置为空
System.out.println("GC No.1");
System.gc(); //调用GC
Thread.sleep(500); //暂停500ms等待finalize执行
if (sInstance != null) {
sInstance.isAlive();
} else {
System.out.println("recycled..");
}
System.out.println("GC No.2");
sInstance = null; //将对象的引用置为空
System.gc(); //再次调用GC
Thread.sleep(500); //暂停500ms等待finalize执行
if (sInstance != null) {
sInstance.isAlive();
} else {
System.out.println("recycled..");
}
}
}

//执行结果
GC No.1
finalize run.
isAlive.
GC No.2
recycled..

如上所述,任何对象的 finalize() 方法都只会被调用一次,当下一次回收时不会再被调用。

参考

Java 8 Class Object

《深入理解 Java 虚拟机》