【Java】 DirectByteBuffer堆外記憶體回收

来源:https://www.cnblogs.com/shanml/archive/2022/10/08/16743427.html
-Advertisement-
Play Games

PhantomReference虛引用 在分析堆外記憶體回收之前,先瞭解下PhantomReference虛引用。 PhantomReference需要與ReferenceQueue引用隊列結合使用,在GC進行垃圾回收的時候,如果發現一個對象只有虛引用在引用它,則認為該對象需要被回收,會將引用該對象的 ...


PhantomReference虛引用

在分析堆外記憶體回收之前,先瞭解下PhantomReference虛引用。

PhantomReference需要與ReferenceQueue引用隊列結合使用,在GC進行垃圾回收的時候,如果發現一個對象只有虛引用在引用它,則認為該對象需要被回收,會將引用該對象的虛引用加入到與其關聯的ReferenceQueue隊列中,開發者可以通過ReferenceQueue獲取需要被回收的對象,然後做一些清理操作,從隊列中獲取過的元素會從隊列中清除,之後GC就可以對該對象進行回收。

虛引用提供了一種追蹤對象垃圾回收狀態的機制,讓開發者知道哪些對象準備進行回收,在回收之前開發者可以進行一些清理工作,之後GC就可以將對象進行真正的回收。

來看一個虛引用的使用例子:

  1. 創建一個ReferenceQueue隊列queue,用於跟蹤對象的回收;
  2. 創建一個obj對象,通過new創建的是強引用,只要強引用存在,對象就不會被回收;
  3. 創建一個虛引用PhantomReference,將obj對象和ReferenceQueue隊列傳入,此時phantomReference裡面引用了obj對象,並關聯著引用隊列queue;
  4. 同樣的方式創建另一個obj1對象和虛引用對象phantomReference1;
  5. 將obj置為NULL,此時強引用關係失效;
  6. 調用 System.gc()進行垃圾回收;
  7. 由於obj的強引用關係失效,所以GC認為該對象需要被回收,會將引用該對象的虛引用phantomReference對象放入到與其關聯的引用隊列queue中;
  8. 通過poll從引用隊列queue中獲取對象,可以發現會獲取到phantomReference對象,poll獲取之後會將對象從引用隊列中刪除,之後會被垃圾回收器回收;
  9. obj1的強引用關係還在,所以從queue中並不會獲取到;
   public static void main(String[] args) {
        // 創建引用隊列
        ReferenceQueue<Object> queue = new ReferenceQueue<Object>();
        // 創建obj對象
        Object obj = new Object();
        // 創建虛引用,虛引用引用了obj對象,並與queue進行關聯
        PhantomReference<Object> phantomReference = new PhantomReference<Object>(obj, queue);
        // 創建obj1對象
        Object obj1 = new Object();
        PhantomReference<Object> phantomReference1 = new PhantomReference<Object>(obj1, queue);
        // 將obj置為NULL,強引用關係失效
        obj = null;
        // 垃圾回收
        System.gc();
        // 從引用隊列獲取對象
        Object o = queue.poll();
        if (null != o) {
            System.out.println(o.toString());
        }
    }

輸出結果:

java.lang.ref.PhantomReference@277c0f21

Reference實例的幾種狀態

Active:初始狀態,創建一個Reference類型的實例之後處於Active狀態,以上面虛引用為例,通過new創建一個PhantomReference虛引用對象之後,虛引用對象就處於Active狀態。

Pending:當GC檢測到對象的可達性發生變化時,會根據是否關聯了引用隊列來決定是否將狀態更改為Pending或者Inactive,虛引用必須與引用隊列結合使用,所以對於虛引用來說,如果它實際引用的對象需要被回收,垃圾回收器會將這個虛引用對象加入到一個Pending列表中,此時處於Pending狀態。

同樣以上面的的虛引用為例,因為obj的強引用關係失效,GC就會把引用它的虛引用對象放入到pending列表中。

Enqueued:表示引用對象被加入到了引用隊列,Reference有一個後臺線程去檢測是否有處於Pending狀態的引用對象,如果有會將引用對象加入到與其關聯的引用隊列中,此時由Pending轉為Enqueued狀態表示對象已加入到引用隊列中。

