JVM源码剖析之软、弱、虚引用的处理细节

news2024/12/27 1:20:39

目录

写在前面:

源码剖析:

Java层面:

JVM层面:

使用危险点:

总结:


版本信息:

jdk版本:jdk8u40
垃圾回收器:Serial new/old

写在前面:

不同的垃圾回收器所对应的算法不一样,效率更不一样。在JDK8中默认为ParallelScavenge new/old。而笔者写文时使用Serial new/old,两者算法一致,只不过ParallelScavenge new/old发挥了多线程的优势,所以在算法细节上大同小异。

对于大大大大大大部分Java业务场景来说都是强引用,基本上不会使用到软、弱、虚引用。而在JDK1.2推出的软、弱、虚引用大部分出现场景都是在缓存中,在JDK类库ThreadLocal、WeakHashMap。框架:Mybatis、Netty、以及各种缓存框架等等。至于为什么要用在缓存中呢,也很好理解,因为这些引用实际上可有可无,完美契合于缓存,在有的时候给系统加速,在系统内存紧张的时候清除缓存给核心业务使用。

源码剖析:

这篇文章的篇幅会比较长,也不容易理解。因为对于软、弱、虚引用处理细节体现在Java层面和JVM层面,恰好JVM层面又与GC垃圾回收细节强关联,所以笔者只能竭尽所能~

Java层面:

在Java层面,就不得不补充一些前置知识,以及Java层面如何处理这些引用。

软、弱、虚引用的基本表示

上图是软、弱、虚引用最基本的表示,这里需要区分2个不同的对象,一个是软、弱、虚对象,一个是软、弱、虚引用的对象。

软、弱、虚对象
软、弱、虚引用的对象

所以下文需要介绍软、弱、虚对象的回收机制和区分具体的使用场景(相信大家八股文多多少少背过,这里跟八股文会有一点点出入)

软:当系统资源紧张但是又没那么那么紧张的时候根据最近最少使用回收软引用(LRU算法),当系统资源非常非常紧张的时候直接全部回收。可以携带引用对象,也可以使用ReferenceQueue去处理伴随对象

弱:只要发生GC就会回收。可以携带引用对象,也可以使用ReferenceQueue去处理伴随对象

虚:只要发生GC就会回收。不能携带引用对象。只能使用ReferenceQueue去处理伴随对象

上文有介绍软、弱、虚对象的回收机制,这里有提到ReferenceQueue队列,所以下文开始介绍Java层面如何使用ReferenceQueue做回收。

// java.lang.ref.Reference类中静态方法
static {
    // 创建一个ReferenceHandler线程。
    Thread handler = new ReferenceHandler(tg, "Reference Handler");

    handler.setPriority(Thread.MAX_PRIORITY);   
    handler.setDaemon(true);    
    handler.start();
}

在java.lang.ref.Reference类中静态方法中创建了一个ReferenceHandler线程。所以接下来看线程的执行体。

public void run() {
    while (true) {
        tryHandlePending(true);
    }
}

static boolean tryHandlePending(boolean waitForNotify) {
    Reference<Object> r;
    …………

    synchronized (lock) {
        if (pending != null) {
            // 如果pedding不为null,那么就代表GC回收到了软、弱、虚引用
            r = pending;
            pending = r.discovered;
            r.discovered = null;
        } else {
            if (waitForNotify) {
                // 当还没产生pending链表的时候(也即没有触发GC回收软、弱、虚引用)
                // 当前线程直接去阻塞,等待被JVM唤醒。
                lock.wait();
            }
            return waitForNotify;
        }
    }
    …………

    // 把GC回收到了软、弱、虚引用放入到对应的ReferenceQueue中。
    // 等待业务自己去处理ReferenceQueue队列。
    ReferenceQueue<? super Object> q = r.queue;
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    return true;
}

