面试中经常被问到Java引用类型原理,带你深入剖析

开发 后端
本篇文章主要是分析软引用、弱引用、虚引用的实现,这三种引用类型都是继承于Reference这个类,主要逻辑也在Reference中。

1.选择唯一性索引

唯一性索引的值是唯一的,可以更快速的通过该索引来确定某条记录。例如,学生表中学号是具有唯一性的字段。为该字段建立唯一性索引可以很快的确定某个学生的信息。如果使用姓名的话,可能存在同名现象,从而降低查询速度。

2.为经常需要排序、分组和联合操作的字段建立索引

经常需要ORDER BY、GROUP BY、DISTINCT和UNION等操作的字段,排序操作会浪费很多时间。如果为其建立索引,可以有效地避免排序操作。

3.为常作为查询条件的字段建立索引

如果某个字段经常用来做查询条件,那么该字段的查询速度会影响整个表的查询速度。因此,为这样的字段建立索引,可以提高整个表的查询速度。

4.限制索引的数目

索引的数目不是越多越好。每个索引都需要占用磁盘空间,索引越多,需要的磁盘空间就越大。修改表时,对索引的重构和更新很麻烦。越多的索引,会使更新表变得很浪费时间。

5.尽量使用数据量少的索引

如果索引的值很长,那么查询的速度会受到影响。例如,对一个CHAR(100)类型的字段进行全文检索需要的时间肯定要比对CHAR(10)类型的字段需要的时间要多。

6.尽量使用前缀来索引

如果索引字段的值很长,最好使用值的前缀来索引。例如,TEXT和BLOG类型的字段,进行全文检索会很浪费时间。如果只检索字段的前面的若干个字符,这样可以提高检索速度。

7.删除不再使用或者很少使用的索引

表中的数据被大量更新,或者数据的使用方式被改变后,原有的一些索引可能不再需要。数据库管理员应当定期找出这些索引,将它们删除,从而减少索引对更新操作的影响。

8.最左前缀匹配原则,非常重要的原则。

mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a 1=”” and=”” b=”2” c=”“> 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。

9.=和in可以乱序。

比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式

10.尽量选择区分度高的列作为索引。

区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就 是0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条 记录

11.索引列不能参与计算,保持列“干净”。

比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本 太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’);

12.尽量的扩展索引,不要新建索引。

比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可

注意:选择索引的最终目的是为了使查询的速度变快。上面给出的原则是最基本的准则,但不能拘泥于上面的准则。读者要在以后的学习和工作中进行不断的实践。根据应用的实际情况进行分析和判断,选择最合适的索引方式。

Java中一共有4种引用类型(其实还有一些其他的引用类型比如FinalReference):强引用、软引用、弱引用、虚引用。

其中强引用就是我们经常使用的Object a = new Object(); 这样的形式,在Java中并没有对应的Reference类。

本篇文章主要是分析软引用、弱引用、虚引用的实现,这三种引用类型都是继承于Reference这个类,主要逻辑也在Reference中。

问题

在分析前,先抛几个问题?

  1. 网上大多数文章对于软引用的介绍是:在内存不足的时候才会被回收,那内存不足是怎么定义的?什么才叫内存不足?
  2. 网上大多数文章对于虚引用的介绍是:形同虚设,虚引用并不会决定对象的生命周期。主要用来跟踪对象被垃圾回收器回收的活动。真的是这样吗?
  3. 虚引用在Jdk中有哪些场景下用到了呢?

Reference