Inactive:通過引用隊列的poll方法可以從引用隊列中獲取引用對象,同時引用對象會從隊列中移除,此時引用對象處於Inactive狀態,之後會被GC回收。

DirectByteBuffer堆外記憶體回收

DirectByteBuffer的構造函數中,在申請記憶體之前,先調用了BitsreserveMemory方法回收記憶體,申請記憶體之後,調用Cleanercreate方法創建了一個Cleaner對象,並傳入了當前對象(DirectByteBuffer)和一個Deallocator類型的對象:

class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer {
    private final Cleaner cleaner;
        
    DirectByteBuffer(int cap) {                   // package-private
        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        // 清理記憶體
        Bits.reserveMemory(size, cap);
        long base = 0;
        try {
            // 分配記憶體
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        // 創建Cleader,傳入了當前對象和Deallocator
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }
}

Cleaner從名字上可以看出與清理有關,BitsreserveMemory方法底層也是通過Cleaner來進行清理,所以Cleaner是重點關註的類。

DeallocatorDirectByteBuffer的一個內部類,並且實現了Runnable介面,在run方法中可以看到對記憶體進行了釋放,接下來就去看下在哪裡觸發Deallocator任務的執行:

class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer {

    private static class Deallocator implements Runnable {
        // ...
        
        private Deallocator(long address, long size, int capacity) {
            assert (address != 0);
            this.address = address; // 設置記憶體地址
            this.size = size;
            this.capacity = capacity;
        }

        public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            // 釋放記憶體
            unsafe.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }

    }
}

Cleaner

Cleaner繼承了PhantomReferencePhantomReferenceReference的子類,所以Cleaner是一個虛引用對象

創建Cleaner

虛引用需要與引用隊列結合使用,所以在Cleaner中可以看到有一個ReferenceQueue,它是一個靜態的變數,所以創建的所有Cleaner對象都會共同使用這個引用隊列

在創建Cleaner的create方法中,處理邏輯如下:

  1. 通過構造函數創建了一個Cleaner對象,構造函數中的referent參數為DirectByteBuffer,thunk參數為Deallocator對象,在構造函數中又調用了父類的構造函數完成實例化;
  2. 調用add方法將創建的Cleaner對象加入到鏈表中,添加到鏈表的時候使用的是頭插法,新加入的節點放在鏈表的頭部,first成員變數是一個靜態變數,它指向鏈表的頭結點,創建的Cleaner都會加入到這個鏈表中;

創建後的Cleaner對象處於Active狀態。

 public class Cleaner extends PhantomReference<Object>{

    // ReferenceQueue隊列
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();

    // 靜態變數,鏈表的頭結點,創建的Cleaner都會加入到這個鏈表中
    static private Cleaner first = null;
     
    // thunk
    private final Runnable thunk;
     
    public static Cleaner create(Object ob, Runnable thunk) {
        if (thunk == null)
            return null;
        // 創建一個Cleaner並加入鏈表
        return add(new Cleaner(ob, thunk));
    }
    
    private Cleaner(Object referent, Runnable thunk) {
        super(referent, dummyQueue); // 調用父類構造函數,傳入引用對象和引用隊列
        this.thunk = thunk; // thunk指向傳入的Deallocator
    }
     
    private static synchronized Cleaner add(Cleaner cl) {
        // 如果頭結點不為空
        if (first != null) {
            // 將新加入的節點作為頭結點
            cl.next = first; 
            first.prev = cl;
        }
        first = cl;
        return cl;
    }
}

Cleaner調用父類構造函數時,最終會進入到父類Reference中的構造函數中:

referent:指向實際的引用對象,上面創建的是DirectByteBuffer,所以這裡指向的是DirectByteBuffer

queue:引用隊列,指向Cleaner中的引用隊列dummyQueue

public class PhantomReference<T> extends Reference<T> {
    // ...
    
    public PhantomReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q); // 調用父類構造函數
    }

}

public abstract class Reference<T> {
    /* 引用對象 */
    private T referent;         
    // 引用隊列
    volatile ReferenceQueue<? super T> queue;
    
    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        // 設置引用隊列
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }

}

啟動ReferenceHandler線程

Reference中有一個靜態方法,裡面創建了一個ReferenceHandler並設置為守護線程,然後啟動了該線程,並創建了JavaLangRefAccess對象設置到SharedSecrets中:

public abstract class Reference<T> {
    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        // 創建ReferenceHandler
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        // 設置優先順序為最高
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();

        // 這裡設置了JavaLangRefAccess
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                // 調用了tryHandlePending
                return tryHandlePending(false);
            }
        });
    }
}

ReferenceHandlerReference的內部類,繼承了Thread,在run方法中開啟了一個迴圈,不斷的執行tryHandlePending方法,處理Reference中pending列表:

public abstract class Reference<T> {
    
    private static class ReferenceHandler extends Thread {
        
        // ...
        
        ReferenceHandler(ThreadGroup g, String name) {
            super(g, name);
        }

        public void run() {
            while (true) {
                // 處理pending列表
                tryHandlePending(true);
            }
        }
    }
 }

Cleaner會啟動一個優先順序最高的守護線程,不斷調用tryHandlePending來檢測是否有需要回收的引用對象(還未進行真正的回收),然後進行處理。

處理pending列表

垃圾回收器會將要回收的引用對象放在Referencepending變數中,從數據類型上可以看出pending只是一個Reference類型的對象,並不是一個list,如果有多個需要回收的對象,如何將它們全部放入pending對象中?

可以把pengding看做是一個鏈表的頭結點,假如有引用對象被判定需要回收,如果pengding為空直接放入即可,如果不為空,將使用頭插法將新的對象加入到鏈表中,也就是將新對象的discovered指向pending對象,然後將pending指向當前要回收的這個對象,這樣就形成了一個鏈表,pending指向鏈表的頭結點。

在pending鏈表中的引用對象處於pending狀態。

接下來看tryHandlePending方法的處理邏輯:

  1. 如果pending不為空,表示有需要回收的對象,此時將pengding指向的對象放在臨時變數r中,並判斷是否是Cleaner類型,如果是將其強制轉為Cleaner,記錄在臨時變數c中,接著更新pending的值為r的discovered,因為discovered中記錄了下一個需要被回收的對象,pengding需要指向下一個需要被回收的對象;

    pending如果為NULL,會進入到else的處理邏輯,返回值為參數傳入的waitForNotify的值。

  2. 判斷Cleaner對象是否為空,如果不為空,調用Cleaner的clean方法進行清理

  3. 獲取引用對象關聯的引用隊列,然後調用enqueue方法將引用對象加入到引用隊列中

  4. 返回true;

public abstract class Reference<T> {
  
    // 指向pending列表中的下一個節點
    transient private Reference<T> discovered; 

    // 靜態變數pending列表,可以看做是一個鏈表,pending指向鏈表的頭結點
    private static Reference<Object> pending = null;
  
    static boolean tryHandlePending(boolean waitForNotify) {
        Reference<Object> r;
        Cleaner c;
        try {
            synchronized (lock) {
                // 如果pending不為空
                if (pending != null) {
                    // 獲取pending執行的對象
                    r = pending;
                    // 如果是Cleaner類型
                    c = r instanceof Cleaner ? (Cleaner) r : null;
                    // 將pending指向下一個節點
                    pending = r.discovered;
                    // 將discovered置為空
                    r.discovered = null;
                } else {
                    // 等待
                    if (waitForNotify) {
                        lock.wait();
                    }
                    return waitForNotify;
                }
            }
        } catch (OutOfMemoryError x) {
            Thread.yield();
            // retry
            return true;
        } catch (InterruptedException x) {
            // retry
            return true;
        }
        if (c != null) {
            // 調用clean方法進行清理
            c.clean();
            return true;
        }
        // 獲取引用隊列
        ReferenceQueue<? super Object> q = r.queue;
        // 如果隊列不為空,將對象加入到引用隊列中
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        // 返回true
        return true;
    }
}
釋放記憶體

Cleaner的clean方法中,可以看到,調用了thunk的run方法,前面內容可知,thunk指向的是Deallocator對象,所以會執行Deallocator的run方法,Deallocator的run方法前面也已經看過,裡面會對DirectByteBuffer的堆外記憶體進行釋放

public class Cleaner extends PhantomReference<Object> {

    public void clean() {
        if (!remove(this))
            return;
        try {
            // 調用run方法
            thunk.run();
        } catch (final Throwable x) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        if (System.err != null)
                            new Error("Cleaner terminated abnormally", x)
                                .printStackTrace();
                        System.exit(1);
                        return null;
                    }});
        }
    }
}

