主頁 > 後端開發 > Java Reference 核心原理分析

Java Reference 核心原理分析

2020-10-20 20:56:47 後端開發

帶著問題,看原始碼針對性會更強一點、印象會更深刻、并且效果也會更好,所以我先賣個關子,提兩個問題(沒準下次跳槽時就被問到),

  • 我們可以用ByteBuffer的allocateDirect方法,申請一塊堆外記憶體創建一個DirectByteBuffer物件,然后利用它去操作堆外記憶體,這些申請完的堆外記憶體,我們可以回收嗎?可以的話是通過什么樣的機制回收的?

  • 大家應該都知道WeakHashMap可以用來實作記憶體相對敏感的本地快取,為什么WeakHashMap合適這種業務場景,其內部實作會做什么特殊處理呢?

GC可到達性與JDK中Reference型別

上面提到的兩個問題,其答案都在JDK的Reference里面,JDK早期版本中并沒有Reference相關的類,這導致物件被GC回收后如果想做一些額外的清理作業(比如socket、堆外記憶體等)是無法實作的,同樣如果想要根據堆記憶體的實際使用情況決定要不要去清理一些記憶體敏感的物件也是法實作的,為此JDK1.2中引入的Reference相關的類,即今天要介紹的Reference、SoftReference、WeakReference、PhantomReference,還有與之相關的Cleaner、ReferenceQueue、ReferenceHandler等,與Reference相關核心類基本都在java.lang.ref包下面,其類關系如下:

![](https://static001.geekbang.org/infoq/4a/4ad1ea5f8488ba94c74f0c0deedbfaa4.png)

其中,SoftReference代表軟參考物件,垃圾回收器會根據記憶體需求酌情回收軟參考指向的物件,普通的GC并不會回收軟參考,只有在即將OOM的時候(也就是最后一次Full GC)如果被參考的物件只有SoftReference指向的參考,才會回收,WeakReference代表弱參考物件,當發生GC時,如果被參考的物件只有WeakReference指向的參考,就會被回收,PhantomReference代表虛參考物件(也有叫幻象參考的,個人認為還是虛參考更加貼切),其是一種特殊的參考型別,不能通過虛參考獲取到其關聯的物件,但當GC時如果其參考的物件被回收,這個事件程式可以感知,這樣我們可以做相應的處理,最后就是最常見強參考物件,也就是通常我們new出來的物件,在繼續介紹Reference相關類的原始碼前,先來簡單的看一下GC如何決定一個物件是否可被回收,其基本思路是從GC Root開始向下搜索,如果物件與GC Root之間存在參考鏈,則物件是可達的,GC會根據是否可到達與可到達性決定物件是否可以被回收,而物件的可達性與參考型別密切相關,物件的可到達性可分為5種,

強可到達,如果從GC Root搜索后,發現物件與GC Root之間存在強參考鏈則為強可到達,強參考鏈即有強參考物件,參考了該物件,

軟可到達,如果從GC Root搜索后,發現物件與GC Root之間不存在強參考鏈,但存在軟參考鏈,則為軟可到達,軟參考鏈即有軟參考物件,參考了該物件,

弱可到達,如果從GC Root搜索后,發現物件與GC Root之間不存在強參考鏈與軟參考鏈,但有弱參考鏈,則為弱可到達,弱參考鏈即有弱參考物件,參考了該物件,

虛可到達,如果從GC Root搜索后,發現物件與GC Root之間只存在虛參考鏈則為虛可到達,虛參考鏈即有虛參考物件,參考了該物件,

不可達,如果從GC Root搜索后,找不到物件與GC Root之間的參考鏈,則為不可到達,

看一個簡單的列子:

![](https://static001.geekbang.org/infoq/8b/8b60c7e32ba011ba09aec69a613b0fe4.png)

ObjectA為強可到達,ObjectB也為強可到達,雖然ObjectB物件被SoftReference ObjcetE 參考但由于其還被ObjectA參考所以為強可到達;而ObjectC和ObjectD為弱參考達到,雖然ObjectD物件被PhantomReference ObjcetG參考但由于其還被ObjectC參考,而ObjectC又為弱參考達到,所以ObjectD為弱參考達到;而ObjectH與ObjectI是不可到達,參考鏈的強弱有關系依次是 強參考 > 軟參考 > 弱參考 > 虛參考,如果有更強的參考關系存在,那么參考鏈到達性,將由更強的參考有關系決定,

Reference核心處理流程

JVM在GC時如果當前物件只被Reference物件參考,JVM會根據Reference具體型別與堆記憶體的使用情況決定是否把對應的Reference物件加入到一個由Reference構成的pending鏈表上,如果能加入pending鏈表JVM同時會通知ReferenceHandler執行緒進行處理,ReferenceHandler執行緒是在Reference類被初始化時呼叫的,其是一個守護行程并且擁有最高的優先級,Reference類靜態初始化塊代碼如下:

static {    //省略部分代碼...    Thread handler = new ReferenceHandler(tg, "Reference Handler");    handler.setPriority(Thread.MAX_PRIORITY);    handler.setDaemon(true);    handler.start();    //省略部分代碼...}
復制代碼

而ReferenceHandler執行緒內部的run方法會不斷地從Reference構成的pending鏈表上獲取Reference物件,如果能獲取則根據Reference的具體型別進行不同的處理,不能則呼叫wait方法等待GC回收物件處理pending鏈表的通知,ReferenceHandler執行緒run方法原始碼:

public void run() {    //死回圈,執行緒啟動后會一直運行    while (true) {        tryHandlePending(true);    }}
復制代碼

run內部呼叫的tryHandlePending原始碼:

static boolean tryHandlePending(boolean waitForNotify) {    Reference<Object> r;    Cleaner c;    try {        synchronized (lock) {            if (pending != null) {                r = pending;                //instanceof 可能會拋出OOME,所以在將r從pending鏈上斷開前,做這個處理                c = r instanceof Cleaner ? (Cleaner) r : null;                //將將r從pending鏈上斷開                pending = r.discovered;                r.discovered = null;            } else {                //等待CG后的通知                if (waitForNotify) {                    lock.wait();                }                //重試                return waitForNotify;            }        }    } catch (OutOfMemoryError x) {        //當拋出OOME時,放棄CPU的運行時間,這樣有希望識訓一些存活的參考并且GC能回收部分空間,同時能避免頻繁地自旋重試,導致連續的OOME例外        Thread.yield();        //重試        return true;    } catch (InterruptedException x) {         //重試        return true;    }    //如果是Cleaner型別的Reference呼叫其clean方法并退出    if (c != null) {        c.clean();        return true;    }    ReferenceQueue<? super Object> q = r.queue;    //如果Reference有注冊ReferenceQueue,則處理pending指向的Reference結點將其加入ReferenceQueue中    if (q != ReferenceQueue.NULL) q.enqueue(r);    return true;}
復制代碼

上面tryHandlePending方法中比較重要的點是c.clean()與q.enqueue?,這個是文章最開始提到的兩個問題答案的入口,Cleaner的clean方法用于完成清理作業,而ReferenceQueue是將被回收物件加入到對應的Reference列隊中,等待其他執行緒的后繼處理,更具體地關于Cleaner與ReferenceQueue后面會再詳細說明,Reference的核心處理流程可總結如下:

![](https://static001.geekbang.org/infoq/58/58765255eb1b5e1a687d22d730c5eaed.png)

對Reference的核心處理流程有整體了解后,再來回過頭細看一下Reference類的原始碼,

/* Reference實體有四種內部的狀態 * Active: 新創建Reference的實體其狀態為Active,當GC檢測到Reference參考的referent可達到狀態發生改變時, * 為改變Reference的狀態為Pending或Inactive,這個取決于創建Reference實體時是否注冊過ReferenceQueue, * 注冊過其狀態會轉換為Pending,同時GC會將其加入pending-Reference鏈表中,否則為轉換為Inactive狀態, * Pending: 代表Reference是pending-Reference鏈表的成員,等待ReferenceHandler執行緒呼叫Cleaner#clean * 或ReferenceQueue#enqueue操作,未注冊過ReferenceQueue的實體不會達到這個狀態 * Enqueued: Reference實體成為其被創建時注冊過的ReferenceQueue的成員,代表已入佇列,當其從ReferenceQueue * 中移除后,其狀態會變為Inactive, * Inactive: 什么也不會做,一旦處理該狀態,就不可再轉換, * 不同狀態時,Reference對應的queue與成員next變數值(next可理解為ReferenceQueue中的下個結點的參考)如下: * Active: queue為Reference實體被創建時注冊的ReferenceQueue,如果沒注冊為Null,此時,next為null, * Reference實體與queue真正產生關系, * Pending: queue為Reference實體被創建時注冊的ReferenceQueue,next為當前實體本身, * Enqueued: queue為ReferenceQueue.ENQUEUED代表當前實體已入佇列,next為queue中的下一實列結點, * 如果是queue尾部則為當前實體本身 * Inactive: queue為ReferenceQueue.NULL,當前實體已從queue中移除與queue無關聯,next為當前實體本身, */public abstract class Reference<T> {// Reference 參考的物件private T referent;/* Reference注冊的queue用于ReferenceHandler執行緒入佇列處理與用戶執行緒取Reference處理, * 其取值會根據Reference不同狀態發生改變,具體取值見上面的分析 */volatile ReferenceQueue<? super T> queue;// 可理解為注冊的queue中的下一個結點的參考,其取值會根據Reference不同狀態發生改變,具體取值見上面的分析volatile Reference next;/* 其由VM維護,取值會根據Reference不同狀態發生改變, * 狀態為active時,代表由GC維護的discovered-Reference鏈表的下個節點,如果是尾部則為當前實體本身 * 狀態為pending時,代表pending-Reference的下個節點的參考,否則為null */transient private Reference<T> discovered;/* pending-Reference 鏈表頭指標,GC回收referent后會將Reference加pending-Reference鏈表, * 同時ReferenceHandler執行緒會獲取pending指標,不為空時Cleaner.clean()或入列queue, * pending-Reference會采用discovered參考接鏈表的下個節點, */private static Reference<Object> pending = null;// 可理解為注冊的queue中的下一個結點的參考,其取值會根據Reference不同狀態發生改變,具體取值見上面的分析volatile Reference next;//用于CG同步Reference成員變數值的物件,static private class Lock { }private static Lock lock = new Lock();//省略部分代碼...}
復制代碼

上面解釋了Reference中的主要成員的作用,其中比較重要是Reference內部維護的不同狀態,其狀態不同成員變數queue、pending、discovered、next的取值都會發生變化,Reference的主要方法如下:

//建構式,指定參考的物件referentReference(T referent) {    this(referent, null);}//建構式,指定參考的物件referent與注冊的queueReference(T referent, ReferenceQueue<? super T> queue) {    this.referent = referent;    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;}//獲取參考的物件referentpublic T get() {    return this.referent;}//將當前物件加入創建時注冊的queue中public boolean enqueue() {    return this.queue.enqueue(this);}
復制代碼

ReferenecQueue與Cleaner原始碼分析

先來看下ReferenceQueue的主要成員變數的含義,

//代表Reference的queue為null,Null為ReferenceQueue子類static ReferenceQueue<Object> NULL = new Null<>();//代表Reference已加入當前ReferenceQueue中,static ReferenceQueue<Object> ENQUEUED = new Null<>();//用于同步的物件private Lock lock = new Lock();//當前ReferenceQueue中的頭節點private volatile Reference<? extends T> head = null;//ReferenceQueue的長度private long queueLength = 0;
復制代碼

ReferenceQueue中比較重要的方法為enqueue、poll、remove方法,

//入列隊enqueue方法,只被Reference類呼叫,也就是上面分析中ReferenceHandler執行緒為呼叫boolean enqueue(Reference<? extends T> r) {  //獲取同步物件lock對應的監視器物件    synchronized (lock) {        //獲取r關聯的ReferenceQueue,如果創建r時未注冊ReferenceQueue則為NULL,同樣如果r已從ReferenceQueue中移除其也為null        ReferenceQueue<?> queue = r.queue;        //判斷queue是否為NULL 或者 r已加入ReferenceQueue中,是的話則入佇列失敗        if ((queue == NULL) || (queue == ENQUEUED)) {            return false;        }        assert queue == this;        //設定r的queue為已入佇列        r.queue = ENQUEUED;        //如果ReferenceQueue頭節點為null則r的next節點指向當前節點,否則指向頭節點        r.next = (head == null) ? r : head;        //更新ReferenceQueue頭節點        head = r;        //列隊長度加1        queueLength++;        //為FinalReference型別參考增加FinalRefCount數量        if (r instanceof FinalReference) {            sun.misc.VM.addFinalRefCount(1);        }        //通知remove操作佇列有節點        lock.notifyAll();        return true;    }}
復制代碼

poll方法原始碼相對簡單,其就是從ReferenceQueue的頭節點獲取Reference,

public Reference<? extends T> poll() {    //頭結點為null直接回傳,代表Reference還沒有加入ReferenceQueue中    if (head == null)        return null;    //獲取同步物件lock對應的監視器物件    synchronized (lock) {        return reallyPoll();    }}//從佇列中真正poll元素的方法private Reference<? extends T> reallyPoll() {    Reference<? extends T> r = head;    //double check 頭節點不為null    if (r != null) {      //保存頭節點的下個節點參考        Reference<? extends T> rn = r.next;        //更新queue頭節點參考        head = (rn == r) ? null : rn;        //更新Reference的queue值,代表r已從佇列中移除     r.queue = NULL;     //更新Reference的next為其本身        r.next = r;        queueLength--;        //為FinalReference節點FinalRefCount數量減1        if (r instanceof FinalReference) {            sun.misc.VM.addFinalRefCount(-1);        }        //回傳獲取的節點        return r;    }    return null;}
復制代碼

remove方法的原始碼如下:

public Reference<? extends T> remove(long timeout) throws IllegalArgumentException, InterruptedException {    if (timeout < 0) {        throw new IllegalArgumentException("Negative timeout value");    }    //獲取同步物件lock對應的監視器物件    synchronized (lock) {     //獲取佇列頭節點指向的Reference        Reference<? extends T> r = reallyPoll();        //獲取到回傳        if (r != null) return r;        long start = (timeout == 0) ? 0 : System.nanoTime();        //在timeout時間內嘗試重試獲取        for (;;) {           //等待佇列上有結點通知            lock.wait(timeout);            //獲取佇列中的頭節點指向的Reference            r = reallyPoll();            //獲取到回傳            if (r != null) return r;            if (timeout != 0) {                long end = System.nanoTime();                timeout -= (end - start) / 1000_000;                //已超時但還沒有獲取到佇列中的頭節點指向的Reference回傳null                if (timeout <= 0) return null;                start = end;            }        }    }}
復制代碼

簡單的分析完ReferenceQueue的原始碼后,再來整體回顧一下Reference的核心處理流程,JVM在GC時如果當前物件只被Reference物件參考,JVM會根據Reference具體型別與堆記憶體的使用情況決定是否把對應的Reference物件加入到一個由Reference構成的pending鏈表上,如果能加入pending鏈表JVM同時會通知ReferenceHandler執行緒進行處理,ReferenceHandler執行緒收到通知后會呼叫Cleaner#clean或ReferenceQueue#enqueue方法進行處理,如果參考當前物件的Reference型別為WeakReference且堆記憶體不足,那么JVM就會把WeakReference加入到pending-Reference鏈表上,然后ReferenceHandler執行緒收到通知后會異步地做入佇列操作,而我們的應用程式中的執行緒便可以不斷地去拉取ReferenceQueue中的元素來感知JVM的堆記憶體是否出現了不足的情況,最終達到根據堆記憶體的情況來做一些處理的操作,實際上WeakHashMap低層便是過通上述程序實作的,只不過實作細節上有所偏差,這個后面再分析,再來看看ReferenceHandler執行緒收到通知后可能會呼叫的另外一個類Cleaner的實作,

同樣先看一下Cleaner的成員變數,再看主要的方法實作,

//繼承了PhantomReference類也就是虛參考,PhantomReference原始碼很簡單只是重寫了get方法回傳nullpublic class Cleaner extends PhantomReference<Object> {   /* 虛佇列,命名很到位,之前說CG把ReferenceQueue加入pending-Reference鏈中后,ReferenceHandler執行緒在處理時     * 是不會將對應的Reference加入列隊的,而是呼叫Cleaner.clean方法,但如果Reference不注冊ReferenceQueue,GC處理時     * 又無法把他加入到pending-Reference鏈中,所以Cleaner里面有了一個dummyQueue成員變數,     */    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();    //Cleaner鏈表的頭結點    private static Cleaner first = null;    //當前Cleaner節點的后續節點    private Cleaner next = null;    //當前Cleaner節點的前續節點    private Cleaner prev = null;    //真正執行清理作業的Runnable物件,實際clean內部呼叫thunk.run()方法    private final Runnable thunk;    //省略部分代碼...}
復制代碼

從上面的成變數分析知道Cleaner實作了雙向鏈表的結構,先看建構式與clean方法,

//私有方法,不能直接newprivate Cleaner(Object var1, Runnable var2) {    super(var1, dummyQueue);    this.thunk = var2;}//創建Cleaner物件,同時加入Cleaner鏈中,public static Cleaner create(Object var0, Runnable var1) {    return var1 == null ? null : add(new Cleaner(var0, var1));}//頭插法將新創意的Cleaner物件加入雙向鏈表,synchronized保證同步private static synchronized Cleaner add(Cleaner var0) {    if (first != null) {        var0.next = first;        first.prev = var0;    }    //更新頭節點參考    first = var0;    return var0;}public void clean() {  //從Cleaner鏈表中先移除當前節點    if (remove(this)) {        try {            //呼叫thunk.run()方法執行對應清理邏輯            this.thunk.run();        } catch (final Throwable var2) {           //省略部分代碼..        }    }}
復制代碼

可以看到Cleaner的實作還是比較簡單,Cleaner實作為PhantomReference型別的參考,當JVM GC時如果發現當前處理的物件只被PhantomReference型別物件參考,同之前說的一樣其會將該Reference加pending-Reference鏈中上,只是ReferenceHandler執行緒在處理時如果PhantomReference型別實際型別又是Cleaner的話,其就是呼叫Cleaner.clean方法做清理邏輯處理,Cleaner實際是DirectByteBuffer分配的堆外記憶體識訓的實作,具體見下面的分析,

DirectByteBuffer堆外記憶體回收與WeakHashMap敏感記憶體回收

繞開了一大圈終于回到了文章最開始提到的兩個問題,先來看一下分配給DirectByteBuffer堆外記憶體是如何回收的,在創建DirectByteBuffer時我們實際是呼叫ByteBuffer#allocateDirect方法,而其實作如下:

public static ByteBuffer allocateDirect(int capacity) {    return new DirectByteBuffer(capacity);}DirectByteBuffer(int cap) {    //省略部分代碼...    try {      //呼叫unsafe分配記憶體        base = unsafe.allocateMemory(size);    } catch (OutOfMemoryError x) {       //省略部分代碼...    }    //省略部分代碼...    //前面分析中的Cleaner物件創建,持有當前DirectByteBuffer的參考    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));    att = null;}
復制代碼

里面和DirectByteBuffer堆外記憶體回收相關的代碼便是Cleaner.create(this, new Deallocator(base, size, cap))這部分,還記得之前說實際的清理邏輯是里面和DirectByteBuffer堆外記憶體回收相關的代碼便是Cleaner里面的Runnable#run方法嗎?直接看Deallocator.run方法原始碼:

public void run() {    if (address == 0) {        // Paranoia        return;    }    //通過unsafe.freeMemory釋放創建的堆外記憶體    unsafe.freeMemory(address);    address = 0;    Bits.unreserveMemory(size, capacity);}
復制代碼

終于找到了分配給DirectByteBuffer堆外記憶體是如何回收的的答案,再總結一下,創建DirectByteBuffer物件時會創建一個Cleaner物件,Cleaner物件持有了DirectByteBuffer物件的參考,當JVM在GC時,如果發現DirectByteBuffer被地方法沒被參考啦,JVM會將其對應的Cleaner加入到pending-reference鏈表中,同時通知ReferenceHandler執行緒處理,ReferenceHandler收到通知后,會呼叫Cleaner#clean方法,而對于DirectByteBuffer創建的Cleaner物件其clean方法內部會呼叫unsafe.freeMemory釋放堆外記憶體,最終達到了DirectByteBuffer物件被GC回收其對應的堆外記憶體也被回收的目的,

再來看一下文章開始提到的另外一個問題WeakHashMap如何實作敏感記憶體的回收,實際WeakHashMap實作上其Entry繼承了WeakReference,

//Entry繼承了WeakReference, WeakReference參考的是Map的keyprivate static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {    V value;    final int hash;    Entry<K,V> next;    /**     * 創建Entry物件,上面分析過的ReferenceQueue,這個queue實際是WeakHashMap的成員變數,     * 創建WeakHashMap時其便被初始化 final ReferenceQueue<Object> queue = new ReferenceQueue<>()     */    Entry(Object key, V value,          ReferenceQueue<Object> queue,          int hash, Entry<K,V> next) {        super(key, queue);        this.value = https://www.cnblogs.com/AIPAOJIAO/archive/2020/10/20/value;        this.hash  = hash;        this.next  = next;    }    //省略部分原碼...}
復制代碼

往WeakHashMap添加元素時,實際都會呼叫Entry的構造方法,也就是會創建一個WeakReference物件,這個物件的參考的是WeakHashMap剛加入的Key,而所有的WeakReference物件關聯在同一個ReferenceQueue上,我們上面說過JVM在GC時,如果發現當前物件只有被WeakReference物件參考,那么會把其對應的WeakReference物件加入到pending-reference鏈表上,并通知ReferenceHandler執行緒處理,而ReferenceHandler執行緒收到通知后,對于WeakReference物件會呼叫ReferenceQueue#enqueue方法把他加入佇列里面,現在我們只要關注queue里面的元素在WeakHashMap里面是在哪里被拿出去啦做了什么樣的操作,就能找到文章開始問題的答案啦,最終能定位到WeakHashMap的expungeStaleEntries方法,

private void expungeStaleEntries() {    //不斷地從ReferenceQueue中取出,那些只有被WeakReference物件參考的物件的Reference    for (Object x; (x = queue.poll()) != null; ) {        synchronized (queue) {            //轉為 entry            Entry<K,V> e = (Entry<K,V>) x;            //計算其對應的桶的下標            int i = indexFor(e.hash, table.length);            //取出桶中元素            Entry<K,V> prev = table[i];            Entry<K,V> p = prev;            //桶中對應位置有元素,遍歷桶鏈表所有元素            while (p != null) {                Entry<K,V> next = p.next;                //如果當前元素(也就是entry)與queue取出的一致,將entry從鏈表中去除                if (p == e) {                    if (prev == e)                        table[i] = next;                    else                        prev.next = next;                    // Must not null out e.next;                    //清空entry對應的value                    e.value = https://www.cnblogs.com/AIPAOJIAO/archive/2020/10/20/null;                    size--;                    break;                }                prev = p;                p = next;            }        }    }}
復制代碼

現在只看一下WeakHashMap哪些地方會呼叫expungeStaleEntries方法就知道什么時候WeakHashMap里面的Key變得軟可達時我們就可以將其對應的Entry從WeakHashMap里面移除,直接呼叫有三個地方分別是getTable方法、size方法、resize方法, getTable方法又被很多地方呼叫如get、containsKey、put、remove、containsValue、replaceAll,最終看下來,只要對WeakHashMap進行操作就行呼叫expungeStaleEntries方法,所有只要操作了WeakHashMap,沒WeakHashMap里面被再用到的Key對應的Entry就會被清除,再來總結一下,為什么WeakHashMap適合作為記憶體敏感快取的實作,當JVM 在GC時,如果發現WeakHashMap里面某些Key沒地方在被參考啦(WeakReference除外),JVM會將其對應的WeakReference物件加入到pending-reference鏈表上,并通知ReferenceHandler執行緒處理,而ReferenceHandler執行緒收到通知后將對應參考Key的WeakReference物件加入到 WeakHashMap內部的ReferenceQueue中,下次再對WeakHashMap做操作時,WeakHashMap內部會清除那些沒有被參考的Key對應的Entry,這樣就達到了每操作WeakHashMap時,自動的檢索并清量沒有被參考的Key對應的Entry的目地,

總結

本文通過兩個問題引出了JDK中Reference相關類的原始碼分析,最終給出了問題的答案,但實際上一般開發規范中都會建議禁止重寫Object#finalize方法同樣與Reference類關系密切(具體而言是Finalizer類),受篇幅的限制本文并未給出分析,有待各位自己看原始碼啦,半年沒有寫文章啦,有點對不住關注的小伙伴,希望看完本文各位或多或少能有所識訓,如果覺得本文不錯就幫忙轉發記得標一下出處,謝謝,后面我還會繼續分享一些自己覺得比較重要的東西給大家,由于個人能力有限,文中不足與錯誤還望指正,

看完三件事??

如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:

  1. 點贊,轉發,有你們的 『點贊和評論』,才是我創造的動力,

  2. 關注公眾號 『 java爛豬皮 』,不定期分享原創知識,

  3. 同時可以期待后續文章ing??

本文作者:葉易

出處:club.perfma.com/article/125…

 

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/182756.html

標籤:其他

上一篇:消失的Java行程-Linux OOM Killer

下一篇:Java8 Stream原始碼分析

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more