Java基礎篇——JVM之GC原理(乾貨滿滿)

来源:https://www.cnblogs.com/baixianlong/archive/2019/04/13/10697554.html
-Advertisement-
Play Games

原創不易,如需轉載,請註明出處 "https://www.cnblogs.com/baixianlong/p/10697554.html" ,多多支持哈! 一、什麼是GC? GC是垃圾收集的意思,記憶體處理是編程人員容易出現問題的地方,忘記或者錯誤的記憶體回收會導致程式或系統的不穩定甚至崩潰,Java提 ...


原創不易,如需轉載,請註明出處https://www.cnblogs.com/baixianlong/p/10697554.html ,多多支持哈!

一、什麼是GC?

GC是垃圾收集的意思,記憶體處理是編程人員容易出現問題的地方,忘記或者錯誤的記憶體回收會導致程式或系統的不穩定甚至崩潰,Java提供的GC功能可以自動監測對象是否超過作用域從而達到自動回收記憶體的目的,Java語言沒有提供釋放已分配記憶體的顯示操作方法。Java程式員不用擔心記憶體管理,因為垃圾收集器會自動進行管理。要請求垃圾收集,可以調用下麵的方法之一:System.gc() 或Runtime.getRuntime().gc()。

二、哪些記憶體需要回收?

哪些記憶體需要回收是垃圾回收機制第一個要考慮的問題,所謂“要回收的垃圾”無非就是那些不可能再被任何途徑使用的對象。那麼如何找到這些對象?

  • 引用計數法:這種演算法不能解決對象之間相互引用的情況,所以這種方法不靠譜
  • 可達性分析法:這個演算法的基本思想是通過一系列稱為“GC Roots”的對象作為起始點,從這些節點向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈(即GC Roots到對象不可達)時,則證明此對象是不可用的。

那麼問題又來了,如何選取GCRoots對象呢?在Java語言中,可以作為GCRoots的對象包括下麵幾種:

  1. 虛擬機棧(棧幀中的局部變數區,也叫做局部變數表)中引用的對象。
  2. 方法區中的類靜態屬性引用的對象。
  3. 方法區中常量引用的對象。
  4. 本地方法棧中JNI(Native方法)引用的對象。

下麵給出一個GCRoots的例子,如下圖,為GCRoots的引用鏈,obj8、obj9、obj10都沒有到GCRoots對象的引用鏈,所以會進行回收。

可達.png

三、四種引用狀以及基於可達性分析的記憶體回收原理

引用.png