總結

Cleaner是一個虛引用,它實際引用的對象DirectByteBuffer如果被GC判定為需要回收,會將引用該對象的Cleaner加入到pending列表,ReferenceHandler線程會不斷檢測pending是否為空,如果不為空,就對其進行處理:

  1. 如果對象類型為Cleaner,就調用Cleaner的clean方法進行清理,Cleaner的clean方法又會調用Deallocator的run方法,裡面調用了freeMemory方法對DirectByteBuffer分配的堆外記憶體進行釋放;
  2. 將Cleaner對象加入到與其關聯的引用隊列中;

引用隊列

ReferenceQueue名字聽起來是一個隊列,實際使用了一個鏈表,使用頭插法將加入的節點串起來,ReferenceQueue中的head變數指向鏈表的頭節點,每個節點是一個Reference類型的對象:

public class ReferenceQueue<T> {

    // head為鏈表頭節點
    private volatile Reference<? extends T> head = null;
}

Reference中除了discovered變數之外,還有一個next變數,discovered指向的是處於pending狀態時pending列表中的下一個元素,next變數指向的是處於Enqueued狀態時,引用隊列中的下一個元素:

public abstract class Reference<T> {

    /* When active:   處於active狀態時為NULL
     *     pending:   this
     *    Enqueued:   Enqueued狀態時,指向引用隊列中的下一個元素
     *    Inactive:   this
     */
    @SuppressWarnings("rawtypes")
    Reference next;
    
    /* When active:   active狀態時,指向GC維護的一個discovered鏈表中的下一個元素
     *     pending:   pending狀態時,指向pending列表中的下一個元素
     *   otherwise:   其他情況為NULL
     */
    transient private Reference<T> discovered;  /* used by VM */
}

enqueue入隊

進入引用隊列中的引用對象處於enqueue狀態。

enqueue的處理邏輯如下:

  1. 判斷要加入的對象關聯的引用隊列,對隊列進行判斷,如果隊列為空或者隊列等於ReferenceQueue中的空隊列ENQUEUED,表示該對象之前已經加入過隊列,不能重覆操作,返回false,如果未加入過繼續下一步;
  2. 將對象所關聯的引用隊列置為ENQUEUED,它是一個空隊列,表示節點已經加入到隊列中;
  3. 判斷頭節點是否為空,如果為空,表示鏈表還沒有節點,將當前對象的next指向自己,如果頭結點不為空,將當前對象的next指向頭結點,然後更新頭結點的值為當前對象(頭插法插入鏈表);
  4. 增加隊列的長度,也就是鏈表的長度;
public class ReferenceQueue<T> {

    // 空隊列
    static ReferenceQueue<Object> ENQUEUED = new Null<>();
    
    // 入隊,將節點加入引用隊列,隊列實際上是一個鏈表
    boolean enqueue(Reference<? extends T> r) {
        synchronized (lock) {
            // 獲取關聯的引用隊列
            ReferenceQueue<?> queue = r.queue;
            // 如果為空或者已經添加到過隊列
            if ((queue == NULL) || (queue == ENQUEUED)) {
                return false;
            }
            assert queue == this;
            // 將引用隊列置為一個空隊列,表示該節點已經入隊
            r.queue = ENQUEUED;
            // 如果頭結點為空將下一個節點置為自己,否則將next置為鏈表的頭結點,可以看出同樣使用的是頭插法將節點插入鏈表
            r.next = (head == null) ? r : head;
            // 更新頭結點為當前節點
            head = r;
            // 增加長度
            queueLength++;
            if (r instanceof FinalReference) {
                sun.misc.VM.addFinalRefCount(1);
            }
            lock.notifyAll();
            return true;
        }
    }
}
poll出隊

在調用poll方法從引用隊列中獲取一個元素並出隊的時候,首先對head頭結點進行判空,如果為空表示引用隊列中沒有數據,返回NULL,否則調用reallyPoll從引用隊列中獲取元素。

