Java中的Object
Object类的几个方法
方法 | 说明 |
---|---|
native void registerNative() | 注册本地方法,负责将其他几个本地方法注册到JVM中 |
native Class<?> getClass() | 获取该对象所属的类对象 |
native int hashCode() | 获取当前对象的哈希码,可用于哈希表中的标识 |
boolean equals(Object) | 和另外一个对象比较是否相等 |
native Object clone() | 创建并返回当前对象的复制品 |
Strig toString() | 返回一个字符串来标识当前对象 |
native void notify() | 唤醒等待当前对象监视器的线程 |
native void notifyAll() | 唤醒等待当前对象监视器的所有线程 |
native void wait(long) | 使当前线程进入等待状态,直到有其他线程调用notify/notify方法。 参数为等待超时时间,如果为0则无限等待 |
void finalize() | 当前对象没有引用时,由垃圾回收器调用该方法。 但是,Java语言中不保证该方法一定会被调用。 |
Class<?> getClass()
获取当前对象的类信息。
//share/native/java/lang/Object.c
//Java层的对象在Native层是用jobject表示的
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);
}
}
JNI 定义了两个关键数据结构,即“JavaVM”和“JNIEnv”。两者本质上都是指向函数表的二级指针。
这里调用了JNIEnv的GetObjectClass函数。
JNI_ENTRY(jclass, jni_GetObjectClass(JNIEnv *env, jobject obj))
JNIWrapper("GetObjectClass");
#ifndef USDT2
DTRACE_PROBE2(hotspot_jni, GetObjectClass__entry, env, obj);
#else /* USDT2 */
HOTSPOT_JNI_GETOBJECTCLASS_ENTRY(
env, obj);
#endif /* USDT2 */
Klass* k = JNIHandles::resolve_non_null(obj)->klass();
jclass ret =
(jclass) JNIHandles::make_local(env, k->java_mirror());
#ifndef USDT2
DTRACE_PROBE1(hotspot_jni, GetObjectClass__return, ret);
#else /* USDT2 */
HOTSPOT_JNI_GETOBJECTCLASS_RETURN(
ret);
#endif /* USDT2 */
return ret;
JNI_END
上面代码中的关键就是Klass* k = JNIHandles::resolve_non_null(obj)->klass();
JNIHandles::resolve_non_null(jobject handle)是将Java层的对象引用转换为JVM中用来表示一个对象的结构体oop指针。
inline oop JNIHandles::resolve_non_null(jobject handle) {
oop result = *(oop*)handle;
return result;
};
oop叫做普通对象指针,它内部包含一个oopDesc类型的指针,oopDesc描述了一个普通对象所包含的内容。oop类定义如下:
//hotspot/src/share/vm/oops/oopsHierarchy.hpp
class oop {
oopDesc* _o;
oopDesc* obj() const volatile { return _o; }
}
因为oopDesc指针位于oop类的起始位置,所以oop指针可以直接转换成oopDesc指针,并调用oopDesc类的函数。通过oopDesc类,就可以清除一个Java对象在虚拟机中是如何被表示的。它包含三部分:markword(标识字段)、类元信息、实例数据。其定义如下:
class oopDesc {
private:
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
static BarrierSet* _bs;
...
}
所以Klass* k = JNIHandles::resolve_non_null(obj)->klass();就是获取到了该Java对象中存储的类元信息Klass指针_klass。然后调用Klass的java_mirror()函数,返回其镜像。
// java/lang/Class instance mirroring this class
oop _java_mirror;
oop java_mirror() const { return _java_mirror; }
int hashCode()
获取一个对象的哈希码,根据注释可以知道该方法有助于提高哈希表的性能,如HashMap,可以根据key快速定位到value。这里需要注意的是,同一个对象多次获取哈希码肯定是相同的,不过并不保证不同对象的哈希码是不同的。
除了getClass()方法外,其他几个方法都是都是通过registerNatives函数,以静态链接的方式来实现函数调用的。它们的实现均在jvm.cpp文件中
// hotspot/src/share/vm/prims/jvm.cpp
JVM_ENTRY(jint, JVM_IHashCode(JNIEnv* env, jobject handle))
JVMWrapper("JVM_IHashCode");
// as implemented in the classic virtual machine; return 0 if object is NULL
return handle == NULL ? 0 : ObjectSynchronizer::FastHashCode (THREAD, JNIHandles::resolve_non_null(handle)) ;
JVM_END
ObjectSysnchronizer::FastHashCode(Thread, oop)函数是真正实现哈希码计算的地方。从名称上看,哈希码的计算还和对象的同步状态有关。
intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) {
//禁用偏向锁
if (UseBiasedLocking) {
if (obj->mark()->has_bias_pattern()) {
Handle hobj (Self, obj) ;
BiasedLocking::revoke_and_rebias(hobj, false, JavaThread::current());
obj = hobj() ;
}
}
ObjectMonitor* monitor = NULL;
markOop temp, test;
intptr_t hash;
markOop mark = ReadStableMark (obj);
if (mark->is_neutral()) {
//处于未锁定状态
//1. 如果标识字中存在哈希码,则直接返回
//2. 否则新生成一个,并通过CAS操作存储到头部标识字中
hash = mark->hash(); // this is a normal header
if (hash) { // if it has hash, just return it
return hash;
}
hash = get_next_hash(Self, obj); // allocate a new hash code
temp = mark->copy_set_hash(hash); // merge the hash code into header
// use (machine word version) atomic operation to install the hash
test = (markOop) Atomic::cmpxchg_ptr(temp, obj->mark_addr(), mark);
if (test == mark) {
return hash;
}
} else if (mark->has_monitor()) {
//处于重量级锁状态
//1. 从标识字取出对象监视器ObjectMonitor的地址
//2. 从ObjectMonitor中取出头部
//3. 从头部中取出哈希码
monitor = mark->monitor();
temp = monitor->header();
hash = temp->hash();
if (hash) {
return hash;
}
} else if (Self->is_lock_owned((address)mark->locker())) {
//处于轻量级锁状态,标识字中存储的是持有锁的线程中的一个栈地址
//1. 取出栈帧中取出标识字
//2. 从标识字中取出哈希码
temp = mark->displaced_mark_helper();
hash = temp->hash();
if (hash) {
return hash;
}
}
// 如果前面获取哈希码失败了,说明发生了竞争,需要升级为重量级锁
monitor = ObjectSynchronizer::inflate(Self, obj);
// Load displaced header and check it has hash code
mark = monitor->header();
hash = mark->hash();
//升级为重量级锁后再次检查是否存在哈希码,如果不存在则重新生成,并设置给监视器锁
if (hash == 0) {
hash = get_next_hash(Self, obj);
temp = mark->copy_set_hash(hash); // merge hash code into header
assert (temp->is_neutral(), "invariant") ;
test = (markOop) Atomic::cmpxchg_ptr(temp, monitor, mark);
if (test != mark) {
hash = test->hash();
}
}
// 最终得到的对象哈希码
return hash;
}
上面是针对不同的锁定状态来执行对应的获取哈希码的方式,简单来说就是把哈希码存储到了对象头的标识字中,保证每次获取的哈希码是保持不变的。
那么哈希码到底是怎么生成的呢?是不是与对象的内存地址有关?
static inline intptr_t get_next_hash(Thread * Self, oop obj) {
intptr_t value = 0 ;
if (hashCode == 0) {
value = os::random() ;
} else
if (hashCode == 1) {
intptr_t addrBits = cast_from_oop<intptr_t>(obj) >> 3 ;
value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
} else
if (hashCode == 2) {
value = 1 ;
} else
if (hashCode == 3) {
value = ++GVars.hcSequence ;
} else
if (hashCode == 4) {
value = cast_from_oop<intptr_t>(obj) ;
} else {
//Thread类构造函数中初始化以下各值
//_hashStateX = os::random() ;
//_hashStateY = 842502087 ;
//_hashStateZ = 0x8767 ; // (int)(3579807591LL & 0xffff) ;
//_hashStateW = 273326509 ;
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 ;
TEVENT (hashCode: GENERATE) ;
return value;
}
从上面代码可以看出,虚拟机使用何种哈希码的生成方式会依据hashCode的取值,一共提供了6种方式。hashCode的默认取值是5(JDK8):
product(intx, hashCode, 5, \
"(Unstable) select hashCode generation algorithm")
在JDK8之前hashCode的取值为0。
在测试时也可以通过-XX:hashCode=2指定。
下面分别描述这6种方式的原理:
- os::random()采用系统随机数(与对象地址无关)
- 通过对象内存地址参与计算生成(对象地址是计算的一部分)
- 固定为1,用于测试 (与对象地址无关)
- 从0开始的自增序列(每次加1)(与对象地址无关)
- 直接将对象内存地址转为整型 (就是对象内存地址)
- 采用Marsaglia’s xor-shift算法+随机数计算而成 (与对象地址无关)
综上所述,对象的哈希码和对象内存地址无关,并且在调用hashCode()方法时才会去计算,计算完成后会缓存在对象头或相关联的对象监视器中。
在Android系统中,Object类的hashCode()方法被重写了。
public int hashCode() {
return identityHashCode(this);
}
static int identityHashCode(Object obj) {
int lockWord = obj.shadow$_monitor_;
final int lockWordStateMask = 0xC0000000; // Top 2 bits.
final int lockWordStateHash = 0x80000000; // Top 2 bits are value 2 (kStateHash).
final int lockWordHashMask = 0x0FFFFFFF; // Low 28 bits.
if ((lockWord & lockWordStateMask) == lockWordStateHash) {
return lockWord & lockWordHashMask;
}
return identityHashCodeNative(obj);
}
@FastNative
private static native int identityHashCodeNative(Object obj);
最终调用还是本地方法identityHashCodeNative,它的实现与前面说的Java标准JDK的实现是一样的。所以唯一的不同就是在调用identityHashCodeNative方法之前,先判断对象头是否存储的是对象的哈希码,如果是就直接取出来了,否则再调用本地函数计算。
boolean equals(Object)
判断两个对象是否相等。
它的默认实现如下,就是比较两个引用是否指向同一个对象。
public boolean equals(Object obj) {
return (this == obj);
}
在某些场景下,我们需要表示出:一个复杂对象,尽管有两个对象引用是不同的,但是它们指向的对象具有相同的属性,我们也应当认为这两个对象引用是相等的。
例如一个学生类,包含了学号、姓名、年龄三个属性,如果这三个属性值是完全一样的,那么我们就认为两个不同的学生对象的引用也是相等的。
具体使用场景就是在哈希表中,在读写时,需要不停的判断键和值是否相等。因为键和值都是引用类型,所以很多情况下,读和写使用的是不同的对象引用,所以此时要重写equals(Object)方法。
下面是String类的equals(Object)方法实现:
//除了比较是否是同一引用之外,还会判断字符串内容是否相等
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;
}
如果重写了equals方法,一定要重写hashCode方法
因为在哈希表中会用到hashCode方法计算存储或查找数据所在的位置,会导致相同属性的不同引用对象产生不同的哈希码(默认hashCode实现方式见第二节),从而存入重复数据或读数据失败。
String类的hashCode()方法实现如下:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
native Object clone()
首先要明确clone()默认实现是浅拷贝。源码实现如下:
JVM_ENTRY(jobject, JVM_Clone(JNIEnv* env, jobject handle))
JVMWrapper("JVM_Clone");
//被克隆的对象
Handle obj(THREAD, JNIHandles::resolve_non_null(handle));
const KlassHandle klass (THREAD, obj->klass());
JvmtiVMObjectAllocEventCollector oam;
//检查是否实现了Cloneable接口
if (!klass->is_cloneable()) {
ResourceMark rm(THREAD);
THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
}
//分配相等的内存空间
const int size = obj->size();
oop new_obj_oop = NULL;
if (obj->is_array()) {
const int length = ((arrayOop)obj())->length();
new_obj_oop = CollectedHeap::array_allocate(klass, size, length, CHECK_NULL);
} else {
new_obj_oop = CollectedHeap::obj_allocate(klass, size, CHECK_NULL);
}
//原样复制,并对齐
Copy::conjoint_jlongs_atomic((jlong*)obj(), (jlong*)new_obj_oop,
(size_t)align_object_size(size) /
HeapWordsPerLong);
//清除对象头的标识字
new_obj_oop->init_mark();
BarrierSet* bs = Universe::heap()->barrier_set();
bs->write_region(MemRegion((HeapWord*)new_obj_oop, size));
Handle new_obj(THREAD, new_obj_oop);
if (java_lang_invoke_MemberName::is_instance(new_obj()) &&
java_lang_invoke_MemberName::is_method(new_obj())) {
Method* method=(Method*)java_lang_invoke_MemberName::vmtarget(new_obj());
if (method != NULL) {
methodHandle m(THREAD, method);
m->method_holder()->add_member_name(new_obj());
}
}
//如果该类实现了finalizer()方法,需要将对象添加到一个队列中
if (klass->has_finalizer()) {
new_obj_oop = InstanceKlass::register_finalizer(instanceOop(new_obj()), CHECK_NULL);
new_obj = Handle(THREAD, new_obj_oop);
}
return JNIHandles::make_local(env, new_obj());
JVM_END
Strig toString()
一个对象的字符串表示。推荐所有子类都重写该方法。Object类的toString()方法实现如下:
//类名+哈希码
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
native void notify()/notifyAll()/wait(long)
这三个本地方法都是用于多线程调度呢,所以放在一起讨论。
wait(long)用于引起当前线程等待,知道超时或由其他线程唤醒。
notify()/notifyAll() 用于唤醒一个或多个处于等待的线程。
这三个方法一般配合Synchronized关键字使用,因为必须在持有对象监视器锁的情况下才允许调用。否则的话会抛出IllegalMonitorStateException异常。下面看它们的源码实现:
JVM_ENTRY(void, JVM_MonitorWait(JNIEnv* env, jobject handle, jlong ms))
JVMWrapper("JVM_MonitorWait");
Handle obj(THREAD, JNIHandles::resolve_non_null(handle));
JavaThreadInObjectWaitState jtiows(thread, ms != 0);
if (JvmtiExport::should_post_monitor_wait()) {
JvmtiExport::post_monitor_wait((JavaThread *)THREAD, (oop)obj(), ms);
}
ObjectSynchronizer::wait(obj, ms, CHECK);
JVM_END
JVM_ENTRY(void, JVM_MonitorNotify(JNIEnv* env, jobject handle))
JVMWrapper("JVM_MonitorNotify");
Handle obj(THREAD, JNIHandles::resolve_non_null(handle));
ObjectSynchronizer::notify(obj, CHECK);
JVM_END
JVM_ENTRY(void, JVM_MonitorNotifyAll(JNIEnv* env, jobject handle))
JVMWrapper("JVM_MonitorNotifyAll");
Handle obj(THREAD, JNIHandles::resolve_non_null(handle));
ObjectSynchronizer::notifyall(obj, CHECK);
JVM_END
我们可以看出,三个方法都是由ObjectSynchronizer类实现的,ObjectSynchronizer类我们不陌生了,前面在计算hashCode时也是通过ObjectSynchronizer实现的,其内部又会创建一个ObjectMonitor来实现线程调度。
由于源码比较长,这里就不贴源码了,我们先了解一下相关的数据结构,并描述一下wait和notify的过程。首先是ObjectWaiter
ObjectWaiter::ObjectWaiter(Thread* thread) {
_next = NULL;
_prev = NULL;
_notified = 0;
TState = TS_RUN ;
_thread = thread;
_event = thread->_ParkEvent ;
_active = false;
assert (_event != NULL, "invariant") ;
}
ObjectWaiter代表了一个等待对象锁的线程,它的属性_event是线程Thread中EventPark类型的_ParkEvent指针,用于休眠当前线程或后续唤醒当前线程。如下所示:
_event.park();//休眠线程
_event.unpark();//唤醒线程
另外通过_next和_prev属性可以看出它是一个双向链表,实际上还是一个循环链表。
另外一个数据结构是ObjectMonitor
ObjectMonitor() {
_header = NULL;//对象头中的标识字
_count = 0;//等待对象监视器的线程数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;//持有对象监视器的线程
_WaitSet = NULL;//处于等待被唤醒状态的线程
_WaitSetLock = 0 ;//操作_WaitSet队列的锁
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;//最近刚刚被阻塞的线程
FreeNext = NULL ;
_EntryList = NULL ;//被阻塞在同步代码块外的线程
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
我们可以看到一共有三个线程队列,它们之间是什么关系?什么时候使用哪个队列呢?下面我们以一个具体的场景描述来说明。
- 线程1进入到Synchronized代码块,获取到锁,然后调用wait()方法。此时将线程1封装成ObjectWaiter1添加到_WaitSet队列中。然后执行释放锁的操作。具体实现为:1. 唤醒_EntryList列表的头结点。2.如果_EntryList为空,则将_cxq队列中的结点复制到_EntryList列表中。3.唤醒_EntryList列表的头结点。
- 线程2在线程1没有调用wait()方法并且释放锁之前,线程2被封装成ObjectWaiter2添加到_cxq队列中。
- 当线程1释放锁后,线程2获得锁,进入到Synchronized代码块,调用notify()方法。此时会判断_EntryList是否为空,如果为空则将_WaitSet的头结点(也就是ObjectWaiter1)赋值给_EntryList;否则将ObjectWaiter1添加到_cxq队列中。
- notify()方法只是处理了队列结点的转移,并不会立即唤醒其他线程。而是要等到线程2执行完Synchronized代码块中的逻辑,释放锁之后。
void finalize()
当一个对象没有任何引用指向它时,该方法会被垃圾回收器调用,子类可以实现该方法来销毁占用的资源(例如网络连接、数据连接等)。
需要注意的是:
- Java语言并没有保证一个对象的finalize()方法一定会被调用,但是可以保证的是,当finalize()方法被调用时,当前线程不会持有同步锁。
- 当finalize()方法抛出异常时,该异常会被忽略,并且finalize的过程会终止。
- finalize()方法只会被调用一次
- 当finalize()被调用后,JVM会再次确认没有被其他活动线程来引用当前对象,在此之前不会对当前对象做任何操作。确认之后,当前对象就会被丢弃。
- 当一个对象覆盖了finalize()方法并且通过GC Root不可达时,会先将该对象放到一个队列中,由一个低优先级的线程逐一执行对象的finalize()方法。否则就会直接回收该对象。如果在finalize()方法又产生了对当前对象的强引用,那么该对象就复活了。
下面是在finalize()方法中复活自己的示例:
class FinalizeTest {
public static TestObject instance;
public static class TestObject {
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("run finalize()");
//重新赋值instance
instance = this;
}
@Override
public String toString() {
return "TestObject{}";
}
}
public static void main(String[] args) {
//创建新对象
instance = new TestObject();
//引用置空,并触发垃圾回收,此时会调用TestObject的finalize()方法
instance = null;
System.gc();
//这里要等待其他线程调用finalize()方法
try {
Thread.sleep(3 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//finalize()被调用后,instance会被重新赋值
checkTestObjectIsAlive();
//第二次引用置空,并触发垃圾回收,此时不会调用TestObject的finalize()方法
instance = null;
System.gc();
try {
Thread.sleep(3 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//此时TestObject已经死了
checkTestObjectIsAlive();
}
private static void checkTestObjectIsAlive() {
if(instance != null) {
System.out.println("TestObject is alive");
} else {
System.out.println("TestObject is dead");
}
}
}
控制台输出为:
run finalize()
TestObject is alive
TestObject is dead
一个对象在JVM中的表示以及一个对象占用多大内存
对象的数据结构oopDesc为:
对象头 + 实例数据 + 对齐填充
其中对象头又可以分为两部分:mark word和类元数据指针
所以一个对象占用的内存等于以上三部分相加。
MarkWord用一个uintptr_t类型表示的,uintptr_t在32位机器上用无符号整型表示(4字节),在64位机器上用无符号长整型表示(8字节)。
类元数据指针是一个指针类型,在32位机器上占用4字节,在64位机器上占用8字节。但是如果JVM开启的指针压缩,则占用4字节。
所以一个空对象在64位机器上,开启指针压缩后的占用内存大小为 8 + 4 + 4 = 16字节。最后的4字节是对齐填充,使得最终占用内存大小必须为8的整数倍。
使用“org.openjdk.jol:jol-core:0.10”工具包可以打印出一个实例对象的内存占用信息。
public class ObjectLayoutTest {
public static void main(String... args) {
Test t = new Test();
String objectLayout = ClassLayout.parseInstance(t).toPrintable();
System.out.println(objectLayout);
}
static class Test {
private boolean b = false;
}
}
上面代码输出如下:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 41 c1 00 f8 (01000001 11000001 00000000 11111000) (-134168255)
12 1 boolean Test.b false
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total