自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Glibc 內(nèi)存分配與釋放機制詳解

開發(fā) 開源
本文以一次線上故障為基礎介紹了使用 glibc 進行內(nèi)存管理可能碰到問題,進而對庫中內(nèi)存分配與釋放機制進行分析,最后提供了相應問題的解決方案。

一、引言

內(nèi)存對象的分配與釋放一直是后端開發(fā)人員代碼設計中需要考慮的問題,考慮不周極易造成內(nèi)存泄漏、內(nèi)存訪問越界等問題。在發(fā)生內(nèi)存異常后,開發(fā)人員往往花費大量時間排查用戶管理層代碼,而忽視了C運行時,庫層和操作系統(tǒng)層本身的實現(xiàn)也可能會帶來內(nèi)存問題。本文先以一次線上內(nèi)存事故引出問題,再逐步介紹 glibc 庫的內(nèi)存布局設計、內(nèi)存分配、釋放邏輯,最后給出相應的解決方案。

二、內(nèi)存告警事件

在一次線上運維過程中發(fā)現(xiàn)服務出現(xiàn)內(nèi)存告警。

【監(jiān)控系統(tǒng)-自定義監(jiān)控-告警-持續(xù)告警】

檢測規(guī)則: xxx內(nèi)存使用率監(jiān)測:一般異常(>4096)
集群id:xxx
集群名稱: xxxxxx
異常對象(當前值): xx.xx.xx.xx-xxxxxxx(11335)
開始時間: 2023-08-10 17:10:30
告警時間: 2023-08-10 18:20:32
持續(xù)時間: 1h10m2s
異常比例: 2.1918 (8/365)
異常級別: 一般
備注:-

隨即查看服務相關監(jiān)控,判斷是業(yè)務流量激增帶來的內(nèi)存短時間增高,或是發(fā)生了內(nèi)存泄漏。

圖片

圖片

通過查看 OPS 和服務自身統(tǒng)計的內(nèi)存監(jiān)控,發(fā)現(xiàn)在告警時間內(nèi)存在業(yè)務流量突增現(xiàn)象,但是內(nèi)存已經(jīng)下降到正常值了。然而告警持續(xù)到了18:20依然沒有恢復,跟監(jiān)控表現(xiàn)不符,登錄機器后發(fā)現(xiàn)實例的內(nèi)存并沒有恢復,隨即懷疑用戶層發(fā)生內(nèi)存泄漏。

經(jīng)過分析,由于內(nèi)存統(tǒng)計代碼每次調(diào)用 new、delete 之后才會對統(tǒng)計值進行增減,而監(jiān)控中服務統(tǒng)計內(nèi)存已經(jīng)下降,說明已經(jīng)正常調(diào)用 delete 進行內(nèi)存釋放,而操作系統(tǒng)層面發(fā)現(xiàn)內(nèi)存依然居高不下,懷疑使用的c運行庫 glibc 存在內(nèi)存釋放問題。

三、glibc 內(nèi)存管理機制

3.1 glibc 簡介

glibc 全稱為 GUN C Library,是一個開源的標準C庫,其對操作系統(tǒng)相關調(diào)用進行了封裝,提供包括數(shù)學、字符串、文件 I/O、內(nèi)存管理、多線程等方面標準函數(shù)和系統(tǒng)調(diào)用接口供用戶使用。

3.2 內(nèi)存管理布局

以 Linux 內(nèi)核 v2.6.7 之后的32位模式下的虛擬內(nèi)存布局方式為例:

圖片


  1. Kernel Space(內(nèi)核空間)— 存儲內(nèi)核和驅(qū)動程序的代碼和數(shù)據(jù);
  2. Stack(棧區(qū))— 存儲程序執(zhí)行期間的本地變量和函數(shù)的參數(shù),從高地址向低地址生長;
  3. Memory Mapping Segment(內(nèi)存映射區(qū))— 簡稱為 mmap,用來文件或其他對象映射進內(nèi)存;
  4. Heap(堆區(qū))— 動態(tài)內(nèi)存分配區(qū)域,通過 malloc、new、free 和 delete 等函數(shù)管理;
  5. BSS segment(未初始化變量區(qū))— 存儲未被初始化的全局變量和靜態(tài)變量;
  6. DATA segment(數(shù)據(jù)區(qū))— 存儲在源代碼中有預定義值的全局變量和靜態(tài)變量;
  7. TEXT segment(代碼區(qū))— 存儲只讀的程序執(zhí)行代碼,即機器指令。