出隊的處理邏輯如下:

  1. 獲取鏈表中的第一個節點也就是頭結點,如果不為空進行下一步;

  2. 如果頭節點的下一個節點是自己,表示鏈表只有一個節點,頭結點出隊之後鏈表為空,所以將頭結點的值更新為NULL;

    如果頭節點的下一個節點不是自己,表示鏈表中還有其他節點,更新head頭節點的值為下一個節點,也就是next指向的對象;

  3. 將需要出隊的節點的引用隊列置為NULL,next節點置為自己,表示節點已從隊列中刪除;

  4. 引用隊列的長度減一;

  5. 返回要出隊的節點;

從出隊的邏輯中可以看出,引用隊列中的對象是後進先出的,poll出隊之後的引用對象處於Inactive狀態,表示可以被GC回收掉。

public class ReferenceQueue<T> {
    /**
     * 從引用隊列中獲取一個節點,進行出隊操作
     */
    public Reference<? extends T> poll() {
        // 如果頭結點為空,表示沒有數據 
        if (head == null)
            return null;
        synchronized (lock) {
            return reallyPoll();
        }
    }
    
    @SuppressWarnings("unchecked")
    private Reference<? extends T> reallyPoll() {     、  /* Must hold lock */
        // 獲取頭結點
        Reference<? extends T> r = head;
        if (r != null) {
            // 如果頭結點的下一個節點是自己,表示鏈表只有一個節點,head置為null,否則head值為r的下一個節點,也就是next指向的對象
            head = (r.next == r) ?
                null :
                r.next;
            // 將引用隊列置為NULL
            r.queue = NULL;
            // 下一個節點置為自己
            r.next = r;
            // 長度減一
            queueLength--;
            if (r instanceof FinalReference) {
                sun.misc.VM.addFinalRefCount(-1);
            }
            // 返回鏈表中的第一個節點
            return r;
        }
        return null;
    }
}

reserveMemory記憶體清理

最開始在DirectByteBuffer的構造函數中看到申請記憶體之前會調用Bits的reserveMemory方法,如果沒有足夠的記憶體,它會從SharedSecrets獲取JavaLangRefAccess對象進行一些處理,由前面的內容可知,Reference中的靜態方法啟動ReferenceHandler之後,創建了JavaLangRefAccess並設置到SharedSecrets中,所以這裡調用JavaLangRefAccesstryHandlePendingReference實際上依舊調用的是Reference中的tryHandlePending方法。