我们先看下Reference.java中的几个字段

  1. public abstract class Reference<T> { 
  2.     //引用的对象 
  3.     private T referent;         
  4.     //回收队列,由使用者在Reference的构造函数中指定 
  5.     volatile ReferenceQueue<? super T> queue; 
  6.      //当该引用被加入到queue中的时候,该字段被设置为queue中的下一个元素,以形成链表结构 
  7.     volatile Reference next
  8.     //在GC时,JVM底层会维护一个叫DiscoveredList的链表,存放的是Reference对象,discovered字段指向的就是链表中的下一个元素,由JVM设置 
  9.     transient private Reference<T> discovered;   
  10.     //进行线程同步的锁对象 
  11.     static private class Lock { } 
  12.     private static Lock lock = new Lock(); 
  13.     //等待加入queue的Reference对象,在GC时由JVM设置,会有一个java层的线程(ReferenceHandler)源源不断的从pending中提取元素加入到queue 
  14.     private static Reference<Object> pending = null

一个Reference对象的生命周期如下:

主要分为Native层和Java层两个部分。

Native层在GC时将需要被回收的Reference对象加入到DiscoveredList中(代码在referenceProcessor.cpp中

process_discovered_references方法),然后将DiscoveredList的元素移动到PendingList中(代码在referenceProcessor.cpp中enqueue_discovered_ref_helper方法),PendingList的队首就是Reference类中的pending对象。

看看Java层的代码

  1. private static class ReferenceHandler extends Thread { 
  2.          ... 
  3.         public void run() { 
  4.             while (true) { 
  5.                 tryHandlePending(true); 
  6.             } 
  7.         } 
  8.   }  
  9. static boolean tryHandlePending(boolean waitForNotify) { 
  10.         Reference<Object> r; 
  11.         Cleaner c; 
  12.         try { 
  13.             synchronized (lock) { 
  14.                 if (pending != null) { 
  15.                     r = pending; 
  16.                      //如果是Cleaner对象,则记录下来,下面做特殊处理 
  17.                     c = r instanceof Cleaner ? (Cleaner) r : null
  18.                     //指向PendingList的下一个对象 
  19.                     pending = r.discovered; 
  20.                     r.discovered = null
  21.                 } else { 
  22.                    //如果pending为null就先等待,当有对象加入到PendingList中时,jvm会执行notify 
  23.                     if (waitForNotify) { 
  24.                         lock.wait(); 
  25.                     } 
  26.                     // retry if waited 
  27.                     return waitForNotify; 
  28.                 } 
  29.             } 
  30.         }  
  31.         ... 
  32.  
  33.         // 如果时CLeaner对象,则调用clean方法进行资源回收 
  34.         if (c != null) { 
  35.             c.clean(); 
  36.             return true
  37.         } 
  38.         //将Reference加入到ReferenceQueue,开发者可以通过从ReferenceQueue中poll元素感知到对象被回收的事件。 
  39.         ReferenceQueue<? super Object> q = r.queue; 
  40.         if (q != ReferenceQueue.NULL) q.enqueue(r); 
  41.         return true
  42.  } 

流程比较简单:就是源源不断的从PendingList中提取出元素,然后将其加入到ReferenceQueue中去,开发者可以通过从ReferenceQueue中poll元素感知到对象被回收的事件。

另外需要注意的是,对于Cleaner类型(继承自虚引用)的对象会有额外的处理:在其指向的对象被回收时,会调用clean方法,该方法主要是用来做对应的资源回收,在堆外内存DirectByteBuffer中就是用Cleaner进行堆外内存的回收,这也是虚引用在java中的典型应用。

看完了Reference的实现,再看看几个实现类里,各自有什么不同。

SoftReference

  1. public class SoftReference<T> extends Reference<T> { 
  2.  
  3.     static private long clock; 
  4.  
  5.     private long timestamp
  6.  
  7.     public SoftReference(T referent) { 
  8.         super(referent); 
  9.         this.timestamp = clock; 
  10.     } 
  11.  
  12.     public SoftReference(T referent, ReferenceQueue<? super T> q) { 
  13.         super(referent, q); 
  14.         this.timestamp = clock; 
  15.     } 
  16.  
  17.     public T get() { 
  18.         T o = super.get(); 
  19.         if (o != null && this.timestamp != clock) 
  20.             this.timestamp = clock; 
  21.         return o; 
  22.     } 
  23.  

软引用的实现很简单,就多了两个字段:clock和timestamp。clock是个静态变量,每次GC时都会将该字段设置成当前时间。timestamp字段则会在每次调用get方法时将其赋值为clock(如果不相等且对象没被回收)。

那这两个字段的作用是什么呢?这和软引用在内存不够的时候才被回收,又有什么关系呢?

这些还得看JVM的源码才行,因为决定对象是否需要被回收都是在GC中实现的。

  1. size_t 
  2. ReferenceProcessor::process_discovered_reflist( 
  3.   DiscoveredList               refs_lists[], 
  4.   ReferencePolicy*             policy, 
  5.   bool                         clear_referent, 
  6.   BoolObjectClosure*           is_alive, 
  7.   OopClosure*                  keep_alive, 
  8.   VoidClosure*                 complete_gc, 
  9.   AbstractRefProcTaskExecutor* task_executor) 
  10.  ... 
  11.    //还记得上文提到过的DiscoveredList吗?refs_lists就是DiscoveredList。 
  12.    //对于DiscoveredList的处理分为几个阶段,SoftReference的处理就在第一阶段 
  13.  ... 
  14.       for (uint i = 0; i < _max_num_q; i++) { 
  15.         process_phase1(refs_lists[i], policy, 
  16.                        is_alive, keep_alive, complete_gc); 
  17.       } 
  18.  ... 
  19.  
  20. //该阶段的主要目的就是当内存足够时,将对应的SoftReference从refs_list中移除。 
  21. void 
  22. ReferenceProcessor::process_phase1(DiscoveredList&    refs_list, 
  23.                                    ReferencePolicy*   policy, 
  24.                                    BoolObjectClosure* is_alive, 
  25.                                    OopClosure*        keep_alive, 
  26.                                    VoidClosure*       complete_gc) { 
  27.  
  28.   DiscoveredListIterator iter(refs_list, keep_alive, is_alive); 
  29.   // Decide which softly reachable refs should be kept alive. 
  30.   while (iter.has_next()) { 
  31.     iter.load_ptrs(DEBUG_ONLY(!discovery_is_atomic() /* allow_null_referent */)); 
  32.     //判断引用的对象是否存活 
  33.     bool referent_is_dead = (iter.referent() != NULL) && !iter.is_referent_alive(); 
  34.     //如果引用的对象已经不存活了,则会去调用对应的ReferencePolicy判断该对象是不时要被回收 
  35.     if (referent_is_dead && 
  36.         !policy->should_clear_reference(iter.obj(), _soft_ref_timestamp_clock)) { 
  37.       if (TraceReferenceGC) { 
  38.         gclog_or_tty->print_cr("Dropping reference (" INTPTR_FORMAT ": %s"  ") by policy"
  39.                                (void *)iter.obj(), iter.obj()->klass()->internal_name()); 
  40.       } 
  41.       // Remove Reference object from list 
  42.       iter.remove(); 
  43.       // Make the Reference object active again 
  44.       iter.make_active(); 
  45.       // keep the referent around 
  46.       iter.make_referent_alive(); 
  47.       iter.move_to_next(); 
  48.     } else { 
  49.       iter.next(); 
  50.     } 
  51.   } 
  52.  ... 

refs_lists中存放了本次GC发现的某种引用类型(虚引用、软引用、弱引用等),而

process_discovered_reflist方法的作用就是将不需要被回收的对象从refs_lists移除掉,refs_lists最后剩下的元素全是需要被回收的元素,最后会将其第一个元素赋值给上文提到过的Reference.java#pending字段。

ReferencePolicy一共有4种实现:NeverClearPolicy,AlwaysClearPolicy,LRUCurrentHeapPolicy,LRUMaxHeapPolicy。

其中NeverClearPolicy永远返回false,代表永远不回收SoftReference,在JVM中该类没有被使用,AlwaysClearPolicy则永远返回true,在referenceProcessor.hpp#setup方法中中可以设置policy为AlwaysClearPolicy,至于什么时候会用到AlwaysClearPolicy,大家有兴趣可以自行研究。

LRUCurrentHeapPolicy和LRUMaxHeapPolicy的should_clear_reference方法则是完全相同:

  1. bool LRUMaxHeapPolicy::should_clear_reference(oop p, 
  2.                                              jlong timestamp_clock) { 
  3.   jlong interval = timestamp_clock - java_lang_ref_SoftReference::timestamp(p); 
  4.   assert(interval >= 0, "Sanity check"); 
  5.  
  6.   // The interval will be zero if the ref was accessed since the last scavenge/gc. 
  7.   if(interval <= _max_interval) { 
  8.     return false
  9.   } 
  10.  
  11.   return true

timestamp_clock就是SoftReference的静态字段clock,

java_lang_ref_SoftReference::timestamp(p)对应是字段timestamp。如果上次GC后有调用SoftReference#get,interval值为0,否则为若干次GC之间的时间差。

_max_interval则代表了一个临界值,它的值在LRUCurrentHeapPolicy和LRUMaxHeapPolicy两种策略中有差异。

  1. void LRUCurrentHeapPolicy::setup() { 
  2.   _max_interval = (Universe::get_heap_free_at_last_gc() / M) * SoftRefLRUPolicyMSPerMB; 
  3.   assert(_max_interval >= 0,"Sanity check"); 
  4.  
  5. void LRUMaxHeapPolicy::setup() { 
  6.   size_t max_heap = MaxHeapSize; 
  7.   max_heap -= Universe::get_heap_used_at_last_gc(); 
  8.   max_heap /= M; 
  9.  
  10.   _max_interval = max_heap * SoftRefLRUPolicyMSPerMB; 
  11.   assert(_max_interval >= 0,"Sanity check"); 

看到这里你就知道SoftReference到底什么时候被被回收了,它和使用的策略(默认应该是LRUCurrentHeapPolicy),堆可用大小,该SoftReference上一次调用get方法的时间都有关系。

WeakReference

  1. public class WeakReference<T> extends Reference<T> { 
  2.  
  3.     public WeakReference(T referent) { 
  4.         super(referent); 
  5.     } 
  6.  
  7.     public WeakReference(T referent, ReferenceQueue<? super T> q) { 
  8.         super(referent, q); 
  9.     } 
  10.  

可以看到,对于Soft references和Weak references clear_referent字段传入的都是true,这也符合我们的预期:对象不可达后,引用字段就会被置为null,然后对象就会被回收(对于软引用来说,如果内存足够的话,在Phase 1,相关的引用就会从refs_list中被移除,到Phase 3时refs_list为空集合)。

但对于Final references和 Phantom references,clear_referent字段传入的是false,也就意味着被这两种引用类型引用的对象,如果没有其他额外处理,只要Reference对象还存活,那引用的对象是不会被回收的。Final references和对象是否重写了finalize方法有关,不在本文分析范围之内,我们接下来看看Phantom references。

可以看到WeakReference在Java层只是继承了Reference,没有做任何的改动。那referent字段是什么时候被置为null的呢?要搞清楚这个问题我们再看下上文提到过的

process_discovered_reflist方法:

 

  1. size_t 
  2. ReferenceProcessor::process_discovered_reflist( 
  3.   DiscoveredList               refs_lists[], 
  4.   ReferencePolicy*             policy, 
  5.   bool                         clear_referent, 
  6.   BoolObjectClosure*           is_alive, 
  7.   OopClosure*                  keep_alive, 
  8.   VoidClosure*                 complete_gc, 
  9.   AbstractRefProcTaskExecutor* task_executor) 
  10.  ... 
  11.  
  12.   //Phase 1:将所有不存活但是还不能被回收的软引用从refs_lists中移除(只有refs_lists为软引用的时候,这里policy才不为null) 
  13.   if (policy != NULL) { 
  14.     if (mt_processing) { 
  15.       RefProcPhase1Task phase1(*this, refs_lists, policy, true /*marks_oops_alive*/); 
  16.       task_executor->execute(phase1); 
  17.     } else { 
  18.       for (uint i = 0; i < _max_num_q; i++) { 
  19.         process_phase1(refs_lists[i], policy, 
  20.                        is_alive, keep_alive, complete_gc); 
  21.       } 
  22.     } 
  23.   } else { // policy == NULL 
  24.     assert(refs_lists != _discoveredSoftRefs, 
  25.            "Policy must be specified for soft references."); 
  26.   } 
  27.  
  28.   // Phase 2: 
  29.   // 移除所有指向对象还存活的引用 
  30.   if (mt_processing) { 
  31.     RefProcPhase2Task phase2(*this, refs_lists, !discovery_is_atomic() /*marks_oops_alive*/); 
  32.     task_executor->execute(phase2); 
  33.   } else { 
  34.     for (uint i = 0; i < _max_num_q; i++) { 
  35.       process_phase2(refs_lists[i], is_alive, keep_alive, complete_gc); 
  36.     } 
  37.   } 
  38.  
  39.   // Phase 3: 
  40.   // 根据clear_referent的值决定是否将不存活对象回收 
  41.   if (mt_processing) { 
  42.     RefProcPhase3Task phase3(*this, refs_lists, clear_referent, true /*marks_oops_alive*/); 
  43.     task_executor->execute(phase3); 
  44.   } else { 
  45.     for (uint i = 0; i < _max_num_q; i++) { 
  46.       process_phase3(refs_lists[i], clear_referent, 
  47.                      is_alive, keep_alive, complete_gc); 
  48.     } 
  49.   } 
  50.  
  51.   return total_list_count; 
  52.  
  53. void 
  54. ReferenceProcessor::process_phase3(DiscoveredList&    refs_list, 
  55.                                    bool               clear_referent, 
  56.                                    BoolObjectClosure* is_alive, 
  57.                                    OopClosure*        keep_alive, 
  58.                                    VoidClosure*       complete_gc) { 
  59.   ResourceMark rm; 
  60.   DiscoveredListIterator iter(refs_list, keep_alive, is_alive); 
  61.   while (iter.has_next()) { 
  62.     iter.update_discovered(); 
  63.     iter.load_ptrs(DEBUG_ONLY(false /* allow_null_referent */)); 
  64.     if (clear_referent) { 
  65.       // NULL out referent pointer 
  66.       //将Reference的referent字段置为null,之后会被GC回收 
  67.       iter.clear_referent(); 
  68.     } else { 
  69.       // keep the referent around 
  70.       //标记引用的对象为存活,该对象在这次GC将不会被回收 
  71.       iter.make_referent_alive(); 
  72.     } 
  73.     ... 
  74.   } 
  75.     ... 

不管是弱引用还是其他引用类型,将字段referent置null的操作都发生在process_phase3中,而具体行为是由clear_referent的值决定的。而clear_referent的值则和引用类型相关。

 

  1. ReferenceProcessorStats ReferenceProcessor::process_discovered_references( 
  2.   BoolObjectClosure*           is_alive, 
  3.   OopClosure*                  keep_alive, 
  4.   VoidClosure*                 complete_gc, 
  5.   AbstractRefProcTaskExecutor* task_executor, 
  6.   GCTimer*                     gc_timer) { 
  7.   NOT_PRODUCT(verify_ok_to_handle_reflists()); 
  8.     ... 
  9.   //process_discovered_reflist方法的第3个字段就是clear_referent 
  10.   // Soft references 
  11.   size_t soft_count = 0; 
  12.   { 
  13.     GCTraceTime tt("SoftReference", trace_time, false, gc_timer); 
  14.     soft_count = 
  15.       process_discovered_reflist(_discoveredSoftRefs, _current_soft_ref_policy, true
  16.                                  is_alive, keep_alive, complete_gc, task_executor); 
  17.   } 
  18.  
  19.   update_soft_ref_master_clock(); 
  20.  
  21.   // Weak references 
  22.   size_t weak_count = 0; 
  23.   { 
  24.     GCTraceTime tt("WeakReference", trace_time, false, gc_timer); 
  25.     weak_count = 
  26.       process_discovered_reflist(_discoveredWeakRefs, NULLtrue
  27.                                  is_alive, keep_alive, complete_gc, task_executor); 
  28.   } 
  29.  
  30.   // Final references 
  31.   size_t final_count = 0; 
  32.   { 
  33.     GCTraceTime tt("FinalReference", trace_time, false, gc_timer); 
  34.     final_count = 
  35.       process_discovered_reflist(_discoveredFinalRefs, NULLfalse
  36.                                  is_alive, keep_alive, complete_gc, task_executor); 
  37.   } 
  38.  
  39.   // Phantom references 
  40.   size_t phantom_count = 0; 
  41.   { 
  42.     GCTraceTime tt("PhantomReference", trace_time, false, gc_timer); 
  43.     phantom_count = 
  44.       process_discovered_reflist(_discoveredPhantomRefs, NULLfalse
  45.                                  is_alive, keep_alive, complete_gc, task_executor); 
  46.   } 
  47.     ... 

可以看到,对于Soft references和Weak references clear_referent字段传入的都是true,这也符合我们的预期:对象不可达后,引用字段就会被置为null,然后对象就会被回收(对于软引用来说,如果内存足够的话,在Phase 1,相关的引用就会从refs_list中被移除,到Phase 3时refs_list为空集合)。

但对于Final references和 Phantom references,clear_referent字段传入的是false,也就意味着被这两种引用类型引用的对象,如果没有其他额外处理,只要Reference对象还存活,那引用的对象是不会被回收的。Final references和对象是否重写了finalize方法有关,不在本文分析范围之内,我们接下来看看Phantom references。

PhantomReference

 

  1. public class PhantomReference<T> extends Reference<T> { 
  2.  
  3.     public T get() { 
  4.         return null
  5.     } 
  6.  
  7.     public PhantomReference(T referent, ReferenceQueue<? super T> q) { 
  8.         super(referent, q); 
  9.     } 
  10.  

可以看到虚引用的get方法永远返回null,我们看个demo。

  1. public static void demo() throws InterruptedException { 
  2.         Object obj = new Object(); 
  3.         ReferenceQueue<Object> refQueue =new ReferenceQueue<>(); 
  4.         PhantomReference<Object> phanRef =new PhantomReference<>(obj, refQueue); 
  5.  
  6.         Object objg = phanRef.get(); 
  7.         //这里拿到的是null 
  8.         System.out.println(objg); 
  9.         //让obj变成垃圾 
  10.         obj=null
  11.         System.gc(); 
  12.         Thread.sleep(3000); 
  13.         //gc后会将phanRef加入到refQueue中 
  14.         Reference<? extends Object> phanRefP = refQueue.remove(); 
  15.          //这里输出true 
  16.         System.out.println(phanRefP==phanRef); 
  17.     } 

从以上代码中可以看到,虚引用能够在指向对象不可达时得到一个'通知'(其实所有继承References的类都有这个功能),需要注意的是GC完成后,phanRef.referent依然指向之前创建Object,也就是说Object对象一直没被回收!

而造成这一现象的原因在上一小节末尾已经说了:对于Final references和 Phantom references,clear_referent字段传入的时false,也就意味着被这两种引用类型引用的对象,如果没有其他额外处理,在GC中是不会被回收的。

对于虚引用来说,从refQueue.remove();得到引用对象后,可以调用clear方法强行解除引用和对象之间的关系,使得对象下次可以GC时可以被回收掉。

End

针对文章开头提出的几个问题,看完分析,我们已经能给出回答:

1.我们经常在网上看到软引用的介绍是:在内存不足的时候才会回收,那内存不足是怎么定义的?为什么才叫内存不足?

软引用会在内存不足时被回收,内存不足的定义和该引用对象get的时间以及当前堆可用内存大小都有关系,计算公式在上文中也已经给出。

2.网上对于虚引用的介绍是:形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。主要用来跟踪对象被垃圾回收器回收的活动。真的是这样吗?

严格的说,虚引用是会影响对象生命周期的,如果不做任何处理,只要虚引用不被回收,那其引用的对象永远不会被回收。所以一般来说,从ReferenceQueue中获得PhantomReference对象后,如果PhantomReference对象不会被回收的话(比如被其他GC ROOT可达的对象引用),需要调用clear方法解除PhantomReference和其引用对象的引用关系。

3.虚引用在Jdk中有哪些场景下用到了呢?

DirectByteBuffer中是用虚引用的子类Cleaner.java来实现堆外内存回收的,后续会写篇文章来说说堆外内存的里里外外。

 

责任编辑:未丽燕 来源: 今日头条
相关推荐

2018-02-01 09:26:12

面试算法题程序员

2019-03-06 14:26:31

Javascript面试前端

2019-02-21 10:49:51

Redis持久化恢复

2019-12-16 15:37:57

JavaScript人生第一份工作浏览器

2019-07-16 10:10:46

JavaScript数据类型

2022-10-09 07:33:38

JavaSPI程序

2009-09-11 11:09:36

C#引用类型

2022-08-12 09:35:36

JavaScript面试

2020-05-14 08:13:56

JDK命令Java

2019-05-15 16:45:13

SpringBoot面试题Java

2024-06-28 09:07:19

2017-11-29 14:41:48

神经网络递归神经网络RNN

2009-09-11 11:17:04

C#引用类型

2010-04-06 13:07:45

Oracle数据库

2020-07-01 17:25:28

Redis数据库内存

2009-03-26 10:33:34

Oracle数据块数据库

2009-03-06 16:48:23

数据块原理Oracle

2022-04-01 12:40:13

MySQL数据库

2022-09-05 22:22:00

Stream操作对象

2010-03-31 17:17:32

点赞
收藏

51CTO技术栈公众号