Java中的Object

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种方式的原理:

  1. os::random()采用系统随机数(与对象地址无关)
  2. 通过对象内存地址参与计算生成(对象地址是计算的一部分)
  3. 固定为1,用于测试 (与对象地址无关)
  4. 从0开始的自增序列(每次加1)(与对象地址无关)
  5. 直接将对象内存地址转为整型 (就是对象内存地址)
  6. 采用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. 线程1进入到Synchronized代码块,获取到锁,然后调用wait()方法。此时将线程1封装成ObjectWaiter1添加到_WaitSet队列中。然后执行释放锁的操作。具体实现为:1. 唤醒_EntryList列表的头结点。2.如果_EntryList为空,则将_cxq队列中的结点复制到_EntryList列表中。3.唤醒_EntryList列表的头结点。
  2. 线程2在线程1没有调用wait()方法并且释放锁之前,线程2被封装成ObjectWaiter2添加到_cxq队列中。
  3. 当线程1释放锁后,线程2获得锁,进入到Synchronized代码块,调用notify()方法。此时会判断_EntryList是否为空,如果为空则将_WaitSet的头结点(也就是ObjectWaiter1)赋值给_EntryList;否则将ObjectWaiter1添加到_cxq队列中。
  4. notify()方法只是处理了队列结点的转移,并不会立即唤醒其他线程。而是要等到线程2执行完Synchronized代码块中的逻辑,释放锁之后。

void finalize()

当一个对象没有任何引用指向它时,该方法会被垃圾回收器调用,子类可以实现该方法来销毁占用的资源(例如网络连接、数据连接等)。
需要注意的是:

  1. Java语言并没有保证一个对象的finalize()方法一定会被调用,但是可以保证的是,当finalize()方法被调用时,当前线程不会持有同步锁。
  2. 当finalize()方法抛出异常时,该异常会被忽略,并且finalize的过程会终止。
  3. finalize()方法只会被调用一次
  4. 当finalize()被调用后,JVM会再次确认没有被其他活动线程来引用当前对象,在此之前不会对当前对象做任何操作。确认之后,当前对象就会被丢弃。
  5. 当一个对象覆盖了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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值