boolean enqueue(Reference<? extends T> r) { 
    synchronized (lock) {
        ReferenceQueue<?> queue = r.queue;
        if ((queue == NULL) || (queue == ENQUEUED)) {
            return false;
        }
        // 头插法
        r.queue = ENQUEUED;
        r.next = (head == null) ? r : head;
        head = r;
        queueLength++;
        lock.notifyAll();
        return true;
    }
}
  1. 判断当前pedding 是否为空
  2. 如果为空,代表当前GC没有触发回收软、弱、虚引用
  3. 如果不为空,代表当前GC回收软、弱、虚引用,并且放入到pedding中
  4. 把pedding的值放入到ReferenceQueue队列中
  5. 业务维护的ReferenceQueue队列,从队列中poll值去做对应的处理。

所以ReferenceQueue队列是业务层面自己维护,传入到Reference中,GC回收软、弱、虚引用后会把当前Reference放入到ReferenceQueue队列中。业务层面再通过poll取到Reference做对应的处理(可以是处理伴随对象)

下面是WeakHashMap对ReferenceQueue的使用。

WeakHashMap的使用

至此,Java层面的处理已经看完,接下来我们需要明白JVM是如何GC处理软、弱、虚引用,并且放入到pedding中,这样就全部闭环~

JVM层面:

具体的GC回收过程本文肯定是忽略,当作黑盒即可~

/hotspot/src/share/vm/memory/genCollectedHeap.cpp 文件中

// /hotspot/src/share/vm/memory/genCollectedHeap.cpp
// 这里是GC垃圾回收的过程
void GenCollectedHeap::do_collection(bool  full,
                                     bool   clear_all_soft_refs,
                                     size_t size,
                                     bool   is_tlab,
                                     int    max_level) {
  …………

  // 是否需要清理所有的软引用
  const bool do_clear_all_soft_refs = clear_all_soft_refs ||
                          collector_policy()->should_clear_all_soft_refs();
  {
    …………

    for (int i = starting_level; i <= max_level; i++) {
      if (_gens[i]->should_collect(full, size, is_tlab)) {
        {
          // 从这里可以看出,不同带都有一个引用的处理器。
          ReferenceProcessor* rp = _gens[i]->ref_processor();

          rp->enable_discovery(true /*verify_disabled*/, true /*verify_no_refs*/);
          // 改变回收策略
          rp->setup_policy(do_clear_all_soft_refs); 
          
          // 不同代进行垃圾回收。
          _gens[i]->collect(full, do_clear_all_soft_refs, size, is_tlab);

          // gc回收后,把回收的软、弱、虚引用赋值给pedding,交给Java层面处理
          // 这里也对应到上下文了。
          if (!rp->enqueuing_is_done()) {
            rp->enqueue_discovered_references();
          } else {    
            rp->set_enqueuing_is_done(false);
          }
        }

      }
    }
    …………
  }
}
  1. 这里根据策略决定是否要清理所有的软引用(一般是内存资源极度不够的时候才会)
  2. 新生代或者老年代的垃圾回收器进行垃圾回收(这也对应了YGC和FullGC)
  3. 在GC回收后把回收到的软、弱、虚引用赋值给pedding,交给Java层面处理

所以接下来需要看到老年代的垃圾回收器进行垃圾回收的时候如何处理的软、弱、虚引用。

/hotspot/src/share/vm/memory/defNewGeneration.cpp 文件中