其中 Heap 和 Mmap 區(qū)域是可以提供給用戶程序使用的虛擬內(nèi)存空間。

Heap 操作

操作系統(tǒng)提供了 brk() 函數(shù),c運行時庫提供了 sbrk() 函數(shù)從 Heap 中申請內(nèi)存,函數(shù)聲明如下:

int brk(void *addr);
void *sbrk(intptr_t increment);
  • brk() 通過設置進程堆的結(jié)束地址進行內(nèi)存分配與釋放,即可以一次性的分配或釋放一整段連續(xù)的內(nèi)存空間。比較適合于一次性分配大塊內(nèi)存的情況,如果設置的結(jié)束地址過大或過小會造成內(nèi)存碎片或內(nèi)存浪費的問題。
  • sbrk 函數(shù)通過傳入的 increment 參數(shù)決定增加或減少堆空間的大小,可以動態(tài)的多次分配或釋放空間達到需要多少內(nèi)存就申請多少內(nèi)存的效果,有效避免了內(nèi)存碎片和浪費問題。

Mmap 操作

在 Linux 中提供了 mmap() 和 munmap() 函數(shù)操作虛擬內(nèi)存空間,函數(shù)聲明如下:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);

其中 mmap 能夠?qū)⑽募蛘咂渌麑ο笥成溥M內(nèi)存,munmap 能夠刪除特定地址區(qū)域的內(nèi)存映射。

3.3 內(nèi)存分配器

開源社區(qū)公開了很多現(xiàn)成的內(nèi)存分配器,包括 dlmalloc、ptmalloc、jemalloc、tcmalloc......,由于 glibc 用的是 ptmalloc 所以本文只對該內(nèi)存分配器進行介紹。

3.3.1 Arena(分配區(qū))

堆管理結(jié)構如下所示:

struct malloc_state {
 mutex_t mutex;                 /* Serialize access. */
 int flags;                       /* Flags (formerly in max_fast). */
 #if THREAD_STATS
 /* Statistics for locking. Only used if THREAD_STATS is defined. */
 long stat_lock_direct, stat_lock_loop, stat_lock_wait;
 #endif
 mfastbinptr fastbins[NFASTBINS];    /* Fastbins */
 mchunkptr top;
 mchunkptr last_remainder;
 mchunkptr bins[NBINS * 2];
 unsigned int binmap[BINMAPSIZE];   /* Bitmap of bins */
 struct malloc_state *next;           /* Linked list */
 INTERNAL_SIZE_T system_mem;
 INTERNAL_SIZE_T max_system_mem;
 };

ptmalloc 對進程內(nèi)存是通過一個個的分配區(qū)進行管理的,而分配區(qū)分為主分配區(qū)(arena)和非主分配區(qū)(narena),兩者區(qū)別在于主分配區(qū)中可以使用 sbrk 和 mmap 向操作系統(tǒng)申請內(nèi)存,而非主分配區(qū)只能通過 mmap 申請內(nèi)存。


圖片

對于一個進程,只有一個主分配區(qū)和若干個非主分配區(qū),主分配區(qū)只能由第一個線程來創(chuàng)建持有,其和非主分配區(qū)由環(huán)形鏈表的形式相互連接,整個分配區(qū)中通過變量互斥鎖支持多線程訪問。

當一個線程調(diào)用 malloc 申請內(nèi)存時,該線程先查看線程私有變量中是否已經(jīng)存在一個分配區(qū)。如果存在,則對該分配區(qū)加鎖,加鎖成功的話就用該分配區(qū)進行內(nèi)存分配;失敗的話則搜索環(huán)形鏈表找一個未加鎖的分配區(qū)。如果所有分配區(qū)都已經(jīng)加鎖,那么 malloc 會開辟一個新的分配區(qū)加入環(huán)形鏈表并加鎖,用它來分配內(nèi)存。釋放操作同樣需要獲得鎖才能進行。

3.3.2 chunk

ptmalloc 通過 malloc_chunk 來管理內(nèi)存,定義如下:

struct malloc_chunk { 
  INTERNAL_SIZE_T      prev_size;    /* Size of previous chunk (if free).  */ 
  INTERNAL_SIZE_T      size;         /* Size in bytes, including overhead. */ 
   
  struct malloc_chunk* fd;           /* double links -- used only if free. */ 
  struct malloc_chunk* bk; 
   
  /* Only used for large blocks: pointer to next larger size.  */ 
  struct malloc_chunk* fd_nextsize;      /* double links -- used only if free. */ 
  struct malloc_chunk* bk_nextsize;
};


  • prev_size:存儲前一個 chunk 的大小。如果前一個 chunk 沒有被使用,則 prev_size 的值表示前一個 chunk 的大小,如果前一 chunk 已被使用,則 prev_size 的值沒有意義。
  • size:表示當前 chunk 的大小,包括所請求的有效數(shù)據(jù)大小,以及堆塊頭部和尾部的管理信息等附加信息的大小。
  • fd 和 bk:表示 chunk 在空閑鏈表中的前一個和后一個堆塊的指針。如果該 chunk 被占用,則這兩個指針沒有意義。
  • fd_nextsize 和 bk_nextsize:表示同一空閑鏈表上下一個堆塊的指針。fd_nextsize 指向下一個比當前 chunk 大小大的第一個空閑 chunk , bk_nextszie 指向前一個比當前 chunk 大小小的第一個空閑 chunk,增加這兩個字段可以加快遍歷空閑 chunk ,并查找滿足需要的空閑 chunk 。

使用該數(shù)據(jù)結(jié)構能夠更快的在鏈表中查找到空閑 chunk 并分配。

3.3.3 空閑鏈表(bins)

在 ptmalloc 中,會將大小相似的 chunk 鏈接起來,叫做空閑鏈表(bins),總共有128個 bin 供 ptmalloc 使用。用戶調(diào)用 free 函數(shù)釋放內(nèi)存的時候,ptmalloc 并不會立即將其歸還操作系統(tǒng),而是將其放入 bins 中,這樣下次再調(diào)用 malloc 函數(shù)申請內(nèi)存的時候,就會從 bins 中取出一塊返回,這樣就避免了頻繁調(diào)用系統(tǒng)調(diào)用函數(shù),從而降低內(nèi)存分配的開銷。

在 ptmalloc 中,bin主要分為以下四種:

  • fast bin
  • unsorted bin
  • small bin
  • large bin

其中根據(jù) bin 的分類,可以分為 fast bin 和 bins,而 bins 又可以分為 unsorted bin、small bin 以及 large bin 。


圖片


fast bin

程序在運行時會經(jīng)常需要申請和釋放一些較小的內(nèi)存空間。當分配器合并了相鄰的幾個小的 chunk 之后,也許馬上就會有另一個小塊內(nèi)存的請求,這樣分配器又需要從大的空閑內(nèi)存中切分出一塊,這樣無疑是比較低效的,故而, malloc 中在分配過程中引入了 fast bins 。

fast bin 總共有10個,本質(zhì)上就是10個單鏈表,每個 fast bin 中所包含的 chunk size 以8字節(jié)逐漸遞增,即如果第一個 fast bin 中 chunk size 均為16個字節(jié),第二個 fast bin 的 chunk size 為24字節(jié),以此類推,最后一個 fast bin 的 chunk size 為80字節(jié)。值得注意的是 fast bin 中 chunk 釋放并不會與相鄰的空閑 chunk 合并,這是由于 fast bin 設計的初衷就是小內(nèi)存的快速分配和釋放,因此系統(tǒng)將屬于 fast bin 的 chunk 的P(未使用標志位)總是設置為1,這樣即使當 fast bin 中有某個 chunk 同一個 free chunk 相鄰的時候,系統(tǒng)也不會進行自動合并操作。

malloc 操作:

在 malloc 申請內(nèi)存的時候,如果申請的內(nèi)存大小范圍在fast bin 以內(nèi),則先在 fast bin 中進行查找,如果 fast bin 中存在空閑 chunk 則返回。否則依次從 small bin、unsorted bin、large bin 中進行查找。

free 操作:

先通過 chunksize 函數(shù)根據(jù)傳入的地址指針獲取該指針對應的 chunk 的大?。蝗缓蟾鶕?jù)這個 chunk 大小獲取該 chunk 所屬的 fast bin,然后再將此 chunk 添加到該 fast bin 的鏈尾。