對於可達性分析演算法而言,未到達的對象並非是“非死不可”的,若要宣判一個對象死亡,至少需要經歷兩次標記階段。

  1. 如果對象在進行可達性分析後發現沒有與GCRoots相連的引用鏈,則該對象被第一次標記併進行一次篩選,篩選條件為是否有必要執行該對象的finalize方法,若對象沒有覆蓋finalize方法或者該finalize方法是否已經被虛擬機執行過了,則均視作不必要執行該對象的finalize方法,即該對象將會被回收。反之,若對象覆蓋了finalize方法並且該finalize方法並沒有被執行過,那麼,這個對象會被放置在一個叫F-Queue的隊列中,之後會由虛擬機自動建立的、優先順序低的Finalizer線程去執行,而虛擬機不必要等待該線程執行結束,即虛擬機只負責建立線程,其他的事情交給此線程去處理。
  2. 對F-Queue中對象進行第二次標記,如果對象在finalize方法中拯救了自己,即關聯上了GCRoots引用鏈,如把this關鍵字賦值給其他變數,那麼在第二次標記的時候該對象將從“即將回收”的集合中移除,如果對象還是沒有拯救自己,那就會被回收。如下代碼演示了一個對象如何在finalize方法中拯救了自己,然而,它只能拯救自己一次,第二次就被回收了。具體代碼如下:

     public class GC { 
    
         public static GC SAVE_HOOK = null; 
    
         public static void main(String[] args) throws InterruptedException {
             // 新建對象,因為SAVE_HOOK指向這個對象,對象此時的狀態是(reachable,unfinalized)
             SAVE_HOOK = new GC(); 
             //將SAVE_HOOK設置成null,此時剛纔創建的對象就不可達了,因為沒有句柄再指向它了,對象此時狀態是(unreachable,unfinalized)
             SAVE_HOOK = null; 
             //強制系統執行垃圾回收,系統發現剛纔創建的對象處於unreachable狀態,並檢測到這個對象的類覆蓋了finalize方法,因此把這個對象放入F-Queue隊列,由低優先順序線程執行它的finalize方法,此時對象的狀態變成(unreachable, finalizable)或者是(finalizer-reachable,finalizable)
             System.gc(); 
             // sleep,目的是給低優先順序線程從F-Queue隊列取出對象並執行其finalize方法提供機會。在執行完對象的finalize方法中的super.finalize()時,對象的狀態變成(unreachable,finalized)狀態,但接下來在finalize方法中又執行了SAVE_HOOK = this;這句話,又有句柄指向這個對象了,對象又可達了。因此對象的狀態又變成了(reachable, finalized)狀態。
             Thread.sleep(500);
             // 這裡樓主說對象處於(reachable,finalized)狀態應該是合理的。對象的finalized方法被執行了,因此是finalized狀態。又因為在finalize方法是執行了SAVE_HOOK=this這句話,本來是unreachable的對象,又變成reachable了。
             if (null != SAVE_HOOK) { //此時對象應該處於(reachable, finalized)狀態 
                 // 這句話會輸出,註意對象由unreachable,經過finalize複活了。
                 System.out.println("Yes , I am still alive"); 
             } else { 
                 System.out.println("No , I am dead"); 
             } 
             // 再一次將SAVE_HOOK放空,此時剛纔複活的對象,狀態變成(unreachable,finalized)
             SAVE_HOOK = null; 
             // 再一次強制系統回收垃圾,此時系統發現對象不可達,雖然覆蓋了finalize方法,但已經執行過了,因此直接回收。
             System.gc(); 
             // 為系統回收垃圾提供機會
             Thread.sleep(500); 
             if (null != SAVE_HOOK) { 
                 // 這句話不會輸出,因為對象已經徹底消失了。
                 System.out.println("Yes , I am still alive"); 
             } else { 
                 System.out.println("No , I am dead"); 
             } 
         } 
    
         @Override 
         protected void finalize() throws Throwable { 
             super.finalize(); 
             System.out.println("execute method finalize()"); 
            // 這句話讓對象的狀態由unreachable變成reachable,就是對象複活
             SAVE_HOOK = this; 
         } 
     }  

