FinalReference 如何使 GC 过程变得拖拖拉拉

本文基于 OpenJDK17 进行讨论,垃圾回收器为 ZGC。

提示: 为了方便大家索引,特将在上篇文章 《以 ZGC 为例,谈一谈 JVM 是如何实现 Reference 语义的》 中讨论的众多主题独立出来。


FinalReference 对于我们来说是一种比较陌生的 Reference 类型,因为我们好像在各大中间件以及 JDK 中并没有见过它的应用场景,事实上,FinalReference 被设计出来的目的也不是给我们用的,而是给 JVM 用的,它和 Java 对象的 finalize() 方法执行机制有关。

public class Object {
    @Deprecated(since="9")
    protected void finalize() throws Throwable { }
}

我们看到 finalize() 方法在 OpenJDK9 中已经被标记为 @Deprecated 了,并不推荐使用。笔者其实一开始也并不想提及它,但是思来想去,本文是主要介绍各类 Refernce 语义实现的,前面笔者已经非常详细的介绍了 SoftReference,WeakReference,PhantomReference 在 JVM 中的实现。

在文章的最后何不利用这个 FinalReference 将前面介绍的内容再次为大家串联一遍,加深一下大家对 Reference 整个处理链路的理解,基于这个目的,才有了本小节的内容。但笔者的本意并不是为了让大家使用它。

下面我们还是按照老规矩,继续从 JDK 以及 JVM 这两个视角全方位的介绍一下 FinalReference 的实现机制,并为大家解释一下这个 FinalReference 如何使整个 GC 过程变得拖拖拉拉,磨磨唧唧~~~

1. 从 JDK 视角看 FinalReference

image

FinalReference 本质上来说它也是一个 Reference,所以它的基本语义和 WeakReference 保持一致,JVM 在 GC 阶段对它的整体处理流程和 WeakReference 也是大致一样的。

唯一一点不同的是,由于 FinalReference 是和被它引用的 referent 对象的 finalize() 执行有关,当一个普通的 Java 对象在整个 JVM 堆中只有 FinalReference 引用它的时候,按照 WeakReference 的基础语义来讲,这个 Java 对象就要被回收了。

但是在这个 Java 对象被回收之前,JVM 需要保证它的 finalize()被执行到,所以 FinalReference 会再次将这个 Java 对象重新标记为 alive,也就是在 GC 阶段重新复活这个 Java 对象。

后面的流程就和其他 Reference 一样了,FinalReference 也会被 JVM 加入到 _reference_pending_list 链表中,ReferenceHandler 线程被唤醒,随后将这个 FinalReference 从 _reference_pending_list 上摘下,并加入到与其关联的 ReferenceQueue 中,这个流程就是我们第三小节主要讨论的内容,大家还记得吗 ?

image

和 Cleaner 不同的是,对于 FinalReference 来说,在 JDK 中还有一个叫做 FinalizerThread 线程来专门处理它,FinalizerThread 线程会不断的从与 FinalReference 关联的 ReferenceQueue 中,将所有需要被处理的 FinalReference 摘下,然后挨个执行被它所引用的 referent 对象的 finalize() 方法。

随后在下一轮的 GC 中,FinalReference 对象以及它引用的 referent 对象才会被 GC 回收掉。

以上就是 FinalReference 被 JVM 处理的整个生命周期,下面让我们先回到最初的起点,这个 FinalReference 是怎么和一个 Java 对象关联起来的呢 ?

我们知道 FinalReference 是和 Java 对象的 finalize() 方法执行有关的,如果一个 Java 类没有重写 finalize() 方法,那么在创建这个 Java 类的实例对象的时候将不会和这个 FinalReference 有任何的瓜葛,它就是一个普通的 Java 对象。

但是如何一个 Java 类重写了 finalize() 方法 ,那么在创建这个 Java 类的实例对象的时候, JVM 就会将一个 FinalReference 实例和这个 Java 对象关联起来。

instanceOop InstanceKlass::allocate_instance(TRAPS) {
  // 判断这个类是否重写了 finalize() 方法
  bool has_finalizer_flag = has_finalizer(); 
  instanceOop i;
  // 创建实例对象
  i = (instanceOop)Universe::heap()->obj_allocate(this, size, CHECK_NULL);
  // 如果该对象重写了  finalize() 方法
  if (has_finalizer_flag && !RegisterFinalizersAtInit) {
    // JVM 这里就会调用 Finalizer 类的静态方法 register
    // 将这个 Java 对象与 FinalReference 关联起来
    i = register_finalizer(i, CHECK_NULL);
  }
  return i;
}