unsorted bin

是 bins 的緩沖區(qū),顧名思義,unsorted bin 中的 chunk 無序,這種設計能夠讓 glibc 的 malloc 機制有第二次機會重新利用最近釋放的 chunk 從而加快內(nèi)存分配的時間。

與 fast bin 不同,unsorted bin 采用的是 FIFO 的方式。

malloc 操作:

當需要的內(nèi)存大小大于 fast bin 的最大大小,則先在 unsorted 中尋找,如果找到了合適的 chunk 則直接返回,否則繼續(xù)在 small bin 和l arge bin中搜索。

free 操作:

當釋放的內(nèi)存大小大于fast bin的最大大小,則將釋放的 chunk 寫入 unsorted bin。

small bin

大小小于512字節(jié)的 chunk 被稱為 small chunk,而保存 small chunks 的 bin 被稱為 small bin。62個 small bin 中,每個相鄰的的 small bin 之間相差8字節(jié),同一個 small bin 中的 chunk 擁有相同大小。

small bin 指向的是包含空閑區(qū)塊的雙向循環(huán)鏈表。內(nèi)存分配和釋放邏輯如下:

malloc 操作:

當需要的內(nèi)存不存在于 fast bin 和 unsorted bin 中,并且大小小于512字節(jié),則在 small bin 中進行查找,如果找到了合適的 chunk 則直接返回。

free 操作:

free 一個 chunk 時會檢查該 chunk 相鄰的 chunk 是否空閑,如果空閑則需要先合并,然后將合并的 chunk 先從所屬的鏈表中刪除然后合并成一個新的 chunk,新的 chunk 會被添加在 unsorted bin 鏈表的前端。

large bin

大小大于等于512字節(jié)的 chunk 被稱為 large chunk,而保存 large chunks 的 bin 被稱為 large bin。large bins 中每一個 bin 分別包含了一個給定范圍內(nèi)的 chunk,其中的 chunk 按大小遞減排序,大小相同則按照最近使用時間排列。63 large bin 中的每一個都與 small bin 的操作方式大致相同,但不是存儲固定大小的塊,而是存儲大小范圍內(nèi)的塊。每個 large bin 的大小范圍都設計為不與 small bin  的塊大小或其他large bin 的范圍重疊。

malloc 操作:

首先確定用戶請求的大小屬于哪一個 large bin,然后判斷該 large bin 中最大的 chunk 的 size 是否大于用戶請求的 size。如果大于,就從尾開始遍歷該 large bin,找到第一個 size 相等或接近的 chunk,分配給用戶。如果該 chunk 大于用戶請求的 size 的話,就將該 chunk 拆分為兩個 chunk:前者返回給用戶,且 size 等同于用戶請求的 size;剩余的部分做為一個新的 chunk 添加到 unsorted bin 中。

free 操作:

large bin 的 fee 操作與 small bin 一致,此處不再贅述。

3.3.4 特殊 chunk

top chunk

top chunk 是堆最上面的一段空間,它不屬于任何 bin,當所有的 bin 都無法滿足分配要求時,就要從這塊區(qū)域里來分配,分配的空間返回給用戶,剩余部分形成新的 top chunk,如果 top chunk 的空間也不滿足用戶的請求,就要使用 brk 或者 mmap 來向系統(tǒng)申請更多的堆空間(主分配區(qū)使用 brk、sbrk,非主分配區(qū)使用 mmap)。

mmaped chunk

當分配的內(nèi)存非常大(大于分配閥值,默認128K)的時候需要被 mmap 映射,則會放到 mmaped chunk 上,釋放 mmaped chunk 上的內(nèi)存的時候會將內(nèi)存直接交還給操作系統(tǒng)。(chunk 中的M標志位置1)

last remainder chunk

如果用戶申請的 size 屬于 small bin 的,但是又不能精確匹配的情況下,這時候采用最佳匹配(比如申請128字節(jié),但是對應的bin是空,只有256字節(jié)的 bin 非空,這時候就要從256字節(jié)的 bin 上分配),這樣會 split chunk 成兩部分,一部分返給用戶,另一部分形成 last remainder chunk,插入到 unsorted bin 中。