運行結果如下:

    leesf
    null
    finalize method executed!
    leesf
    yes, i am still alive :)
    no, i am dead : (

  由結果可知,該對象拯救了自己一次,第二次沒有拯救成功,因為對象的finalize方法最多被虛擬機調用一次。此外,從結果我們可以得知,一個堆對象的this(放在局部變數表中的第一項)引用會永遠存在,在方法體內可以將this引用賦值給其他變數,這樣堆中對象就可以被其他變數所引用,即不會被回收。

四、方法區的垃圾回收

1、方法區的垃圾回收主要回收兩部分內容:

  • 廢棄常量
  • 無用的類

2、既然進行垃圾回收,就需要判斷哪些是廢棄常量,哪些是無用的類?

  • 如何判斷廢棄常量呢?以字面量回收為例,如果一個字元串“abc”已經進入常量池,但是當前系統沒有任何一個String對象引用了叫做“abc”的字面量,那麼,如果發生垃圾回收並且有必要時,“abc”就會被系統移出常量池。常量池中的其他類(介面)、方法、欄位的符號引用也與此類似。
  • 如何判斷無用的類呢?需要滿足以下三個條件:

    1. 該類的所有實例都已經被回收,即Java堆中不存在該類的任何實例。
    2. 載入該類的ClassLoader已經被回收。
    3. 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

五、垃圾收集演算法(垃圾回收器都是基於這些演算法來實現)

1、標記-清除(Mark-Sweep)演算法

  這是最基礎的演算法,標記-清除演算法就如同它的名字樣,分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,標記完成後統一回收所有被標記的對象。這種演算法的不足主要體現在效率和空間,從效率的角度講,標記和清除兩個過程的效率都不高;從空間的角度講,標記清除後會產生大量不連續的記憶體碎片, 記憶體碎片太多可能會導致以後程式運行過程中在需要分配較大對象時,無法找到足夠的連續記憶體而不得不提前觸發一次垃圾收集動作。標記-清除演算法執行過程如圖:

mark-sweep.png

2、複製(Copying)演算法

  複製演算法是為瞭解決效率問題而出現的,它將可用的記憶體分為兩塊,每次只用其中一塊,當這一塊記憶體用完了,就將還存活著的對象複製到另外一塊上面,然後再把已經使用過的記憶體空間一次性清理掉。這樣每次只需要對整個半區進行記憶體回收,記憶體分配時也不需要考慮記憶體碎片等複雜情況,只需要移動指針,按照順序分配即可。複製演算法的執行過程如圖:

copying.png

  不過這種演算法有個缺點,記憶體縮小為了原來的一半,這樣代價太高了。現在的商用虛擬機都採用這種演算法來回收新生代,不過研究表明1:1的比例非常不科學,因此新生代的記憶體被劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。每次回收時,將Eden和Survivor中還存活著的對象一次性複製到另外一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。HotSpot虛擬機預設Eden區和Survivor區的比例為8:1,意思是每次新生代中可用記憶體空間為整個新生代容量的90%。當然,我們沒有辦法保證每次回收都只有不多於10%的對象存活,當Survivor空間不夠用時,需要依賴老年代進行分配擔保(Handle Promotion)。

3、標記-整理(Mark-Compact)演算法

  複製演算法在對象存活率較高的場景下要進行大量的複製操作,效率很低。萬一對象100%存活,那麼需要有額外的空間進行分配擔保。老年代都是不易被回收的對象,對象存活率高,因此一般不能直接選用複製演算法。根據老年代的特點,有人提出了另外一種標記-整理演算法,過程與標記-清除演算法一樣,不過不是直接對可回收對象進行清理,而是讓所有存活對象都向一端移動,然後直接清理掉邊界以外的記憶體。標記-整理演算法的工作過程如圖:

mark-sweep.png

六、垃圾收集器

  垃圾收集器就是上面講的理論知識的具體實現了。不同虛擬機所提供的垃圾收集器可能會有很大差別,我們使用的是HotSpot,HotSpot這個虛擬機所包含的所有收集器如圖:

垃圾回收器總覽.png

  上圖展示了7種作用於不同分代的收集器,如果兩個收集器之間存在連線,那說明它們可以搭配使用。虛擬機所處的區域說明它是屬於新生代收集器還是老年代收集器。多說一句,我們必須明確一個觀點:沒有最好的垃圾收集器,更加沒有萬能的收集器,只能選擇對具體應用最合適的收集器。這也是HotSpot為什麼要實現這麼多收集器的原因。OK,下麵一個一個看一下收集器。

Serial收集器

  最基本、發展歷史最久的收集器,這個收集器是一個採用複製演算法的單線程的收集器,單線程一方面意味著它只會使用一個CPU或一條線程去完成垃圾收集工作,另一方面也意味著它進行垃圾收集時必須暫停其他線程的所有工作,直到它收集結束為止。後者意味著,在用戶不可見的情況下要把用戶正常工作的線程全部停掉,這對很多應用是難以接受的。不過實際上到目前為止,Serial收集器依然是虛擬機運行在Client模式下的預設新生代收集器,因為它簡單而高效。用戶桌面應用場景中,分配給虛擬機管理的記憶體一般來說不會很大,收集幾十兆甚至一兩百兆的新生代停頓時間在幾十毫秒最多一百毫秒,只要不是頻繁發生,這點停頓是完全可以接受的。Serial收集器運行過程如下圖所示:

Serial收集器.png

  說明:1. 需要STW(Stop The World),停頓時間長。2. 簡單高效,對於單個CPU環境而言,Serial收集器由於沒有線程交互開銷,可以獲取最高的單線程收集效率。

ParNew收集器

  ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集外,其餘行為和Serial收集器完全一樣,包括使用的也是複製演算法。ParNew收集器除了多線程以外和Serial收集器並沒有太多創新的地方,但是它卻是Server模式下的虛擬機首選的新生代收集器,其中有一個很重要的和性能無關的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作(看圖)。CMS收集器是一款幾乎可以認為有劃時代意義的垃圾收集器,因為它第一次實現了讓垃圾收集線程與用戶線程基本上同時工作。ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至由於線程交互的開銷,該收集器在兩個CPU的環境中都不能百分之百保證可以超越Serial收集器。當然,隨著可用CPU數量的增加,它對於GC時系統資源的有效利用還是很有好處的。它預設開啟的收集線程數與CPU數量相同,在CPU數量非常多的情況下,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。ParNew收集器運行過程如下圖所示:

ParNew收集器.png

Parallel Scavenge收集器

  Parallel Scavenge收集器也是一個新生代收集器,也是用複製演算法的收集器,也是並行的多線程收集器,但是它的特點是它的關註點和其他收集器不同。介紹這個收集器主要還是介紹吞吐量的概念。CMS等收集器的關註點是儘可能縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是打到一個可控制的吞吐量。所謂吞吐量的意思就是CPU用於運行用戶代碼時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總運行100分鐘,垃圾收集1分鐘,那吞吐量就是99%。另外,Parallel Scavenge收集器是虛擬機運行在Server模式下的預設垃圾收集器

  停頓時間短適合需要與用戶交互的程式,良好的響應速度能提升用戶體驗;高吞吐量則可以高效率利用CPU時間,儘快完成運算任務,主要適合在後臺運算而不需要太多交互的任務。

  虛擬機提供了-XX:MaxGCPauseMillis和-XX:GCTimeRatio兩個參數來精確控制最大垃圾收集停頓時間和吞吐量大小。不過不要以為前者越小越好,GC停頓時間的縮短是以犧牲吞吐量和新生代空間換取的。由於與吞吐量關係密切,Parallel Scavenge收集器也被稱為“吞吐量優先收集器”。Parallel Scavenge收集器有一個-XX:+UseAdaptiveSizePolicy參數,這是一個開關參數,這個參數打開之後,就不需要手動指定新生代大小、Eden區和Survivor參數等細節參數了,虛擬機會根據當前系統的運行情況以及性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。如果對於垃圾收集器運作原理不太瞭解,以至於在優化比較困難的時候,使用Parallel Scavenge收集器配合自適應調節策略,把記憶體管理的調優任務交給虛擬機去完成將是一個不錯的選擇。

Serial Old收集器

Serial收集器的老年代版本,同樣是一個單線程收集器,使用“標記-整理演算法”,這個收集器的主要意義也是在於給Client模式下的虛擬機使用。

Parallel Old收集器

  Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”演算法。這個收集器在JDK 1.6之後的出現,“吞吐量優先收集器”終於有了比較名副其實的應用組合,在註重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge收集器+Parallel Old收集器的組合。運行過程如下圖所示:

Parallel Old收集器.png

CMS收集器

CMS(Conrrurent Mark Sweep)收集器是以獲取最短回收停頓時間為目標的收集器。使用標記 - 清除演算法,收集過程分為如下四步:

  1. 初始標記,標記GCRoots能直接關聯到的對象,時間很短。
  2. 併發標記,進行GCRoots Tracing(可達性分析)過程,時間很長。
  3. 重新標記,修正併發標記期間因用戶程式繼續運作而導致標記產生變動的那一部分對象的標記記錄,時間較長。
  4. 併發清除,回收記憶體空間,時間很長。

其中,併發標記與併發清除兩個階段耗時最長,但是可以與用戶線程併發執行。運行過程如下圖所示:

CMS收集器.png

說明:

  1. 對CPU資源非常敏感,可能會導致應用程式變慢,吞吐率下降。
  2. 無法處理浮動垃圾,因為在併發清理階段用戶線程還在運行,自然就會產生新的垃圾,而在此次收集中無法收集他們,只能留到下次收集,這部分垃圾為浮動垃圾,同時,由於用戶線程併發執行,所以需要預留一部分老年代空間提供併發收集時程式運行使用。
  3. 由於採用的標記 - 清除演算法,會產生大量的記憶體碎片,不利於大對象的分配,可能會提前觸發一次Full GC。虛擬機提供了-XX:+UseCMSCompactAtFullCollection參數來進行碎片的合併整理過程,這樣會使得停頓時間變長,虛擬機還提供了一個參數配置,-XX:+CMSFullGCsBeforeCompaction,用於設置執行多少次不壓縮的Full GC後,接著來一次帶壓縮的GC。

G1收集器

  G1演算法將堆劃分為若幹個區域(Region),它仍然屬於分代收集器。不過,這些區域的一部分包含新生代,新生代的垃圾收集依然採用暫停所有應用線程的方式,將存活對象拷貝到老年代或者Survivor空間。老年代也分成很多區域,G1收集器通過將對象從一個區域複製到另外一個區域,完成了清理工作。這就意味著,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會有cms記憶體碎片問題的存在了。

  在G1中,還有一種特殊的區域,叫Humongous區域。 如果一個對象占用的空間超過了分區容量50%以上,G1收集器就認為這是一個巨型對象。這些巨型對象,預設直接會被分配在年老代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成負面影響。為瞭解決這個問題,G1劃分了一個Humongous區,它用來專門存放巨型對象。如果一個H區裝不下一個巨型對象,那麼G1會尋找連續的H分區來存儲。為了能找到連續的H區,有時候不得不啟動Full GC。

G1.png

G1主要有以下特點:

  1. 並行和併發。使用多個CPU來縮短Stop The World停頓時間,與用戶線程併發執行。
  2. 分代收集。獨立管理整個堆,但是能夠採用不同的方式去處理新創建對象和已經存活了一段時間、熬過多次GC的舊對象,以獲取更好的收集效果。
  3. 空間整合。基於標記 - 整理演算法,無記憶體碎片產生。
  4. 可預測的停頓。能簡歷可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。

  在G1之前的垃圾收集器,收集的範圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的記憶體佈局與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分(可以不連續)Region的集合

七、CMS和G1對比(過去 vs 未來)

CMS垃圾回收器

 CMS堆記憶體結構劃分:

cms-記憶體結構.png

  • 新生代:eden space + 2個survivor
  • 老年代:old space
  • 持久代:1.8之前的perm space
  • 元空間:1.8之後的metaspace

  註意:這些space必須是地址連續的空間

CMS中垃圾回收模式

  • 對象分配

    1. 優先在Eden區分配

        在JVM記憶體模型一文中, 我們大致瞭解了VM年輕代堆記憶體可以劃分為一塊Eden區和兩塊Survivor區. 在大多數情況下, 對象在新生代Eden區中分配, 當Eden區沒有足夠空間分配時, VM發起一次Minor GC, 將Eden區和其中一塊Survivor區內尚存活的對象放入另一塊Survivor區域, 如果在Minor GC期間發現新生代存活對象無法放入空閑的Survivor區, 則會通過空間分配擔保機制使對象提前進入老年代(空間分配擔保見下).

    2. 大對象直接進入老年代

        Serial和ParNew兩款收集器提供了-XX:PretenureSizeThreshold的參數, 令大於該值的大對象直接在老年代分配, 這樣做的目的是避免在Eden區和Survivor區之間產生大量的記憶體複製(大對象一般指 需要大量連續記憶體的Java對象, 如很長的字元串和數組), 因此大對象容易導致還有不少空閑記憶體就提前觸發GC以獲取足夠的連續空間.

        然而取歷次晉升的對象的平均大小也是有一定風險的, 如果某次Minor GC存活後的對象突增,遠遠高於平均值的話,依然可能導致擔保失敗(Handle Promotion Failure, 老年代也無法存放這些對象了), 此時就只好在失敗後重新發起一次Full GC(讓老年代騰出更多空間).

    3. 空間分配擔保

        在執行Minor GC前, VM會首先檢查老年代是否有足夠的空間存放新生代尚存活對象, 由於新生代使用複製收集演算法, 為了提升記憶體利用率, 只使用了其中一個Survivor作為輪換備份, 因此當出現大量對象在Minor GC後仍然存活的情況時, 就需要老年代進行分配擔保, 讓Survivor無法容納的對象直接進入老年代, 但前提是老年代需要有足夠的空間容納這些存活對象. 但存活對象的大小在實際完成GC前是無法明確知道的, 因此Minor GC前, VM會先首先檢查老年代連續空間是否大於新生代對象總大小或歷次晉升的平均大小, 如果條件成立, 則進行Minor GC, 否則進行Full GC(讓老年代騰出更多空間).

  • 對象晉升

    1. 年齡閾值

        VM為每個對象定義了一個對象年齡(Age)計數器, 對象在Eden出生如果經第一次Minor GC後仍然存活, 且能被Survivor容納的話, 將被移動到Survivor空間中, 並將年齡設為1. 以後對象在Survivor區中每熬過一次Minor GC年齡就+1. 當增加到一定程度(-XX:MaxTenuringThreshold, 預設15), 將會晉升到老年代.

    2. 提前晉升: 動態年齡判定

        然而VM並不總是要求對象的年齡必須達到MaxTenuringThreshold才能晉升老年代: 如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半, 年齡大於或等於該年齡的對象就可以直接進入老年代, 而無須等到晉升年齡.

G1垃圾回收器

G1堆記憶體結構劃分(它將整個Java堆劃分為多個大小相等的獨立區域Region)

G1-記憶體結構.png

G1中提供了三種垃圾回收模式:young gc、mixed gc 和 full gc

  • Young GC

    發生在年輕代的GC演算法,一般對象(除了巨型對象)都是在eden region中分配記憶體,當所有eden region被耗盡無法申請記憶體時,就會觸發一次young gc,這種觸發機制和之前的young gc差不多,執行完一次young gc,活躍對象會被拷貝到survivor region或者晉升到old region中,空閑的region會被放入空閑列表中,等待下次被使用。

  • Mixed GC

    當越來越多的對象晉升到老年代old region時,為了避免堆記憶體被耗盡,虛擬機會觸發一個混合的垃圾收集器,即mixed gc,該演算法並不是一個old gc,除了回收整個young region,還會回收一部分的old region,這裡需要註意:是一部分老年代,而不是全部老年代,可以選擇哪些old region進行收集,從而可以對垃圾回收的耗時時間進行控制。
  • Full GC

    如果對象記憶體分配速度過快,mixed gc來不及回收,導致老年代被填滿,就會觸發一次full gc,G1的full gc演算法就是單線程執行的serial old gc,會導致異常長時間的暫停時間,需要進行不斷的調優,儘可能的避免full gc.

八、各種垃圾收集器的選用

  • 首先查看你使用的垃圾回收器是什麼?

    java -XX:+PrintCommandLineFlags -version
  • 根據自身系統需求選擇最合適的垃圾回收器(沒有最好的,只有最是適合的)

    各種收集器配置.png

九、總結

  • 到此GC的記憶體就差不多了,其中不免有些錯誤的地方,或者理解有偏頗的地方歡迎大家提出來!
  • 關於GC更細粒度的調優,沒敢妄言,今後有了實戰事例在補上!!!

個人博客地址:

csdn:https://blog.csdn.net/tiantuo6513
cnblogs:https://www.cnblogs.com/baixianlong
segmentfault:https://segmentfault.com/u/baixianlong
github:https://github.com/xianlongbai

本文參考:
https://www.cnblogs.com/xiaoxi/p/6486852.html
https://zhuanlan.zhihu.com/p/25539690


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

-Advertisement-
Play Games
更多相關文章
  • 後端搭起大體的框架後,接著涉及到的就是如何將數據持久化的問題,也就是對資料庫進行 CURD 操作。 關於資料庫方案, mongodb 和 mysql 都使用過,但我選用的是 mysql ,原因: 1. 目前為止 mysql 與 mongodb 性能相差不大,尤其是 mysql 8.0 版本,速度非常 ...
  • 本文簡要介紹UML及類圖的相關知識。用少量例子講述類圖的一些語法規則,常用的符號表示。 ...
  • 定義: 為子系統中的一組介面提供一個一致的界面,此模式定義了一個高層介面,這個介面使得這一子系統更加容易使用。 結構:(書中圖,侵刪) 結構:(書中圖,侵刪) 一個簡潔易用的外觀類 一個複雜的子系統 實例: 書中提到了理財的例子,找理財經理就不用自己研究各種股票債券什麼了;致使我一度腦子裡只想到了房 ...
  • 首先我們知道Java是一門面向對象的語言 面向對象三大特性,封裝、繼承、多態。 封裝、繼承、多態 ↓ 無論是學習路線,還是眾人的口語習慣,都是按照這個這樣進行排序,這是有原因的。因為封裝好了才能繼承,封裝和繼承都是為多態做準備的。 多態的三個前提條件: 1、繼承關係 2、方法的重寫 3、向上轉型(即 ...
  • 定義: 定義: 定義一個操作中的演算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以不改變一個演算法的結構即可重定義該演算法的某些特定步驟。 結構:(書中圖,侵刪) 一個定義整體框架的父類 若幹不同具體實現的子類 實例: 我瞬間就想到了很多RPG游戲。 不同職業在新手村完成任務,最多就是打的怪名字 ...
  • 前言 對於大部分程式員來說,主要工作都是進行編碼以及一些簡單的中間件安裝,這就導致了很多人對於“運維”相關的工作會比較生疏。例如當我們擁有一臺自己的伺服器以後,可能會在上面跑一跑自己blog程式,mysql,nginx等等。當程式越來越多了沒有一個統一的入口管理啟停,也可能會遇到一些特殊的原因導致程 ...
  • 繼承定義 繼承是使代碼可以復用的重要手段,也是面向對象程式設計的核心思想之一。 繼承就是不修改原有的類,直接利用原來的類的屬性和方法併進行擴展。原來的類稱為基類,繼承的類稱為派生類,他們的關係就像父子一樣,所以又叫父類和子類。 一般格式如下: 派生類成員可以訪問基類的 public成員 和 prot ...
  • 1、字典 字典:一系列鍵-值對,每一個鍵都與每一個值相關聯。與鍵相關聯的值可以是數字、字元串、列表和字典。 最簡單的字典只有一個鍵值對。 eg: alien = {'color':'green','points':5} 2、訪問字典 print(alien['color']) 列印結果:green ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...