嚼一嚼Object类中的方法
Java 中的所有类都有一个共同的祖先,那就是 Object 类,现在我们就来看看这个类中有哪些方法。
1、Object 类中方法源码(JDK8)
-
private static native void registerNatives();
-
public final native Class<?> getClass();
-
public native int hashCode();
-
public boolean equals(Object obj) { return (this == obj);}
-
protected native Object clone() throws CloneNotSupportedException;
-
public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }
-
public final native void notify();
-
public final native void notifyAll();
-
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); }
-
protected void finalize() throws Throwable { }
2、registerNatives 方法
2.1、本地方法
在很多类中,都会像 Object 类一样,存在如下代码:
private static native void registerNatives();
static {
registerNatives();
}
native 修饰的方法是本地方法,那么我们先弄清楚何为本地方法。
Java有两种方法:Java方法和本地方法。Java方法是由Java语言编写,编译成字节码,存储在class文件中。本地方法是由其他语言(比如C,C++,或者汇编)编写的,编译成和处理器相关的机器代码。本地方法保存在动态连接库中,格式是各个平台专有的。Java方法是平台无关的,但本地方法却不是。 运行中的Java程序调用本地方法时,虚拟机装载包含这个本地方法的动态库,并调用这个方法。本地方法是联系 Java 程序和底层主机操作系统的连接方法。
2.2、registerNatives 方法作用
这里的 registerNatives 就是一个本地方法,从方法名可以判断,这个方法是用来注册本地方法的。那么注册哪些本地方法呢,我猜你已经想到,就是 Object 类中的其他 native 方法。
一个 Java 程序要想调用一个本地方法,需要执行两个步骤:第一,通过 System.loadLibrary()
将包含本地方法实现的动态文件加载进内存;第二,当Java程序需要调用本地方法时,虚拟机在加载的动态文件中定位并链接该本地方法,从而得以执行本地方法。
Object 类中的 registerNatives 方法的作用就是,让程序主动将本地方法链接到调用方,当 Java 程序需要调用本地方法时,就不需要虚拟机定位链接,而是可以直接调用。
使用 registerNatives 方法的几点好处:
- 通过 registerNatives 方法在类被加载的时候就主动将本地方法链接到调用方,比当方法被使用时再由虚拟机来定位和链接更方便有效
- 如果本地方法在程序运行中更新了,可以通过调用 registerNative 方法进行更新
- Java 程序需要调用一个本地应用提供的方法时,因为虚拟机只会检索本地动态库,因而虚拟机是无法定位到本地方法实现的,这个时候就只能使用 registerNatives 方法进行主动链接
- 通过 registerNatives 方法,在定义本地方法实现的时候,可以不遵守JNI命名规范
JNI 命名规范:
- 前缀:Java_
- 类的全限定名,用下划线进行分隔
- 方法名
例如 Java_java_lang_Object_registerNatives.
2.3、registerNatives 方法原理
传统 Java JNI 方式:
- 编写带有 native 方法的 Java 类
- 使用 javah 命令生成 .h 头文件
- 编写代码实现头文件中的方法
上述方式每次都需要通过 javah 依据 Java 类的全类名生成对应的 native 函数全名称,其实可以使用 registerNatives 方法把 C/C++ 中的方法隐射到 Java 中的 native 方法。
贴上 Object 类的中 registerNatives 方法的 C 语言实现:
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]));
}
代码中的 *env
为 JNI 环境,RegisterNatives 方法即是进行动态注册。
3、getClass 方法
返回当前对象的运行时类,即一个 Class.
Class 是用来描述字节码的,是一个描述类的类。
4、hashCode 方法
hashCode 方法是根据一定的规则,将对象相关的信息映射成一个数值,这个数值称为散列值。
hashCode 方法的主要作用是为了配合基于散列的集合一起正常运行,例如 HashSet、HashMap、HashTable等。
贴一下 Object 中 hashCode 的本地方法实现:
static inline intptr_t get_next_hash(Thread * Self, oop obj) {
intptr_t value = 0 ;
if (hashCode == 0) {
// This form uses an unguarded global Park-Miller RNG,
// so it's possible for two threads to race and generate the same RNG.
// On MP system we'll have lots of RW access to a global, so the
// mechanism induces lots of coherency traffic.
value = os::random() ;
} else
if (hashCode == 1) {
// This variation has the property of being stable (idempotent)
// between STW operations. This can be useful in some of the 1-0
// synchronization schemes.
intptr_t addrBits = intptr_t(obj) >> 3 ;
value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
} else
if (hashCode == 2) {
value = 1 ; // for sensitivity testing
} else
if (hashCode == 3) {
value = ++GVars.hcSequence ;
} else
if (hashCode == 4) {
value = intptr_t(obj) ;
} else {
// Marsaglia's xor-shift scheme with thread-specific state
// This is probably the best overall implementation -- we'll
// likely make this the default in future releases.
unsigned t = Self->_hashStateX ;
t ^= (t << 11) ;
Self->_hashStateX = Self->_hashStateY ;
Self->_hashStateY = Self->_hashStateZ ;
Self->_hashStateZ = Self->_hashStateW ;
unsigned v = Self->_hashStateW ;
v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
Self->_hashStateW = v ;
value = v ;
}
value &= markOopDesc::hash_mask;
if (value == 0) value = 0xBAD ;
assert (value != markOopDesc::no_hash, "invariant") ;
TEVENT (hashCode: GENERATE) ;
return value;
}
总的来说有 6 种生成 hashCode 的方式 ,JDK6 和 JDK7 中使用方式 1 随机数的形式,hash 值是存在对象头中的,所以多次调用也不会出现不一致的情况。JDK8 使用方式 5,走程序的 else 条件,即使用 Xorshift.
5、equals 方法
判断两个对象是否等价,返回 true 表示等价,返回 false 表示不等。
Object 类中是直接使用 == 关系运算符进行比较,== 运算符比较可分为两种情况:
- 基本数据类型:比较值
- 引用数据类型:比较内存中的存放地址
显然对于引用数据类型,Object 中的 equals 方法并不太适用,所以一般会进行重写,下面贴一下 String 中的 equals 方法源码:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
可以看到,String 是先比较内存地址,内存地址不同的情况下,逐个字段进行比较,这样使用 equals 比较的结果更符合逻辑。
需要注意的是,当 equals 方法被重写时,通常都要重写 hashCode 方法,使之满足以下规律:
- equals 为 true ==> hashCode 相同
- equals 为 false ==> hashCode 不同
- hashCode 相同 ==> equals 未知
- hashCode 不同 ==> equals 为 false
6、clone 方法
clone 的意思是复制,顾名思义,这个方法用作对象的复制。
对象复制,或者称之为拷贝,可分为两种,深拷贝和浅拷贝:
- 深拷贝:假设 B 复制了 A,当修改 A 时,任何情况下 B 不会发生变化,这就是深拷贝
- 浅拷贝:修改 A 时 B 也会发生变化,这就是浅拷贝
深拷贝与浅拷贝出现的根源在于引用类型,浅拷贝时,被复制的对象的所有变量与原来对象的值相同,但是引用类型的变量,仍然指向原来的对象,即对象中的对象,其实是共享的。深拷贝是一个独立的对象拷贝,从最外层的对象到最里面的引用对象,都复制一遍。所以深拷贝速度较慢且花销较大。
Object 中的 clone 方法是浅拷贝。
7、toString 方法
数据对象的信息,这里需要注意一点,@ 后面的十六进制数字,并不是内存地址,只是 hashCode 的十六进制形式。
8、wait 和 notify 方法
先了解两个概念,锁池和等待池:
- 锁池:假设线程 A 已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个 synchronized 方法(或者 synchronized 块),由于这些线程在进入对象的 synchronized 方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程 A 拥有,所以这些线程就进入了该对象的锁池中(锁释放后去竞争)
- 等待池:假设一个线程 A 调用了某个对象的 wait 方法,线程 A 就会释放该对象的锁,进入到该对象的等待池中(不会竞争对象锁)
理解了锁池和等待池,那么 wait 方法的作用也就很清晰了,是将已经获得对象锁的线程立即移到等待池中。
再来说说 notify 的作用:
- 作用对象:调用 a 对象的 wait 方法后,线程处于 a 对象的等待池中,等待池中的线程不会竞争 a 对象的锁。notify 作用的对象就是这些线程
- 作用结果:调用 notify 后会有一个线程由等待池进入锁池,参与锁的竞争(随机唤醒)。而 notifyAll 会将对象等待池中的所有对象移到锁池中
- 作用时机:当调用 notify 或 notifyAll 的线程执行完同步方法或者同步代码块中的所有代码,才会释放这把锁
下面说说需要注意的点:
- 调用 a.wait() 或者 a.notify() 前必须要先拿到 a 对象的锁,故 wait 和 notify 方法要放在该对象的同步方法或者步代码块中
- wait 方法会立即释放锁、释放 CPU
- notify 方法不会立即释放锁,需等 notify 所在线程执行完同步块
- 未获得该对象锁时调用该对象的 wait 或 notify 方法,会抛出
java.lang.IllegalMonitorStateException
异常 - yield、sleep 不会释放锁,但会释放 CPU
- join 释放锁,不会释放CPU
- wait、sleep、join 都可以通过 interrupt 打断线程暂停状态,使线程立即抛出 InterruptedException
聊聊 Thread 中的 join 方法:
-
作用:假如在 main 线程调用 t.join(),那么 main 线程阻塞,直到 t 线程运行结束
-
原理:查看源码,发现 join 方法是同步方法,在内部使用了 wait 方法。当 main 线程调用 t.join() 时,先获取到了 t 线程对象的锁,然后在 join 方法内部执行 wait 方法,导致 main 线程阻塞(因为是 main 线程间接调用了 wait 方法,而不是 t 线程中调用的)。而结束阻塞的方式,是 t.isAlive() 结果返回 false,即线程已经是不可用状态如已经运行结束
9、wait(long timeout) 方法
与 wait 方法的区别是,此方法可以指定一个超时,超时之后将自己唤醒线程。使用 notify 或 notifyAll 可以在超时前唤醒线程。需要注意的是,wait(0) 与 wait() 是等价的。
wait(long timeout,int nanos)这是另一个提供相同功能的方法,唯一的区别是这个可以提供更高的精度。如果 nanos 在 [0,1000000) 之间,那么超时时间为 timeout+1.
10、finalize 方法
- 当垃圾回收器要回收对象所占内存之前,会先调用该对象的 finalize 方法
- finalize 方法只会在对象被回收前调用一次,调用具有不确定性,只保证方法会被调用,但是不保证会等待它运行完,原因是如果 finalize 方法运行缓慢或者发生死循环,会导致内存回收系统崩溃
- finalize 方法是对象逃脱被回收的最后一次机会,并且最多只能用一次
- 不建议使用 finalize 方法,它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序
参考
- https://blog.csdn.net/Saintyyu/article/details/90452826
- https://blog.csdn.net/j3T9Z7H/article/details/107852624
- https://blog.csdn.net/djzhao/article/details/79410229
- https://baijiahao.baidu.com/s?id=1655232869611610920&wfr=spider&for=pc