// 新生代的垃圾回收
void DefNewGeneration::collect(bool   full,
                               bool   clear_all_soft_refs,
                               size_t size,
                               bool   is_tlab) {
  …………

  // 用于扫描软、弱、虚引用是否存活。
  ScanWeakRefClosure scan_weak_ref(this);

  // 对象扫描器,用于GC root的复制
  FastScanClosure fsc_with_no_gc_barrier(this, false);
  FastScanClosure fsc_with_gc_barrier(this, true);

  // Klass的GC root扫描。
  KlassScanClosure klass_scan_closure(&fsc_with_no_gc_barrier,
                                      gch->rem_set()->klass_rem_set());

  // GC Root广度搜索的扫描器
  // 也就是找到GC Root的引用作为下一批GC Root,直到找完所有的存活对象。
  FastEvacuateFollowersClosure evacuate_followers(gch, _level, this,
                                                  &fsc_with_no_gc_barrier,
                                                  &fsc_with_gc_barrier);
  // 寻找根GC Root。
  // 因为是新生代的算法,所以这里会把根GC Root复制到to区或者是老年代。
  gch->gen_process_strong_roots(_level,
                                true,  // Process younger gens, if any,
                                       // as strong roots.
                                true,  // activate StrongRootsScope
                                true,  // is scavenging
                                SharedHeap::ScanningOption(so),
                                &fsc_with_no_gc_barrier,
                                true,   // walk *all* scavengable nmethods
                                &fsc_with_gc_barrier,
                                &klass_scan_closure);

  // 根据GC Root找出GC Root所有的引用
  // 因为这里是处理引用,所以这里会处理软、弱、虚等等引用。
  evacuate_followers.do_void();

  // 用于处理引用对象的存活。
  FastKeepAliveClosure keep_alive(this, &scan_weak_ref);
  
  ReferenceProcessor* rp = ref_processor();
  // 根据clear_all_soft_refs这个bool字段决定是否清理全部的软引用。
  rp->setup_policy(clear_all_soft_refs);
  // 具体的处理细节。
  const ReferenceProcessorStats& stats =
  rp->process_discovered_references(&is_alive, &keep_alive, &evacuate_followers,
                                    NULL, _gc_timer);
                                    
  …………
}

以上是YGC时,新生代的回收,不管是Full GC还是YGC都会对软、弱、虚引用做处理,所以挑选YGC来做分析(因为YGC简单一些,但是对于软、弱、虚引用做处理都是一样的)

由于处理软、弱、虚引用一定会和GC回收细节强关联,所以很多是GC回收的细节代码,笔者有吧注释给上,并且当作黑盒就好。

  1. 创建好各种GC回收所需要扫描器
  2. 这些扫描器最终都有一个共同的任务,就是把存活对象复制到to区或者老年代
  3. GC Root的扫描
  4. 根据已有的GC Root做广度遍历,找出GC Root引用的对象作为下一批GC Root继续找引用,直到遍历完整个堆
  5. 软、弱、虚引的处理(这也是接下来的重点)

经过GC Root全部查找后,Java堆的对象排布可能是这样

注意,这里的软、弱、虚对象和软、弱、虚对象所引用对象是有区别的,复制算法只会把软、弱、虚对象做复制,软、弱、虚对象引用的对象要后续再做处理。

在看ReferenceProcessor类process_discovered_references方法之前,需要介绍一下ReferenceProcessor类。

/hotspot/src/share/vm/memory/referenceProcessor.hpp 文件中

class ReferenceProcessor : public CHeapObj<mtGC> {
	protected:
		static ReferencePolicy*   _default_soft_ref_policy;

	  static ReferencePolicy*   _always_clear_soft_ref_policy;

	  ReferencePolicy*          _current_soft_ref_policy;

	  uint             _num_q;             

	  uint             _max_num_q;          

	  // 作为基地址。
	  DiscoveredList* _discovered_refs;     

	  DiscoveredList* _discoveredSoftRefs;    // 基于基地址的第一部分
	  DiscoveredList* _discoveredWeakRefs;    // 基于基地址的第二部分
	  DiscoveredList* _discoveredFinalRefs;   // 基于基地址的第三部分
	  DiscoveredList* _discoveredPhantomRefs; // 基于基地址的第四部分
}

可以很清楚的看到,这里有策略对象和几个DiscoveredList链表。链表中是保存了被处理的软、弱、虚的Java对象。并且在遍历完所有的GC Root后,这里会把软、弱、虚的Java对象行程如下的链表。

所以接下来,看到process_discovered_references方法具体处理细节。

/hotspot/src/share/vm/memory/referenceProcessor.cpp 文件中

