Java中的Object

深入了解Java中的Object类 在Java开发中,Object类是一个非常重要的类。它是所有类的超类,也就是说,所有的对象都是Object类的实例。因此,了解Object类的方法和属性对于Java开发者来说是非常必要的。本文将详细介绍Object类的相关知识,包括其定义、常用方法和应用场景等,希望能够帮助读者更好地理解和使用Object类。Object类是Java语言中最基本的类之一,它是所有类的祖先。在Java中,所有的类都是直接或者间接继承自Object类。因此,Object类的属性和方法对于Java开发者来说非常重要。 阅读详情

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
containerd镜像加速配置全解析:从host.toml到config.toml的演进与实践 本文全面解析了containerd镜像加速的配置方法,重点对比了传统的config.toml集中式配置与现代的host.toml模块化配置的演进与优劣。文章通过实战演示,详细指导如何为docker.io、registry.k8s.io等常见仓库配置host.toml加速,并提供了生产环境下的最佳实践、故障排查与性能优化建议,帮助用户显著提升镜像拉取效率与系统稳定性。 阅读详情

相关推荐

单总线协议(1—Wire bus)【DS2431】

1—Wire 协议定义了复位脉冲、 应答脉冲、写0、写1、读0和读1时序等几种信号类型。所有的单总线命令序列(初始化ROM 命令功能命令)都是由这些基本的信号类型组成的。识别单总线器件(ROM 命令)单总线器件交换数据(功能命令)初始化单总线器件(初始化)

qq_41883039的博客 1146

ObjectMonitor,ObjectWaiter 实现wait(),notify()

0、java对象锁监视器 在JVM的规范中,有这么一些话:“在JVM中,每个对象和类在逻辑上都是和一个监视器相关联的”“为了实现监视器的排他性监视能力,JVM为每一个对象和类都关联一个锁”“锁住了一个对象,就是获得对象相关联的监视器”   监视器好比一做建筑,它有一个很特别的房间,房间里有一些数据,而且在同一时间只能被一个线程占据, 进入这个建筑叫做"进入监视器" 进入建筑中的那个特别的...

weixin_33973609的博客 312

前馈神经网络求解XOR问题

摘要 前馈神经网络是一种最简单的神经网络,各神经元分层排列。每个神经元只与前一层的神经元相连。接收前一层的输出,并输出给下一层.各层之间没有反馈。是目前应用最广泛、发展最迅速的人工神经网络之一。研究从20世纪60年代开始,目前理论研究和实际应用达到了很高的水平[1]。本节通过设计包含输入层、一个隐含层和输出层的前馈神经网络,实现XOR运算,并基于计算数据或实验数据分析隐含层决策面的特点,进而总结前馈神经网络隐含层的作用。 1.1 神经网络结构设计 1.1.1 XOR问题描述 ...

sinat_28178805的博客 2358

Java并发编程之Object.wait()/notify()详解

等待/通知机制一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另外一个线程。前者是生产者,后者是消费者,这种模式隔离了“做什么”(What)和“怎么做”(How),在功能层面上实现了解耦,体系结构上具备良好的伸缩性,在Java语言中是如何实现类似的等待/通知机制的呢?最简单的方式就是让消费者不断地循环检查变量是否符合预期,如下面代码所...

Stay Hungry, Stay Foolish 2万+

实验室LIMS系统选型指南:从赛默飞到金现代,5大主流方案横评

本文深入分析了实验室LIMS系统选型的5大核心维度,包括架构选择、行业适配性、合规性设计、智能化程度和成本模型。通过对比赛默飞、金现代等主流厂商的优劣势,提供实战避坑策略,帮助实验室管理者做出明智决策,提升运营效率和合规水平。

plum99的博客 257

【GLM5】手把手教你在openclaw中白嫖英伟达羊毛

openclaw很强,但是也很贵,每一次请求都会消耗大量的token。而英伟达平台正好可以白嫖免费的模型,于是,我们一拍即合。

Science prince的博客 3820

JVM源码分析之Object.wait/notify实现

原文:https://www.jianshu.com/p/f4454164c017 简书 占小狼 转载请注明原创出处,谢谢! 最简单的东西,往往包含了最复杂的实现,因为需要为上层的存在提供一个稳定的基础,Object作为java中所有对象的基类,其存在的价值不言而喻,其中wait和notify方法的实现多线程协作提供了保证。 public class Wai

notOnlyRush的博客 910

基于转录逆转的药物重定向 GPS 模型

GPS是一个基于分子结构预测基因表达扰动的开源平台,由三个模块组成:RCL(在LINCS扰动数据上训练抗噪MLP)、GPS4Drugs(预测新化合物的基因表达谱并进行逆转评分)、MolSearch(基于MCTS的分子优化)。平台使用Morgan指纹和Gene Ontology注释作为特征输入,将基因表达扰动离散为三分类标签。训练数据来自LINCS Phase I的18,746个化合物和978个landmark基因,支持四细胞系独立训练,可用于药物重定向与化合物筛选。

听雨草堂 83

java JUC 中 Object里wait()、notify() 实现原理及实战讲解