在調用Reference中的tryHandlePending方法處理需要回收的對象之後,調用tryReserveMemory方法判斷是否有足夠的記憶體,如果記憶體依舊不夠,會調用` System.gc()觸發垃圾回收,然後開啟一個迴圈,處理邏輯如下:

  1. 判斷記憶體是否充足,如果充足直接返回;

  2. 判斷睡眠次數是否小於限定的最大值,如果小於繼續下一步,否則終止迴圈;

  3. 調用tryHandlePendingReference處理penging列表中的引用對象,前面在處理pending列表的邏輯中可以知道,如果pending列表不為空,會返回true,tryHandlePendingReference也會返回true,此時意味著清理了一部分對象,所以重新進入到第1步進行檢查;

    如果pending列表為空,會返回參數中傳入的waitForNotify的值,從JavaLangRefAccess的tryHandlePendingReference中可以看出這裡傳入的是false,所以會進行如下處理:

    • 通過 Thread.sleep(sleepTime)讓當前線程睡眠一段時間,這樣可以避免reserveMemory方法一直在占用資源;
    • 對睡眠次數加1;
  4. 如果以上步驟處理之後還沒有足夠的空間會拋出拋出OutOfMemoryError異常;

reserveMemory方法的作用是保證在申請記憶體之前有足夠的記憶體,如果沒有足夠的記憶體會進行清理,達到指定清理次數之後依舊沒有足夠的記憶體空間,將拋出OutOfMemoryError異常。

class Bits {

    static void reserveMemory(long size, int cap) {

        if (!memoryLimitSet && VM.isBooted()) {
            maxMemory = VM.maxDirectMemory();
            memoryLimitSet = true;
        }

        // 是否有足夠記憶體
        if (tryReserveMemory(size, cap)) {
            return;
        }
        // 獲取JavaLangRefAccess
        final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
        // 調用tryHandlePendingReference
        while (jlra.tryHandlePendingReference()) {
            // 判斷是否有足夠的記憶體
            if (tryReserveMemory(size, cap)) {
                return;
            }
        }

        // 調用gc進行垃圾回收
        System.gc();

        boolean interrupted = false;
        try {
            long sleepTime = 1;
            int sleeps = 0;
            // 開啟迴圈
            while (true) {
                // 是否有足夠記憶體
                if (tryReserveMemory(size, cap)) {
                    return;
                }
                // 如果次數小於最大限定次數,終止
                if (sleeps >= MAX_SLEEPS) {
                    break;
                }
                // 再次處理penging列表中的對象
                if (!jlra.tryHandlePendingReference()) {
                    try {
                        // 睡眠一段時間
                        Thread.sleep(sleepTime);
                        sleepTime <<= 1;
                        sleeps++; // 睡眠次數增加1
                    } catch (InterruptedException e) {
                        interrupted = true;
                    }
                }
            }
            // 拋出OutOfMemoryError異常
            throw new OutOfMemoryError("Direct buffer memory");

        } finally {
            if (interrupted) {
                // don't swallow interrupts
                Thread.currentThread().interrupt();
            }
        }
    }
}

public abstract class Reference<T> {
    static {
        // ...
        // 這裡設置了JavaLangRefAccess
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                // 調用tryHandlePending,這裡waitForNotify參數傳入的是false
                return tryHandlePending(false);
            }
        });
    }
}

參考

Reference源碼解析

一文讀懂java中的Reference和引用類型

Java 源碼剖析——徹底搞懂 Reference 和 ReferenceQueue


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

-Advertisement-
Play Games
更多相關文章
  • 最近在用rust 寫一個redis的數據校驗工具。redis-rs中具備 redis::ConnectionLike trait,藉助它可以較好的來抽象校驗過程。在開發中,不免要定義struct 中的某些元素為 trait object,從而帶來一些rust語言中的生命周期問題。 本文不具體討論 r ...
  • 一、CentOS 7.9 安裝 rocketmq-4.9.2 地址: https://rocketmq.apache.org https://github.com/apache/rocketmq https://archive.apache.org/dist/rocketmq/4.9.2/rocke ...
  • #背景 用css動畫讓你的頁面交互動起來 #開始 <body> <button id="button">開始</button> <div id="block"></div> </body> <script> document.getElementById("button").addEventList ...
  • 近年來,越來越多的零售企業大力發展全渠道業務。在銷售額增長上,通過線上的小程式、直播、平臺渠道等方式,拓展流量變現渠道。在會員增長方面,通過多樣的互動方式,全渠道觸達消費者,擴大會員規模。而全渠道的庫存管理,逐漸變成零售商在渠道運營方面的核心活動,也是提高庫存周轉率,保證利潤的關鍵所在。 在全渠道模 ...
  • SSM整合以及相關補充 我們在前面已經學習了Maven基本入門,Spring,SpringMVC,MyBatis三件套 現在我們來通過一些簡單的案例,將我們最常用的開發三件套整合起來,進行一次完整的項目展示 溫馨提示:在閱讀本篇文章前,請學習Maven,Spring,SpringMVC,MyBati ...
  • 前言 之前學習muduo網路庫的時候,看到作者陳碩用到了enable_shared_from_this和shared_from_this,一直對此概念是一個模糊的認識,隱約記著這個機制是在計數器智能指針傳遞時才會用到的,今天對該機制進行梳理總結一下吧。 如果不熟悉C++帶引用計數的智能指針share ...
  • 位運算及進位轉換 1.1 標識符的命名規則和規範 1.1.1 標識符概念 Java對各種變數、方法和類等命名時使用的字元序列稱為標識符 凡是自己可以起名字的地方都叫做標識符 int num = 88; 1.1.2 標識符的命名規則 由26個英文字母大小寫,0-9,_或$組成 不可以以數字開頭。int ...
  • Python表白小程式。 python表白玫瑰花繪製 python表白玫瑰花繪製——情人節表白 python表白玫瑰花繪製——情人節表白 一、玫瑰花繪製—深紅色 二、玫瑰花繪製—五顏六色 三、玫瑰花繪製—粉紅色 四、玫瑰花繪製—紅色 五、桃花繪製 搬運不易,路過的各位大佬請點個贊 一、玫瑰花繪製—深 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...