ReferenceProcessorStats ReferenceProcessor::process_discovered_references(
  BoolObjectClosure*           is_alive,
  OopClosure*                  keep_alive,
  VoidClosure*                 complete_gc,
  AbstractRefProcTaskExecutor* task_executor,
  GCTimer*                     gc_timer) {

  _soft_ref_timestamp_clock = java_lang_ref_SoftReference::clock();

  // 软引用的处理
  size_t soft_count = 0;
  {
    GCTraceTime tt("SoftReference", trace_time, false, gc_timer);
    soft_count =
      process_discovered_reflist(_discoveredSoftRefs, _current_soft_ref_policy, true,
                                 is_alive, keep_alive, complete_gc, task_executor);
  }

  // 修改时间戳。
  // 时间戳用于LRU算法,寻找最近最少使用的软引用。
  update_soft_ref_master_clock();

  // 弱引用的处理
  size_t weak_count = 0;
  {
    GCTraceTime tt("WeakReference", trace_time, false, gc_timer);
    weak_count =
      process_discovered_reflist(_discoveredWeakRefs, NULL, true,
                                 is_alive, keep_alive, complete_gc, task_executor);
  }

  // 最终引用处理,这个一般是用于收尾工作
  size_t final_count = 0;
  {
    GCTraceTime tt("FinalReference", trace_time, false, gc_timer);
    final_count =
      process_discovered_reflist(_discoveredFinalRefs, NULL, false,
                                 is_alive, keep_alive, complete_gc, task_executor);
  }

  // 虚引用处理
  size_t phantom_count = 0;
  {
    GCTraceTime tt("PhantomReference", trace_time, false, gc_timer);
    phantom_count =
      process_discovered_reflist(_discoveredPhantomRefs, NULL, false,
                                 is_alive, keep_alive, complete_gc, task_executor);
  }

  return ReferenceProcessorStats(soft_count, weak_count, final_count, phantom_count);
}

可以看到不管是软、弱、虚引用的处理都是调用process_discovered_reflist方法。

/hotspot/src/share/vm/memory/referenceProcessor.cpp 文件中

size_t
ReferenceProcessor::process_discovered_reflist(
  DiscoveredList               refs_lists[],
  ReferencePolicy*             policy,
  bool                         clear_referent,
  BoolObjectClosure*           is_alive,
  OopClosure*                  keep_alive,
  VoidClosure*                 complete_gc,
  AbstractRefProcTaskExecutor* task_executor)
{
  // 根据策略决定是否能处理引用。
  // 策略只有软引用才有。
  // 弱、虚引用是不配有策略的,弱、虚引用只要发生GC久回收
  if (policy != NULL) {
    for (uint i = 0; i < _max_num_q; i++) {
      process_phase1(refs_lists[i], policy,
                     is_alive, keep_alive, complete_gc);
    }
  } 

  // 遍历剩下的队列,继续做过滤操作
  // 这个过滤是判断软、弱、虚对象引用的对象是否还活着,如果活着那就不能处理这个引用。
  for (uint i = 0; i < _max_num_q; i++) {
    process_phase2(refs_lists[i], is_alive, keep_alive, complete_gc);
  }

  // 根据clear_referent变量决定最终是否处理引用。
  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;
}
  1. 软引用才有策略,根据策略决定是否回收对象,如果策略不让回收的对象,那么就需要从DiscoveredList链表中remove,并且保持存活,直到下次GC 再尝试回收
  2. 经过策略的决策后,活下来的对象继续做过滤,这次过滤是判断软、弱、虚对象引用的对象是否还活着,如果活着那就不能处理这个引用(所以用不好,随时可能内存泄漏),如果引用对象是存活的,那么就需要从DiscoveredList链表中remove,并且保持存活,直到下次GC 再尝试回收
  3. 经过第二步的过滤,活下来的对象还要根据clear_referent变量决定最终是否处理引用对象。这一步只有虚引用才不能处理引用(因为虚对象不能引用对象),如果clear_reference为false,那么就需要从DiscoveredList链表中remove,并且保持存活,直到下次GC 再尝试回收,但是虚引用为false也没关系,因为他指向本来就是null。

所以接下来可以看一下软引用的策略处理。

这里就比较简单了,要不永远回收、要不永远不回收,要不根据LRU算法得到最近最少使用的软引用,优先回收没用的~

所以在本文最上面写到:软引用,在内存紧张的时候但是不是非常紧张的时候会回收最少使用的(根据LRU算法),在内存非常非常紧张的时候策略直接是AlwaysCLearPolicy策略了,就回收所有软引用~