在多核环境下,多个线程有可能同时执行monitorenter指令,并获取lock对象关联的monitor,但只有一个线程可以和monitor建立关联,这个线程执行到wait方法时,wait方法会将当前线程放入wait set,使其进行等待直到被唤醒,并放弃lock对象上的所有同步声明,意味着该线程释放了锁,其他线程可以重新执行加锁操作,notify方法会选择wait set中任意一个线程进行唤醒,notifyAll方法会唤醒monitor的wait set中所有线程。4、最终底层的park方法会挂起线程。

LoveSummer 5万+

某招标公告网和vcaptcha验证码识别逆向

本文分析了某验证码系统的加密流程及破解思路。首先通过反混淆技术定位到加密函数,拆解了包含指纹函数、环境参数等组成的加密字符串。研究发现验证码生成涉及canvas指纹、环境检测等反爬机制,可通过模拟浏览器环境绕过。验证过程需要解密图片顺序,通过work2.js计算还原图片位置,再结合滑动轨迹、时间等参数生成验证请求。文章仅提供技术研究思路,强调需遵守法律法规,严禁非法破解信息系统。完整流程包括环境模拟、加密参数还原、图片处理和轨迹生成等步骤,成功率较高。

码王吴彦祖的博客 577

GEE:面对对象土地利用分类(结合SNIC、GLCM 和机器学习算法)

本文将介绍在Google Earth Engine中结合SNIC、GLCM和机器学习算法的的土地利用/土地覆盖(LULC)分类方法。结果如下图所示,

养乐多的博客 5614

ThreadLocal源码详解

threadlocal源码图文详解

程冯冯的博客 755

Unity之ASE+ShaderGraph+VFX入门到精通 目录

这里是ASE+ShaderGraph+VFX从入门到精通专栏的目录,不停更新中,有问题随时留言或者私信。

TxNet 2840

Java 中的 Object

摘要: Java中java.lang.Object是所有类的根类,提供11个核心方法支撑面向对象特性。关键方法包括:equals()(默认地址比较,需与hashCode()同步重写)、hashCode()(哈希表基础)、toString()(对象字符串表示)、clone()(需实现Cloneable接口的浅拷贝)及线程协作的wait()/notify()。Object的特殊性体现在多态支持、泛化处理能力(如早期集合类使用Object[]),并规定了对象生命周期行为(如finalize())。面试重点涉及==

Giants2024的博客 1270

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

Java中的Object类

Object作为所有类的父类,是面向对象学习中最重要的一个类。Object类是java默认的提供的一个类,Object类是所有类的父类,也就是说任何一个类的定义的时候如果没有明确的继承一个父类的话,那么它就是Object的子类

iqqcode 9万+

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

JAVA中的Object类

在 Java 中,Object类是所有类的父类,也就是说 Java 的所有类都继承了Object,Object。Object 类是在java.lang包中,编译时会自动导入,我们创建一个类时,如果没有明确继承一个父类,那么它就会自动继承Object,成为Object的子类。Object类可以,也可以:显式继承。

vincent_vang的博客 2640

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

浅谈Java中的Object类

在Java编程中,Object类是一个非常重要的类,它是所有类的超类,意味着每个Java类都直接或间接地继承自Object类。b. 谨慎使用clone()方法:clone()方法可以用于创建对象的副本,但需要注意对对象中引用类型的处理。默认的clone()方法执行的是浅拷贝,即对象中的引用类型字段仍然指向原始对象,可能引发意外的副作用。在默认情况下,equals()方法使用的是对象的引用比较。摘要:Java中的Object类是所有类的根类,它提供了一些核心方法和基本行为,为其他类提供了通用的功能。

ynwg1225的博客 674

Java中的Object对象

Object类是java.lang包下的核心类,是所有类的父类 所有的Java对象都隐式地继承了Object对象(不用写extends继承) 所有的对象都拥有Object默认的方法

雨云21的博客 4043

Java中的object类

在 Java 中,类是所有类的根类(超类)。也就是说,Java 中所有的类都直接或间接继承自Object类。即使在定义类时没有显式声明父类,编译器也会默认将Object类作为其父类。Object类定义了所有 Java 对象都具有的基本行为和属性,提供了 11 个核心方法。

hyh1795844373的博客 871

java 中的 object

weixin_42056607的博客 118

Java中的Object2

奇妙能力倩 375

java的object_Java中的Object是什么?

展开全部1).Object类是所有类的父类,位于java.lang包中。2).数组也是Object类的子类。3).Object类的常用方法有:--- toString();--- equals();--- hashCode();Object类在32313133353236313431303231363533e78988e69d8331333365653866java.lang包下,是所有类的根。任...

weixin_39844515的博客 227

Java中的Object类中的常用方法

API和Object API(Application Programming Interface)应用程序编程接口. Java API 就是Java提供的类这些类将底层的实现封装起来 Object类 是所有类的根类,所有类都直接或者间接的继承自该类. 构造方法 public Object() Object类中的hashCode()方法。    pubic int hashCode()     ...

xupt_rl的博客 1312

java中的Object类

java中的Object类 Object类:Object类是所有类的父类,如果一个类没有使用extends关键字明确标识继承另一个类,那么这个类默认继承Object类。Object类中的方法,适合所有子类。 1、toString()方法:在Object类里面定义toString()方法的时候返回的对象的哈希code码(对象地址字符串)。 可以通过重写toString()方法表示出对象的属性。...

zytmaster的博客 237
上一篇: JNI
下一篇: 分享网页到FaceBook的问题
joye123
博客等级 码龄15年 36粉丝 65原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值