摘要:調(diào)用函數(shù)時(shí),它將用戶釋放的內(nèi)存塊連接到空閑鏈上。這個(gè)聯(lián)合體共占用字節(jié)。是數(shù)字,且順序遞增位置固定,如訪問是的元素,即,就直接訪問數(shù)組的第個(gè)位置即可即,這樣就不需要前面的索引數(shù)組。
baiyan
全部視頻:https://segmentfault.com/a/11...
原視頻地址:http://replay.xesv5.com/ll/24...
本筆記中部分圖片截自視頻中的片段,圖片版權(quán)歸視頻原作者所有。
malloc函數(shù)深入在PHP內(nèi)存管理1筆記中提到,malloc()函數(shù)會(huì)在分配的內(nèi)存空間前面額外分配32位,用來存儲(chǔ)分配的大小和幾個(gè)標(biāo)志位,如圖:
那么究竟是否是這樣的呢?我們寫一段測(cè)試代碼驗(yàn)證一下:
#includeint main() { void *ptr = malloc(8); return 1; }
利用gdb調(diào)試這段代碼:
首先打印ptr的地址,為0x602010,利用x命令往后看20個(gè)內(nèi)存單元(1個(gè)內(nèi)存單元 = 4個(gè)字節(jié)),故一共展示了80個(gè)字節(jié),后面的x是以16進(jìn)制打印內(nèi)容。
我們發(fā)現(xiàn)緊鄰0x602010地址的上面32位均是0,沒有任何內(nèi)容,不符合我們的預(yù)期。
上圖只是一個(gè)最簡(jiǎn)單的思路,但絕大多數(shù)操作系統(tǒng)是按照如下的方式實(shí)現(xiàn)的:
操作系統(tǒng)中有一個(gè)記錄空閑內(nèi)存地址的鏈表。當(dāng)操作系統(tǒng)收到程序的申請(qǐng)時(shí),就會(huì)遍歷該鏈表,然后就尋找第一個(gè)空間大于所申請(qǐng)空間的堆結(jié)點(diǎn),然后就將該結(jié)點(diǎn)從空閑結(jié)點(diǎn)鏈表中刪除,并將該結(jié)點(diǎn)的空間分配給程序。malloc函數(shù)的實(shí)質(zhì)體現(xiàn)在,它有一個(gè)將可用的內(nèi)存塊連接為一個(gè)長(zhǎng)長(zhǎng)的列表的所謂空閑鏈表(Free List)。調(diào)用malloc函數(shù)時(shí),它沿連接表尋找一個(gè)大到足以滿足用戶請(qǐng)求所需要的內(nèi)存塊(根據(jù)不同的算法而定(將最先找到的不小于申請(qǐng)的大小內(nèi)存塊分配給請(qǐng)求者,將最合適申請(qǐng)大小的空閑內(nèi)存分配給請(qǐng)求者,或者是分配最大的空閑塊內(nèi)存塊)。然后,將該內(nèi)存塊一分為二(一塊的大小與用戶請(qǐng)求的大小相等,另一塊的大小就是剩下的字節(jié))。接下來,將分配給用戶的那塊內(nèi)存?zhèn)鹘o用戶,并將剩下的那塊(如果有的話)返回到連接表上。調(diào)用free函數(shù)時(shí),它將用戶釋放的內(nèi)存塊連接到空閑鏈上。到最后,空閑鏈會(huì)被切成很多的小內(nèi)存片段,如果這時(shí)用戶申請(qǐng)一個(gè)大的內(nèi)存片段,那么空閑鏈上可能沒有可以滿足用戶要求的片段了。于是,malloc函數(shù)請(qǐng)求延時(shí),并開始在空閑鏈上翻箱倒柜地檢查各內(nèi)存片段,對(duì)它們進(jìn)行整理,將相鄰的小空閑塊合并成較大的內(nèi)存塊。如果無法獲得符合要求的內(nèi)存塊,malloc函數(shù)會(huì)返回NULL指針,因此在調(diào)用malloc動(dòng)態(tài)申請(qǐng)內(nèi)存塊時(shí),一定要進(jìn)行返回值的判斷。結(jié)構(gòu)體與聯(lián)合體 結(jié)構(gòu)體
在PHP內(nèi)存管理2筆記中,我們談到了一種特殊情況:
在b是char類型的時(shí)候,a和b的內(nèi)存地址是緊鄰的;如果b是int類型的話,就會(huì)出現(xiàn)如圖所示的情況。我們可以這樣記憶:不看b之后的字段,a和b之前也是按照它們的最小公倍數(shù)對(duì)齊的(如果b是int類型,a和b的最小公倍數(shù)是4,按4對(duì)齊;如果b是char類型,最小公倍數(shù)為1,按1對(duì)齊,就會(huì)出現(xiàn)a和b緊鄰的情況)
如果不想對(duì)齊,有如下解決方案:
編譯的時(shí)候不加優(yōu)化參數(shù)
代碼層面:在struct后加關(guān)鍵字,例如redis中的sds簡(jiǎn)單動(dòng)態(tài)字符串的實(shí)現(xiàn):
struct __attribute__ ((packed)) sdshdr16 { uint16_t len; uint16_t alloc; unsigned char flags; char buf[]; }聯(lián)合體
所有字段共用一段內(nèi)存,用于PHP中變量值的存儲(chǔ)(因?yàn)樽兞恐挥幸环N類型),也可以用來判斷機(jī)器的大小端問題。
宏定義宏就是替換。
關(guān)于下面這段代碼的復(fù)雜宏替換問題,在PHP內(nèi)存管理3筆記中已經(jīng)有詳細(xì)解釋,此處不再贅述。
#define _BIN_DATA_SIZE(num, size, elements, pages, x, y) size, static const uint32_t bin_data_size[] = { ZEND_MM_BINS_INFO(_BIN_DATA_SIZE, x, y) };
關(guān)于C語(yǔ)言宏定義中的##等特殊符號(hào)的用法,參考:#define宏定義中的#,##,@#, 這些符號(hào)的神奇用法
PHP7中的基本變量在PHP7中,所有變量都以zval結(jié)構(gòu)體來表示。一個(gè)zval是16字節(jié);在PHP5中,一個(gè)zval是48字節(jié)。
struct _zval_struct { zend_value value; union u1; union u2; };
存儲(chǔ)變量需要考慮兩個(gè)要素:值與類型。
變量值的存放在PHP7中,變量的值存在zend_value 這個(gè)聯(lián)合體中。只有整型和浮點(diǎn)型是直接存在zend_value中,其余類型都只存放了一個(gè)指向?qū)iT存放該類型的結(jié)構(gòu)體指針。這個(gè)聯(lián)合體共占用8字節(jié)。
typedef union _zend_value { zend_long lval; //整型 double dval; //浮點(diǎn) zend_refcounted *counted; //引用計(jì)數(shù) zend_string *str; //字符串 zend_array *arr; //數(shù)組 zend_object *obj; //對(duì)象 zend_resource *res; //資源 zend_reference *ref; //引用 zend_ast_ref *ast; //抽象語(yǔ)法樹 zval *zv; //內(nèi)部使用 void *ptr; //不確定類型,取出來之后強(qiáng)轉(zhuǎn) zend_class_entry *ce; //類 zend_function *func;//函數(shù) struct { uint32_t w1; uint32_t w2; } ww; //這個(gè)union一共8B,這個(gè)結(jié)構(gòu)體每個(gè)字段都是4B,因?yàn)樗新?lián)合體字段共用一塊內(nèi)存,故相當(dāng)于取了一半的union } zend_value;變量類型的存放
在PHP7中,其變量的類型存放在zval中的u1聯(lián)合體中:
... union { struct { ZEND_ENDIAN_LOHI_4( zend_uchar type, /* 在這里用unsigned char存放PHP變量值的類型 */ zend_uchar type_flags, zend_uchar const_flags, zend_uchar reserved) /* call info for EX(This) */ } v; uint32_t type_info; } u1; ...
PHP7中所有的變量類型:
/* regular data types */ #define IS_UNDEF 0 #define IS_NULL 1 #define IS_FALSE 2 #define IS_TRUE 3 #define IS_LONG 4 #define IS_DOUBLE 5 #define IS_STRING 6 #define IS_ARRAY 7 #define IS_OBJECT 8 #define IS_RESOURCE 9 #define IS_REFERENCE 10 /* constant expressions */ #define IS_CONSTANT 11 #define IS_CONSTANT_AST 12 /* fake types */ #define _IS_BOOL 13 #define IS_CALLABLE 14 #define IS_ITERABLE 19 #define IS_VOID 18 /* internal types */ #define IS_INDIRECT 15 #define IS_PTR 17 #define _IS_ERROR 20PHP7中的字符串 字符串基本結(jié)構(gòu)
設(shè)計(jì)字符串存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)兩大要素:字符串值和長(zhǎng)度。
PHP7字符串存儲(chǔ)結(jié)構(gòu)的設(shè)計(jì):
struct _zend_string { zend_refcounted_h gc; /*引用計(jì)數(shù),與垃圾回收相關(guān),暫不展開*/ zend_ulong h; /* 冗余的hash值,計(jì)算數(shù)組key的哈希值時(shí)避免重復(fù)計(jì)算*/ size_t len; /* 長(zhǎng)度 */ char val[1]; /* 柔性數(shù)組,真正存放字符串值 */ };
由為什么存長(zhǎng)度引申出二進(jìn)制安全的問題。二進(jìn)制安全:寫入的數(shù)據(jù)和讀出來的數(shù)據(jù)完全相同,就是二進(jìn)制安全的,詳情見PHP字符串筆記
字符串寫時(shí)復(fù)制看下面一段PHP代碼:
利用gdb調(diào)試這段代碼,觀察其引用計(jì)數(shù)情況。
在第一個(gè)echo語(yǔ)句處打斷點(diǎn),并查看$a中zend_stinrg中的引用計(jì)數(shù)gc.refcount = 1(下簡(jiǎn)稱refcount)。因?yàn)楝F(xiàn)在只有一個(gè)$a引用zend_string。
利用gdb的c命令繼續(xù)運(yùn)行下一行PHP代碼$b = $a,然后觀察$a的zend_sting,我們發(fā)現(xiàn)$a引用的zend_string的refcount變?yōu)?:
查看此時(shí)的$b,發(fā)現(xiàn)引用的zend_string的refcount也是2,且地址均是0x7ffff5e6b0f0,說明$a與$b所引用的是同一個(gè)zend_string。
此時(shí)的內(nèi)存結(jié)構(gòu)如圖所示:
這樣做的優(yōu)點(diǎn)就是僅僅需要1個(gè)zend_string就可以存儲(chǔ)兩個(gè)PHP變量的值,而不是2個(gè)zend_string,節(jié)省了1個(gè)zend_string的內(nèi)存空間。
那么我們看接下來$b = "new string",這樣的話,$a和$b由于存儲(chǔ)的內(nèi)容不同,故不可以繼續(xù)引用同一個(gè)zend_string,這時(shí)就會(huì)發(fā)生寫時(shí)復(fù)制。我們繼續(xù)gdb調(diào)試,看一下是否符合預(yù)期:
給$b賦值后,觀察$a的存儲(chǔ)情況:
我們看到,此時(shí)$a所指向的zend_string的refcount變?yōu)榱?,接下來再看一下$b的存儲(chǔ)情況:
注意此時(shí)$b所指向的zend_string的refcount變?yōu)榱?(注意這里為什么是0而不是1呢?下面會(huì)講),而且b指向的zend_string的地址為0x7ffff5e6a5c8,與$a所指向的zend_string的地址0x7ffff5e6b0f0不同,說明發(fā)生了寫時(shí)復(fù)制,即由于字符串值的改變,被迫生成了一個(gè)新的zend_string結(jié)構(gòu)體,用來專門存儲(chǔ)$b的值;而$a指向的zend_string只是refcount減少了1,其余并未發(fā)生變化。
那么為什么$b所指向的zend_string的refcount是0呢,我們先給PHP中的字符串分個(gè)類:
常量字符串:在PHP代碼中硬編碼的字符串,在編譯階段初始化,存儲(chǔ)在全局變量表中,refcount一直為0,其在請(qǐng)求結(jié)束之后才被銷毀(方便重復(fù)利用)。
臨時(shí)字符串:計(jì)算出來的臨時(shí)字符串,是執(zhí)行階段經(jīng)過zend虛擬機(jī)執(zhí)行opcode計(jì)算出來的字符串,存儲(chǔ)在臨時(shí)變量區(qū)。
我們舉一個(gè)例子:
這里$a由于調(diào)用了time()函數(shù),所以最終的值是不確定的,是臨時(shí)字符串。
$b也可以叫做字面量,是被硬編碼在PHP代碼中的,是常量字符串。
我們畫一下最終$a與$b的內(nèi)存結(jié)構(gòu)圖:
由此我們可以清晰地看到,$a與$b不在引用同一個(gè)zend_string。那么我們給寫時(shí)復(fù)制下一個(gè)定義:給$b重新賦值而導(dǎo)致不能與$a共用一個(gè)zend_string的現(xiàn)象,叫做寫時(shí)復(fù)制。
PHP7中的數(shù)組PHP7中的數(shù)組是一個(gè)hashtable,key-value對(duì)存儲(chǔ)于bucket中。
PHP7數(shù)組基本結(jié)構(gòu):
struct _zend_array { zend_refcounted_h gc; union { struct { ZEND_ENDIAN_LOHI_4( zend_uchar flags, zend_uchar nApplyCount, zend_uchar nIteratorsCount, zend_uchar consistency) } v; uint32_t flags; } u; uint32_t nTableMask; //數(shù)組大小減一,用來做或運(yùn)算,packed array初始值是-2,hash array初始值是-8 Bucket *arData; //指針,指向?qū)嶋H存儲(chǔ)數(shù)組元素的bucket uint32_t nNumUsed; //使用了多少bucket,但是unset的時(shí)候這個(gè)值不減少 uint32_t nNumOfElements; //真正有多少元素,unset的時(shí)候會(huì)減少 uint32_t nTableSize; //bucket的個(gè)數(shù) uint32_t nInternalPointer; zend_long nNextFreeElement; //支持$arr[] = 1;語(yǔ)法,沒插入1個(gè)元素就會(huì)遞增1 dtor_func_t pDestructor; }; typedef struct _zend_array HashTable;此結(jié)構(gòu)在內(nèi)存中的結(jié)構(gòu)圖如下:
思考:為什么要存儲(chǔ)gc字段?因?yàn)間c字段冗余存儲(chǔ)了變量的類型,給任意一個(gè)變量,把它強(qiáng)轉(zhuǎn)成zend_refcounted_h類型,都可以拿到它的類型,zend_refcounted_h類型結(jié)構(gòu)如下:
typedef struct _zend_refcounted_h { uint32_t refcount; /* 引用計(jì)數(shù) */ union { struct { ZEND_ENDIAN_LOHI_3( zend_uchar type, zend_uchar flags, /* used for strings & objects */ uint16_t gc_info) /* keeps GC root number (or 0) and color */ } v; uint32_t type_info; } u; } zend_refcounted_h;進(jìn)行強(qiáng)制類型轉(zhuǎn)換之后,通過取該變量的u.type字段,就可以拿到當(dāng)前變量的類型了。
我們接著看一下bucket的結(jié)構(gòu):
typedef struct _Bucket { zval val; //元素的值,注意這里直接存了zval而不是一個(gè)zval指針 zend_ulong h; //冗余的哈希值,避免重復(fù)計(jì)算哈希值 zend_string *key; //元素的key值,指向一個(gè)zend_string結(jié)構(gòu)體 } Bucket;思考如果利用$arr[] = 1;語(yǔ)法進(jìn)行數(shù)組賦值,key字段的值是多少?答案是0x0,就是一個(gè)空指針。
hashtable的問題:哈希沖突,解決沖突的方法有開放定制法和鏈地址法,常用的是鏈地址法。
PHP7中并沒有采用真正的鏈表結(jié)構(gòu),而是利用數(shù)組模擬鏈表。這個(gè)時(shí)候需要在Bucket數(shù)組之前額外開辟一段內(nèi)存空間(叫做索引數(shù)組,每個(gè)索引數(shù)組的單元叫一個(gè)slot),來存儲(chǔ)同一hash值的第一個(gè)bucket的索引下標(biāo)。
看一個(gè)簡(jiǎn)單的數(shù)組查找過程:
經(jīng)過time33哈希算法算出哈希值h
計(jì)算出索引數(shù)組的nIndex = h | nTableMask = -7(假設(shè)),這個(gè)nIndex也別稱做slot
訪問索引數(shù)組,取出索引為-7位置上的元素值為3
訪問bucket數(shù)組,取出索引為3位置上的key,為x,發(fā)現(xiàn)并不等于s,那么繼續(xù)查找,訪問val.u2.next指針,為2
取出索引為2位置上的key,為s,發(fā)現(xiàn)正好是我們要找的那個(gè)key
取出對(duì)應(yīng)的val值3
注意如果bucket的存儲(chǔ)空間滿了,需要重新計(jì)算和nIndex(即slot)的值并將值放到正確的bucket位置上,這個(gè)過程也叫做rehash。
具體的插入過程詳見PHP基本變量筆記的文章末尾。
PHP7中的數(shù)組分為兩種:packed array與hash array。
packed array:
key是數(shù)字,且順序遞增
位置固定,如訪問key是0的元素,即$arr1[0],就直接訪問bucket數(shù)組的第0個(gè)位置即可(即arData[0]),這樣就不需要前面的索引數(shù)組。
如果不滿足上述條件,就是hash array
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/31324.html
摘要:在這里使用學(xué)而思網(wǎng)校的錄像設(shè)備,記錄每天學(xué)習(xí)的內(nèi)容閆昌李樂階段李樂李樂李樂李樂李樂李樂馬運(yùn)運(yùn)李樂李樂李樂源碼集群閆昌源碼閆昌源碼主從復(fù)制李樂源碼施洪寶源碼施洪寶韓天 在這里使用學(xué)而思網(wǎng)校的錄像設(shè)備,記錄每天學(xué)習(xí)的內(nèi)容: 2019-06-24 ~ 2019-06-28 06-27 nginx by 閆昌 06-26 nginx module by 李樂 06-25 nginx http ...
摘要:在這里使用學(xué)而思網(wǎng)校的錄像設(shè)備,記錄每天學(xué)習(xí)的內(nèi)容執(zhí)行潘森執(zhí)行潘森執(zhí)行潘森趙俊峰紅黑樹景羅紅黑樹景羅配置三叉樹田志澤新建模塊馬運(yùn)運(yùn)配置田志澤田志澤田志澤李樂田志澤田志澤文件系統(tǒng) 在這里使用學(xué)而思網(wǎng)校的錄像設(shè)備,記錄每天學(xué)習(xí)的內(nèi)容: 2019-07-15 ~ 2019-07-19 07-18 nginx http 執(zhí)行 by 潘森 07-17 nginx http 執(zhí)行 by 潘森 07...
閱讀 1437·2023-04-25 18:34
閱讀 3547·2021-11-19 09:40
閱讀 2853·2021-11-17 09:33
閱讀 3001·2021-11-12 10:36
閱讀 2866·2021-09-26 09:55
閱讀 2683·2021-08-05 10:03
閱讀 2548·2019-08-30 15:54
閱讀 2895·2019-08-30 15:54