当经过层层过滤后,最终存活的软、弱、虚对象就存在不同DiscoveredList链表中。我们在Java层面是从pedding获取到对象,所以这边还需要把不同的DiscoveredList链表设置到pedding中。

所以接下来回到GenCollectedHeap::do_collection方法,看到enqueue_discovered_references方法

/hotspot/src/share/vm/memory/referenceProcessor.cpp 文件中

bool ReferenceProcessor::enqueue_discovered_references(AbstractRefProcTaskExecutor* task_executor) {
  return enqueue_discovered_ref_helper<oop>(this, task_executor); 
}

template <class T>
bool enqueue_discovered_ref_helper(ReferenceProcessor* ref,
                                   AbstractRefProcTaskExecutor* task_executor) {
  // 拿到Reference类中的pedding变量的地址,因为pending是一个静态变量,所以从mirror拿。
  T* pending_list_addr = (T*)java_lang_ref_Reference::pending_list_addr();

  // 把链表链到pedding上
  ref->enqueue_discovered_reflists((HeapWord*)pending_list_addr, task_executor);
  return old_pending_list_value != *pending_list_addr;
}

void ReferenceProcessor::enqueue_discovered_reflists(HeapWord* pending_list_addr,
  AbstractRefProcTaskExecutor* task_executor) {
  // 串行化遍历4个链表。
  for (uint i = 0; i < _max_num_q * number_of_subclasses_of_ref(); i++) {
    // 只需要把每个链表的头部链到pending就行了。
    enqueue_discovered_reflist(_discovered_refs[i], pending_list_addr);
    _discovered_refs[i].set_head(NULL);
    _discovered_refs[i].set_length(0);
  }
}

这里就是把经过层层筛选的软、弱、虚链表中的对象链到Reference类中pedding字段上。最终交给Java层面的ReferenceHandler线程去处理。

使用危险点:

上面我们把所有的处理细节都分析完了,所以接下来回忆到一处细节点。

/hotspot/src/share/vm/memory/referenceProcessor.cpp 文件中,process_discovered_reflist方法,这个方法是做过滤处理,在process_phase2这个方法做过滤的时候,会判断软、弱、虚对象的引用对象是否存活,如果存活的情况下是不能做回收的。所以这里很容易发生内存泄露,看到如下的Java代码。

public class ReferenceTest {

    public static void main(String[] args) {

        WeakHashMap<Object,User> weakHashMap = new WeakHashMap<>();
        Object o1 = new Object();

        weakHashMap.put(o1,new User("lihayyds"));   // 只要o1不释放这就是内存泄露。
        weakHashMap.put("1",new User("lihayyds"));  // "1"是JVM字符串常量池指向的,所以这也是一个内存泄露

        byte[] bytes1 = new byte[1024 * 1024 * 1024];
        byte[] bytes2 = new byte[1024 * 1024 * 1024];
        byte[] bytes3 = new byte[1024 * 1024 * 1024];
        byte[] bytes4 = new byte[1024 * 1024 * 1024];
        byte[] bytes5 = new byte[1024 * 1024 * 1024];

        // 手动Full GC。
        System.gc();

        // Reference Queue 处理后的大小,因为在size里面会去处理
        System.out.println("Reference Queue 处理后的大小为:"+weakHashMap.size());
    }
}

class User{

    String name;

    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

结果如上图所示,发生GC后,弱引用根本没有回收,就是因为弱引用指向的对象被其他地方强引用,导致于在做筛选的过程中,被筛选出去了,不能去回收它。那么如果外部的这个强引用不释放,那么这个弱引用引用的对象和弱引用对象永远无法回收,从而无法达到弱引用的优势,变相地说,这就是内存泄漏~

那么下面改进一下Java代码。

public class ReferenceTest {