我们看到,在 JVM 创建对象实例的时候,会首先通过 has_finalizer() 方法判断这个 Java 类有没有重写 finalize() 方法,如果重写了就会调用 register_finalizer 方法,JVM 最终会调用 JDK 中的 Finalizer 类的静态方法 register。

final class Finalizer extends FinalReference<Object> {
    static void register(Object finalizee) {
        new Finalizer(finalizee);
    }
}

在这里 JVM 会将刚刚创建出来的普通 Java 对象 —— finalizee,与一个 Finalizer 对象关联起来, Finalizer 对象的类型正是 FinalReference 。这里我们可以看到,当一个 Java 类重写了 finalize() 方法的时候,每当创建一个该类的实例对象,JVM 就会自动创建一个对应的 Finalizer 对象

Finalizer 的整体设计和之前介绍的 Cleaner 非常相似,不同的是 Cleaner 是一个 PhantomReference,而 Finalizer 是一个 FinalReference。

它们都有一个 ReferenceQueue,只不过 Cleaner 中的那个基本没啥用,但是 Finalizer 中的这个 ReferenceQueue 却有非常重要的作用。

它们内部都有一个双向链表,里面包含了 JVM 堆中所有的 Finalizer 对象,用来确保这些 Finalizer 在执行 finalizee 对象的 finalize() 方法之前不会被 GC 回收掉。

final class Finalizer extends FinalReference<Object> { 

    private static ReferenceQueue<Object> queue = new ReferenceQueue<>();

    // 双向链表,保存 JVM 堆中所有的 Finalizer 对象,防止 Finalizer 被 GC 掉
    private static Finalizer unfinalized = null;

    private Finalizer next, prev;

    private Finalizer(Object finalizee) {
        super(finalizee, queue);
        // push onto unfinalized
        synchronized (lock) {
            if (unfinalized != null) {
                this.next = unfinalized;
                unfinalized.prev = this;
            }
            unfinalized = this;
        }
    }
}

在创建 Finalizer 对象的时候,首先会调用父类方法,将被引用的 Java 对象以及 ReferenceQueue 关联注册到 FinalReference 中。

    Reference(T referent, ReferenceQueue<? super T> queue) {
        // 被引用的普通 Java 对象
        this.referent = referent;
        //  Finalizer 中的 ReferenceQueue 实例(全局)
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }

最后将这个 Finalizer 对象插入到双向链表 —— unfinalized 中。

image

这个结构是不是和第三小节中我们介绍的 Cleaner 非常相似。

image

而 Cleaner 最后是被 ReferenceHandler 线程执行的,那这个 Finalizer 最后是被哪个线程执行的呢 ?

这里就要引入另一个 system thread 了,在 Finalizer 类初始化的时候会创建一个叫做 FinalizerThread 的线程。

final class Finalizer extends FinalReference<Object> { 
    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        // 获取 system thread group
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        // 创建 system thread : FinalizerThread
        Thread finalizer = new FinalizerThread(tg);
        finalizer.setPriority(Thread.MAX_PRIORITY - 2);
        finalizer.setDaemon(true);
        finalizer.start();
    }
}

FinalizerThread 的优先级被设置为 Thread.MAX_PRIORITY - 2,还记得 ReferenceHandler 线程的优先级吗 ?

public abstract class Reference<T> {

    static {
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        // 设置 ReferenceHandler 线程的优先级为最高优先级
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();  
    }
}

而一个普通的 Java 线程,它的默认优先级是多少呢 ?

    /**
     * The default priority that is assigned to a thread.
     */
    public static final int NORM_PRIORITY = 5;

我们可以看出这三类线程的调度优先级为:ReferenceHandler > FinalizerThread > Java 业务 Thead