3.3.5 hunk 的合并與切分

合并

當 chunk 釋放時,如果前后兩個相鄰的 chunk 均空閑,則會與前后兩個相鄰 chunk 合并,隨后將合并結(jié)果放入 unsorted bin 中。

切分

當需要分配的內(nèi)存小于待分配的 chunk 塊,則會將待分配 chunk 塊切割成兩個 chunk 塊,其中一個 chunk 塊大小等同于用戶需要分配內(nèi)存的大小。需要注意的是分裂后的兩個 chunk 必須均大于 chunk 的最小大小,否則不會進行拆分。

3.4 內(nèi)存分配

內(nèi)存分配流程可以分為三步:

第一步:根據(jù)用戶請求大小轉(zhuǎn)換為實際需要分配 chunk 空間的大??;

第二步:在 bins 中搜索還沒有歸還給操作系統(tǒng)的 chunk 塊,具體流程如下圖所示。

圖片


  • 如果所需分配的 chunk 大小小于等于 max_fast (fast bins 中要求的最大 chunk 大小,默認為64B),則嘗試在 fast bins 中獲取 chunk,如果獲取 chunk 則返回。否則進入下一步。
  • 判斷所需大小是否可能處于 small bins 中,即判斷 chunk_size < 512B是否成立。如果 chunk 大小處在 small bins 中則在 small bins 中搜索合適的 chunk,即找到合適的 small bin,然后從該 bin 的尾部摘取一個滿足大小要求的 chunk 返回。如果 small bins 中無法找到合適的 chunk 則進入下一步。
  • 到這一步說明待分配的內(nèi)存塊要么是一個大的 chunk,要么只是沒有在 small bin 中找到。分配器先在 fast bin 中嘗試合并 chunk,并將 chunk 寫入 unsorted chunk 中,此時再遍歷 unsorted chunk 如果能夠找到合適的 chunk 則按需將該 chunk 切分(可能不需要),將生成的 chunk 中其中一個放入 small bins 或者 large bins 中,另一個與待分配內(nèi)存塊相同大小的 chunk 則返回。
  • 在 large bins 中搜索合適的 chunk,如果能夠找到則將該 chunk 切分成需要分配的內(nèi)存大小,另一部分則繼續(xù)寫入 bins 中。如果無法找到合適的 chunk,則進入下一步。
  • 嘗試從 top chunk 中分配一塊內(nèi)存給用戶,剩下一部分生成新的 top chunk 。

第三步:如果 top chunk 依然無法滿足分配請求,通過 sbrk 或 mmap 增加 top chunk 的大小并分配內(nèi)存給用戶。

3.5 內(nèi)存釋放

圖片


  1. 判斷當前 chunk 是否是 mmap 映射區(qū)域映射的內(nèi)存,如果是則直接使用 munmap 釋放這塊內(nèi)存映射(內(nèi)存映射的內(nèi)存能夠通過標記進行識別);
  2. 判斷 chunk 是否與 top chunk 相鄰,如果相鄰則直接與 top chunk 合并;
  3. 如果 chunk 的大小大于 max_fast(64B),則將其放入 unsorted bin,
    并檢查是否有合并,如果能夠合并則將 chunk 合并后根據(jù)大小加入合適的 bin 中;
  4. 如果 chunk 的大小小于
    max_fast(64B),則直接放入 fast bin 中,如果沒有合并情況則 free 內(nèi)存。如果在當前 chunk 相鄰的 chunk 空閑,則觸發(fā)合并,并將合并后的結(jié)果寫入 unsorted bin 中,此時如果合并后的結(jié)果大于 max_fast(64B),則觸發(fā)整個 fast bins 的合并操作,此時 fast bins 將會被遍歷,將所有相鄰的空閑 chunk 進行合并,然后將合并后的 chunk 寫入 unsorted bin 中,fast bin 此時會變?yōu)榭铡H绻喜⒑蟮?chunk 與 top chunk 相鄰則會合并到 top chunk 中;
  5. 如果 top chunk 大小大于 mmap 收縮閾值(默認128KB),如果是,則對于主分配區(qū)則會試圖歸還 top chunk 中一部分給操作系統(tǒng),此時 free 結(jié)束。

3.6 內(nèi)存碎片

圖片

