JVM G1源码分析——引用_jvmg1源码分析和调优-程序员宅基地

技术标签: jvm  # JVM  

引用指的是引用类型。Java引入引用的目的在于JVM能更加柔性地管理内存,比如对引用对象来说,当内存足够,垃圾回收器不去回收这些内存。因为引用的特殊性,引用对象的使用和回收与一般对象的使用和回收并不相同。本章将介绍:JDK如何实现引用,JVM如何发现引用对象、处理引用,最后分析了引用相关的日志并介绍了如何调优。G1并没有对引用做额外的处理,所以本章介绍的内容也适用于其他的垃圾回收器。

引用概述

我们这里所说的引用主要指:软引用、弱引用和虚引用。另外值得一提的是Java中的Finalize也是通过引用实现的,JDK定义了一种新的引用类型FinalReference,这个类型的处理和其他三种引用都稍有不同。另外在非公开的JDK包中还有一个sun.misc.cleaner,通常用它来释放非堆内存资源,它在JVM内部也是用一个CleanerReference实现。要理解引用处理需要先从Java代码入手。先看看java.lang.ref包里面的部分代码,这一部分代码不在Hotspot中,通常可以在JDK安装目录下找到它,其代码如下所示:

jdk/src/share/classes/java/lang/ref/Reference.java
public abstract class Reference<T> {
  // Reference指向的对象
  private T referent;         /* Treated specially by GC */
/*Reference所指向的队列,如果我们创建引用对象的时候没有指定队列,那么队列就是ReferenceQueue.NULL,这是一个空队列,这个时候所有插入队列的对象都被丢弃。这个字段是引用的独特之处。这个队列一般是我们自定义,然后可以自己处理。典型的例子就是weakhashmap和FinalReference,他们都有自己的代码处理这个队列从而达到自己的目的。*/
  volatile ReferenceQueue< super T> queue;
  // next指针是用于形成链表,具体也是在JVM中使用。
  Reference next;
  // 这个字段是私有,在这里明确注释提到它在JVM中使用。它的目的是发现可收回的引用,
  // 在后面的discover_reference里面可以看到更为详细的信息。
  transient private Reference<T> discovered;  /* used by VM */
// 这是一个静态变量,前面提到垃圾回收线程做的事情就是把discovered的元素
// 赋值到Pending中,并且把JVM中的Pending链表元素放到Reference类中Pending链表中
private static Reference<Object> pending = null;
}

我们都知道JVM在启动之后有几个线程,其中之一是ReferenceHandler。这个线程做的主要工作就是把上面提到的pending里面的元素送到队列中。具体功能在tryHandlePending中,代码如下所示:

private static class ReferenceHandler extends Thread {
  ……
  public void run() {
      while (true) {
        tryHandlePending(true);
      }
    }
  }
……
  static boolean tryHandlePending(boolean waitForNotify) {
    Reference<Object> r;
    Cleaner c;
    try {
      synchronized (lock) {
        if (pending != null) {
          r = pending;
          c = r instanceof Cleaner  (Cleaner) r : null;
          pending = r.discovered;
          r.discovered = null;
        } else {
          if (waitForNotify) {
            lock.wait();
          }
          return waitForNotify;
        }
      }
    } catch (OutOfMemoryError x) {
      Thread.yield();
      return true;
    } catch (InterruptedException x) {
      return true;
    }
    // Fast path for cleaners
    if (c != null) {
      c.clean();
      return true;
    }
    ReferenceQueue< super Object> q = r.queue;
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    return true;
  }

这里的discovered就是在垃圾回收中发现可回收的对象,什么是可回收的对象?指对象只能从引用这个根到达,没有任何强引用使用这个对象。所以说可回收的对象在被垃圾回收器发现后会被垃圾回收器放入pending这个队列,pending的意思就是等待被回收,如果我们自定义引用队列,那么引用线程ReferenceHandler把它加入到引用队列,供我们进一步处理。比如Finalizer里面就会激活一个线程,让这个线程把队列里面的对象拿出来,然后执行对象的finalize()方法。具体代码在runFinalization中,代码如下所示:

jdk/src/share/classes/java/lang/ref/Finalizer.java
static void runFinalization() {
  if (!VM.isBooted()) {
    return;
  }
  forkSecondaryFinalizer(new Runnable() {
    private volatile boolean running;
    public void run() {
      if (running)
        return;
      final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
      running = true;
      for (;;) {
      // 获取可回收对象
        Finalizer f = (Finalizer)queue.poll();
        if (f == null) break;
        // 执行对象的finialize方法
        f.runFinalizer(jla);
      }
    }
  });

在Reference.java这个类中描述了Reference的4个可能的状态:

  • Active:对象是活跃的,这个活跃的意思是指GC可以通过可达性分析找到对象或者对象是软引用对象,且符合软引用活跃的规则。从活跃状态可以到Pending状态或者Inactive状态。新创建的对象总是活跃的。
  • Pending:指对象进入上面的pengding_list,即将被送入引用队列。
  • Enqueued:指引用线程ReferenceHandler把pending_list的对象加入引用队列。
  • Inactive:对象不活跃,可以将对象回收了。

状态转换图如下图所示。

其中除了Pending到Enqueued状态是有引用线

程ReferenceHandler参与的,其他的变化都是GC线程完成的。另外值得一提的是,这些状态是虚拟状态,是为了便于大家理解引用是如何工作的,并没有一个字段来描述状态。所以在注释中我们看到对象所处状态的确定是通过queue这个字段和next这个字段来标记的。

可回收对象发现

在GC的标记阶段,从根对象出发对所有的对象进行标记,如果对象是引用对象,在JVM内部对应的类型为InstanceRefKlass,在对象遍历的时候会处理对象的每一个字段。在前面YGC的时候,我们提到copy_to_survior会执行obj->oop_iterate_backwards(&_scanner),在这里就会执行宏InstanceRefKlass_SPECIALIZED_OOP_ITERATE展开的代码,在这段代码里面有个关键的方法ReferenceProcessor::discover_reference,这个方法就是把从引用对象类型中的可回收对象放入链表中。

我们先看一下宏代码片段,代码如下所示:

hotspot/src/share/vm/oops/instanceRefKlass.cpp
#define InstanceRefKlass_SPECIALIZED_OOP_ITERATE(T, nv_suffix, contains)       \
  ……                                                                          \
                                                                                \
  T* referent_addr = (T*)java_lang_ref_Reference::referent_addr(obj);           \
  T heap_oop = oopDesc::load_heap_oop(referent_addr);                           \
  ReferenceProcessor* rp = closure->_ref_processor;                             \
  if (!oopDesc::is_null(heap_oop)) {                                            \
    oop referent = oopDesc::decode_heap_oop_not_null(heap_oop);                 \
    if (!referent->is_gc_marked() && (rp != NULL) &&                            \
        rp->discover_reference(obj, reference_type())) {                        \
      return size;                                                              \
    } else if (contains(referent_addr)) {                                       \
      /* treat referent as normal oop */                                        \
      SpecializationStats::record_do_oop_call##nv_suffix(SpecializationStats::irk);\
      closure->do_oop##nv_suffix(referent_addr);                                \
    }                                                                           \
  }

我们发现只有当引用里面的对象还没有标记时才需要去处理引用,否则说明对象还存在强引用。注意在这里discover_reference返回true表示后续不需要进行处理,否则继续。根据前面的分析,后续的动作将会对引用对象里面的对象进行处理(其实就是复制对象到新的位置,处理方法已经介绍过了)。代码如下所示:

hotspot/src/share/vm/memory/referenceProcessor.cpp

bool ReferenceProcessor::discover_reference(oop obj, ReferenceType rt) {
/*判断是否不需要处理,_discovering_refs在执行GC的时候设置为true表示不执行;
在执行完GC或者CM时,设置为false,表示可以执行RegisterReferences由参数控制。*/
if (!_discovering_refs || !RegisterReferences)    return false;
  // 我们在前面提到,next是用于形成链表,如果非空说明引用里面的对象已经被处理过了。
  oop next = java_lang_ref_Reference::next(obj);
  if (next != NULL)     return false;
  HeapWord* obj_addr = (HeapWord*)obj;
  if (RefDiscoveryPolicy == ReferenceBasedDiscovery && !_span.contains(obj_
    addr))     return false;
/*可以通过参数RefDiscoveryPolicy选择引用发现策略,默认值为0,即ReferenceBasedDiscovery,
使用1则表示ReferentBasedDiscovery。策略的选择将会影响处理的速度。*/
  // 引用里面对象如果有强引用则无需处理
  if (is_alive_non_header() != NULL) {
  if (is_alive_non_header()->do_object_b(java_lang_ref_Reference::referent(obj)))    
    return false;  // referent is reachable
  }
  if (rt == REF_SOFT) {
    if (!_current_soft_ref_policy->should_clear_reference(obj, _soft_ref_
      timestamp_clock))      return false;
  }
/*在上面的处理逻辑中,可以看出在JVM内部,并没有针对Reference重新建立相应的处理结构来维护
相应的处理链,而是直接采用Java中的Reference对象链来处理,只不过这些对象的关系由JVM在内部
进行处理。在Java中discovered对象只会被方法tryHandlePending修改,而此方法只会处理pending
链中的对象。而在上面的处理过程中,相应的对象并没有在pending中,因此两个处理过程是不相干的。*/
  HeapWord* const discovered_addr = java_lang_ref_Reference::discovered_addr(obj);
  const oop  discovered = java_lang_ref_Reference::discovered(obj);
  // 已经处理过了则不再处理。如果是ReferentBasedDiscovery,引用对象在处理范围,
  // 或者引用里面的对象在处理范围内
  if (RefDiscoveryPolicy == ReferentBasedDiscovery) {
    // RefeventBased Discovery策略指的是引用对象在处理范围内或者引用对象里面的对象在
    // 处理范围内
    if (_span.contains(obj_addr) ||
        (discovery_is_atomic() &&
         _span.contains(java_lang_ref_Reference::referent(obj)))) {
      // should_enqueue = true;
    } else {
      return false;
    }
  }
/*把引用里面的对象放到引用对象的discovered字段里面。同时还会把对象放入DiscoveredList。
上面提到的5种引用类型,在JVM内部定义了5个链表分别处理。分别为:_discoveredSoftRefs、
_discoveredWeakRefs、_discoveredFinalRefs、_discoveredPhantomRefs、_discoveredCleanerRefs */
  DiscoveredList* list = get_discovered_list(rt);
  if (list == NULL) {
    return false;
  }
// 链表里面的每一个节点都对应着Java中的reference对象。
  if (_discovery_is_mt) {
    // 并行处理
    add_to_discovered_list_mt(*list, obj, discovered_addr);
  } else {
    oop current_head = list->head();
    oop next_discovered = (current_head != NULL)  current_head : obj;
// 这里采用头指针加上一个长度字段来描述需要处理的reference对象。在这里面存放的对象都是
// 在相应的处理过程中还没有被放入java Reference中pending结构的对象。
    oop_store_raw(discovered_addr, next_discovered);
    list->set_head(obj);
    list->inc_length(1);
  }
  return true;
}

判断对象是否有强引用的方法是通过G1STWIsAliveClosure::do_object_b,判断依据也非常简单,就是对象所在分区不在CSet中或者对象在CSet但没有被复制到新的分区。代码如下所示:

hotspot/src/share/vm/gc_implementation/g1/g1CollectedHeap.cpp
bool G1STWIsAliveClosure::do_object_b(oop p) {
  return !_g1->obj_in_cs(p) || p->is_forwarded();
}

软引用处理有点特殊,它用到_soft_ref_timestamp_clock,来自于java.lang.ref.SoftReference对象,有一个全局的变量clock(实际上就是java.lang.ref.SoftReference的类变量clock):其记录了最后一次GC的时间点(时间单位为毫秒),即每一次GC发生时,该值均会被重新设置。另外对于软引用里面的对象,JVM并不会立即清除,也是通过参数控制,有两种策略可供选择:·C2(服务器模式)编译使用的是LRUMaxHeapPolicy。·非C2编译用的是LRUCurrentHeapPolicy。需要注意的是策略的选择是通过编译选项控制的,而不像其他的参数可以由使用者控制,代码如下所示:

hotspot/src/share/vm/memory/referenceProcessor.cpp
_default_soft_ref_policy = new COMPILER2_PRESENT(LRUMaxHeapPolicy())  
  NOT_COMPILER2(LRUCurrentHeapPolicy())。

通常生产环境中使用服务器模式,所以我们看一下LRUMaxHeapPolicy。它有一个重要的函数should_clear_reference,目的是为了判断软引用里面对象是否可以回收,代码如下所示:

hotspot/src/share/vm/memory/referencePolicy.cpp
void LRUMaxHeapPolicy::setup() {
  size_t max_heap = MaxHeapSize;
  max_heap -= Universe::get_heap_used_at_last_gc();
  max_heap /= M;
// 根据最大可用的内存来估算软引用对象最大的生存时间
  _max_interval = max_heap * SoftRefLRUPolicyMSPerMB;
}
bool LRUMaxHeapPolicy::should_clear_reference(oop p, jlong timestamp_clock) {
  jlong interval = timestamp_clock - java_lang_ref_SoftReference::timestamp(p);
  if(interval <= _max_interval) return false;
  return true;
}

在这个代码片段中,可以看到软引用对象是否可以回收的条件是:对象存活时间是否超过了阈值_max_interval。如果你继续探究策略LRUCurrentHeapPolicy,你会发现LRUCurrentHeapPolicy中的should_clear_reference函数和这里介绍的完全一样。其实这两种策略的区别是_max_interval的计算不同,但都受控于参数SoftRefLRUPolicyMSPerMB,其中LRUMaxHeapPolicy是基于最大内存来设置软引用的存活时间,LRUCurrentHeapPolicy是根据当前可用内存来计算软引用的存活时间。

在GC时的处理发现列表

处理已发现的可回收对象会根据不同的引用类型分别处理,入口函数在process_discovered_references。其主要工作在process_discovered_reflist中,代码如下所示:

hotspot/src/share/vm/memory/referenceProcessor.cpp

ReferenceProcessor::process_discovered_reflist(...)
{
  bool mt_processing = task_executor != NULL && _processing_is_mt;
  bool must_balance = _discovery_is_mt;
  // 平衡引用队列,具体介绍可以参考8.6节
  if ((mt_processing && ParallelRefProcBalancingEnabled) || must_balance) {
    balance_queues(refs_lists);
  }
  size_t total_list_count = total_count(refs_lists);
  if (PrintReferenceGC && PrintGCDetails) {
    gclog_or_tty->print(", %u refs", total_list_count);
  }
  // 处理软引用(soft reference)
  if (policy != NULL) {
    if (mt_processing) {
      RefProcPhase1Task phase1(*this, refs_lists, policy, true /*marks_oops_
        alive*/);
      task_executor->execute(phase1);
    } else {
      for (uint i = 0; i < _max_num_q; i++) {
        process_phase1(refs_lists[i], policy,
                       is_alive, keep_alive, complete_gc);
      }
    }
  } else { // policy == NULL
    ......
  }
  // Phase 2:
  if (mt_processing) {
    RefProcPhase2Task phase2(*this, refs_lists, !discovery_is_atomic() /*marks_
      oops_alive*/);
    task_executor->execute(phase2);
  } else {
    for (uint i = 0; i < _max_num_q; i++) {
      process_phase2(refs_lists[i], is_alive, keep_alive, complete_gc);
    }
  }
  // Phase 3:
  if (mt_processing) {
    RefProcPhase3Task phase3(*this, refs_lists, clear_referent, true /*marks_
      oops_alive*/);
    task_executor->execute(phase3);
  } else {
    for (uint i = 0; i < _max_num_q; i++) {
      process_phase3(refs_lists[i], clear_referent,
                     is_alive, keep_alive, complete_gc);
    }
  }
  return total_list_count;
}

这里唯一的注意点就是当mt_processing为真时,阶段一(phase1)、阶段二(phase2)、阶段三(phase3)中多个任务分别可以并行执行(阶段之间还是串行执行);否则阶段中的多个任务串行执行。mt_processing主要受控于参数ParallelRefProcEnabled。下面介绍这三个阶段的主要工作:

·process_phase1针对软引用,如果对象已经死亡并且满足软引用清除策略才需要进一步处理,否则认为对象还活着,把它从这个链表中删除,并且重新把对象复制到Survivor或者Old区,代码如下所示:

hotspot/src/share/vm/memory/referenceProcessor.cpp
ReferenceProcessor::process_phase1(…) {
  DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
  while (iter.has_next()) {
    iter.load_ptrs(DEBUG_ONLY(!discovery_is_atomic() /* allow_null_referent */));
    bool referent_is_dead = (iter.referent() != NULL) && !iter.is_referent_
      alive();
    if (referent_is_dead && !policy->should_clear_reference(iter.obj(), _soft_
      ref_timestamp_clock)) {
      // 如果对象还需要挽救,重新激活它
      iter.remove();
      iter.make_active();
      iter.make_referent_alive();
      iter.move_to_next();
    } else {
      iter.next();
    }
  }
  complete_gc->do_void();
}

把对象重新激活的做法就是在卡表中标示对象的状态,并且把对象复制到新的分区。keep_live就是G1CopyingKeepAliveClosure,它是真正做复制动作的地方,代码如下所示:

hotspot/src/share/vm/memory/referenceProcessor.hpp
// 对象激活
inline void make_referent_alive() {
  if (UseCompressedOops) {
    _keep_alive->do_oop((narrowOop*)_referent_addr);
  } else {
    _keep_alive->do_oop((oop*)_referent_addr);
  }
}

·process_phase2识别引用对象里面的对象是否活跃,如果活跃,把引用对象从这个链表里面删除。为什么要有这样的处理?关键在于discover_reference中可能会误标记,比如引用对象先于强引用对象执行,这个时候就发生了误标记,所以需要调整;这个阶段比较简单,不再列出源码。

·process_phase3清理引用关系,首先把对象复制到新的分区,为什么呢?因为在前面提到discovered列表会被放到pending列表,而pending列表会进入到引用队列供后续处理,然后把引用对象里面的对象设置为NULL,那么原来的对象没有任何引用了,就有可能被回收了。代码如下所示:

hotspot/src/share/vm/memory/referenceProcessor.cpp
void ReferenceProcessor::process_phase3(…) {
  DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
  while (iter.has_next()) {
    // 先执行update_discovered,就是把对象复制到新的分区
    iter.update_discovered();
    iter.load_ptrs(DEBUG_ONLY(false /* allow_null_referent */));
    if (clear_referent) {
      // 如果不是软引用,则清理指针,此时除了链表不会有任何对象引用它了
      iter.clear_referent();
    } else {
      // 再次确保对象被复制
      iter.make_referent_alive();
    }
    iter.next();
  }
  // 更新链表
  iter.update_discovered();
  complete_gc->do_void();
}

上面的clear_referent就是把对象的引用关系打断了,所以设置为NULL,代码如下所示:

hotspot/src/share/vm/memory/referenceProcessor.hpp
void DiscoveredListIterator::clear_referent() {
  oop_store_raw(_referent_addr, NULL);
}

上面的update_discovered就是把待回收的对象复制到新的分区,形成新的链表,供后续pending列表处理。代码如下所示:

hotspot/src/share/vm/memory/referenceProcessor.hpp
inline void DiscoveredListIterator::update_discovered() {
  // _prev_next指向DiscoveredList
  if (UseCompressedOops) {
    if (!oopDesc::is_null(*(narrowOop*)_prev_next)) {
      _keep_alive->do_oop((narrowOop*)_prev_next);
    }
  } else {
    if (!oopDesc::is_null(*(oop*)_prev_next)) {
      _keep_alive->do_oop((oop*)_prev_next);
    }
  }
}

重新激活可达的引用

正如我们前面提到的,在引用处理的时候,pending会加入引用队列,所以待回收的对象还不能马上被回收,而且待回收的对象都已经放入discovered链表,所以这个时候只需要把discovered链表放入pending形成的链表中。主要代码在enqueue_discovered_ref_helper中。这个处理比较简单,不再列出源码。

日志解读

本节通过一个例子来分析引用处理。代码如下所示:

public class ReferenceTest {
  public static void main(String[] args) {
    Map<Integer, SoftReference<String>> map = new HashMap<>();
    int i = 0;
    while (i < 10000000) {
      String p = "" + i;
      map.put(i, new SoftReference<String>(p));
      i++;
    }
    System.out.println("done");
  }
}

运行参数设置如下所示:

-Xmx256M -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintReferenceGC 
-XX:+PrintGCTimeStamps -XX:+TraceReferenceGC -XX:SoftRefLRUPolicyMSPerMB=0

得到日志片段如下:

0.193: [GC pause (G1 Evacuation Pause) (young)0.208: [SoftReference, 8285 
  refs, 0.0008413 secs]0.208: [WeakReference, 4 refs, 0.0000137 secs]0.208: 
  [FinalReference, 1 refs, 0.0000083 secs]0.208: [PhantomReference, 0 
  refs, 0 refs, 0.0000094 secs]0.208: [JNI Weak Reference, 0.0000063 
  secs], 0.0158259 secs]
……
      [Ref Proc: 1.1 ms]
      [Ref Enq: 0.2 ms]

可以看到在这一次YGC中,一共有8285个软引用被处理。

参数介绍和调优

软引用在实际工作中关注的并不多,原因主要有两点。第一,软引用作为较难的知识点,实际工作中真正使用的并不多;第二,介绍软引用对象回收细节的文章也不多。本章较为详细地介绍了G1中软引用回收的步骤,下面介绍一下软引用相关的参数和优化:

·参数PrintReferenceGC,默认值为false,可以打开该参数以输出更多信息。如果是调试版本还可以打开TraceReferenceGC获得更多的引用信息。

·参数ParallelRefProcBalancingEnabled,默认值为true,在处理引用的时候,引用(软/弱/虚/final/cleaner)对象在同一类型的队列中可能是不均衡的,如果打开该参数则表示可以把链表均衡一下。注意这里的均衡不是指不同引用类型之间的均衡,而是同一引用类型里面有多个队列,同一引用类型多个队列之间的均衡。

·参数ParallelRefProcEnabled,默认值为false,打开之后表示在处理一个引用的时候可以使用多线程的处理方式。这个参数主要是控制引用列表的并发处理。另外引用的处理在GC回收和并发标记中都会执行,在GC中执行的引用处理使用的线程数目和GC线程数目一致,在并发标记中处理引用使用的线程数目和并发标记线程数一致。实际中通常打开该值,减少引用处理的时间。

·参数RegisterReferences,默认值true,表示可以在遍历对象的时候发现引用对象类型中的对象是否可以回收,false表示在遍历对象的时候不处理引用对象。目前的设计中在GC发生时不会去遍历引用对象是否可以回收。需要注意的是该参数如果设置为false,则在GC时会执行软引用对象是否可以回收,这将会增加GC的时间,所以通常不要修改这个值。

·参数G1UseConcMarkReferenceProcessing,默认值true,表示在并发标记的时候发现对象。该值为实验选项,需要使用-XX:+UnlockExperimentalVMOptions才能改变选项。

·参数RefDiscoveryPolicy,默认值为0,0表示ReferenceBasedDiscovery,指如果引用对象在我们的处理范围内,则对这个引用对象进行处理。1表示ReferentBasedDiscovery,指如果引用对象在我们的处理范围内或者引用对象里面的对象在处理范围内,则对引用对象处理。1会导致处理的对象更多。

·参数SoftRefLRUPolicyMSPerMB,默认值为1000,即对软引用的清除参数为每MB的内存将会存活1s,如最大内存为1GB,则软引用的存活时间为1024s,大约为17分钟,但是如果内存为100GB,这个参数不调整,软引用对象将存活102400s,大约为28.5小时。所以需要根据总内存大小以及预期软引用的存活时间来调整这个参数。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_16500963/article/details/133699350

智能推荐

基于51单片机冰箱温度控制器设计_基于51单片机的智能冰箱控制系统设计-程序员宅基地

文章浏览阅读871次,点赞11次,收藏3次。*单片机设计介绍, 基于51单片机冰箱温度控制器设计。_基于51单片机的智能冰箱控制系统设计

ubuntu创建sftp和ftp服务器及相应的用户管理_ubuntu sftp服务器查看用户和密码-程序员宅基地

文章浏览阅读4.8k次。一、sftp服务器进入root模式(下面的操作默认都是在root用户下)#安装openssh-serverapt-get install -y openssh-server创建sftp的组和用户#创建sftp-users组groupadd sftp-users#创建sftp用户目录alicemkdir /home/alice#创建sftp用户alice,并且绑定其主目..._ubuntu sftp服务器查看用户和密码

关于在simulink中使用s-function后出现State derivatives returned by S-function during flag=1 call must be a rea_state derivatives returned by s-function 'pmsm' in-程序员宅基地

文章浏览阅读5.9k次,点赞9次,收藏16次。解决了在simulink中使用s-function遇到的报错:State derivatives returned by S-function 'demo' in 'test/S-Function' during flag=1 call must be a real vector of length 2 _state derivatives returned by s-function 'pmsm' in 'ipmsm/ipmsm/s-function1

Sublime Text 关闭自动更新 | Mac_mac sublime text 取消更新提示-程序员宅基地

文章浏览阅读3.1k次。1. 打开配置文件Mac 如下图2. 在文件内部添加这段文字,就可以了:"update_check":false _mac sublime text 取消更新提示

Linux系统下DNS配置指南_linux 服务器修改网络dns-程序员宅基地

文章浏览阅读548次,点赞10次,收藏6次。Linux系统下DNS配置指南_linux 服务器修改网络dns

Springboot/java/node/python/php基于springboot+vue手机售后管理系统【2024年毕设】-程序员宅基地

文章浏览阅读779次,点赞19次,收藏24次。springboot微信小程序的小疾病问诊服务系统的设计与实现。springboot基于spring的物业管理系统的设计与实现。springboot基于Java的高校学生请假系统。ssm基于Android的购物商场APP设计与实现。springboot基于微信小程序的智慧校园系统。ssm基于Android的英语词典的设计与开发。ssm基于SSM+Vue的学生实践管理平台开发。ssm基于android的企业员工考勤系统。ssm基于web的暗香小店系统的设计与实现。ssm基于Web的高等学校公费医疗管理系统。

随便推点

C#反序列化无法找到程序集_未能找到程序集“g:\c#%5cc#%20%e4%b8%8a%e4%bd%8d%e6%9c%ba%5-程序员宅基地

文章浏览阅读7.7k次。反序列化无法找到程序集提示找不到程序集. 原因是序列化时把序列化类的命名空间等信息保存了,但应用程序和类库的命名空间可能是不一样的,所以提示找不到程序集. 解决方法如下: 方法1.将dll加入强名称,注册到全局程序集缓存中 方法2.在反序列化使用的IFormatter 对象加入Binder 属性,使其获取要反序列化的对象所在的程序集_未能找到程序集“g:\c#%5cc#%20%e4%b8%8a%e4%bd%8d%e6%9c%ba%5c%e7%a9%ba%e5%8e%8

Jet-Lube EZY-Turn Kopr-Kote Deacon_dpezy-程序员宅基地

文章浏览阅读234次。Jet-Lube EZY-Turn Kopr-Kote Deacon Moly-Lith 12|34 21 (Oilfield) 21 (Water Well) 21 Arctic (Oilfield) 21 Arctic (Water Well) 550 Extreme 550 Extreme All Weather 713-670-5700 769 Lubricant 930 Whitmore Drive Alco-EP Alco-EP ECF Alco-EP-73 Plus AP-1 AP-1W _dpezy

mpi4py 中的单边通信相关操作_win.fence() win.free()-程序员宅基地

文章浏览阅读511次。本文从本人简书博客同步过来在上一篇中我们简要地介绍了 mpi4py 中的单边通信概念,下面我们将介绍单边通信的相关操作。创建/释放窗口对象注意:在使用单边通信操作之前,所有进程都须通过共同参与的创建窗口操作公开声明自己可供访问的内存空间。创建和释放窗口对象的方法(MPI.Win 类方法)接口如下:Create(type cls, memory, int disp_unit=..._win.fence() win.free()

MIT 6.824 Lab1 MapReduce实现思路_mitmapreduce实现csdn-程序员宅基地

文章浏览阅读551次,点赞23次,收藏7次。mit6.824的lab1,实现MapReduce_mitmapreduce实现csdn

使用Android studio创建一个简单项目_android studio简单项目-程序员宅基地

文章浏览阅读6.6k次,点赞10次,收藏82次。在刚开始学Android开发时,下载好了Android studio,但是不知道如何下手,现在就通过一个简单的小项目熟悉如何使用这个软件进行Android开发。前提:下载好Android studio并配置好相关环境。首先介绍一下Android开发过程中需要修改的三类文件:位于java包下的各类activity文件:实现了用户与软件的交互,主要为java代码实现。位于res包下的.xml文件:在layout包里的为布局文件,即Android界面显示的视图,而drawable里则放置了某个控件的_android studio简单项目

Bootstrap 弹出框-程序员宅基地

文章浏览阅读3.5k次。一、Bootstrap 弹出框弹出框控件类似于提示框,它在鼠标点击到元素后显示,与提示框不同的是它可以显示更多的内容。注意: 弹出框要写在 jQuery 的初始化代码里: 然后在指定的元素上调用 popover() 方法。1.1 基本弹出框通过向元素添加 data-toggle=“popover” 来来创建弹出框。title 属性的内容为弹出框的标题,data-content 属性..._bootstrap 弹出框

推荐文章

热门文章

相关标签