PHP新的垃圾回收機制:Zend GC詳解

来源:http://www.cnblogs.com/orlion/archive/2016/04/03/5350844.html
-Advertisement-
Play Games

概述 在5.2及更早版本的PHP中,沒有專門的垃圾回收器GC(Garbage Collection),引擎在判斷一個變數空間是否能夠被釋放的時候是依據這個變數的zval的refcount的值,如果refcount為0,那麼變數的空間可以被釋放,否則就不釋放,這是一種非常簡單的GC實現。然而在這種簡單 ...


概述

    在5.2及更早版本的PHP中,沒有專門的垃圾回收器GC(Garbage Collection),引擎在判斷一個變數空間是否能夠被釋放的時候是依據這個變數的zval的refcount的值,如果refcount為0,那麼變數的空間可以被釋放,否則就不釋放,這是一種非常簡單的GC實現。然而在這種簡單的GC實現方案中,出現了意想不到的變數記憶體泄漏情況(Bug:http://bugs.php.net/bug.php?id=33595),引擎將無法回收這些記憶體,於是在PHP5.3中出現了新的GC,新的GC有專門的機制負責清理垃圾數據,防止記憶體泄漏。本文將詳細的闡述PHP5.3中新的GC運行機制。

    目前很少有詳細的資料介紹新的GC,本文將是目前國內最為詳細的從源碼角度介紹PHP5.3中GC原理的文章。其中關於垃圾產生以及演算法簡介部分由筆者根據手冊翻譯而來,當然其中融入了本人的一些看法。手冊中相關內容:Garbage Collection

    在介紹這個新的GC之前,讀者必須先瞭解PHP中變數的內部存儲相關知識,請先閱讀 變數的內部存儲:引用和計數 

 

什麼算垃圾

    首先我們需要定義一下“垃圾”的概念,新的GC負責清理的垃圾是指變數的容器zval還存在,但是又沒有任何變數名指向此zval。因此GC判斷是否為垃圾的一個重要標準是有沒有變數名指向變數容器zval。

    假設我們有一段PHP代碼,使用了一個臨時變數$tmp存儲了一個字元串,在處理完字元串之後,就不需要這個$tmp變數了,$tmp變數對於我們來說可以算是一個“垃圾”了,但是對於GC來說,$tmp其實並不是一個垃圾,$tmp變數對我們沒有意義,但是這個變數實際還存在,$tmp符號依然指向它所對應的zval,GC會認為PHP代碼中可能還會使用到此變數,所以不會將其定義為垃圾。

    那麼如果我們在PHP代碼中使用完$tmp後,調用unset刪除這個變數,那麼$tmp是不是就成為一個垃圾了呢。很可惜,GC仍然不認為$tmp是一個垃圾,因為$tmp在unset之後,refcount減少1變成了0(這裡假設沒有別的變數和$tmp指向相同的zval),這個時候GC會直接將$tmp對應的zval的記憶體空間釋放,$tmp和其對應的zval就根本不存在了。此時的$tmp也不是新的GC所要對付的那種“垃圾”。那麼新的GC究竟要對付什麼樣的垃圾呢,下麵我們將生產一個這樣的垃圾。  

 

頑固垃圾的產生過程

    如果讀者已經閱讀了變數內部存儲相關的內容,想必對refcount和isref這些變數內部的信息有了一定的瞭解。這裡我們將結合手冊中的一個例子來介紹垃圾的產生過程:

 

<?php

$a = "new string";

?>

在這麼簡單的一個代碼中,$a變數內部存儲信息為

a: (refcount=1, is_ref=0)='new string'

 

當把$a賦值給另外一個變數的時候,$a對應的zval的refcount會加1

<?php

$a = "new string";

$b = $a;

?>
此時$a和$b變數對應的內部存儲信息為

a,b: (refcount=2, is_ref=0)='new string'

當我們用unset刪除$b變數的時候,$b對應的zval的refcount會減少1

<?php

$a = "new string"; //a: (refcount=1, is_ref=0)='new string'

$b = $a;                 //a,b: (refcount=2, is_ref=0)='new string'

unset($b);              //a: (refcount=1, is_ref=0)='new string'

?>

 

對於普通的變數來說,這一切似乎很正常,但是在複合類型變數(數組和對象)中,會發生比較有意思的事情:

<?php

$a = array('meaning' => 'life', 'number' => 42);

?>

a的內部存儲信息為:

a: (refcount=1, is_ref=0)=array (
   'meaning' => (refcount=1, is_ref=0)='life',
   'number' => (refcount=1, is_ref=0)=42
)

數組變數本身($a)在引擎內部實際上是一個哈希表,這張表中有兩個zval項 meaning和number,

所以實際上那一行代碼中一共生成了3個zval,這3個zval都遵循變數的引用和計數原則,用圖來表示:

 

 

 

 

 下麵在$a中添加一個元素,並將現有的一個元素的值賦給新的元素:

<?php

$a = array('meaning' => 'life', 'number' => 42);

$a['life'] = $a['meaning'];

?>

那麼$a的內部存儲為:

a: (refcount=1, is_ref=0)=array (
   'meaning' => (refcount=2, is_ref=0)='life',
   'number' => (refcount=1, is_ref=0)=42,
   'life' => (refcount=2, is_ref=0)='life'
)
其中的meaning元素和life元素之指向同一個zval的:

 

 

現在,如果我們試一下,將數組的引用賦值給數組中的一個元素,有意思的事情就發生了:

<?php

$a = array('one');

$a[] = &$a;

?>

這樣$a數組就有兩個元素,一個索引為0,值為字元one,另外一個索引為1,為$a自身的引用,內部存儲如下:

a: (refcount=2, is_ref=1)=array (
   0 => (refcount=1, is_ref=0)='one',
   1 => (refcount=2, is_ref=1)=…
)

“…”表示1指向a自身,是一個環形引用:

 

這個時候我們對$a進行unset,那麼$a會從符號表中刪除,同時$a指向的zval的refcount減少1

<?php

$a = array('one');

$a[] = &$a;

unset($a);

?>

那麼問題也就產生了,$a已經不在符號表中了,用戶無法再訪問此變數,但是$a之前指向的zval的refcount變為1而不是0,因此不能被回收,這樣產生了記憶體泄露:

 

這樣,這麼一個zval就成為了一個真是意義的垃圾了,新的GC要做的工作就是清理這種垃圾。

 

為解決這種垃圾,產生了新的GC

    在PHP5.3版本中,使用了專門GC機制清理垃圾,在之前的版本中是沒有專門的GC,那麼垃圾產生的時候,沒有辦法清理,記憶體就白白浪費掉了。在PHP5.3源代碼中多了以下文件:{PHPSRC}/Zend/zend_gc.h {PHPSRC}/Zend/zend_gc.c, 這裡就是新的GC的實現,我們先簡單的介紹一下演算法思路,然後再從源碼的角度詳細介紹引擎中如何實現這個演算法的。

 

新的GC演算法

    在較新的PHP手冊中有簡單的介紹新的GC使用的垃圾清理演算法,這個演算法名為 Concurrent Cycle Collection in Reference Counted Systems , 這裡不詳細介紹此演算法,根據手冊中的內容來先簡單的介紹一下思路:

首先我們有幾個基本的準則:

1:如果一個zval的refcount增加,那麼此zval還在使用,不屬於垃圾

2:如果一個zval的refcount減少到0, 那麼zval可以被釋放掉,不屬於垃圾

3:如果一個zval的refcount減少之後大於0,那麼此zval還不能被釋放,此zval可能成為一個垃圾

 

只有在準則3下,GC才會把zval收集起來,然後通過新的演算法來判斷此zval是否為垃圾。那麼如何判斷這麼一個變數是否為真正的垃圾呢?

簡單的說,就是對此zval中的每個元素進行一次refcount減1操作,操作完成之後,如果zval的refcount=0,那麼這個zval就是一個垃圾。這個原理咋看起來很簡單,但是又不是那麼容易理解,起初筆者也無法理解其含義,直到挖掘了源代碼之後才算是瞭解。如果你現在不理解沒有關係,後面會詳細介紹,這裡先把這演算法的幾個步驟描敘一下,首先引用手冊中的一張圖:

 

 

 

 

A:為了避免每次變數的refcount減少的時候都調用GC的演算法進行垃圾判斷,此演算法會先把所有前面準則3情況下的zval節點放入一個節點(root)緩衝區(root buffer),並且將這些zval節點標記成紫色,同時演算法必須確保每一個zval節點在緩衝區中之出現一次。當緩衝區被節點塞滿的時候,GC才開始開始對緩衝區中的zval節點進行垃圾判斷。

B:當緩衝區滿了之後,演算法以深度優先對每一個節點所包含的zval進行減1操作,為了確保不會對同一個zval的refcount重覆執行減1操作,一旦zval的refcount減1之後會將zval標記成灰色。需要強調的是,這個步驟中,起初節點zval本身不做減1操作,但是如果節點zval中包含的zval又指向了節點zval(環形引用),那麼這個時候需要對節點zval進行減1操作。

C:演算法再次以深度優先判斷每一個節點包含的zval的值,如果zval的refcount等於0,那麼將其標記成白色(代表垃圾),如果zval的refcount大於0,那麼將對此zval以及其包含的zval進行refcount加1操作,這個是對非垃圾的還原操作,同時將這些zval的顏色變成黑色(zval的預設顏色屬性)

D:遍歷zval節點,將C中標記成白色的節點zval釋放掉。

 

這ABCD四個過程是手冊中對這個演算法的介紹,這還不是那麼容易理解其中的原理,這個演算法到底是個什麼意思呢?我自己的理解是這樣的:

比如還是前面那個變成垃圾的數組$a對應的zval,命名為zval_a,  如果沒有執行unset, zval_a的refcount為2,分別由$a和$a中的索引1指向這個zval。  用演算法對這個數組中的所有元素(索引0和索引1)的zval的refcount進行減1操作,由於索引1對應的就是zval_a,所以這個時候zval_a的refcount應該變成了1,這樣zval_a就不是一個垃圾。如果執行了unset操作,zval_a的refcount就是1,由zval_a中的索引1指向zval_a,用演算法對數組中的所有元素(索引0和索引1)的zval的refcount進行減1操作,這樣zval_a的refcount就會變成0,於是就發現zval_a是一個垃圾了。 演算法就這樣發現了頑固的垃圾數據。

舉了這個例子,讀者大概應該能夠知道其中的端倪:

對於一個包含環形引用的數組,對數組中包含的每個元素的zval進行減1操作,之後如果發現數組自身的zval的refcount變成了0,那麼可以判斷這個數組是一個垃圾。

這個道理其實很簡單,假設數組a的refcount等於m, a中有n個元素又指向a,如果m等於n,那麼演算法的結果是m減n,m-n=0,那麼a就是垃圾,如果m>n,那麼演算法的結果m-n>0,所以a就不是垃圾了

 

m=n代表什麼?  代表a的refcount都來自數組a自身包含的zval元素,代表a之外沒有任何變數指向它,代表用戶代碼空間中無法再訪問到a所對應的zval,代表a是泄漏的記憶體,因此GC將a這個垃圾回收了。

 

PHP中運用新的GC的演算法

    在PHP中,GC預設是開啟的,你可以通過ini文件中的 zend.enable_gc 項來開啟或則關閉GC。當GC開啟的時候,垃圾分析演算法將在節點緩衝區(roots buffer)滿了之後啟動。緩衝區預設可以放10,000個節點,當然你也可以通過修改Zend/zend_gc.c中的GC_ROOT_BUFFER_MAX_ENTRIES 來改變這個數值,需要重新編譯鏈接PHP。當GC關閉的時候,垃圾分析演算法就不會運行,但是相關節點還會被放入節點緩衝區,這個時候如果緩衝區節點已經放滿,那麼新的節點就不會被記錄下來,這些沒有被記錄下來的節點就永遠也不會被垃圾分析演算法分析。如果這些節點中有迴圈引用,那麼有可能產生記憶體泄漏。之所以在GC關閉的時候還要記錄這些節點,是因為簡單的記錄這些節點比在每次產生節點的時候判斷GC是否開啟更快,另外GC是可以在腳本運行中開啟的,所以記錄下這些節點,在代碼運行的某個時候如果又開啟了GC,這些節點就能被分析演算法分析。當然垃圾分析演算法是一個比較耗時的操作。

    在PHP代碼中我們可以通過gc_enable()和gc_disable()函數來開啟和關閉GC,也可以通過調用gc_collect_cycles()在節點緩衝區未滿的情況下強制執行垃圾分析演算法。這樣用戶就可以在程式的某些部分關閉或則開啟GC,也可強制進行垃圾分析演算法。 

   

新的GC演算法的性能

1.防止泄漏節省記憶體

    新的GC演算法的目的就是為了防止迴圈引用的變數引起的記憶體泄漏問題,在PHP中GC演算法,當節點緩衝區滿了之後,垃圾分析演算法會啟動,並且會釋放掉發現的垃圾,從而回收記憶體,在PHP手冊上給了一段代碼和記憶體使用狀況圖:

 

 

<?php
class Foo
{
    public $var = '3.1415962654';
}

$baseMemory = memory_get_usage();

for ( $i = 0; $i <= 100000; $i++ )
{
    $a = new Foo;
    $a->self = $a;
    if ( $i % 500 === 0 )
    {
        echo sprintf( '%8d: ', $i ), memory_get_usage() - $baseMemory, "/n";
    }
}
?>

這段代碼的迴圈體中,新建了一個對象變數,並且用對象的一個成員指向了自己,這樣就形成了一個迴圈引用,當進入下一次迴圈的時候,又一次給對象變數重新賦值,這樣會導致之前的對象變數記憶體泄漏,在這個例子裡面有兩個變數泄漏了,一個是對象本身,另外一個是對象中的成員self,但是這兩個變數只有對象會作為垃圾收集器的節點被放入緩衝區(因為重新賦值相當於對它進行了unset操作,滿足前面的準則3)。在這裡我們進行了100,000次迴圈,而GC在緩衝區中有10,000節點的時候會啟動垃圾分析演算法,所以這裡一共會進行10次的垃圾分析演算法。從圖中可以清晰的看到,在5.3版本PHP中,每次GC的垃圾分析演算法被觸發後,記憶體會有一個明顯的減少。而在5.2版本的PHP中,記憶體使用量會一直增加。

 

 

2:運行效率影響

    啟用了新的GC後,垃圾分析演算法將是一個比較耗時的操作,手冊中給了一段測試代碼:

 

 

 

<?php
class Foo
{
    public $var = '3.1415962654';
}

for ( $i = 0; $i <= 1000000; $i++ )
{
    $a = new Foo;
    $a->self = $a;
}

echo memory_get_peak_usage(), "/n";
?>

然後分別在GC開啟和關閉的情況下執行這段代碼:

time php -dzend.enable_gc=0 -dmemory_limit=-1 -n example2.php
# and
time php -dzend.enable_gc=1 -dmemory_limit=-1 -n example2.php

最終在該機器上,第一次執行大概使用10.7秒,第二次執行大概使用11.4秒,性能大約降低7%,不過記憶體的使用量降低了98%,從931M降低到了10M。當然這並不是一個比較科學的測試方法,但是也能說明一定的問題。這種代碼測試的是一種極端惡劣條件,實際代碼中,特別是在WEB的應用中,很難出現大量迴圈引用,GC的分析演算法的啟動不會這麼頻繁,小規模的代碼中甚至很少有機會啟動GC分析演算法。

總結:

當GC的垃圾分析演算法執行的時候,PHP腳本的效率會受到一定的影響,但是小規模的代碼一般不會有這個機會運行這個演算法。如果一旦腳本中GC分析演算法開始運行了,那麼將花費少量的時間節省出來了大量的記憶體,是一件非常划算的事情。新的GC對一些長期運行的PHP腳本效果更好,比如PHP的DAEMON守護進程,或則PHP-GTK進程等等。

 

 

 

 

引擎內部GC的實現

   前面已經介紹了新的GC的基本原理以及性能相關的內容,其中一些都是在手冊中有簡單介紹了,那麼這裡我們將從源代碼的角度來分析一下PHP如何實現新的GC。

1.zval的變化

    在文件Zend/zend_gc.h中,重新定義了分配一個zval結構的巨集:

 

 

[cpp] view plain copy

  1. #undef  ALLOC_ZVAL  

  2. #define ALLOC_ZVAL(z)                                   /  

  3.     do {                                                /  

  4.         (z) = (zval*)emalloc(sizeof(zval_gc_info));     /  

  5.         GC_ZVAL_INIT(z);                                /  

  6.     } while (0)  

ALLOC_ZVAL的原始定義是在Zend/zend_alloc.h中,原始的定義只是分配一個zval結構的記憶體空間,然後在新的GC使用後,分配一個zval空間實際上是分配了一個zval_gc_info結構的空間,下麵看看zval_gc_info結構定義:

 

[cpp] view plain copy

  1. typedef struct _zval_gc_info {  

  2.     zval z;  

  3.     union {  

  4.         gc_root_buffer       *buffered;  

  5.         struct _zval_gc_info *next;  

  6.     } u;  

  7. } zval_gc_info;  

zval_gc_info這個結構的第一個成員就是一個zval結構,第二個成員是一個聯合體u,是一個指向gc_root_buffer的指針和一個指向_zval_gc_info的指針。  第一個成員為zval結構,這就保證了對zval_gc_info類型指針做類型轉換後和zval等價。在ALLOC_ZVAL巨集中,分配了一個zval_gc_info的空間後,是將空間的指針轉換成了(zval *)。這樣就相當於分配了一個zval的空間。然後GC_ZVAL_INIT巨集會把zval_gc_info中的成員u的buffered欄位設置成NULL:

 

[cpp] view plain copy

  1. #define GC_ZVAL_INIT(z) /  

  2.     ((zval_gc_info*)(z))->u.buffered = NULL  

這個u.buffered指針就是用來表示這個zval對應的節點信息指針。

新的GC會為所有的zval分配一個空間存放節點信息指針,只有當zval被GC放入節點緩衝區的時候,節點信息指針才會被指向一個節點信息結構,否則節點信息指針一直是NULL。

具體方式是通過分配一個zval_gc_info結構來實現,這個結構包含了zval和節點信息指針buffered。

 

 

2.節點信息

  zval的節點信息指針buffered指向一個gc_root_buffer類型,這個類型的定義如下:

 

[cpp] view plain copy

  1. typedef struct _gc_root_buffer {  

  2.     struct _gc_root_buffer   *prev;     /* double-linked list               */  

  3.     struct _gc_root_buffer   *next;  

  4.     zend_object_handle        handle;   /* must be 0 for zval               */  

  5.     union {  

  6.         zval                 *pz;  

  7.         zend_object_handlers *handlers;  

  8.     } u;  

  9. } gc_root_buffer;  

這是一個雙鏈表的節點結構類型,prev和next用來指向前一個節點和後一個節點,handel是和對象相關的,對象類型的變數比較特殊,我們這裡不討論,u是一個聯合體,u.pz用來指向這個節點所對應的zval結構。 這樣每一個zval結構和zval對應的節點信息互相被關聯在一起了:

通過一個zval指針pz找到節點指針: pr = ((zval_gc_info *)pz)->u.buffered

通過一個節點指針pr找到zval指針: pz = pr->u.pz

 

3.為zval設置節點信息以及節點顏色信息

    這裡GC應用了一些小技巧,先看看下麵相關的巨集:

 

[cpp] view plain copy

  1. #define GC_COLOR  0x03  

  2.   

  3. #define GC_BLACK  0x00  

  4. #define GC_WHITE  0x01  

  5. #define GC_GREY   0x02  

  6. #define GC_PURPLE 0x03  

  7.   

  8. #define GC_ADDRESS(v) /  

  9.     ((gc_root_buffer*)(((zend_uintptr_t)(v)) & ~GC_COLOR))  

  10. #define GC_SET_ADDRESS(v, a) /  

  11.     (v) = ((gc_root_buffer*)((((zend_uintptr_t)(v)) & GC_COLOR) | ((zend_uintptr_t)(a))))  

  12. #define GC_GET_COLOR(v) /  

  13.     (((zend_uintptr_t)(v)) & GC_COLOR)  

  14. #define GC_SET_COLOR(v, c) /  

  15.     (v) = ((gc_root_buffer*)((((zend_uintptr_t)(v)) & ~GC_COLOR) | (c)))  

  16. #define GC_SET_BLACK(v) /  

  17.     (v) = ((gc_root_buffer*)(((zend_uintptr_t)(v)) & ~GC_COLOR))  

  18. #define GC_SET_PURPLE(v) /  

  19.     (v) = ((gc_root_buffer*)(((zend_uintptr_t)(v)) | GC_PURPLE))  

  20.   

  21. #define GC_ZVAL_INIT(z) /  

  22.     ((zval_gc_info*)(z))->u.buffered = NULL  

  23. #define GC_ZVAL_ADDRESS(v) /  

  24.     GC_ADDRESS(((zval_gc_info*)(v))->u.buffered)  

  25. #define GC_ZVAL_SET_ADDRESS(v, a) /  

  26.     GC_SET_ADDRESS(((zval_gc_info*)(v))->u.buffered, (a))  

  27. #define GC_ZVAL_GET_COLOR(v) /  

  28.     GC_GET_COLOR(((zval_gc_info*)(v))->u.buffered)  

  29. #define GC_ZVAL_SET_COLOR(v, c) /  

  30.     GC_SET_COLOR(((zval_gc_info*)(v))->u.buffered, (c))  

  31. #define GC_ZVAL_SET_BLACK(v) /  

  32.     GC_SET_BLACK(((zval_gc_info*)(v))->u.buffered)  

  33. #define GC_ZVAL_SET_PURPLE(v) /  

  34.     GC_SET_PURPLE(((zval_gc_info*)(v))->u.buffered)  

 

其中巨集GC_ZVAL_SET_ADDRESS(v, a)是為v這個zval設置節點信息的指針a,這個巨集先得到v中的節點信息指針欄位u.buffered,然後調用GC_ADDRESS(v,a)巨集,將u.buffered欄位設置成指針a。

GC_ADDRESS(v, a)巨集的功能是將地址a賦給v,但是它的實現很奇怪:

(v) = ((gc_root_buffer*)((((zend_uintptr_t)(v)) & GC_COLOR) | ((zend_uintptr_t)(a))))

 

為什麼需要這麼一個複雜的過程,而且設置指針值為何還要牽扯到GC_COLOR顏色這個巨集?

這裡就得先說說節點的顏色信息保存方式。

在前面GC的演算法簡介中,提到了需要為節點上色,而實際在我們節點結構gc_root_buffer中並沒有哪一個欄位用來標識節點的顏色,這裡GC運用了一個小的技巧:利用節點指針的低兩位來標識顏色屬性。可能讀者會有疑問,用指針中的位來保存顏色屬性,那麼設置顏色後,指針不就變化了嗎,那麼還能查找到指針對應的結構

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

-Advertisement-
Play Games
更多相關文章
  • retrofit:一套RESTful架構的Android(Java)客戶端實現。 好處: 基於註解 提供JSON to POJO,POJO to JSON,網路請求(POST,GET,PUT,DELETE等)封裝 可以看做是對HttpClient的再次封裝 1、為了做測試,建立了一個新的spring ...
  • 協程,又稱微線程,纖程。英文名Coroutine。單進程的非同步編程模型稱為協程,有了協程的支持,就可以基於事件驅動編寫高效的多任務程式 協程的概念很早就提出來了,但直到最近幾年才在某些語言(如Lua)中得到廣泛應用。 子程式,或者稱為函數,在所有語言中都是層級調用,比如A調用B,B在執行過程中又調用 ...
  • 假設當前表結構如下: food表欄位有foodid,name,外鍵businessid,外鍵type business表欄位有,name,外鍵type type表欄位有id,name,foodid Hibernate生成的對應POJO分別是Food,Business,Type 需要查詢food表部分 ...
  • 我們在升級系統的時候,經常碰到需要更新伺服器端數據結構等操作,之前的方式是通過手工編寫alter sql腳本處理,經常會發現遺漏,導致程式發佈到伺服器上後無法正常使用。 現在我們可以使用Flask-Migrate插件來解決之,Flask-Migrate插件是基於Alembic,Alembic是由大名 ...
  • 在利用Python進行系統管理的時候,特別是同時操作多個文件目錄,或者遠程式控制制多台主機,並行操作可以節約大量的時間。當被操作對象數目不大時,可以直接利用multiprocessing中的Process動態成生多個進程,10幾個還好,但如果是上百個,上千個目標,手動的去限制進程數量卻又太過繁瑣,這時候 ...
  • 欄位表集合 這個class文件的解析,分析得有點太久了.前面介紹類魔數,次版本號,主板本號,常量池入口,常量池,訪問標誌,類索引,父類索引和介面索引集合.下麵就應該到欄位表集合了. 欄位表集合 這個class文件的解析,分析得有點太久了.前面介紹類魔數,次版本號,主板本號,常量池入口,常量池,訪問標 ...
  • 泛型是Java SE 1.5的新特性,泛型的本質是參數化類型,也就是說所操作的數據類型被指定為一個參數。這種參數類型可以用在類、介面和方法的創建中,分別稱為泛型類、泛型介面、泛型方法。 Java語言引入泛型的好處是安全簡單。 在Java SE 1.5之前,沒有泛型的情況的下,通過對類型Object的 ...
  • 原文鏈接:http://www.orlion.ga/189/ 一、scope bean的scope屬性中常用的有兩種:singleton(單例,預設)和prototype(原型,每次創建新對象) 例:beans.xml 在java文件中: 二、集合註入 UserDAOImpl.java: beans ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...