FinalizerThread 线程在运行起来之后,会不停的从一个 queue 中获取 Finalizer 对象,然后执行 Finalizer 中的 runFinalizer 方法,这个逻辑是不是和 ReferenceHandler 线程不停的从 _reference_pending_list 中获取 Cleaner 对象,然后执行 Cleaner 的 clean 方法非常相似。

    private static class FinalizerThread extends Thread {

        public void run() {
            for (;;) {
                try {
                    Finalizer f = (Finalizer)queue.remove();
                    f.runFinalizer(jla);
                } catch (InterruptedException x) {
                    // ignore and continue
                }
            }
        }
    }

这个 queue 就是 Finalizer 中定义的 ReferenceQueue,在 JVM 创建 Finalizer 对象的时候,会将重写了 finalize() 方法的 Java 对象与这个 ReferenceQueue 一起注册到 FinalReference 中。

final class Finalizer extends FinalReference<Object> { 
    private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
    private Finalizer(Object finalizee) {
        super(finalizee, queue);
    }
}

那这个 ReferenceQueue 中的 Finalizer 对象是从哪里添加进来的呢 ?这就又和我们第三小节中介绍的内容遥相呼应起来了,就是 ReferenceHandler 线程添加进来的。

private static class ReferenceHandler extends Thread {
    private static void processPendingReferences() {
        // ReferenceHandler 线程等待 JVM 向 _reference_pending_list 填充 Reference 对象
        waitForReferencePendingList();
        // 用于指向 JVM 的 _reference_pending_list
        Reference<?> pendingList;
        synchronized (processPendingLock) {
            // 获取 _reference_pending_list,随后将 _reference_pending_list 置为 null
            // 方便 JVM 在下一轮 GC 处理其他 Reference 对象
            pendingList = getAndClearReferencePendingList();
        }
        // 将 pendingList 中的 Reference 对象挨个从链表中摘下处理
        while (pendingList != null) {
            // 从 pendingList 中摘下 Reference 对象
            Reference<?> ref = pendingList;
            pendingList = ref.discovered;
            ref.discovered = null;
            
            // 如果该 Reference 对象是 Cleaner 类型,那么在这里就会调用它的 clean 方法
            if (ref instanceof Cleaner) {
                 // Cleaner 的 clean 方法就是在这里调用的
                ((Cleaner)ref).clean();
            } else {
                // 这里处理除 Cleaner 之外的其他 Reference 对象
                // 比如,其他 PhantomReference,WeakReference,SoftReference,FinalReference
                // 将他们添加到各自注册的 ReferenceQueue 中
                ref.enqueueFromPending();
            }
        }
    }
}

当一个 Java 对象在 JVM 堆中只有 Finalizer 对象引用,除此之外没有任何强引用或者软引用之后,JVM 首先会将这个 Java 对象复活,在本次 GC 中并不会回收它,随后会将这个 Finalizer 对象插入到 JVM 内部的 _reference_pending_list 中,然后从 waitForReferencePendingList() 方法上唤醒 ReferenceHandler 线程。

ReferenceHandler 线程将 _reference_pending_list 中的 Reference 对象挨个摘下,注意 _reference_pending_list 中保存的既有 Cleaner,也有其他的 PhantomReference,WeakReference,SoftReference,当然也有本小节的 Finalizer 对象。

如果摘下的是 Cleaner 对象那么就执行它的 clean 方法,如果是其他 Reference 对象,比如这里的 Finalizer,那么就通过 ref.enqueueFromPending(),将这个 Finalizer 对象插入到它的 ReferenceQueue 中。

当这个 ReferenceQueue 有了 Finalizer 对象之后,FinalizerThread 线程就会被唤醒,然后执行 Finalizer 对象的 runFinalizer 方法。

image

Finalizer 的内部有一个双向链表 —— unfinalized,它保存了当前 JVM 堆中所有的 Finalizer 对象,目的是为了避免在执行其引用的 referent 对象的 finalize() 方法之前被 GC 掉。

在 runFinalizer 方法中首先要做的就是将这个 Finalizer 对象从双向链表 unfinalized 上摘下,然后执行 referent 对象的 finalize() 方法。这里我们可以看到,大家在 Java 类中重写的 finalize() 方法就是在这里被执行的。

    private void runFinalizer(JavaLangAccess jla) {
        synchronized (lock) {
            if (this.next == this)      // already finalized
                return;
            // 将 Finalizer 对象从双向链表 unfinalized 上摘下
            if (unfinalized == this)
                unfinalized = this.next;
            else
                this.prev.next = this.next;
            if (this.next != null)
                this.next.prev = this.prev;
            this.prev = null;
            this.next = this;           // mark as finalized
        }

        try {
            // 获取 Finalizer 引用的 Java 对象
            Object finalizee = this.get();

            if (!(finalizee instanceof java.lang.Enum)) {
                // 执行 java 对象的 finalize() 方法
                jla.invokeFinalize(finalizee);
            }
        } catch (Throwable x) { }
        // 调用 FinalReference 的 clear 方法,将其引用的 referent 对象置为 null
        // 下一轮 gc 的时候这个  FinalReference 以及它的 referent 对象就会被回收掉了。
        super.clear();
    }

