接上篇繼續(xù)總結(jié)內(nèi)存管理基礎(chǔ)承粤。
五择葡、內(nèi)存回收
無(wú)論計(jì)算機(jī)上有多少內(nèi)存都是不夠的肴茄,因而linux kernel需要通過(guò)內(nèi)存回收策略來(lái)保證系統(tǒng)持續(xù)有內(nèi)存使用纯丸。
5.1 基本概念
1. 頁(yè)分類(按有無(wú)文件背景頁(yè)面主要分兩種):
文件頁(yè)(file-backed page)
:有文件背景頁(yè)面偏形。可以直接和硬盤對(duì)應(yīng)的文件進(jìn)行交換觉鼻。
匿名頁(yè)(anonymous page)
:無(wú)文件背景頁(yè)面俊扭。如進(jìn)程堆、棧坠陈、數(shù)據(jù)段使用的頁(yè)等萨惑,無(wú)法直接跟磁盤交換,但是可以跟swap區(qū)進(jìn)行交換仇矾。
2. 緩存:
對(duì)于有文件背景的頁(yè)面庸蔼,程序去讀文件時(shí),可以通過(guò)read也可以通過(guò)mmap去讀贮匕。通過(guò)任何一種方式從磁盤讀文件時(shí)姐仅,內(nèi)核都會(huì)給你申請(qǐng)一個(gè)page cache,來(lái)緩存硬盤上的內(nèi)容刻盐。這樣掏膏,讀過(guò)一遍的數(shù)據(jù)下次再讀的時(shí)候就直接從page cache里去拿,提升了性能和訪問(wèn)速度敦锌。
而這里需要補(bǔ)充兩對(duì)概念:
1)對(duì)比兩種文件操作的方式:read/write 和 mmap
read/write: 常規(guī)文件操作為了提高讀寫效率和保護(hù)磁盤馒疹,使用了page cache機(jī)制。這樣造成讀文件時(shí)需要先將文件頁(yè)從磁盤拷貝到頁(yè)緩存中乙墙,由于頁(yè)緩存處在內(nèi)核空間颖变,不能被用戶進(jìn)程直接尋址,所以還需要將頁(yè)緩存中數(shù)據(jù)頁(yè)再次拷貝到內(nèi)存對(duì)應(yīng)的用戶空間中伶丐。這樣悼做,通過(guò)了兩次數(shù)據(jù)拷貝過(guò)程,才能完成進(jìn)程對(duì)文件內(nèi)容的獲取任務(wù)哗魂。寫操作也是一樣肛走,待寫入的buffer在內(nèi)核空間不能直接訪問(wèn),必須要先拷貝至內(nèi)核空間對(duì)應(yīng)的主存录别,再寫回磁盤中(延遲寫回)朽色,也是需要兩次數(shù)據(jù)拷貝邻吞。但是讀過(guò)的數(shù)據(jù)下次再讀就直接從page cache里去拿了, 這時(shí)效率也是很高的。
mmap:創(chuàng)建新的虛擬內(nèi)存區(qū)域和建立文件磁盤地址和虛擬內(nèi)存區(qū)域映射這兩步葫男,沒(méi)有任何文件拷貝操作抱冷。而之后訪問(wèn)數(shù)據(jù)時(shí)發(fā)現(xiàn)內(nèi)存中并無(wú)數(shù)據(jù)而發(fā)起的缺頁(yè)異常過(guò)程,可以通過(guò)已經(jīng)建立好的映射關(guān)系梢褐,只使用一次數(shù)據(jù)拷貝旺遮,就從磁盤中將數(shù)據(jù)傳入內(nèi)存的用戶空間中,供進(jìn)程使用盈咳。對(duì)文件的讀取操作跨過(guò)了頁(yè)緩存耿眉,減少了數(shù)據(jù)的拷貝次數(shù),用內(nèi)存讀寫取代I/O讀寫鱼响,提高了文件讀取效率鸣剪。
2)cache 與 buffer
通過(guò)文件系統(tǒng)來(lái)訪問(wèn)文件產(chǎn)生的緩存記錄為cache。
直接操作磁盤產(chǎn)生的緩存記錄為buffer丈积。
這里cache提升了文件讀寫的性能和速度筐骇,但是也占用了物理內(nèi)存空間,并且在程序運(yùn)行結(jié)束后江滨,cache memory也不會(huì)自動(dòng)釋放铛纬,而需要通過(guò)內(nèi)存回收策略來(lái)釋放。
5.2 哪些內(nèi)存可以回收
屬于內(nèi)核的大部分頁(yè)框是不能夠進(jìn)行回收的牙寞,比如內(nèi)核棧饺鹃、內(nèi)核代碼段莫秆、內(nèi)核數(shù)據(jù)段以及大部分內(nèi)核使用的頁(yè)框间雀。
進(jìn)程使用的頁(yè)框可以進(jìn)行回收的,比如進(jìn)程代碼段镊屎、進(jìn)程數(shù)據(jù)段惹挟、進(jìn)程堆棧、進(jìn)程訪問(wèn)文件時(shí)映射的文件頁(yè)缝驳、進(jìn)程間共享內(nèi)存使用的頁(yè)连锯。
5.3 頁(yè)回收方式
頁(yè)回寫
:如果一個(gè)很少使用的頁(yè)的后備存儲(chǔ)器是一個(gè)塊設(shè)備(例如文件映射),則可以將內(nèi)存直接同步到塊設(shè)備用狱,騰出的頁(yè)面可以被重用运怖。
頁(yè)交換
:如果頁(yè)面沒(méi)有后備存儲(chǔ)器,則可以交換到特定swap分區(qū)夏伊,再次被訪問(wèn)時(shí)再交換回內(nèi)存摇展。
頁(yè)丟棄
:如果頁(yè)面的后備存儲(chǔ)器是一個(gè)文件,但文件內(nèi)容在內(nèi)存不能被修改(例如可執(zhí)行文件)溺忧,那么在當(dāng)前不需要的情況下可直接丟棄咏连。
5.4 頁(yè)回收算法-LRU
當(dāng)Linux系統(tǒng)內(nèi)存有盈余時(shí)盯孙,內(nèi)核會(huì)盡量多地使用內(nèi)存作為page cache,提高系統(tǒng)性能祟滴,page cache會(huì)被加入到文件類型的LRU鏈表中振惰,當(dāng)系統(tǒng)內(nèi)存緊張時(shí),會(huì)按一定的算法來(lái)回收內(nèi)存垄懂,下面簡(jiǎn)單了解下:
LRU鏈表按zone來(lái)配置骑晶,每個(gè)zone中都有一整套LRU鏈表。
而一個(gè)lru鏈表描述符中總共有5個(gè)雙向鏈表頭草慧,它們分別描述五中不同類型的鏈表:
-
LRU_INACTIVE_ANON
:稱為非活動(dòng)匿名頁(yè)lru鏈表(swap) -
LRU_ACTIVE_ANON
:稱為活動(dòng)匿名頁(yè)lru鏈表(swap) -
LRU_INACTIVE_FILE
:稱為非活動(dòng)文件頁(yè)lru鏈表(磁盤) -
LRU_ACTIVE_FILE
:稱為活動(dòng)文件頁(yè)lru鏈表(磁盤) -
LRU_UNEVICTABLE
:此鏈表中保存的是此zone中所有禁止換出的頁(yè)的描述符透罢。
那么lru鏈表進(jìn)行的操作主要有以下幾種:
- 將不處于lru鏈表的新頁(yè)放入到lru鏈表中
- 將非活動(dòng)lru鏈表中的頁(yè)移動(dòng)到非活動(dòng)lru鏈表尾部
- 將處于活動(dòng)lru鏈表的頁(yè)移動(dòng)到非活動(dòng)lru鏈表
- 將處于非活動(dòng)lru鏈表的頁(yè)移動(dòng)到活動(dòng)lru鏈表
- 將頁(yè)從lru鏈表中移除
LRU老化規(guī)則:頁(yè)面通過(guò)lru批處理,轉(zhuǎn)來(lái)轉(zhuǎn)去冠蒋,從活動(dòng)鏈表轉(zhuǎn)到非活動(dòng)鏈表羽圃,從非活動(dòng)鏈表靠前轉(zhuǎn)到鏈尾,在內(nèi)存回收時(shí)抖剿,非活動(dòng)鏈表鏈尾的頁(yè)被回收掉朽寞。
當(dāng)內(nèi)存緊張時(shí),優(yōu)先換出無(wú)臟數(shù)據(jù)的page cache(文件頁(yè)包含page cache)斩郎,直接丟棄脑融。其次才是匿名頁(yè)和有臟數(shù)據(jù)的文件頁(yè)的回收。遵循URL老化規(guī)則缩宜。通過(guò)Swappiness來(lái)確定更傾向于回收哪種更多一點(diǎn)肘迎,swappiness越大,越傾向于回收匿名頁(yè)锻煌,反之越傾向于回收文件頁(yè)妓布。將swapness=0則意味著不再交換匿名頁(yè),swapness=100, 盡量交換匿名頁(yè),Swappiness默認(rèn)值為60宋梧。
5.4 頁(yè)回收時(shí)機(jī)
周期性回收(被動(dòng)觸發(fā))
:這是由后臺(tái)運(yùn)行的守護(hù)進(jìn)程 kswapd 完成的匣沼,回收的時(shí)機(jī)由水位控制。
直接頁(yè)面回收(主動(dòng)觸發(fā))
:“內(nèi)存嚴(yán)重不足”事件的觸發(fā)捂龄。
如果操作系統(tǒng)在進(jìn)行了內(nèi)存回收操作之后仍然無(wú)法回收到足夠多的頁(yè)面以滿足上述內(nèi)存要求释涛,那么操作系統(tǒng)只有最后一個(gè)選擇,那就是使用 OOM( out of memory )killer倦沧,它從系統(tǒng)中挑選一個(gè)最合適的進(jìn)程殺死它唇撬,并釋放該進(jìn)程所占用的所有頁(yè)面。
5.4.1 水位控制
名稱 | 描述 |
---|---|
high | 內(nèi)存回收到該值時(shí)停止回收展融。 |
low | 內(nèi)存到該值時(shí)觸發(fā)kswapd線程的內(nèi)存回收窖认。 |
min | 如果剩余內(nèi)存減少到觸及這個(gè)水位,可認(rèn)為內(nèi)存嚴(yán)重不足,當(dāng)前進(jìn)程就會(huì)被堵住耀态,kernel會(huì)直接在這個(gè)進(jìn)程的進(jìn)程上下文里面做直接頁(yè)面回收轮傍。 |
注:由于每個(gè)ZONE是分別管理各自內(nèi)存的,因此每個(gè)ZONE都有這三個(gè)水位首装。
5.4.2 回收代碼調(diào)用路徑
直接頁(yè)面回收:
系統(tǒng)會(huì)調(diào)用函數(shù) try_to_free_pages() 去檢查當(dāng)前內(nèi)存區(qū)域中的頁(yè)面创夜,回收那些最不常用的頁(yè)面。該函數(shù)會(huì)反復(fù)調(diào)用 shrink_zones() 以及 shrink_slab() 釋放一定數(shù)目的頁(yè)面仙逻,默認(rèn)值是 32 個(gè)頁(yè)面驰吓。如果在特定的循環(huán)次數(shù)內(nèi)沒(méi)有能夠成功釋放 32 個(gè)頁(yè)面,那么頁(yè)面回收會(huì)調(diào)用 OOM killer 選擇并殺死一個(gè)進(jìn)程系奉,然后釋放它占用的所有頁(yè)面檬贰。
注:OOM_killer是Linux自我保護(hù)的方式,當(dāng)內(nèi)存不足時(shí)不至于出現(xiàn)太嚴(yán)重問(wèn)題缺亮,有點(diǎn)壯士斷腕的意味翁涤。在kernel 2.6,內(nèi)存不足將喚醒oom_killer萌踱,挑出/proc/<pid>/oom_score最大者并將之kill掉葵礼。
定期(周期性)回收:
kswapd進(jìn)程以水線為觸發(fā)點(diǎn),按LRU鏈表來(lái)進(jìn)行回收并鸵。系統(tǒng)會(huì)調(diào)用函數(shù)balance_pgdat()鸳粉,它主要調(diào)用的函數(shù)是 shrink_zone() 和 shrink_slab()。
5.4.3 函數(shù)介紹
shrink_zone()
該函數(shù)主要做了兩件事情:
1)將某些頁(yè)面從 active 鏈表移到 inactive 鏈表园担,這是由函數(shù) shrink_active_list() 實(shí)現(xiàn)的届谈。
2)從 inactive 鏈表中選定一定數(shù)目的頁(yè)面,將其放到一個(gè)臨時(shí)鏈表中弯汰,這由函數(shù) shrink_inactive_list() 完成艰山。該函數(shù)最終會(huì)調(diào)用 shrink_page_list() 去回收這些頁(yè)面。
shrink_slab()
該函數(shù)用來(lái)回收磁盤緩存所占用的頁(yè)面的蝙泼。Linux 操作系統(tǒng)并不清楚這類頁(yè)面是如何使用的程剥,所以如果希望操作系統(tǒng)回收磁盤緩存所占用的頁(yè)面劝枣,那么必須要向操作系統(tǒng)內(nèi)核注冊(cè) shrinker 函數(shù)汤踏,shrinker 函數(shù)會(huì)在內(nèi)存較少的時(shí)候主動(dòng)釋放一些該磁盤緩存占用的空間。函數(shù) shrink_slab() 會(huì)遍歷 shrinker 鏈表舔腾,從而對(duì)所有注冊(cè)了 shrinker 函數(shù)的磁盤緩存進(jìn)行處理溪胶。Android內(nèi)核的lowmemorykiller機(jī)制就是注冊(cè)了shrinker,內(nèi)存過(guò)低時(shí)選擇性殺死進(jìn)程來(lái)回收內(nèi)存稳诚。
shrink_page_list()
邏輯流程圖:
六哗脖、用戶空間內(nèi)存管理
6.1 Android用戶空間進(jìn)程劃分
-
Native進(jìn)程
:不包含虛擬機(jī)實(shí)例的linux進(jìn)程。 -
Java進(jìn)程
:包含了虛擬機(jī)實(shí)例的linux進(jìn)程。
6.2 內(nèi)存管理
6.2.1 Natvie進(jìn)程
1)內(nèi)存區(qū)域劃分:
Native進(jìn)程與Linux進(jìn)程一樣才避,虛擬內(nèi)存區(qū)域分為:代碼區(qū)橱夭、只讀常量區(qū)、全局區(qū)桑逝、BSS段棘劣、堆區(qū)、棧區(qū)
代碼區(qū)
:存放函數(shù)體的二進(jìn)制代碼楞遏。
只讀常量區(qū)
:存放字符串常量茬暇,以及const修飾的全局變量 。
全局區(qū)/數(shù)據(jù)區(qū)
:存放已經(jīng)初始化的全局變量和已經(jīng)初始化用static修飾的局部變量寡喝。
BSS段
:存放沒(méi)有初始化的全局變量和未初始化靜態(tài)局部變量糙俗,該區(qū)域會(huì)在main函數(shù)執(zhí)行前進(jìn)行自動(dòng)清零。
堆區(qū)
:一般由程序員分配釋放预鬓,若程序員不釋放巧骚,程序結(jié)束時(shí)可能由OS回收。注意它與數(shù)據(jù)結(jié)構(gòu)中的堆是兩回事格二,分配方式倒是類似于鏈表网缝。
棧區(qū)
:由編譯器自動(dòng)分配釋放,存放函數(shù)的參數(shù)值蟋定,局部變量的值等粉臊。其操作方式類似于數(shù)據(jù)結(jié)構(gòu)中的棧。
注意:棧區(qū)和堆區(qū)之間并沒(méi)有嚴(yán)格分割線驶兜,可以進(jìn)行微調(diào)扼仲,并且堆區(qū)分配一般從低地址到高地址分配,而棧區(qū)分配一般從高地址到低地址分配抄淑。
以上是標(biāo)準(zhǔn)劃分屠凶,但是對(duì)于一個(gè)進(jìn)程的內(nèi)存空間,邏輯上可以分為以下三部分:
程序區(qū)
: 程序的二進(jìn)制文件肆资。
靜態(tài)存儲(chǔ)區(qū)
:(只讀常量區(qū)矗愧、全局區(qū)、BSS段)全局變量和靜態(tài)變量郑原。
動(dòng)態(tài)存儲(chǔ)區(qū)
:(堆區(qū)唉韭、棧區(qū))本地變量。
注:
局部變量
:在一個(gè)有限的范圍內(nèi)的變量犯犁,作用域是有限的属愤,對(duì)于程序來(lái)說(shuō),在一個(gè)函數(shù)體內(nèi)部聲明的普通變量都是局部變量酸役,局部變量會(huì)在棧上申請(qǐng)空間住诸,函數(shù)結(jié)束后驾胆,申請(qǐng)的空間會(huì)自動(dòng)釋放。
全局變量
:在函數(shù)體外申請(qǐng)的贱呐,會(huì)被存放在全局(靜態(tài)區(qū))上丧诺,知道程序結(jié)束后才會(huì)被結(jié)束,這樣它的作用域就是整個(gè)程序奄薇。
靜態(tài)變量
:和全局變量的存儲(chǔ)方式相同锅必,在函數(shù)體內(nèi)聲明為static就可以使此變量像全局變量一樣使用,不用擔(dān)心函數(shù)結(jié)束而被釋放惕艳。
2)內(nèi)存分配與回收
內(nèi)存的靜態(tài)分配和動(dòng)態(tài)分配的區(qū)別主要是兩個(gè):
- 時(shí)間上:靜態(tài)分配發(fā)生在程序編譯和連接時(shí)搞隐,動(dòng)態(tài)分配則發(fā)生在程序調(diào)入和執(zhí)行時(shí)。
- 空間上:堆都是動(dòng)態(tài)分配的远搪,沒(méi)有靜態(tài)分配的堆劣纲。棧有2種分配方式:靜態(tài)分配和動(dòng)態(tài)分配。靜態(tài)分配是編譯器完成的谁鳍,比如局部變量的分配癞季。動(dòng)態(tài)分配由函數(shù)malloc進(jìn)行分配。不過(guò)棧的動(dòng)態(tài)分配和堆不同倘潜,他的動(dòng)態(tài)分配是由編譯器進(jìn)行釋放绷柒,無(wú)需我們手工實(shí)現(xiàn)。
動(dòng)態(tài)內(nèi)存分配與回收
所謂動(dòng)態(tài)內(nèi)存分配涮因,就是指在程序執(zhí)行的過(guò)程中動(dòng)態(tài)地分配或者回收存儲(chǔ)空間的分配內(nèi)存的方法废睦。動(dòng)態(tài)內(nèi)存分配不象數(shù)組等靜態(tài)內(nèi)存分配方法那樣需要預(yù)先分配存儲(chǔ)空間,而是由系統(tǒng)根據(jù)程序的需要即時(shí)分配养泡,且分配的大小就是程序要求的大小嗜湃。
分配和回收操作函數(shù)介紹:
malloc
:動(dòng)態(tài)內(nèi)存分配,用于在堆上申請(qǐng)一塊連續(xù)的指定大小的內(nèi)存區(qū)域澜掩,但是并沒(méi)有初始化购披。
calloc
:則將初始化這部分的內(nèi)存,設(shè)置為0. calloc = malloc + memset(初始化工作)。
alloca
:是向棧申請(qǐng)內(nèi)存,因此無(wú)需釋放肩榕。
realloc
:則對(duì)malloc申請(qǐng)的內(nèi)存進(jìn)行大小的調(diào)整刚陡。
(注:這四個(gè)函數(shù)都是由free來(lái)釋放內(nèi)存。)
new
:new 基于 malloc株汉,卻又高于malloc筐乳,是它的一個(gè)提升版本。首先new不是庫(kù)函數(shù)郎逃,它是一個(gè)關(guān)鍵字哥童,通過(guò)new操作符申請(qǐng)的內(nèi)存都在自由存儲(chǔ)區(qū),且不需要指定內(nèi)存塊大小褒翰。內(nèi)存分配失敗時(shí),會(huì)拋出bac_alloc異常,而不是返回一個(gè)NULL等等优训。new是通過(guò)delete來(lái)釋放內(nèi)存朵你,它同樣也是一個(gè)關(guān)鍵字。
6.2.2 Java進(jìn)程
從之前寫的系統(tǒng)啟動(dòng)流程中我們了解了揣非,zygote是java進(jìn)程的鼻祖抡医,它通過(guò)了Runtime啟動(dòng)了虛擬機(jī),并通過(guò)fork早敬,把虛擬機(jī)作為環(huán)境帶給了每一個(gè)應(yīng)用進(jìn)程忌傻。虛擬機(jī)的設(shè)計(jì)除了提供跨平臺(tái)能力之外,也提供了對(duì)象生命周期的管理搞监,內(nèi)存管理水孩,線程管理,安全和異常的管理等統(tǒng)一的處理方案琐驴。
1)內(nèi)存區(qū)域劃分
這部分之前文章有總結(jié):俘种,如下是JVM內(nèi)存劃分模型,其實(shí)Dalvik和ART都一樣绝淡,就是Heap的space結(jié)構(gòu)會(huì)有區(qū)別:
程序計(jì)數(shù)器
:是一塊較小的線程私有的內(nèi)存空間宙刘,用來(lái)記錄正在執(zhí)行的虛擬機(jī)字節(jié)碼指令,以此來(lái)記錄當(dāng)前線程的運(yùn)行狀態(tài)牢酵;它是一個(gè)指針悬包,指向執(zhí)行引擎正在執(zhí)行的指令的地址。
虛擬機(jī)棧
:棧是一塊連續(xù)的內(nèi)存區(qū)域馍乙,大小是由操作系統(tǒng)預(yù)定好的(2M左右)玉罐,它是先進(jìn)后出的隊(duì)列,進(jìn)出一一對(duì)應(yīng)潘拨,不產(chǎn)生碎片吊输,運(yùn)行效率穩(wěn)定高。局部變量的基本數(shù)據(jù)類型和引用存儲(chǔ)于棧中铁追,因?yàn)樗鼈儗儆诜椒ㄖ械淖兞考韭欤芷陔S方法而結(jié)束。
本地方法棧
:針對(duì)Native方法的琅束,功能與虛擬機(jī)棧一致扭屁。
靜態(tài)存儲(chǔ)區(qū)(方法區(qū))
:內(nèi)存在程序編譯的時(shí)候就已經(jīng)分配好,這塊內(nèi)存在程序整個(gè)運(yùn)行期間都存在涩禀。它主要存放靜態(tài)數(shù)據(jù)料滥、全局static數(shù)據(jù)和包含常量池。
堆
:堆是不連續(xù)的內(nèi)存區(qū)域(因?yàn)橄到y(tǒng)是用鏈表來(lái)存儲(chǔ)空閑內(nèi)存地址艾船,自然不是連續(xù)的)葵腹,堆大小受限于計(jì)算機(jī)系統(tǒng)中有效的虛擬內(nèi)存(32bit系統(tǒng)理論上是4G)高每,所以堆的空間比較靈活,比較大践宴。對(duì)于堆呆万,頻繁的分配和回收內(nèi)存會(huì)造成大量?jī)?nèi)存碎片结闸,使程序效率降低。堆內(nèi)存用于存放引用的對(duì)象實(shí)體、成員變量全部存儲(chǔ)于堆中(包括基本數(shù)據(jù)類型总寻,引用和引用的對(duì)象實(shí)體剖膳,因?yàn)樗鼈儗儆陬惛绒龋悓?duì)象終究是要被new出來(lái)使用的)熙侍。
Java內(nèi)存玩的就是虛擬機(jī)劃分的一畝三分地,而大小是由系統(tǒng)設(shè)置的柒室,內(nèi)存的分配與回收都是虛擬機(jī)負(fù)責(zé)的渡贾。申請(qǐng)的內(nèi)存超過(guò)了一畝三分地就會(huì)oom,內(nèi)存不足會(huì)觸發(fā)gc伦泥,而gc又分串行g(shù)c與并行g(shù)c剥啤,art虛擬機(jī)優(yōu)化了gc環(huán)節(jié),大大縮短了全線程block的時(shí)長(zhǎng)不脯,但是如果明顯的內(nèi)存抖動(dòng)還是會(huì)造成卡頓問(wèn)題府怯。
好了,虛擬機(jī)內(nèi)存管理暫時(shí)不分析了防楷,之后有機(jī)會(huì)再單獨(dú)開(kāi)系列來(lái)分析牺丙,內(nèi)存管理基礎(chǔ)暫時(shí)就寫這么多,歇了复局。
參考:
https://blog.csdn.net/jasonchen_gbd/article/details/79462014
http://www.wowotech.net/memory_management/233.html
https://www.ibm.com/developerworks/cn/linux/l-cn-pagerecycle/
https://www.cnblogs.com/fah936861121/p/6878699.html
https://blog.csdn.net/Luoshengyang/article/details/42492621
https://blog.csdn.net/Luoshengyang/article/details/42555483