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
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...