最后调用 Finalizer 对象(FinalReference类型)的 clear 方法,将其引用的 referent 对象置为 null , 在下一轮 GC 的时候, 这个 Finalizer 对象以及它的 referent 对象就会被 GC 掉。

2. 从 JVM 视角看 FinalReference

现在我们已经从 JVM 的外围熟悉了 JDK 处理 FinalReference 的整个流程,本小节,笔者将继续带着大家深入到 JVM 的内部,看看在 GC 的时候,JVM 是如何处理 FinalReference 的。

在本文 5.1 小节中,笔者为大家介绍了 ZGC 在 Concurrent Mark 阶段如何处理 Reference 的整个流程,只不过当时我们偏重于 Reference 基础语义的实现,还未涉及到 FinalReference 的处理。

但我们在明白了 Reference 基础语义的基础之上,再来看 FinalReference 的语义实现就很简单了,总体流程是一样的,只不过在一些地方做了些特殊的处理。

image

在 ZGC 的 Concurrent Mark 阶段,当 GC 线程遍历标记到一个 FinalReference 对象的时候,首先会通过 should_discover 方法来判断是否应该将这个 FinalReference 对象插入到 _discovered_list 中。判断逻辑如下:

bool ZReferenceProcessor::should_discover(oop reference, ReferenceType type) const {
  // 获取 referent 对象的地址视图
  volatile oop* const referent_addr = reference_referent_addr(reference);
  // 调整 referent 对象的视图为 remapped + mark0 也就是 weakgood 视图
  // 获取 FinalReference 引用的 referent 对象
  const oop referent = ZBarrier::weak_load_barrier_on_oop_field(referent_addr);

  // 如果 Reference 的状态就是 inactive,那么这里将不会重复将 Reference 添加到 _discovered_list 重复处理
  if (is_inactive(reference, referent, type)) {
    return false;
  }
  // referent 还被强引用关联,那么 return false 也就是说不能被加入到 discover list 中
  if (is_strongly_live(referent)) {
    return false;
  }
  // referent 还被软引用有效关联,那么 return false 也就是说不能被加入到 discover list 中
  if (is_softly_live(reference, type)) {
    return false;
  }

  return true;
}

首先获取这个 FinalReference 对象所引用的 referent 对象,如果这个 referent 对象在 JVM 堆中已经没有任何强引用或者软引用了,那么就会将 FinalReference 对象插入到 _discovered_list 中。

但是在插入之前还要通过 is_inactive 方法判断一下这个 FinalReference 对象是否在上一轮 GC 中被处理过了,

bool ZReferenceProcessor::is_inactive(oop reference, oop referent, ReferenceType type) const {
  if (type == REF_FINAL) {
    return reference_next(reference) != NULL;
  } else {
    return referent == NULL;
  }
}

对于 FinalReference 来说,inactive 的标志是它的 next 字段不为空。

public abstract class Reference<T> {
   volatile Reference next;
}

这里的 next 字段是干嘛的呢 ?比如说,这个 FinalReference 对象在上一轮的 GC 中已经被处理过了,那么在发生本轮 GC 之前,ReferenceHandler 线程就已经将这个 FinalReference 插入到一个 ReferenceQueue 中,这个 ReferenceQueue 是哪来的呢 ?

正是上小节中我们介绍的,JVM 创建 Finalizer 对象的时候传入的这个 queue。

final class Finalizer extends FinalReference<Object> { 
    private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
    private Finalizer(Object finalizee) {
        super(finalizee, queue);
    }
}

而 ReferenceQueue 中的 FinalReference 对象就是通过它的 next 字段链接起来的,当一个 FinalReference 对象被 ReferenceHandler 线程插入到 ReferenceQueue 中之后,它的 next 字段就不为空了,也就是说一个 FinalReference 对象一旦进入 ReferenceQueue,它的状态就变为 inactive 了。

