FinalReference 如何使 GC 過程變得拖拖拉拉

来源:https://www.cnblogs.com/binlovetech/p/18253193
-Advertisement-
Play Games

本文基於 OpenJDK17 進行討論,垃圾回收器為 ZGC。 提示: 為了方便大家索引,特將在上篇文章 《以 ZGC 為例,談一談 JVM 是如何實現 Reference 語義的》 中討論的眾多主題獨立出來。 FinalReference 對於我們來說是一種比較陌生的 Reference 類型,因 ...


本文基於 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 對象。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ‍ 寫在開頭 點贊 + 收藏 學會 前言 在這之前公司項目的文檔預覽的方式都是通過微軟線上預覽服務,但是微軟的線上服務有文件大小限制,想完整使用得花錢,一些圖片文件就通過組件庫antd實現,因為我們項目存在多種類型的文件,所以為了改善用戶的體驗,決定把文件預覽單獨弄一個拆出一個項 ...
  • 這篇文章介紹了Tailwind CSS框架的特點與優勢,包括其作為實用性的CSS框架如何通過預設的樣式類實現快速佈局和設計,以及如何在不犧牲響應式和自適應性的同時減少開發時間。此外,還提及了框架的可定製性,允許開發者輕鬆創建符合項目需求的樣式規則,從而提高前端開發效率。 ...
  • Chrome 在 121 版本開始,原生支持了兩個滾動條樣式相關的樣式 scrollbar-color 和 scrollbar-width。 要知道,在此前,雖然有 ::-webkit-scrollbar 規範可以控制滾動條,可是,::-webkit-scrollbar 是非標準特性,在 MDN 文 ...
  • Spring Cloud是一個相對比較成熟的微服務框架。雖然,Spring Cloud於2016年才推出1.0的release版本, 時間最短, 但是相比Dubbo等RPC框架, Spring Cloud提供的全套的分散式系統解決方案。 Spring Cloud是一系列框架的有序集合。它利用Spri ...
  • Windows應用軟體開發,會有很多常用的模塊,比如資料庫、配置文件、日誌、後臺通信、進程通信、埋點、瀏覽器等等。下麵是目前我們公司windows梳理的部分組件,梳理出來方便大家瞭解組件概念以及依賴關係: 每個應用里,現在或者以後都可能會存在這些模塊。以我團隊開發的全家桶為例,十多個應用對後臺訪問, ...
  • 通過本文我們深入瞭解了RabbitMQ的集群模式及其優缺點。無論是普通集群還是鏡像集群,都有其適用的場景和局限性。普通集群利用Erlang語言的集群能力,但消息可靠性和高可用性方面存在一定挑戰;而鏡像集群通過主動消息同步提高了消息的可靠性和高可用性,但可能會占用大量網路帶寬。因此,在選擇集群方案時,... ...
  • 寫在前面 在現目前項目開發中,一般都是前後端分離項目。前端小姐姐負責開發前端,苦逼的我們負責後端開發 事實是一個人全乾,在這過程中編寫介面文檔就顯得尤為重要了。然而作為一個程式員,最怕的莫過於自己寫文檔和別人不寫文檔 大家都不想寫文檔,那這活就交給今天的主角Swagger來實現了 一、專業名詞介紹 ...
  • 1 Zero-shot learning 零樣本學習。 1.1 任務定義 利用訓練集數據訓練模型,使得模型能夠對測試集的對象進行分類,但是訓練集類別和測試集類別之間沒有交集;期間需要藉助類別的描述,來建立訓練集和測試集之間的聯繫,從而使得模型有效。 Zero-shot learning 就是希望我們 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...