    public static void main(String[] args) {

        WeakHashMap<Object,User> weakHashMap = new WeakHashMap<>();
        
        // 这里直接不让外部引用这个Object对象
        weakHashMap.put(new Object(),new User("lihayyds"));   
        weakHashMap.put(new Object(),new User("lihayyds"));  

        byte[] bytes1 = new byte[1024 * 1024 * 1024];
        byte[] bytes2 = new byte[1024 * 1024 * 1024];
        byte[] bytes3 = new byte[1024 * 1024 * 1024];
        byte[] bytes4 = new byte[1024 * 1024 * 1024];
        byte[] bytes5 = new byte[1024 * 1024 * 1024];

        // 手动Full GC。
        System.gc();

        // Reference Queue 处理后的大小,因为在size里面会去处理
        System.out.println("Reference Queue 处理后的大小为:"+weakHashMap.size());
    }
}

class User{

    String name;

    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

如上图所示,弱引用引用的对象不让外部有强引用后,直接正常了,发生GC就回收了~

总结:

因为流程特别大,强关联GC回收部分,所以笔者只能竭尽所能,源码注释+总结+画图来尽量描述明白~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1194907.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PROFINET和UDP、MODBUS-RTU通信速度对比实验

这篇博客我们介绍PROFINET 和MODBUS-RTU通信实验时的数据刷新速度,以及这种速度不同对控制系统带来的挑战都有哪些,在介绍这篇对比实验之前大家可以参考下面的文章链接: S7-1200PLC和SMART PLC的PN智能从站通信 S7-200 SMART 和 S7-1200PLC进行PROFINET IO通信-CSDN博客文…

远程电脑未连接显示器时分辨率太小的问题处理

背景&#xff1a;单位电脑显示器坏了&#xff0c;使用笔记本通过向日葵远程连接&#xff0c;发现分辨率只有800*600并且不能修改&#xff0c;网上找了好久找到了处理方法这里记录一下&#xff0c;主要用到的是一个虚拟显示器软件usbmmidd_v2 1)下载usbmmidd_v2 2&#xff09;…

Flink SQL自定义表值函数(Table Function)

使用场景&#xff1a; 表值函数即 UDTF&#xff0c;⽤于进⼀条数据&#xff0c;出多条数据的场景。 开发流程&#xff1a; 实现 org.apache.flink.table.functions.TableFunction 接⼝实现⼀个或者多个⾃定义的 eval 函数&#xff0c;名称必须叫做 eval&#xff0c;eval ⽅法…

什么是进程等待?

什么是进程等待 在了解进程等待之前&#xff0c;我们要回顾一下什么是僵尸进程&#xff1a;是指一个已经终止执行的进程&#xff0c;但其父进程还没有通过 wait() 系统调用来获取该进程的退出状态信息。当一个进程正常退出或者被终止时&#xff0c;其所占用的系统资源会被操作…

基于SSM的智能物业管理网站的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

rabbitMq虚拟主机概念

虚拟主机是RabbitMQ中的一种逻辑隔离机制&#xff0c;用于将消息队列、交换机以及其他相关资源进行隔离。 在RabbitMQ中&#xff0c;交换机&#xff08;Exchange&#xff09;用于接收生产者发送的消息&#xff0c;并根据特定的路由规则将消息分发到相应的队列中。而虚拟主机则…

使用 pubsub-js 进行消息发布订阅

npm 包地址 github 包地址 pubsub-js 是一个轻量级的 JavaScript 基于主题的消息订阅发布库 &#xff0c;压缩后小于1b。它具有使用简单、性能高效、支持多平台等优点&#xff0c;可以很好地满足各种需求。 功能特点&#xff1a; 无依赖同步解耦ES3 兼容。pubsub-js 能够在…

快速修复因相机断电导致视频文件打不开的问题

3-5 本文主要解决因相机突然断电导致拍摄的视频文件打不开的问题。 在日常工作中&#xff0c;有时候需要使用相机拍摄视频&#xff0c;比如现在有不少短视频拍摄的需求&#xff0c;如果因电池突然断电的原因&#xff0c;导致拍出来的视频播放不了&#xff0c;这时候就容易出大…

el-table 多表格弹窗嵌套数据显示异常错乱问题

1、业务背景 使用vueelement开发报表功能时&#xff0c;需要列表上某列的超链接按钮弹窗展示&#xff0c;在弹窗的el-table列表某列中再次使用超链接按钮点开弹窗&#xff0c;以此类推多表格弹窗嵌套&#xff0c;本文以弹窗两次为例 最终效果如下示例页面 2、具体实现和问题…

hub.docker访问不了的问题(一步解决)

暂时我也不清楚&#xff0c;但是下面这个网址可以用(可以先用着)Docker Hub Container Image Library | App Containerization (axlinux.top)https://hub.axlinux.top/

美格智能5G RedCap模组顺利完成中国联通5G物联网OPENLAB开放实验室认证

近日&#xff0c;美格智能5G RedCap模组SRM813Q顺利通过中国联通5G物联网OPENLAB开放实验室端到端的测试验收&#xff0c;并获得OPENLAB实验室的认证证书。这标志着该模组产品各项性能均已符合RedCap商用标准&#xff0c;为5G RedCap规模商用奠定了坚实基础。 中国联通5G物联网…

使用WinDbg分析软件突然崩溃的问题

为了测试windbg有多么牛逼&#xff0c;所以仅仅只是测试一下&#xff0c;属于事后诸葛亮型&#xff0c;也只是为了验证一下&#xff0c;把此方法学会即可。 模拟场景&#xff1a; 软件运行后&#xff0c;点击按钮&#xff0c;直接崩溃掉&#xff0c;什么提示都没有。因此&…

统计学习笔记第 1 部分:Hoeffding 的不等式推导与模拟

照片由Unsplash上的Luca Bravo拍摄 1&#xff1a;背景与动机 霍夫丁不等式是数理统计和机器学习 (ML) 中的一个重要的集中不等式&#xff0c;广泛应用于统计学习理论等理论领域以及强化学习等应用领域。 我注意到&#xff0c;在机器学习社区的一些地方&#xff0c;通常将 Hoeff…

图数据库Neo4j详解

文章目录 第一章 图和Neo4j1.1 图数据库概念1.1.1 图论起源1.1.2 节点-关系及图1.1.3 图数据库1.1.4 图数据库分类1.1.4 图数据库应用场景1.1.5 与关系型数据库对比1.1.6 图数据库优势 1.2 Neo4j介绍1.2.1 Neo4j是什么1.2.2 Neo4j特点1.2.3 Neo4j的优势1.2.4 Neo4j的限制1.2.5 …

网络安全(黑客)-高效自学

首先给大家简单介绍一下网络安全&#xff1a; 1.什么是网络安全&#xff1f; 网络安全可以基于攻击和防御视角来分类&#xff0c;我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、“安全运营”、“安全运维”则研究防御技术。 无论网络、…

eNsp使用技巧

文章目录 显示网格对齐到网络水平对齐和垂直对齐 显示所有接口添加文本进入CLI界面数据抓包方式一方式二 显示网格 对齐到网络 水平对齐和垂直对齐 显示所有接口 添加文本 进入CLI界面 数据抓包 方式一 方式二

No source control providers registered

使用vscode时碰到这个问题 git扩展没启动

Linux前言

目录 Linux的应用场景 Linux的应用现状 Linux的版本 操作系统 什么是Linux操作系统&#xff1f; 为什么要用操作系统&#xff1f; 上篇我们介绍了Linux的历史背景和安装环境。 Linux的应用场景 因为Linux操作系统是开源&#xff0c;所以它流向各个领域。 场景1&…

【有限元方法】Newton-Raphson Method

Newton-Raphson Method Linear vs Nonlinear Analysis: At this point, we can conduct a linear analysis no problem ∫ ∑ i , j 1 3 σ i j ε i j ∗ d v ∫ t n ⋅ u ∗ d s ∫ ρ b ⋅ u ∗ d v ⇒ ∫ e [ B ] T [ C ] [ B ] d x ⏟ k e u e ∫ ∂ e [ N ] T t n …

2022最新版-李宏毅机器学习深度学习课程-P50 BERT的预训练和微调

模型输入无标签文本&#xff08;Text without annotation&#xff09;&#xff0c;通过消耗大量计算资源预训练&#xff08;Pre-train&#xff09;得到一个可以读懂文本的模型&#xff0c;在遇到有监督的任务是微调&#xff08;Fine-tune&#xff09;即可。 最具代表性是BERT&…