那么在下一轮的 GC 中如果一个 FinalReference 对象的状态是 inactive,表示它已经被处理过了,那么就不在重复添加到 _discovered_list 中了。

如果一个 FinalReference 对象之前没有被处理过,并且它引用的 referent 对象当前也没有任何强引用或者软引用关联,那么是不是说明这个 referent 就该被回收了 ?想想 FinalReference 的语义是什么 ? 是不是就是在 referent 对象被回收之前还要调用它的 finalize() 方法 。

所以为了保证 referent 对象的 finalize() 方法得到调用,JVM 就会在 discover 方法中将其复活。随后会将 FinalReference 对象插入到 _discovered_list 中,这样在 GC 之后 ,FinalizerThread 就会调用 referent 对象的 finalize() 方法了,这里是不是和上一小节的内容呼应起来了。

void ZReferenceProcessor::discover(oop reference, ReferenceType type) {
  // 复活 referent 对象
  if (type == REF_FINAL) {
    // 获取 referent 地址视图
    volatile oop* const referent_addr = reference_referent_addr(reference);
    // 如果是 FinalReference 那么就需要对 referent 进行标记,视图改为 finalizable 表示只能通过 finalize 方法才能访问到 referent 对象
    // 因为 referent 后续需要通过 finalize 方法被访问,所以这里需要对它进行标记,不能回收
    ZBarrier::mark_barrier_on_oop_field(referent_addr, true /* finalizable */);
  }

  // Add reference to discovered list
  // 确保 reference 不在 _discovered_list 中,不能重复添加
  assert(reference_discovered(reference) == NULL, "Already discovered");
  oop* const list = _discovered_list.addr();
  // 头插法,reference->discovered = *list
  reference_set_discovered(reference, *list);
  // reference 变为 _discovered_list 的头部
  *list = reference;
}

那么 JVM 如何将一个被 FinalReference 引用的 referent 对象复活呢 ?

uintptr_t ZBarrier::mark_barrier_on_finalizable_oop_slow_path(uintptr_t addr) {
  // Mark,这里的 Finalizable = true
  return mark<GCThread, Follow, Finalizable, Overflow>(addr);
}
template <bool gc_thread, bool follow, bool finalizable, bool publish>
uintptr_t ZBarrier::mark(uintptr_t addr) {
  uintptr_t good_addr;

  // Mark,在 _livemap 标记位图中将 referent 对应的 bit 位标记为 1
  if (should_mark_through<finalizable>(addr)) {
    ZHeap::heap()->mark_object<gc_thread, follow, finalizable, publish>(good_addr);
  }

  if (finalizable) {
    // 调整 referent 对象的视图为 finalizable
    return ZAddress::finalizable_good(good_addr);
  }

  return good_addr;
}

其实很简单,首先通过 ZPage::mark_object 将 referent 对应在标记位图 _livemap 的 bit 位标记为 1。其次调整 referent 对象的地址视图为 finalizable,表示该对象在回收阶段被 FinalReference 复活。

inline bool ZPage::mark_object(uintptr_t addr, bool finalizable, bool& inc_live) {
  // Set mark bit, 获取 referent 对象在标记位图的索引 index 
  const size_t index = ((ZAddress::offset(addr) - start()) >> object_alignment_shift()) * 2;
  // 将 referent 对应的 bit 位标记为 1
  return _livemap.set(index, finalizable, inc_live);
}

到现在 FinalReference 对象已经被加入到 _discovered_list 中了,referent 对象也被复活了,随后在 ZGC 的 Concurrent Process Non-Strong References 阶段,JVM 就会将 _discovered_list 中的所有 Reference 对象(包括这里的 FinalReference)统统转移到 _reference_pending_list 中,并唤醒 ReferenceHandler 线程去处理。

随后 ReferenceHandler 线程将 _reference_pending_list 中的 FinalReference 对象在添加到 Finalizer 中的 ReferenceQueue 中。随即 FinalizerThread 线程就会被唤醒,然后执行 Finalizer 对象的 runFinalizer 方法,最终就会执行到 referent 对象的 finalize() 方法。这是不是就和上一小节中的内容串起来了。

image

