本文最后更新于 198 天前,其中的信息可能已经有所发展或是发生改变。
Object 类结构
概述
已知所有类的基类——java.lang.Object
- Object 类是所有类的基类,当一个类没有直接继承某个类时,默认继承Object类
- Object 类属于 java.lang 包,此包下的所有类在使用时无需手动导入,系统会在程序编译期间自动导入。
那么, 在编译源代码时,当一个类没有显式标明继承的父类时,编译器会为其指定一个默认的父类(一般为Object),而交给虚拟机处理这个类时,由于这个类已经有一个默认的父类了,因此,VM仍然会按照常规的方法像处理其他类一样来处理这个类。
这里有7个native方法:registerNatives()、getClass()、hashCode()、clone()、notify()、notifyAll()、wait(long)
JNI:Java Native Interface:
①、标准的 Java 类库不支持应用程序平台所需的平台相关功能。
②、我们已经用另一种语言编写了一个类库,如何用Java代码调用?
③、某些运行次数特别多的方法代码,为了加快性能,我们需要用更接近硬件的语言(比如汇编)编写。
上面这三种需求,其实说到底就是如何用 Java 代码调用不同语言编写的代码。那么 JNI 应运而生了
native 用来修饰方法,用 native 声明的方法表示告知 JVM 调用,该方法在外部定义,我们可以用任何语言去实现它。 简单地讲,一个native Method就是一个 Java 调用非 Java 代码的接口。
源码解析
| package java.lang; |
| |
| public class Object { |
| |
| |
| |
| |
| private static native void registerNatives(); |
| |
| |
| |
| |
| static { |
| 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 { |
| } |
| } |
类构造器
一个类必须要有一个构造器的存在,如果没有显示声明,那么系统会默认创造一个无参构造器,在JDK的Object类源码中,是看不到构造器的,系统会自动添加一个无参构造器。我们可以通过:
| |
| Object obj = new Object(); |
equals 方法
源码
| public boolean equals(Object obj) { |
| |
| return (this == obj); |
| } |
结论:
- 在 Object 类中,== 运算符和 equals 方法是等价的,都是比较两个对象的引用是否相等,从另一方面来讲,如果两个对象的引用相等,那么这两个对象一定是相等的
- 对于我们自定义的一个对象,如果不重写 equals 方法,那么在比较对象的时候就是调用 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 是引用类型,比较时不能比较引用是否相等,重点是字符串的内容是否相等。
所以 String 类定义两个对象相等的标准是字符串内容都相同。
在Java规范中,对 equals 方法的使用必须遵循以下几个原则:
- 自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。
- 对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。
- 传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。
- 一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改
- 对于任何非空引用值 x,x.equals(null) 都应返回 false。
重写 equals 原则
定义一个 Person 类,然后重写其equals 方法,比较两个 Person 对象:
| public class Person { |
| private String pname; |
| private int page; |
| |
| public Person(){} |
| |
| public Person(String pname,int page){ |
| this.pname = pname; |
| this.page = page; |
| } |
| public int getPage() { |
| return page; |
| } |
| |
| public void setPage(int page) { |
| this.page = page; |
| } |
| |
| public String getPname() { |
| return pname; |
| } |
| |
| public void setPname(String pname) { |
| this.pname = pname; |
| } |
| @Override |
| public boolean equals(Object obj) { |
| if(this == obj){ |
| return true; |
| } |
| |
| |
| |
| |
| |
| if(obj == null || (getClass() != obj.getClass())){ |
| return false; |
| } |
| |
| Person otherPerson = (Person)obj; |
| if(otherPerson.getPname().equals(this.getPname()) && otherPerson.getPage()==this.getPage()){ |
| return true; |
| } |
| return false; |
| } |
| |
| public static void main(String[] args) { |
| Person p1 = new Person("Tom",21); |
| Person p2 = new Person("Marry",20); |
| System.out.println(p1==p2); |
| System.out.println(p1.equals(p2)); |
| |
| Person p3 = new Person("Tom",21); |
| System.out.println(p1.equals(p3)); |
| } |
| |
| } |
通过重写 equals 方法,我们自定义两个对象相等的标尺为Person对象的两个属性都相等,则对象相等,否则不相等。
如果不重写 equals 方法,那么始终是调用 Object 类的equals 方法,也就是用 == 比较两个对象在栈内存中的引用地址是否相等。
重写equals方法总结建议:
| @Override |
| public boolean equals(Object otherObject) { |
| |
| if(this == otherObject){ |
| return true; |
| } |
| |
| if(otherObject == null ){ |
| return false; |
| } |
| |
| |
| if(this.getClass() != otherObject.getClass()){ |
| return false; |
| } |
| |
| if(!(otherObject instanceof Person)){ |
| return false; |
| } |
| |
| |
| Person other = (Person) otherObject; |
| |
| |
| |
| return Objects.equals(this.pname,other.pname) && this.page == other.page; |
| |
| |
| |
| |
| } |
注意,无论何时重写此方法,通常都必须重写hashCode方法,以维护hashCode方法的一般约定,该方法声明相等对象必须具有相同的哈希代码
hashCode 方法
hashCode 作用
前面我们说过判断一个元素是否相等可以通过 equals 方法,没增加一个元素,那么我们就通过 equals 方法判断集合中的每一个元素是否重复,但是如果集合中有10000个元素了,但我们新加入一个元素时,那就需要进行10000次equals方法的调用,这显然效率很低。
hashCode 由哈希算法(散列算法)生成,是将数据依特定算法产生的结果直接指定到一个地址上。
当集合要添加新的元素时,先调用这个元素的 hashCode 方法,定位到它应该放置的物理位置上:
- 如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了
- 如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了
- 不相同的话,也就是发生了Hash key相同导致冲突的情况,那么就在这个Hash key的地方产生一个链表,将所有产生相同HashCode的对象放到这个单链表
对象内存布局
当一个对象在堆内存中分配好并且初始化完成之后的结构如下:
- 添加对齐填充是为了保证对象的总大小是8的整数倍个字节
- 类型指针占4个字节是因为默认开启了指针压缩,如果不开启指针压缩,则占8个字节
hashCode 的值存在Java对象头里的,Hotspot虚拟机的对象头主要包括两部分数据:
- Mark Word(标记字段):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
- Class Pointer(类型指针):用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键
对象头结构如下:
源码解析
源码 Object 类中定义如下:
| |
| public native int hashCode(); |
- HashCode的存在主要是为了查找的快捷性
- HashCode是用来在散列存储结构中确定对象的存储地址
查看:src\share\native\java\lang\Object.c
| JNIEXPORT void JNICALL |
| |
| Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls) |
| { |
| |
| (*env)->RegisterNatives(env, cls, |
| methods, sizeof(methods)/sizeof(methods[0])); |
| } |
对应方法:
| 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}, |
| }; |
调用 src\share\vm\prims\jvm.cpp JVM_IHashCod 方法:
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| JVM_ENTRY(jint, JVM_IHashCode(JNIEnv* env, jobject handle)) |
| JVMWrapper("JVM_IHashCode"); |
| |
| return handle == NULL ? 0 : ObjectSynchronizer::FastHashCode (THREAD, JNIHandles::resolve_non_null(handle)) ; |
| JVM_END |
调用 src/hotspot/share/runtime/synchronizer.cpp 内部的 ObjectSynchronizer::FastHashCode:
| intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) { |
| |
| if (UseBiasedLocking) { |
| |
| if (obj->mark()->has_bias_pattern()) { |
| Handle hobj (Self, obj) ; |
| assert (Universe::verify_in_progress() || |
| !SafepointSynchronize::is_at_safepoint(), |
| "biases should not be seen by VM thread here"); |
| |
| BiasedLocking::revoke_and_rebias(hobj, false, JavaThread::current()); |
| obj = hobj() ; |
| |
| assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now"); |
| } |
| } |
| |
| |
| |
| ObjectMonitor* monitor = NULL; |
| markOop temp, test; |
| intptr_t hash; |
| |
| |
| markOop mark = ReadStableMark (obj); |
| |
| |
| if (mark->is_neutral()) { |
| |
| hash = mark->hash(); |
| |
| if (hash) { |
| return hash; |
| } |
| |
| hash = get_next_hash(Self, obj); |
| |
| temp = mark->copy_set_hash(hash); |
| |
| test = (markOop) Atomic::cmpxchg_ptr(temp, obj->mark_addr(), mark); |
| if (test == mark) { |
| return hash; |
| } |
| |
| |
| |
| |
| |
| |
| |
| } else if (mark->has_monitor()) { |
| |
| monitor = mark->monitor(); |
| temp = monitor->header(); |
| assert (temp->is_neutral(), "invariant") ; |
| |
| hash = temp->hash(); |
| if (hash) { |
| return hash; |
| } |
| |
| } else if (Self->is_lock_owned((address)mark->locker())) { |
| |
| temp = mark->displaced_mark_helper(); |
| assert (temp->is_neutral(), "invariant") ; |
| hash = temp->hash(); |
| |
| if (hash) { |
| return hash; |
| } |
| } |
总结:
通过分析虚拟机源码我们证明了hashCode不是直接用的内存地址,而是采取一定的算法来生成,hashcode值的存储在mark word里,与锁共用一段bit位,这就造成了跟锁状态相关性:
-
如果是无锁态:调用 hashcode 方法侯,hashcode 存储在该对象的对象头中
-
如果是偏向锁:一旦调用hashcode,偏向锁将被撤销,hashcode被保存占位mark word,对象被打回无锁状态
-
如果无法撤销偏向锁:对象再也回不到偏向锁状态而是升级为重量级锁hash code跟随mark word被移动到c的object monitor,从那里取
getClass 方法
getClass()在 Object 类中如下,作用是返回对象的运行时类。
| public final native Class<?> getClass(); |
这是一个用 native 关键字修饰的方法
native 用来修饰方法,用 native 声明的方法表示告知 JVM 调用,该方法在外部定义,我们可以用任何语言去实现它。
简单地讲,一个native Method就是一个 Java 调用非 Java 代码的接口。
这里我们要知道用 native 修饰的方法我们不用考虑,由操作系统帮我们实现,该方法的作用是返回一个对象的运行时类,通过这个类对象我们可以获取该运行时类的相关属性和方法
toString 方法
| public String toString() { |
| return getClass().getName() + "@" + Integer.toHexString(hashCode()); |
| } |
- getClass().getName()是返回对象的全类名(包含包名),Integer.toHexString(hashCode()) 是以16进制无符号整数形式返回此哈希码的字符串表示形式。
- 打印某个对象时,默认是调用 toString 方法,比如 System.out.println(person),等价于 System.out.println(person.toString())
clone方法
| |
| |
| |
| protected native Object clone() throws CloneNotSupportedException; |
保护方法,实现对象的浅拷贝,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。
finalize 方法
| protected void finalize() throws Throwable { } |
当 GC 确定不再有对该对象的引用时,GC 会调用对象的 finalize()
方法来清除回收。
Java VM 会确保一个对象的 finalize()
方法只被调用一次,而且程序中不能直接调用 finalize()
方法。
finalize()
方法通常也不可预测,而且很危险,一般情况下,不必要覆盖 finalize()
方法。
registerNatives 方法
| private static native void registerNatives(); |
这是一个本地方法,我们要知道一个类定义了本地方法后,想要调用操作系统的实现,必须还要装载本地库
| static { |
| registerNatives(); |
| } |
静态代码块就是一个类在初始化过程中必定会执行的内容,所以在类加载的时候是会执行该方法的,通过该方法来注册本地方法。