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