按照 glibc 的內(nèi)存分配策略,我們考慮下如下場景:

1.假設 brk 起始地址為512k

2.malloc 40k 內(nèi)存,即 chunk A,brk = 512k + 40k = 552k

3.malloc 50k 內(nèi)存,即 chunk B,brk = 552k + 50k = 602k

4.malloc 60k 內(nèi)存,即 chunk C,brk = 602k + 60k = 662k

5.free chunk A。

此時 chunk A 為空閑塊,但是如果 chunk C 和 chunk B 一直不釋放無法直接通過移動brk指針來釋放 chunk A 的內(nèi)存,必須等待 chunk B 和 chunk C 釋放才能和 top chunk 合并并將內(nèi)存歸還給操作系統(tǒng)。

四、問題分析與解決

通過前面的內(nèi)存分配器運行原理能夠很容易得出原因,由于程序中連續(xù)調(diào)用 free/delete 釋放內(nèi)存僅僅只是將內(nèi)存寫入內(nèi)存分配器的 bins 中,并沒有將其歸還給操作系統(tǒng),所以會出現(xiàn)疑似內(nèi)存未回收的情況。并且如果每次 delete 的內(nèi)存都不與 top chunk 相鄰,會導致 chunk 塊長時間留在空閑鏈表中無法合并到 top chunk,從而出現(xiàn)內(nèi)存無法釋放給操作系統(tǒng)的現(xiàn)象。

4.1 優(yōu)化辦法

  1. 通過限制服務端內(nèi)存最大大小能夠有效避免內(nèi)存被c運行庫撐的太高,導致服務器 OOM 的情況。
  2. c運行庫替換成 jemalloc,jemalloc 與 glibc 的實現(xiàn)方式不同,能夠更快將內(nèi)存歸還給操作系統(tǒng)。

4.2 效果對比測試

為了驗證優(yōu)化后的內(nèi)存使用效果,編寫測試代碼,模擬線上 pipline 模式下的3000萬次連續(xù)請求,對比請求過程中的內(nèi)存峰值、連接斷開后的內(nèi)存使用狀況:

glibc內(nèi)存分配器

內(nèi)存峰值

圖片

連接斷開后內(nèi)存占用

圖片

jemalloc內(nèi)存分配器

內(nèi)存峰值

圖片


連接斷開后內(nèi)存占用

圖片

根據(jù)測試結(jié)果,jemalloc 相較于 glibc 釋放空閑內(nèi)存速度快12%。

參考鏈接

  1. https://www.gnu.org/software/libc/manual/html_node/
  2. https://github.com/hustfisher/ptmalloc/blob/master/README
  3. https://stackoverflow.com/questions/13480235/libc-memory-management
  4. https://zhuanlan.zhihu.com/p/637659294
責任編輯:龐桂玉 來源: vivo互聯(lián)網(wǎng)技術
相關推薦

2011-08-16 15:13:49

IOS編程內(nèi)存

2023-10-18 13:31:00

Linux內(nèi)存

2011-05-26 15:41:25

java虛擬機

2011-07-15 01:10:13

C++內(nèi)存分配

2009-09-02 09:23:26

.NET內(nèi)存管理機制

2023-12-27 13:55:00

C++內(nèi)存分配機制new

2009-08-26 14:52:19

.NET Framew

2010-09-26 13:23:13

JVM內(nèi)存管理機制

2009-06-10 22:03:40

JavaScript內(nèi)IE內(nèi)存泄漏

2013-10-12 13:01:51

Linux運維內(nèi)存管理

2010-12-10 15:40:58

JVM內(nèi)存管理

2011-06-29 17:20:20

Qt 內(nèi)存 QOBJECT

2018-05-08 08:46:47

Linux內(nèi)存釋放

2009-06-03 15:52:34

堆內(nèi)存棧內(nèi)存Java內(nèi)存分配

2023-04-03 08:25:02

Linux內(nèi)存slub

2020-06-22 08:30:42

Linux內(nèi)存手動釋放

2020-08-18 19:15:44

Redis內(nèi)存管理

2022-02-23 16:49:19

Linux內(nèi)存數(shù)據(jù)結(jié)構

2022-10-08 10:10:58

內(nèi)存技術安全

2011-12-20 10:43:21

Java
點贊
收藏

51CTO技術棧公眾號