当 referent 对象的 finalize() 方法被 FinalizerThread 执行完之后,下一轮 GC 的这时候,这个 referent 对象以及与它关联的 FinalReference 对象就会一起被 GC 回收了。

总结

从整个 JVM 对于 FinalReference 的处理过程可以看出,只要我们在一个 Java 类中重写了 finalize() 方法,那么当这个 Java 类对应的实例可以被回收的时候,它的 finalize() 方法是一定会被调用的。

调用的时机取决于 FinalizerThread 线程什么时候被 OS 调度到,但是从另外一个侧面也可以看出,由于 FinalReference 的影响,一个原本该被回收的对象,在 GC 的过程又会被 JVM 复活。而只有当这个对象的 finalize() 方法被调用之后,该对象以及与它关联的 FinalReference 只能等到下一轮 GC 的时候才能被回收。

如果 finalize() 方法执行的很久又或者是 FinalizerThread 没有被 OS 调度到,这中间可能已经发生好几轮 GC 了,那么在这几轮 GC 中,FinalReference 和他的 referent 对象就一直不会被回收,表现的现象就是 JVM 堆中存在大量的 Finalizer 对象。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python - 各种计算器合集【附源码】

计算器合集 一&#xff1a;极简版计算器二&#xff1a;简易版计算器三&#xff1a;不简易的计算器四&#xff1a;还可以计算器 一&#xff1a;极简版计算器 运行效果&#xff1a; import tkinter as tk import tkinter.messagebox win tk.Tk() win.title("计算器")…

如何高效应用与精准选择温补晶振

温补晶振(TCXO)是一种重要的时序元件&#xff0c;因其高精度和高稳定性在通信、导航、测控等多个领域中扮演着关键角色。晶发电子接下来将为您详细阐述温补晶振的选用和使用方法&#xff0c;助您更好地理解和运用这一核心元件。 一、温补晶振的工作原理 温补晶振能够实现在广…

绿茶集团重启IPO:流量渐退、业绩波动,还能讲出好故事吗?

近日&#xff0c;绿茶集团有限公司(下称“绿茶集团”)向港交所递交上市申请&#xff0c;花旗、招银国际为其联席保荐人。 回望绿茶集团的上市之路&#xff0c;可谓有诸多坎坷。该公司于2021年3月首度向港交所发起冲击&#xff0c;但却将中文版招股书中的“流动负债总额”错写成…

1.4自然语言的分布式表示-word2vec实操

文章目录 0写在前面1数据准备2CBOW模型结构的实现3交叉熵损失函数的前向计算3.1关于cross_entropy_error的计算3.2关于softmax 0写在前面 代码都位于&#xff1a;nlp&#xff1b;其他相关内容详见专栏&#xff1a;深度学习自然语言处理基础_骑着蜗牛环游深度学习世界的博客-CS…

深度学习模型训练中 学习率参数 设置大小问题及设置合适值

&#x1f4aa; 专业从事且热爱图像处理&#xff0c;图像处理专栏更新如下&#x1f447;&#xff1a; &#x1f4dd;《图像去噪》 &#x1f4dd;《超分辨率重建》 &#x1f4dd;《语义分割》 &#x1f4dd;《风格迁移》 &#x1f4dd;《目标检测》 &#x1f4dd;《暗光增强》 &a…

Mybatis Plus 详解 IService、BaseMapper、自动填充、分页查询功能

结构直接看目录 前言 MyBatis-Plus 是一个 MyBatis 的增强工具&#xff0c;在 MyBatis 的基础上只做增强不做改变&#xff0c;为简化开发、提高效率而生。 愿景 我们的愿景是成为 MyBatis 最好的搭档&#xff0c;就像 魂斗罗 中的 1P、2P&#xff0c;基友搭配&#xff0c;效…

Linux系统ubuntu20.04 无人机PX4 开发环境搭建(失败率很低)

Linux系统ubuntu20.04 无人机PX4 开发环境搭建 PX4固件下载开发环境搭建MAVROS安装安装地面站QGC PX4固件下载 PX4的源码处于GitHub&#xff0c;因为众所周知的原因git clone经常失败&#xff0c;此处从Gitee获取PX4源码和依赖模块。 git clone https://gitee.com/voima/PX4-…

使用 Python 中的美丽汤进行网络数据解析的完整指南

