內(nèi)存優(yōu)化是性能優(yōu)化的重頭戲,因此這部分也花了很多時(shí)間來梳理。老規(guī)矩,先上大綱:
一、基礎(chǔ)知識
1.1 Android內(nèi)存管理框架:
這里針對上圖進(jìn)行簡單描述:
1)物理地址與虛擬地址:
虛擬內(nèi)存是程序和物理內(nèi)存之間引入的中間層,目的是解決直接使用物理內(nèi)存帶來的安全性問題、超過物理內(nèi)存大小需求無法滿足等等問題。而Linux的內(nèi)存管理就是建立在虛擬內(nèi)存之上的。虛擬地址與物理地址通過頁表建立映射關(guān)系,CPU通過MMU(Memory Management Unit :內(nèi)存管理單元)訪問頁表來查詢虛擬地址對應(yīng)的物理地址。虛擬地址分為內(nèi)核空間和用戶空間,它們對應(yīng)的虛擬地址分別為進(jìn)程共享和進(jìn)程隔離的。
2)內(nèi)核空間內(nèi)存管理:
內(nèi)核把page作為內(nèi)存管理的基本單位。對特性不同的page又以zone來做劃分,zone又由node來管理。
以32位為例,主要關(guān)注的區(qū)有3個(gè):
區(qū) | 描述 |
---|---|
ZONE_DMA | 直接內(nèi)存訪問,無需映射 |
ZONE_NORMAL | 一一對應(yīng)映射頁 |
ZONE_HIGHMEM | 動態(tài)映射頁 |
每個(gè)zone中內(nèi)存的組織形式是基于buddy伙伴算法,把空閑的page以2的n次方為單位進(jìn)行管理。因此Linux最底層的內(nèi)存申請都是以2的n次方為單位來申請page的。Buddy伙伴算法以產(chǎn)生內(nèi)部碎片為代價(jià)來避免外部碎片的產(chǎn)生。 Linux針對大內(nèi)存的物理地址分配,采用Buddy伙伴算法,如果是針對小于一個(gè)page的內(nèi)存,頻繁的分配和釋放,則不宜用Buddy伙伴算法,取而代之的是Slab。Slab是為頻繁分配/釋放的對象建立高速緩存。
ION是內(nèi)存管理器,用來支持不同的內(nèi)存分配機(jī)制,如CARVOUT(PMEM),物理連續(xù)內(nèi)存(kmalloc), 虛擬地址連續(xù)但物理不連續(xù)內(nèi)存(vmalloc), IOMMU等。用戶空間和內(nèi)核空間都可以使用ION,用戶空間是通過/dev/ion來創(chuàng)建client的。
3)用戶空間內(nèi)存管理:
用戶空間主要分兩部分,一個(gè)是面向C++的native層,一個(gè)是基于虛擬機(jī)的java層。
native內(nèi)存劃分:
- Data 用于保存全局變量
- Bss 用于保存全局未初始化變量
- Code 程序代碼段
- Stack 線程函數(shù)執(zhí)行的內(nèi)存
- Heap malloc分配管理的內(nèi)存
java基于虛擬機(jī)的內(nèi)存劃分:
- Program Counter Register 它是一個(gè)指針,指向執(zhí)行引擎正在執(zhí)行的指令的地址。
- VM stack 基于方法中的局部變量,包括基本數(shù)據(jù)類型以及對象引用等。
- Native Method Stack 針對native方法,功能與虛擬機(jī)棧一致。
- Method Area 虛擬機(jī)加載的類信息、常量、靜態(tài)變量等。
- Heap 對象實(shí)體。
Heap | 分配 | 手動釋放 | 自動回收 | 實(shí)現(xiàn) |
---|---|---|---|---|
c | malloc | free | 無 | libc |
c++ | new | delete | 可選/智能指針 | 可重構(gòu) |
java | new | 支持 | jvm |
1.2 linux內(nèi)存分配與回收
內(nèi)存分配:
在調(diào)用alloc_page()或者alloc_pages()等接口進(jìn)行一次內(nèi)存分配時(shí),最后都會調(diào)用到__alloc_pages_nodemask()函數(shù),這個(gè)函數(shù)是內(nèi)存分配的心臟,對內(nèi)存分配流程做了一個(gè)整體的組織。該流程牽涉到的分配過程有兩個(gè):
快速內(nèi)存分配
:是get_page_from_freelist()函數(shù),通過low閥值從zonelist中獲取合適的zone進(jìn)行分配,如果zone沒有達(dá)到low閥值,則會進(jìn)行快速內(nèi)存回收,快速內(nèi)存回收后再嘗試分配。慢速內(nèi)存分配
:當(dāng)快速分配失敗后,也就是zonelist中所有zone在快速分配中都沒有獲取到內(nèi)存,則會使用min閥值進(jìn)行慢速分配,在慢速分配(slow path)過程中主要做三件事,異步內(nèi)存壓縮、直接內(nèi)存回收以及輕同步內(nèi)存壓縮,最后視情況進(jìn)行oom分配。并且在這些操作完成后,都會調(diào)用一次快速內(nèi)存分配嘗試獲取頁框。
內(nèi)存回收:
內(nèi)存回收是以zone為單位進(jìn)行的(也會以memcg為單位,這里不討論這種情況),而系統(tǒng)判斷一個(gè)zone需不需要進(jìn)行內(nèi)存回收是由水線watermark來判斷的。
-
high
當(dāng)zone的空閑頁框數(shù)量高于這個(gè)值時(shí),表示zone的空閑頁框較多,不需要再繼續(xù)進(jìn)行內(nèi)存回收。 -
low
快速分配的默認(rèn)閥值,在分配內(nèi)存過程中,如果zone的空閑頁框數(shù)量低于此閥值,系統(tǒng)會對zone執(zhí)行快速內(nèi)存回收。 -
min
在快速分配失敗后的慢速分配中會使用此閥值進(jìn)行分配,如果慢速分配過程中使用此值還是無法進(jìn)行分配,那就會執(zhí)行直接內(nèi)存回收和快速內(nèi)存回收。
當(dāng)linux系統(tǒng)內(nèi)存壓力就大時(shí),就會對系統(tǒng)的每個(gè)壓力大的zone進(jìn)程內(nèi)存回收,它針對三樣?xùn)|西進(jìn)程回收:slab、lru鏈表中的頁、buffer_head。這里主要看lru鏈表中的頁是怎么回收的。lru鏈表主要用于管理進(jìn)程空間中使用的頁,類型分為:文件頁、匿名頁、shmem頁。
-
文件頁(file-backed page)
:有文件背景頁面。可以直接和硬盤對應(yīng)的文件進(jìn)行交換。 -
匿名頁(anonymous page)
:無文件背景頁面。如進(jìn)程堆、棧、數(shù)據(jù)段使用的頁等,無法直接跟磁盤交換,但是可以跟swap區(qū)進(jìn)行交換。 -
mmap頁(tmpfs/shmem的page)
:它具有文件的屬性,能夠像操作文件一樣去操作它。但是它無文件背景,因此也有匿名頁屬性,內(nèi)核在內(nèi)存緊缺時(shí)不能簡單的將page從它們的page cache中丟棄,而需要swap-out。
Lru鏈表回收算法
Lru鏈表有5個(gè)雙向鏈表:LRU_INACTIVE_ANON
、LRU_ACTIVE_ANON
、LRU_INACTIVE_FILE
、LRU_ACTIVE_FILE
、LRU_UNEVICTABLE
。
老化過程:將不處于lru鏈表的新頁放入到lru鏈表中->將處于活動lru鏈表的頁移動到非活動lru鏈表->將非活動lru鏈表中的頁移動到非活動lru鏈表尾部->回收頁然后將頁從lru鏈表中移除。
頁回收方式
-
頁回寫
:文件頁保存的數(shù)據(jù)與磁盤中文件對應(yīng)的數(shù)據(jù)不一致,則認(rèn)定此文件頁為臟頁,需要先將此文件頁回寫到磁盤中對應(yīng)數(shù)據(jù)所在位置上,然后再將此頁作為空閑頁框釋放到伙伴系統(tǒng)中。 -
頁交換
:不經(jīng)常使用的匿名頁,將它們寫入到swap分區(qū)中,然后作為空閑頁框釋放到伙伴系統(tǒng)。 -
頁丟棄
:文件頁中保存的內(nèi)容與磁盤中文件對應(yīng)內(nèi)容一致,說明此文件頁是一個(gè)干凈的文件頁,就不需要進(jìn)行回寫,直接將此頁作為空閑頁框釋放到伙伴系統(tǒng)中。
當(dāng)內(nèi)存緊張時(shí),優(yōu)先換出無臟數(shù)據(jù)的page cache(文件頁包含page cache),直接丟棄。其次才是匿名頁和有臟數(shù)據(jù)的文件頁的回收。遵循URL老化規(guī)則。通過Swappiness來確定更傾向于回收哪種更多一點(diǎn),swappiness越大,越傾向于回收匿名頁,反之越傾向于回收文件頁。
內(nèi)存回收手段
因?yàn)樵诓煌膬?nèi)存分配路徑中,會觸發(fā)不同的內(nèi)存回收方式,內(nèi)存回收針對的目標(biāo)有兩種,一種是針對zone的,另一種是針對一個(gè)memcg的,而這里我們只討論針對zone的內(nèi)存回收,個(gè)人把針對zone的內(nèi)存回收方式分為三種,分別是快速內(nèi)存回收、直接內(nèi)存回收、kswapd內(nèi)存回收。
快速內(nèi)存回收
:處于get_page_from_freelist()函數(shù)中,在遍歷zonelist過程中,對每個(gè)zone都在分配前進(jìn)行判斷,如果分配后zone的空閑內(nèi)存數(shù)量 < 閥值 + 保留頁框數(shù)量,那么此zone就會進(jìn)行快速內(nèi)存回收,即使分配前此zone空閑頁框數(shù)量都沒有達(dá)到閥值,都會進(jìn)行此zone的快速內(nèi)存回收。注意閥值可能是min/low/high的任何一種,因?yàn)樵诳焖賰?nèi)存分配,慢速內(nèi)存分配和oom分配過程中如果回收的頁框足夠,都會調(diào)用到get_page_from_freelist()函數(shù),所以快速內(nèi)存回收不僅僅發(fā)生在快速內(nèi)存分配中,在慢速內(nèi)存分配過程中也會發(fā)生。直接內(nèi)存回收
:處于慢速分配過程中,直接內(nèi)存回收只有一種情況下會使用,在慢速分配中無法從zonelist的所有zone中以min閥值分配頁框,并且進(jìn)行異步內(nèi)存壓縮后,還是無法分配到頁框的時(shí)候,就對zonelist中的所有zone進(jìn)行一次直接內(nèi)存回收。注意,直接內(nèi)存回收是針對zonelist中的所有zone的,它并不像快速內(nèi)存回收和kswapd內(nèi)存回收,只會對zonelist中空閑頁框不達(dá)標(biāo)的zone進(jìn)行內(nèi)存回收。并且在直接內(nèi)存回收中,有可能喚醒flush內(nèi)核線程。kswapd內(nèi)存回收
:發(fā)生在kswapd內(nèi)核線程中,每個(gè)node有一個(gè)swapd內(nèi)核線程,也就是kswapd內(nèi)核線程中的內(nèi)存回收,是只針對所在node的,并且只會對 分配了order頁框數(shù)量后空閑頁框數(shù)量 < 此zone的high閥值 + 保留頁框數(shù)量 的zone進(jìn)行內(nèi)存回收,并不會對此node的所有zone進(jìn)行內(nèi)存回收。
這三種內(nèi)存回收雖然是在不同狀態(tài)下會被觸發(fā),但是如果當(dāng)內(nèi)存不足時(shí),kswapd內(nèi)存回收和直接內(nèi)存回收很大可能是在并發(fā)的進(jìn)行內(nèi)存回收的。而實(shí)際上,這三種回收再怎么不同,進(jìn)行內(nèi)存回收的執(zhí)行代碼是一樣的,只是在內(nèi)存回收前做的一些處理和判斷不同。
內(nèi)存分配過程,在調(diào)用alloc_page()或者alloc_pages()等接口進(jìn)行一次內(nèi)存分配時(shí),最終都會調(diào)用到__alloc_pages_nodemask()函數(shù),先嘗試使用low閥值的快速內(nèi)存分配,遍歷zonelist,判斷是否有zone滿足分配連續(xù)頁框,如果不滿足走快速內(nèi)存回收。如果快速內(nèi)存分配失敗,代表當(dāng)前已經(jīng)無法分配出連續(xù)物理內(nèi)存,怎么進(jìn)入slowpath慢速內(nèi)存分配流程,先喚醒所有node的kswapd內(nèi)核線程,嘗試以min閥值進(jìn)行快速內(nèi)存分配,如果做不到則觸發(fā)kswapd內(nèi)存回收,還是回收不到怎么通過壓縮規(guī)整系統(tǒng)可移動頁,如果還是不滿足,則對zonelist中的所有zone進(jìn)行一次直接內(nèi)存回收(這里不同于快速內(nèi)存回收和kswapd內(nèi)存回收,只會對zonelist中空閑頁框不達(dá)標(biāo)的zone進(jìn)行內(nèi)存回收)。最后如果還是無法回收到滿足條件的內(nèi)存,那就觸發(fā)oom。
1.3 Art虛擬機(jī)內(nèi)存分配與回收
Art堆劃分:
Image Space
連續(xù)地址空間,不進(jìn)行垃圾回收,存放系統(tǒng)預(yù)加載類,而這些對象是存放system@framework@boot.art@classes.oat這個(gè)OAT文件中的,每次開機(jī)啟動只需把系統(tǒng)類映射到Image Space。
Zygote Space
連續(xù)地址空間,匿名共享內(nèi)存,進(jìn)行垃圾回收,管理Zygote進(jìn)程在啟動過程中預(yù)加載和創(chuàng)建的各種對象、資源。
Allocation Space
與Zygote Space性質(zhì)一致,在Zygote進(jìn)程fork第一個(gè)子進(jìn)程之前,就會把Zygote Space一分為二,原來的已經(jīng)被使用的那部分堆還叫Zygote Space,而未使用的那部分堆就叫Allocation Space。以后的對象都在Allocation Space上分配。
Large Object Space
離散地址空間,進(jìn)行垃圾回收,用來分配一些大于12K的大對象。
注:Image Space和Zygote Space在Zygote進(jìn)程和應(yīng)用程序進(jìn)程之間進(jìn)行共享,而Allocation Space就每個(gè)進(jìn)程都獨(dú)立地?fù)碛幸环荨W⒁猓m然Image Space和Zygote Space都是在Zygote進(jìn)程和應(yīng)用程序進(jìn)程之間進(jìn)行共享,但是前者的對象只創(chuàng)建一次,而后者的對象需要在系統(tǒng)每次啟動時(shí)根據(jù)運(yùn)行情況都重新創(chuàng)建一遍。
當(dāng)滿足以下三個(gè)條件時(shí),在large object heap上分配,否則在zygote或者allocation space上分配:
- 請求分配的內(nèi)存大于等于Heap類的成員變量large_object_threshold_指定的值。這個(gè)值等于3 * kPageSize,即3個(gè)頁面的大小。
- 已經(jīng)從Zygote Space劃分出Allocation Space,即Heap類的成員變量have_zygote_space_的值等于true。
- 被分配的對象是一個(gè)原子類型數(shù)組,即byte數(shù)組、int數(shù)組和boolean數(shù)組等。
Art運(yùn)行時(shí)為新創(chuàng)建對象分配內(nèi)存過程:
執(zhí)行GC的三個(gè)階段:
- 階段一:首先會進(jìn)行一次輕量級的GC, GC完成后嘗試分配。如果分配失敗,則選取下一個(gè)GC策略,再進(jìn)行一次輕量級GC。每次GC完成后都嘗試分配,直到三種GC策略都被輪詢了一遍還是不能完成分配,則進(jìn)入下一階段。
- 階段二:允許堆進(jìn)行增長的情況下進(jìn)行對象的分配。
- 階段三:進(jìn)行一次允許回收軟引用的GC的情況下進(jìn)行對象的分配。
這里牽涉到幾種引用類型:
強(qiáng)引用(StrongReference)
:JVM 寧可拋出 OOM ,也不會讓 GC 回收具有強(qiáng)引用的對象;
軟引用(SoftReference)
:只有在內(nèi)存空間不足時(shí),才會被回的對象;
弱引用(WeakReference)
:在 GC 時(shí),一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當(dāng)前內(nèi)存空間足夠與否,都會回收它的內(nèi)存;
虛引用(PhantomReference)
:任何時(shí)候都可以被GC回收,當(dāng)垃圾回收器準(zhǔn)備回收一個(gè)對象時(shí),如果發(fā)現(xiàn)它還有虛引用,就會在回收對象的內(nèi)存之前,把這個(gè)虛引用加入到與之關(guān)聯(lián)的引用隊(duì)列中。程序可以通過判斷引用隊(duì)列中是否存在該對象的虛引用,來了解這個(gè)對象是否將要被回收。可以用來作為GC回收Object的標(biāo)志。
Art GC
與GC有關(guān)參數(shù)
[dalvik.vm.heapgrowthlimit]: [256m] 默認(rèn)情況下, App可使用的Heap的最大值, 超過這個(gè)值就會產(chǎn)生OOM。
[dalvik.vm.heapsize]: [512m] 如果App的manifest配置了largeHeap屬性, 則App可使用的Heap的最大值為此項(xiàng)設(shè)定值。
[dalvik.vm.heapstartsize]: [8m] App啟動后, 系統(tǒng)分配給它的Heap初始大小. 隨著App使用會增加。
[dalvik.vm.heapmaxfree]: [8m] GC后,堆最大空閑值
[dalvik.vm.heapminfree]: [512k] GC后,堆最小空閑值
[dalvik.vm.heaptargetutilization]:[0.75] GC后,堆目標(biāo)利用率
這三個(gè)指標(biāo)動態(tài)調(diào)整堆的大小,預(yù)留一定空間用于下個(gè)對象申請,多余空間還給系統(tǒng)。
虛擬機(jī)主流GC算法:
引用計(jì)數(shù)算法(jdk1.2之前)
:堆中的每個(gè)對象對應(yīng)一個(gè)引用計(jì)數(shù)器,創(chuàng)建對象置為1,每次引用到此對象+1,其中一個(gè)引用銷毀-1,變?yōu)?即滿足回收。致命缺點(diǎn):循環(huán)引用的對象無法進(jìn)行回收。
可達(dá)性算法(jdk1.2之后)
:確定GC root,尋找路徑可達(dá)的引用節(jié)點(diǎn),形成可達(dá)性樹,不在樹上的節(jié)點(diǎn)即滿足回收條件。
標(biāo)記-清除算法(Mark-Sweep):遍歷所有的GC Roots,然后將所有GC Roots可達(dá)的對象標(biāo)記為存活的對象。對沒標(biāo)記的對象全部清除。
優(yōu)點(diǎn):對不存活對象進(jìn)行處理,在存活對象高的情況下非常高效。
缺點(diǎn):清除對象不會整理,造成內(nèi)存碎片,這部分內(nèi)存碎片屬于內(nèi)部碎片。復(fù)制算法(Coping): 遍歷所有的GC Roots,將可達(dá)的對象復(fù)制到另一塊內(nèi)存空間,遍歷完后清空原來的內(nèi)存空間(剩下的都是不可達(dá)對象)。
優(yōu)點(diǎn):對可達(dá)對象進(jìn)行復(fù)制,在存活的對象比較少時(shí)極為高效。
缺點(diǎn):需要額外的內(nèi)存空間。標(biāo)記-整理算法(Mark-Compact):在標(biāo)記-清除算法基礎(chǔ)上,增加存活對象內(nèi)存整理。
優(yōu)點(diǎn):不造成內(nèi)存碎片,也不需要額外內(nèi)存空間
缺點(diǎn):整理過程耗時(shí),效率不高。
Art的三種GC策略
Sticky GC
:只回收上一次GC到本次GC之間申請的內(nèi)存。
Partial GC
:局部垃圾回收,除了Image Space和Zygote Space空間以外的其他內(nèi)存垃圾。
Full GC
: 全局垃圾回收,除了Image Space之外的Space的內(nèi)存垃圾。
策略的對比:(gc pause 時(shí)間越長,對應(yīng)用的影響越大)
GC 暫停時(shí)間:Sticky GC < Partial GC < Full GC
回收垃圾的效率:Sticky GC > Partial GC > Full GC
前后臺GC
應(yīng)用從前臺切到后臺,從后臺切到前臺都會發(fā)生一次gc,應(yīng)用程序在前臺運(yùn)行時(shí),響應(yīng)性是最重要的,因此也要求執(zhí)行的GC是高效的。相反,應(yīng)用程序在后臺運(yùn)行時(shí),響應(yīng)性不是最重要的,這時(shí)候就適合用來解決堆的內(nèi)存碎片問題。因此,Mark-Sweep GC適合作為Foreground GC,而Compacting GC適合作為Background GC。
Art運(yùn)行時(shí)GC過程:
非并行GC
1)調(diào)用子類實(shí)現(xiàn)的成員函數(shù)InitializePhase執(zhí)行GC初始化階段。
2)掛起所有的ART運(yùn)行時(shí)線程。
3)調(diào)用子類實(shí)現(xiàn)的成員函數(shù)MarkingPhase執(zhí)行GC標(biāo)記階段。
4)調(diào)用子類實(shí)現(xiàn)的成員函數(shù)ReclaimPhase執(zhí)行GC回收階段。
5)恢復(fù)第2步掛起的ART運(yùn)行時(shí)線程。
6)調(diào)用子類實(shí)現(xiàn)的成員函數(shù)FinishPhase執(zhí)行GC結(jié)束階段。
除了當(dāng)前執(zhí)行GC的線程之外,其它的ART運(yùn)行時(shí)線程都會被掛起,整個(gè)標(biāo)記過程會稍長。
并行GC:
1)調(diào)用子類實(shí)現(xiàn)的成員函數(shù)InitializePhase執(zhí)行GC初始化階段。
2)獲取用于訪問Java堆的鎖。
3)調(diào)用子類實(shí)現(xiàn)的成員函數(shù)MarkingPhase執(zhí)行GC并行標(biāo)記階段。
4)釋放用于訪問Java堆的鎖。
5)掛起所有的ART運(yùn)行時(shí)線程。
6)調(diào)用子類實(shí)現(xiàn)的成員函數(shù)HandleDirtyObjectsPhase處理在GC并行標(biāo)記階段被修改的對象。
7)恢復(fù)第4步掛起的ART運(yùn)行時(shí)線程。
8)重復(fù)第5到第7步,直到所有在GC并行階段被修改的對象都處理完成。
9)獲取用于訪問Java堆的鎖。
10)調(diào)用子類實(shí)現(xiàn)的成員函數(shù)ReclaimPhase執(zhí)行GC回收階段。
11)釋放用于訪問Java堆的鎖。
12)調(diào)用子類實(shí)現(xiàn)的成員函數(shù)FinishPhase執(zhí)行GC結(jié)束階段。
ART運(yùn)行時(shí)充分地利用了設(shè)備的CPU多核特性,在并行GC的執(zhí)行過程中,將每一個(gè)并發(fā)階段的工作劃分成多個(gè)子任務(wù),然后提交給一個(gè)線程池執(zhí)行,這樣就可以更高效率地完成整個(gè)GC過程,縮短了gc 暫停時(shí)間,避免長時(shí)間暫停對應(yīng)用程序造成停頓。
注:串行執(zhí)行效率太低了,現(xiàn)在Art虛擬機(jī)默認(rèn)都是并行GC。
GC打印log分析:
I/art: <GC_Reason> <GC_Name> <Objects_freed>(<Size_freed>) AllocSpace Objects, <Large_objects_freed>(<Large_object_size_freed>) <Heap_stats> LOS objects, <Pause_time(s)>
GC_Reason:GC觸發(fā)原因
-
Concurrent
: 并發(fā)GC,該GC是在后臺線程運(yùn)行的,并不會阻止內(nèi)存分配。 -
Alloc
:當(dāng)堆內(nèi)存已滿時(shí),App嘗試分配內(nèi)存而引起的GC,這個(gè)GC會發(fā)生在正在分配內(nèi)存的線程。 -
Explicit
:App顯示的請求垃圾收集,例如調(diào)用System.gc()。 -
NativeAlloc
:Native內(nèi)存分配時(shí)觸發(fā)的GC。 -
CollectorTransition
:由堆轉(zhuǎn)換引起的回收,這是運(yùn)行時(shí)切換GC而引起的。收集器轉(zhuǎn)換包括將所有對象從空閑列表空間復(fù)制到碰撞指針空間(反之亦然)。當(dāng)前,收集器轉(zhuǎn)換僅在以下情況下出現(xiàn):在內(nèi)存較小的設(shè)備上,App將進(jìn)程狀態(tài)從可察覺的暫停狀態(tài)變更為可察覺的非暫停狀態(tài)(反之亦然)。 -
HomogeneousSpaceCompact
:齊性空間壓縮是指空閑列表到壓縮的空閑列表空間,通常發(fā)生在當(dāng)App已經(jīng)移動到可察覺的暫停進(jìn)程狀態(tài)。這樣做的主要原因是減少了內(nèi)存使用并對堆內(nèi)存進(jìn)行碎片整理。 -
DisableMovingGc
:不是真正的觸發(fā)GC原因,發(fā)生并發(fā)堆壓縮時(shí),由于使用了 GetPrimitiveArrayCritical,收集會被阻塞。一般情況下,強(qiáng)烈建議不要使用 GetPrimitiveArrayCritical,因?yàn)樗谝苿邮占鞣矫婢哂邢拗啤?/li> -
HeapTrim
:不是觸發(fā)GC原因,但是請注意,收集會一直被阻塞,直到堆內(nèi)存整理完畢。
具體可參考:art/runtime/gc/gc_cause.h
GC_Name:垃圾收集器名稱
-
Concurrent mark sweep (CMS)
:CMS收集器是一種以獲取最短收集暫停時(shí)間為目標(biāo)收集器,采用了標(biāo)記-清除算法(Mark-Sweep)實(shí)現(xiàn)。 它是完整的堆垃圾收集器,能釋放除了Image Space之外的所有的空間。 -
Concurrent partial mark sweep
:部分完整的堆垃圾收集器,能釋放除了Image Space和Zygote Spaces之外的所有空間。 -
Concurrent sticky mark sweep
:分代收集器,它只能釋放自上次GC以來分配的對象。這個(gè)垃圾收集器比一個(gè)完整的或部分完整的垃圾收集器掃描的更頻繁,因?yàn)樗觳⑶矣懈痰臅和r(shí)間。 -
Marksweep + semispace
:非并發(fā)的GC,復(fù)制GC用于堆轉(zhuǎn)換以及齊性空間壓縮(堆碎片整理)。
Objects freed:本次GC從非Large Object Space中回收的對象的數(shù)量。
Size_freed:本次GC從非Large Object Space中回收的字節(jié)數(shù)。
Large objects freed: 本次GC從Large Object Space中回收的對象的數(shù)量。
Large object size freed:本次GC從Large Object Space中回收的字節(jié)數(shù)。
Heap stats:堆的空閑內(nèi)存百分比 (已用內(nèi)存)/(堆的總內(nèi)存)。
Pause times:暫停時(shí)間。
1.4 Android內(nèi)存回收主要手段
應(yīng)用層:
onTrimMemory
由lmk觸發(fā)的,AMS拋出的應(yīng)用層面的回調(diào),目的是讓應(yīng)用程序配合做一些內(nèi)存釋放工作,同時(shí)也可以作為系統(tǒng)內(nèi)存過低的監(jiān)聽。
gc
虛擬機(jī)層面的垃圾回收內(nèi)存。
系統(tǒng)層:
Process.kill
通過signal 9 kill進(jìn)程,釋放占用的內(nèi)存。
lmk(lowmemorykiller)
在內(nèi)存不足時(shí)殺掉優(yōu)先級較低的進(jìn)程來回收內(nèi)存的策略。具體內(nèi)容可以參考之前文章lowmemorykiller總結(jié)
lmk演變:
- android 8.1之前 -kernelspace lmk 監(jiān)聽:kswapd觸發(fā)的shrink回調(diào)
- android 8.1 - 9.0 -userspace lmk 監(jiān)聽:vmpressure
- android 10 -userspace lmk 監(jiān)聽:PSI(Pressure stall information)
app compaction
Android 10版本引入的,AMS與Kernel層聯(lián)動對滿足一定條件的App進(jìn)行內(nèi)存壓縮,在保證后臺進(jìn)程盡量不被殺的基礎(chǔ)上減少它們的內(nèi)存占用。具體內(nèi)容可參考之前文章 app compaction
kswapd
根據(jù)水線進(jìn)行周期內(nèi)存回收。對當(dāng)前非活躍lru鏈表鏈尾的文件頁、匿名頁進(jìn)行回收,其中匿名頁回收會寫入zram區(qū)間。
這里單獨(dú)介紹下zram:
zram是Linux內(nèi)核的一項(xiàng)功能, 它將內(nèi)存的部分區(qū)域劃分為壓縮空間, 在內(nèi)存較低時(shí)先通過內(nèi)存壓縮來變現(xiàn)獲取更多內(nèi)存使用空間, 耗盡之后才使用磁盤, 變相提高內(nèi)存利用率。本身會消耗部分CPU占用率來換內(nèi)存空間的相對增加。
cat proc/meminfo
SwapTotal: 2306044 kB zram配置大小
SwapFree: 1737380 kB 當(dāng)前zram剩余大小
如果為0,則表明zram沒有打開。
二、分析工具
1)adb命令
對應(yīng)用進(jìn)程和系統(tǒng)整體內(nèi)存狀態(tài)做一個(gè)宏觀把控。
具體分析參考之前文章:性能優(yōu)化工具(十)- Android內(nèi)存分析命令
2)Memory Profiler
操作應(yīng)用程序過程中,以實(shí)時(shí)圖表的形式反饋當(dāng)前的內(nèi)存情況,對像明顯的內(nèi)存抖動、內(nèi)存泄漏能做一個(gè)初步分析。
具體使用參數(shù)之前文章:性能優(yōu)化工具(十三)-使用 Memory Profiler 查看 Java 堆和內(nèi)存分配
3)leakCanary
傻瓜式內(nèi)存泄漏檢測工具,對于Activity/Fragment的內(nèi)存泄漏檢測非常好用。
具體使用參考之前文章:性能優(yōu)化工具(九)-LeakCanary
4)MAT
內(nèi)存問題全面分析工具,也可以說是兜底工具,使用相對復(fù)雜點(diǎn)。
具體使用參考之前文章:性能優(yōu)化工具(八)-MAT
三、內(nèi)存問題分析
常規(guī)內(nèi)存問題主要分三種:
內(nèi)存溢出
:指程序在申請內(nèi)存時(shí),沒有足夠的內(nèi)存空間供其使用,造成OOM。往往是由內(nèi)存抖動、內(nèi)存泄漏等問題量變到質(zhì)變之后出現(xiàn)。
內(nèi)存溢出伴隨的是crash日志,它分兩種情況:一種是大對象直接造成的OOM,比如bitmap,這種看報(bào)錯(cuò)日志就能定位到問題,比較好解決; 還有一種是存在內(nèi)存泄漏,壓死駱駝的最后一根稻草報(bào)的crash信息并不能準(zhǔn)確反映出oom的真正問題。可以結(jié)合內(nèi)存泄漏一起分析。
內(nèi)存抖動
:短時(shí)間內(nèi)有大量的對象被創(chuàng)建或者被回收。頻繁創(chuàng)建和銷毀對象造成頻繁GC, 導(dǎo)致內(nèi)存不足及碎片。
內(nèi)存抖動主要就是循環(huán)或者頻繁調(diào)用處短時(shí)間內(nèi)創(chuàng)建和銷毀大量對象,這里值得警惕的是頻繁調(diào)用點(diǎn)的字符串”+”拼接的log日志,還有用ArrayList做大量的非尾部remove操作等。
內(nèi)存泄漏
:指程序在申請內(nèi)存后,無法釋放已申請的內(nèi)存空間。造成可用內(nèi)存逐漸減少。
內(nèi)存泄漏分析整體思路:
首先通過adb命令對整個(gè)內(nèi)存狀況做個(gè)宏觀把握:
cat proc/meminfo
MemTotal: 2914764 kB
MemFree: 78008 kB 系統(tǒng)空閑內(nèi)存(系統(tǒng)尚未被使用的,total-free = used)
MemAvailable: 440972 kB 可用內(nèi)存(memfree + 可回收內(nèi)存(部分buffer/cached,slab也能回收一部分))
...
SwapTotal: 1048572 kB 交換空間的總大小(設(shè)置的zram交換空間大小)
SwapFree: 471124 kB 未被使用交換空間的大小
...
Slab: 176044 kB 內(nèi)核中slab分配的內(nèi)存大小(slab = SReclaimable+SUnreclaim)
SReclaimable: 55528 kB 可收回Slab的內(nèi)存大小
SUnreclaim: 120516 kB 不可收回Slab的內(nèi)存大小
這里主要關(guān)注:
系統(tǒng)剩余內(nèi)存MemAvailable,如果比較低代表當(dāng)前系統(tǒng)內(nèi)存整體不足;Zram開沒開,SwapFree還剩余多少;Slab占用內(nèi)存多大,其中SUnreclaim部分占用內(nèi)存是多少,看是否有kernel泄漏。
dumpsys meminfo
Total PSS by process: Java層存活的進(jìn)程及其占用內(nèi)存情況
241,086K: system (pid 1479)
161,423K: surfaceflinger (pid 544)
137,754K: com.android.systemui (pid 4843 / activities)
...
Total PSS by OOM adjustment: Native存活的進(jìn)程及其占用內(nèi)存情況
376,783K: Native
161,423K: surfaceflinger (pid 544)
14,303K: audioserver (pid 725)
9,247K: zygote (pid 719)
...
576,007K: Persistent 按進(jìn)程優(yōu)先級分別來統(tǒng)計(jì)對應(yīng)的進(jìn)程及其內(nèi)存使用情況
241,086K: system (pid 1479)
...
219,381K: Foreground
167,657K: com.tengxin.youqianji (pid 29421 / activities)
...
317,970K: B Services
33,115K: com.UCMobile:channel (pid 25225)
...
410,541K: Cached
36,294K: com.android.vending (pid 13418)
...
這里主要看下當(dāng)前存活的進(jìn)程是否有占用內(nèi)存明顯異常的,另外看看不同優(yōu)先級進(jìn)程的比例如何,如果當(dāng)前可用內(nèi)存比較低,但是B services和cache類型進(jìn)程數(shù)量還比較高,那得看framework內(nèi)存管理策略是否有問題。一般來說,內(nèi)存比較低時(shí),lmk會從cache開始?xì)⑦M(jìn)程,并且b services進(jìn)程會降級為cache,變相增加lmk第一檔查殺數(shù)量。
dumpsys meminfo pid
** MEMINFO in pid 1479 [system] **
Pss Private Private SwapPss Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 26106 25984 104 8739 73728 34267 39460
Dalvik Heap 63706 63676 4 1858 56528 40144 16384
Dalvik Other 6704 6668 12 24
Stack 2388 2364 24 1352
Ashmem 8004 8000 0 0
Gfx dev 532 124 0 0
Other dev 55 8 36 0
.so mmap 1561 444 780 488
.jar mmap 0 0 0 8
.apk mmap 145 0 0 0
.dex mmap 28702 0 4428 40
.oat mmap 78494 0 49992 0
.art mmap 3068 2684 76 856
Other mmap 34 4 0 0
GL mtrack 1424 1424 0 0
Unknown 1275 1084 188 2882
TOTAL 238445 112464 55644 16247 130256 74411 55844
App Summary
Pss(KB)
------
Java Heap: 66436 從Java或Kotlin代碼分配的對象內(nèi)存。受dalvik.vm相關(guān)配置限制
Native Heap: 25984 從C或C ++代碼分配的對象內(nèi)存。不受限
Code: 55644 應(yīng)用用于處理代碼和資源(如dex字節(jié)碼,已優(yōu)化或已編譯的dex碼,.so庫和字體)的內(nèi)存。
Stack: 2364 應(yīng)用中的原生堆棧和Java堆棧使用的內(nèi)存。這通常與您的應(yīng)用運(yùn)行多少線程有關(guān)。
Graphics: 1548 圖形緩沖區(qū)隊(duì)列向屏幕顯示像素所使用的內(nèi)存。這是與CPU共享的內(nèi)存,不是GPU專用內(nèi)存。
Private Other: 16132
System: 70337
TOTAL: 238445 TOTAL SWAP PSS: 16247
Objects
Views: 3 ViewRootImpl: 1
AppContexts: 20 Activities: 0 當(dāng)前存活的activity
Assets: 8 AssetManagers: 6
Local Binders: 888 Proxy Binders: 1652
Parcel memory: 1697 Parcel count: 751
Death Recipients: 753 OpenSSL Sockets: 0
如果在dumpsys meminfo中發(fā)現(xiàn)明顯內(nèi)存異常的進(jìn)程,那么直接dump對應(yīng)的進(jìn)程詳細(xì)內(nèi)存分配數(shù)據(jù),看看是什么原因。Java、Native或者Graphics圖形占據(jù)內(nèi)存比較高,是否存在泄漏情況,另外關(guān)注Activities,比如以前就看到過由相機(jī)進(jìn)入相冊重復(fù)創(chuàng)建activity的情況,造成大量activity泄漏。
另外還可以關(guān)注下當(dāng)前系統(tǒng)內(nèi)存碎片情況:
cat /proc/pagetypeinfo
Number of blocks type Unmovable Movable Reclaimable CMA HighAtomic Isolate
Node 0, zone DMA 167 239 8 43 0 0
Node 0, zone Normal 228 266 11 0 1 0
Unmovable 超過總數(shù)的20%,可能存在內(nèi)存碎片。
另外還可以dumpsys cpuinfo 關(guān)注下當(dāng)前kswapd0 cpu占用率 ,側(cè)面反映內(nèi)存不足。
在有現(xiàn)場和有有效的bugreport信息的情況下,通過adb命令分析,基本對內(nèi)存問題做一個(gè)基本面的判斷,是否存在內(nèi)存不足,如果內(nèi)存不足是哪個(gè)層面的問題?user space內(nèi)存問題 還是kernel space內(nèi)存問題,user space的話,看是系統(tǒng)進(jìn)程內(nèi)存問題,還是應(yīng)用進(jìn)程內(nèi)存問題,同時(shí)也還能細(xì)分到是java heap的問題 還是native heap的問題。
那么總結(jié)下,最終問題會分為三類:java heap問題、native heap問題、kernel問題。
1)java內(nèi)存泄漏分析
Android提供了hprof機(jī)制,通過MAT進(jìn)行定位分析。
分析步驟:
首先如果是android studio抓取的hprof文件,需要做下轉(zhuǎn)換才能在MAT上打開:hprof-conv 源文件路徑 轉(zhuǎn)換文件路徑,然后通過MAT來分析轉(zhuǎn)換后的hprof文件,MAT提供了若干分析視角:
-
histogram
基于類的角度
列舉所有對象情況,可以group by class、package等切換視角。可以在里面具體檢索某個(gè)類,對類可以看它當(dāng)前的引用關(guān)系:outgoing 我引用了哪些類 ,incoming 哪些類引用了我 內(nèi)存泄漏看這個(gè)。
基本數(shù)據(jù)字段說明:
object 對象數(shù)目
shallow 對象自己占多少內(nèi)存
retained 在我這個(gè)引用鏈之上,對象總共占用多少內(nèi)存 -
dominator_tree
基于對象的角度
每個(gè)對象的支配數(shù),percentage 是對象在所有對象中占的百分比。 -
top consumers
占用內(nèi)存比較高的對象 -
leak suspects
內(nèi)存泄漏懷疑點(diǎn) -
OQL
sql操作
2)native內(nèi)存泄漏分析
Android提供了分析native進(jìn)程內(nèi)存泄漏的方法---malloc debug。
詳細(xì)使用參考kernel文檔:bionic/libc/malloc_debug/README.md
步驟:
1)adb root
2)adb shell setenforce 0
3)adb shell chmod 0777 /data/local/tmp
4)adb shell setprop libc.debug.malloc.program app_process64 //跟蹤zygote及zygote的子進(jìn)程
5)adb shell setprop libc.debug.malloc.options "backtrace_enable_on_signal leak_track"
6)adb shell stop
7)adb shell start
8)adb shell kill -45 <需要跟蹤的進(jìn)程號> //enable backtrace
9)adb shell kill -47 <需要跟蹤的進(jìn)程號> //在/data/locat/tmp/目錄下會生成名為backtrace_heap.<pid>.txt,logcat中也會打印出內(nèi)存泄漏調(diào)用棧信息,如下圖片所示
…
當(dāng)復(fù)現(xiàn)出問題時(shí),可以通過幾次抓取的log,對比找出一直malloc沒有free的調(diào)用棧,即內(nèi)存泄漏點(diǎn)。
10)使用native_heapdump_viewer.py解析backtrace_heap生成.html文件,分析內(nèi)存占用情況。
以9.0的代碼為例,調(diào)試audioserver進(jìn)程
1)adb root
2) # setenforce 0 //避免由于selinux權(quán)限問題,無法生成heap文件
3) # setprop libc.debug.malloc.program audioserver // 調(diào)試audioserver進(jìn)程
4) # setprop libc.debug.malloc.options "backtrace_enable_on_signal leak_track"
5) #kill -9 pid_ audioserver // 殺掉audioserver進(jìn)程, 該進(jìn)程會重啟
logcat日志會有如下信息:
# logcat -v time | grep malloc_debug
09-26 20:19:41.573 I/malloc_debug( 6934): /system/bin/audioserver: Run: 'kill -45 6934' to enable backtracing.
09-26 20:19:41.573 I/malloc_debug( 6934): /system/bin/audioserver: Run: 'kill -47 6934' to dump the backtrace.
6) #kill -45 pid_ audioserver_new(6934) // 使能backtrace,然后復(fù)測內(nèi)存泄漏問題,當(dāng)audioserver進(jìn)程存在內(nèi)存泄漏時(shí),執(zhí)行下一步生成內(nèi)存泄漏的backtrace
7) # kill -47 pid_ audioserver_new(6934) // 在/data/local/tmp目錄下面生成backtrace_heap文件
/data/local/tmp # ls -al
-rw------- 1 audioserver audio 89704 2019-09-26 20:30 backtrace_heap.6934.txt
8)使用native_heapdump_viewer.py解析backtrace_heap.6934.txt
命令如下:
python development/scripts/native_heapdump_viewer.py --verbose --html backtrace_heap.6934.txt --symbols ./out/target/product/raphael/symbols > backtrace_heap.html
在調(diào)試native進(jìn)程內(nèi)存泄漏問題的時(shí)候要注意以下兩個(gè)方面:
- 需要setenforce 0,關(guān)閉selinux權(quán)限,否則無法在/data/local/tmp目錄下面生成backtrace heap文件;解析backtrace heap文件
- 用到native_heapdump_viewer.py這個(gè)腳本,9.0源碼目錄下面development/scripts/的這個(gè)腳本文件不能正常解析,需要使用10.0源碼里面的這個(gè)文件,或者從下面這個(gè)地方去下載:https://android.googlesource.com/platform/development/+/master/scripts/native_heapdump_viewer.py
3)kernel內(nèi)存泄漏分析
到這一層已經(jīng)比較深了,這部分沒有深入玩過,之前使用page_owner分析過一個(gè)問題。page_owner的目的是存儲頁面分配時(shí)的調(diào)用棧信息, 這樣我們就能知道每一個(gè)頁面是由誰分配的。
詳細(xì)使用參考kernel文檔:kernel/msm-4.19/Documentation/vm/page_owner.rst
步驟:
Usage
=====
1) Build user-space helper::
cd tools/vm
make page_owner_sort
2) Enable page owner: add "page_owner=on" to boot cmdline.
3) Do the job what you want to debug
4) Analyze information from page owner::
cat /sys/kernel/debug/page_owner > page_owner_full.txt
grep -v ^PFN page_owner_full.txt > page_owner.txt
./page_owner_sort page_owner.txt sorted_page_owner.txt
See the result about who allocated each page
in the ``sorted_page_owner.txt``.
通過page_owner工具抓出kernel的調(diào)用棧,用腳本捋出top問題,分析其調(diào)用棧來排查。
這里我自己寫了個(gè)sort腳本:
#__author__ = 'stan'
import argparse
import re
parser = argparse.ArgumentParser()
parser.add_argument('input')
args = parser.parse_args()
f = open(args.input, 'r')
current_page = ''
current_stack = ''
page_dict = {'':[]}
page_mem_dict = {}
page_order = ''
totalMemory = 0
count = 0
for line in f:
if (len(line) == 1):
if (current_stack != '') and (current_stack.find('___slab_alloc') >= 0):
sum_mem = pow(2,int(page_order)) * 4
if(page_mem_dict.get(current_stack) != None):
sum_mem = sum_mem + page_mem_dict.get(current_stack)
page_mem_dict[current_stack] = sum_mem
page_list = page_dict.setdefault(current_stack, [])
page_list.append(current_page)
current_stack = ''
current_page = ''
# print len(page_list)
elif ('Page allocated via order' in line):
page_order = line.split(",")[0].split(' ')[4]
# print page_order
elif ('type Unmovable' in line):
page_info = re.findall(r"\d+", line)
#print page_info
current_page = page_info[0]
elif ((current_page != '') and ('+0x' in line)):
current_stack += line
# print current_stack
items = sorted(page_mem_dict.items(), lambda x, y: cmp(x[1], y[1]), reverse=True)
for item in items:
count = count+1;
if(count < 11):
pages = page_dict.get(item[0])
total_len = len(pages)
discrete_len = total_len
for i in range(1, total_len):
if (int(pages[i]) - int(pages[i-1]) == 1):
discrete_len -= 1
print "discrete: "+str(discrete_len)+" , times: "+str(total_len)+" , memory: "+str(item[1]) +" KB"
print '\n' * 2
print item[0]
print "============================"
totalMemory = totalMemory + item[1]
print "#########"
print "total unreclaim memory:"+str(totalMemory)
print "#########"
出來的數(shù)據(jù)是這樣的:
//離散程度 //次數(shù) //內(nèi)存占用
discrete: 9336 , times: 9336 , memory: 74688 KB
//調(diào)用棧
get_page_from_freelist+0x850/0x924
__alloc_pages_nodemask+0x104/0xebc
allocate_slab+0x7c/0x464
___slab_alloc.isra.62.constprop.67+0x5d4/0x694
__slab_alloc.isra.63.constprop.66+0x24/0x34
kmem_cache_alloc+0x16c/0x2ac
create_object+0x5c/0x2b8
kmemleak_alloc+0x5c/0xc8
kmem_cache_alloc+0x1c0/0x2ac
alloc_buffer_head+0x18/0xac
alloc_page_buffers+0x74/0xd4
create_empty_buffers+0x20/0x150
ext4_block_write_begin+0x94/0x508
ext4_da_write_begin+0x160/0x5a4
generic_perform_write+0xb4/0x1b0
__generic_file_write_iter+0x154/0x190
如果是單個(gè)某個(gè)驅(qū)動出現(xiàn)的大量內(nèi)存泄漏,可能page owner能定位到,但是如何泄漏點(diǎn)太分散的話,也不太好定位。
四、內(nèi)存監(jiān)控方案
內(nèi)存監(jiān)控方案這部分參考張紹文在內(nèi)存優(yōu)化部分做的分享,主要還是針對app的
1)采集方式
用戶在前臺的時(shí)候,可以每 5 分鐘采集一次 PSS、Java 堆、圖片總內(nèi)存。我建議通過采樣只統(tǒng)計(jì)部分用戶,需要注意的是要按照用戶抽樣,而不是按次抽樣。簡單來說一個(gè)用戶如果命中采集,那么在一天內(nèi)都要持續(xù)采集數(shù)據(jù)。
2)計(jì)算指標(biāo)
通過上面的數(shù)據(jù),我們可以計(jì)算下面一些內(nèi)存指標(biāo)。
內(nèi)存異常率
:可以反映內(nèi)存占用的異常情況,如果出現(xiàn)新的內(nèi)存使用不當(dāng)或內(nèi)存泄漏的場景,這個(gè)指標(biāo)會有所上漲。其中 PSS 的值可以通過 Debug.MemoryInfo 拿到。
內(nèi)存 UV 異常率 = PSS 超過 400MB 的 UV / 采集 UV
觸頂率
:可以反映 Java 內(nèi)存的使用情況,如果超過 85% 最大堆限制,GC 會變得更加頻繁,容易造成 OOM 和卡頓。
內(nèi)存 UV 觸頂率 = Java 堆占用超過最大堆限制的 85% 的 UV / 采集 UV
其中是否觸頂可以通過下面的方法計(jì)算得到。
long javaMax = runtime.maxMemory();
long javaTotal = runtime.totalMemory();
long javaUsed = javaTotal - runtime.freeMemory();
// Java 內(nèi)存使用超過最大限制的 85%
float proportion = (float) javaUsed / javaMax;
一般客戶端只上報(bào)數(shù)據(jù),所有計(jì)算都在后臺處理,這樣可以做到靈活多變。后臺還可以計(jì)算平均 PSS、平均 Java 內(nèi)存、平均圖片占用這些指標(biāo),它們可以反映內(nèi)存的平均情況。通過平均內(nèi)存和分區(qū)間內(nèi)存占用這些指標(biāo),我們可以通過版本對比來監(jiān)控有沒有新增內(nèi)存相關(guān)的問題。
因?yàn)樯蠄?bào)了前臺時(shí)間,我們還可以按照時(shí)間維度看應(yīng)用內(nèi)存的變化曲線。比如可以觀察一下我們的應(yīng)用是不是真正做到了“用時(shí)分配,及時(shí)釋放”。如果需要,我們還可以實(shí)現(xiàn)按照場景來對比內(nèi)存的占用。3. GC 監(jiān)控在實(shí)驗(yàn)室或者內(nèi)部試用環(huán)境,我們也可以通過 Debug.startAllocCounting 來監(jiān)控 Java 內(nèi)存分配和 GC 的情況,需要注意的是這個(gè)選項(xiàng)對性能有一定的影響,雖然目前還可以使用,但已經(jīng)被 Android 標(biāo)記為 depre
3)GC 監(jiān)控
在實(shí)驗(yàn)室或者內(nèi)部試用環(huán)境,我們也可以通過 Debug.startAllocCounting 來監(jiān)控 Java 內(nèi)存分配和 GC 的情況,需要注意的是這個(gè)選項(xiàng)對性能有一定的影響,雖然目前還可以使用,但已經(jīng)被 Android 標(biāo)記為 deprecated。
通過監(jiān)控,我們可以拿到內(nèi)存分配的次數(shù)和大小,以及 GC 發(fā)起次數(shù)等信息。
long allocCount = Debug.getGlobalAllocCount();
long allocSize = Debug.getGlobalAllocSize();
long gcCount = Debug.getGlobalGcInvocationCount();
上面的這些信息似乎不太容易定位問題,在 Android 6.0 之后系統(tǒng)可以拿到更加精準(zhǔn)的 GC 信息。
// 運(yùn)行的GC次數(shù)
Debug.getRuntimeStat("art.gc.gc-count");
// GC使用的總耗時(shí),單位是毫秒
Debug.getRuntimeStat("art.gc.gc-time");
// 阻塞式GC的次數(shù)
Debug.getRuntimeStat("art.gc.blocking-gc-count");
// 阻塞式GC的總耗時(shí)
Debug.getRuntimeStat("art.gc.blocking-gc-time”);
需要特別注意阻塞式 GC 的次數(shù)和耗時(shí),因?yàn)樗鼤和?yīng)用線程,可能導(dǎo)致應(yīng)用發(fā)生卡頓。我們也可以更加細(xì)粒度地分應(yīng)用場景統(tǒng)計(jì),例如啟動、進(jìn)入朋友圈、進(jìn)入聊天頁面等關(guān)鍵場景。
縱觀通篇,我更多的篇幅是花在了內(nèi)存基礎(chǔ)總結(jié),以及如何分析具體內(nèi)存問題上。對大部分app開發(fā)者或者rom開發(fā)者來說基本是通過存量內(nèi)存問題的分析去倒逼內(nèi)存優(yōu)化產(chǎn)出。至于內(nèi)存監(jiān)控,我本身也沒有太多經(jīng)驗(yàn),因此就參考張紹文微信的方案,了解下思路點(diǎn)到為止。
寫在最后:
今天是2020年4月12日,周日。我依然來到公司寫博客,感覺還是在公司寫效率高些,這應(yīng)該是性能優(yōu)化盤點(diǎn)的最后一篇文章,自己定的計(jì)劃就算有再大困難也要去完成,明天是我在小米的最后一天,本來想著離職這段時(shí)間好好放松下,結(jié)果天天寫博客到零點(diǎn),哈哈哈,這怕就是命了。
一段經(jīng)歷的結(jié)束也意味著新的旅程的開始,感謝幫助我成長的每一個(gè)人,也感謝我自己,一切都是最好的安排!加油!
參考:
linux內(nèi)核tmpfs/shmem淺析
linux內(nèi)存源碼分析 - 內(nèi)存回收(整體流程)
直接內(nèi)存回收中的等待隊(duì)列
linux內(nèi)存源碼分析 - 內(nèi)存壓縮(實(shí)現(xiàn)流程)
Linux內(nèi)存管理 (16)內(nèi)存規(guī)整
linux內(nèi)存源碼分析 - 內(nèi)存壓縮(同步關(guān)系)
ART運(yùn)行時(shí)為新創(chuàng)建對象分配內(nèi)存的過程分析
ART運(yùn)行時(shí)垃圾收集機(jī)制簡要介紹和學(xué)習(xí)計(jì)劃
ART運(yùn)行時(shí)垃圾收集(GC)過程分析
Android內(nèi)存優(yōu)化(二)DVM和ART的GC日志分析
04 | 內(nèi)存優(yōu)化(下):內(nèi)存優(yōu)化這件事,應(yīng)該從哪里著手?