Beautiful Soup 是一个广泛使用的 Python 库&#xff0c;在数据提取方面发挥着重要作用。它为解析 HTML 和 XML 文档提供了强大的工具&#xff0c;使从网页中轻松提取有价值的数据成为可能。该库简化了处理互联网上非结构化内容的复杂过程&#xff0c;使您可以将原始网页数据转…

【nginx】 nginx核心功能

【nginx】 nginx核心功能 1.nginx核心功能 1. 反向代理 2. 负载均衡 3. 动静分离 4. nginx的高可用2. 反向代理 正向代理: 该服务器代理的是客户端&#xff0c;对于服务器来说&#xff0c;不知道真实客户端的ip。比如: 翻墙软件。 访问国外的服务器---使用了翻墙软件----对…

2024年【R1快开门式压力容器操作】考试及R1快开门式压力容器操作考试内容

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年R1快开门式压力容器操作考试为正在备考R1快开门式压力容器操作操作证的学员准备的理论考试专题&#xff0c;每个月更新的R1快开门式压力容器操作考试内容祝您顺利通过R1快开门式压力容器操作考试。 1、【多选题…

开源技术:在线教育系统源码及教育培训APP开发指南

本篇文章&#xff0c;小编将探讨如何利用开源技术开发在线教育系统及教育培训APP&#xff0c;旨在为有志于此的开发者提供全面的指导和实践建议。 一、在线教育系统的基本构架 1.1架构设计 包括前端、后端和数据库三个主要部分。 1.2前端技术 在前端开发中&#xff0c;HTML…

【我是产品经理_注册安全分析报告】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞 …

论文学习 Learning Robust Representations via Multi-View Information Bottleneck

Code available at https://github.com/mfederici/Multi-View-Information-Bottleneck 摘要&#xff1a;信息瓶颈原理为表示学习提供了一种信息论方法&#xff0c;通过训练编码器保留与预测标签相关的所有信息&#xff0c;同时最小化表示中其他多余信息的数量。然而&#xff0…

HCIA-速查-ENSP模拟器2步清空配置

需求&#xff1a;清空模拟器配置 清空当前图中配置 步骤1&#xff1a;reset saved-configuration 后输入y确认 步骤2&#xff1a;reboot后输入n否认再输入y确认 验证已经清空配置

QT利用QGraphicsDropShadowEffect效果及自定义按钮来实现一个炫酷键盘

1、效果 2、核心代码 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent<

阿里云 邮件系统DNS域名解析 搭配 postfix+dovecot 邮件服务器

1 创建邮箱域名A记录(一般邮箱客户端&#xff0c;增加pop,imap,stmp 3条记录) 登录阿里云控制台--云解析DNS 2 MX记录 3 SPF记录

chatgpt: linux 下用纯c 编写ui

在Linux下用纯C语言编写用户界面&#xff08;UI&#xff09;&#xff0c;通常会使用GTK或Xlib。GTK是一个更高级的库&#xff0c;提供了丰富的控件和功能&#xff0c;而Xlib则是一个更底层的库&#xff0c;提供了直接操作X Window系统的功能。 下面是一个使用GTK在Linux上创建…

R语言dplyr统计指定列里面种类个数和比例

输入数据框&#xff1a;dfuorf&#xff0c;Type列有uORF和overlpaORF两种类型 dfuorf1 <- dfuorf %>%group_by(Type) %>% summarise(Countn()) %>% mutate(percentCount/sum(Count)) %>% mutate(percent1 (paste0(round((Count/sum(Count)), 2)*100,"%&…

【因果推断python】46_估计量2

目录 连续型干预变量案例 非线性处理效果 关键思想 连续型干预变量案例 目标转换方法的另一个明显缺点是它仅适用于离散或二元处理。这是你在因果推理文献中经常看到的东西。大多数研究都是针对二元干预案例进行的&#xff0c;但您找不到很多关于连续干预的研究。这让我很困…

【深度学习】GELU激活函数是什么?

torch.nn.GELU 模块在 PyTorch 中实现了高斯误差线性单元&#xff08;GELU&#xff09;激活函数。GELU 被用于许多深度学习模型中&#xff0c;包括Transformer&#xff0c;因为它相比传统的 ReLU&#xff08;整流线性单元&#xff09;函数能够更好地近似神经元的真实激活行为。…