OOM與內(nèi)存優(yōu)化一

內(nèi)存管理基礎(chǔ)
Java內(nèi)存分配模型


image.jpeg

Java的對象生命周期


image.jpeg

1.創(chuàng)建
為對象分配內(nèi)存空間 構(gòu)造對象
2.應(yīng)用

此時 對象至少被一個強(qiáng)引用持有
3.不可見階段
沒有強(qiáng)引用,沒有任何引用了, 對象此時還是存在內(nèi)存里面的,如果被gc掃描到了,就會被后面的可達(dá)性分析,如果不可達(dá)了就會被回收
4.不可達(dá)
5.收集
6.終結(jié) 垃圾回收
7.對象空間重新分配
對象的內(nèi)存布局


image.jpeg

image.jpeg

可回收對象的判定
image.jpeg
Java的四種引用
  • 1.強(qiáng)引用(Strong Reference)
    在代碼中普遍使用的,類似Person person = new Person();如果一個對象具有強(qiáng)引用,則無論在什么情況下,GC都不會回收被引用的對象。當(dāng)內(nèi)存空間不足時,JAVA虛擬機(jī)寧可拋出OutOfMemoryError終止應(yīng)用程序也不會回收具有強(qiáng)引用的對象。
    2.軟引用(Soft Reference)
    表示一個對象處在有用但非必須的狀態(tài)。如果一個對象具有軟引用,在內(nèi)存空間充足時,GC就不會回收該對象,當(dāng)內(nèi)存空間不足時,GC會回收該對象的內(nèi)存(回收發(fā)生在OutOfMemoryError之前)
    軟引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用,如果軟引用所引用的對象被GC回收,Java虛擬機(jī)就會把這個軟引用加入到與之關(guān)聯(lián)的引用隊列中,以便在恰當(dāng)?shù)臅r候?qū)⒃撥浺没厥?,但是由于GC線程的優(yōu)先級較低,通常手動調(diào)用System.gc()并不能立即執(zhí)行GC,因此弱引用所引用的對象并不一定會被馬上回收。
    3.弱引用(Weak Reference)
    用來描述非必須的對象,它類似軟引用,但是強(qiáng)度比軟引用更弱一些,弱引用具有更短的生命,GC在掃描的過程中,一旦發(fā)現(xiàn)只具有被弱引用關(guān)聯(lián)的對象,都會回收掉被弱引用關(guān)聯(lián)的對象,無論當(dāng)前內(nèi)存是否緊缺,GC都將回收被弱引用關(guān)聯(lián)的對象。
    4.虛引用(Phantom Reference)
    虛引用等同于沒有引用,這意味著在任何時候都可能被GC回收,設(shè)置虛引用的目的是為了被虛引用關(guān)聯(lián)的對象在被垃圾回收器時,能夠收到一個系統(tǒng)通知。(被用來跟蹤對象被GC回收的活動)虛引用和弱引用的區(qū)別在于,虛引用在使用時必須和引用隊列(ReferenceQueue)聯(lián)合使用,其在GC回收期間的活動如下:
    ReferenceQueue queue = new ReferenceQueue();
    PhantomReference pr = new PhantomReference(object.quque);
    也即是GC在回收一個對象時,如果發(fā)現(xiàn)該對象具有虛引用,,那么在回收之前會首先該對象的虛引用加入到與之關(guān)聯(lián)的引用隊列中,程序可以通過判斷引用隊列中是否已經(jīng)加入虛引用來了解被引用的對象是否被GC回收。
GC垃圾回收算法
  • 標(biāo)記清除算法
    特點(diǎn)
    位置不連續(xù),產(chǎn)生碎片
    效率略低
    兩遍掃描


    image.jpeg

會掃描兩遍,第一遍哪些對象是存活的,哪些是沒使用的內(nèi)存空間,哪些是可回收的,標(biāo)記出來,第二次掃描之后就把可回收的全部給回收掉
缺點(diǎn):多次清除后,內(nèi)存就會千瘡百孔,產(chǎn)生大量的內(nèi)存碎片

  • 標(biāo)記整理算法
    特點(diǎn)
    沒有內(nèi)存碎片
    效率偏低
    兩遍掃描、指針需要調(diào)整
image.jpeg
  • 復(fù)制算法
    特點(diǎn)
    位置不連續(xù),產(chǎn)生碎片
    效率略低
    利用率只有一半
image.jpeg

分代收集算法 綜合運(yùn)用
新生代 采用復(fù)制算法
老年代 對象存活久 不容易產(chǎn)生垃圾 標(biāo)記整理算法,雖然效率慢點(diǎn),回收頻率不需要那么高。

App內(nèi)存組成以及限制

Android 給每個App分配一個VM,讓App運(yùn)行在dalvik上,這樣即使App崩潰也不會影響到系統(tǒng),系統(tǒng)給VM分配了一定的內(nèi)存大小,App可以申請使用的內(nèi)存大小不能超過此硬性邏輯限制,就算物理內(nèi)存富余,如果應(yīng)用超出VM最大內(nèi)存,就會出現(xiàn)內(nèi)存溢出crash。
由程序控制操作的內(nèi)存空間在heap上,分 java heapsize 和native heapsize
Java申請的內(nèi)存在vm heap上,所以如果java申請的內(nèi)存大小超過VM的邏輯內(nèi)存限制,就會出現(xiàn)內(nèi)存溢出的異常。
native層內(nèi)存申請不受其限制,native層收native process 對內(nèi)存大小的限制
通過代碼獲取內(nèi)存大小的限制
ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE) activityManager.getMemoryClass();//以m為單位

Android內(nèi)存分配與回收機(jī)制
  • 內(nèi)存分配
    Android的Heap空間是一個 Generational Heap Memory 的模型,最近分配的對象會存放在Young Generation區(qū)域,當(dāng)一個對象在這個區(qū)域停留的時間達(dá)到一定程度,它會被移動到old Generation,最后累積一定時間再移動到Permanent Generation區(qū)域。
image.jpeg

1、Young Generation
由一個Eden區(qū)和兩個Survivor區(qū)組成,程序中生成的大部分新的對象都在Eden區(qū)中,當(dāng)Eden區(qū)滿時,還存活的對象將被復(fù)制到其中一個Survivor區(qū),當(dāng)Survivor區(qū)滿時,此區(qū)存活的對象又被復(fù)制到另一個Survivor區(qū),當(dāng)這個Survivor區(qū)也滿時,會將其中存活的對象復(fù)制到老年代。
2、Old Generation
一般情況下,老年代中的對象生命周期都比較長。
3、Permanent Generation
用于存放靜態(tài)的類和方法,持久代對垃圾回收沒有顯著影響。
總結(jié):內(nèi)存對象的處理過程如下

  • 1.對象創(chuàng)建后在Eden區(qū)
    2.執(zhí)行GC后,如果對象仍然存活,則復(fù)制到SO區(qū)
    3.當(dāng)SO區(qū)滿時,該區(qū)域存活對象將復(fù)制到S1區(qū),然后SO清空,接下來SO和S1角色互換。
    4.當(dāng)?shù)?步達(dá)到一定次數(shù)(系統(tǒng)版本不同會有差異)后,存活對象將被復(fù)制到Old Generation。
    5.當(dāng)這個對象再Old Generation區(qū)域停留的時間達(dá)到一定程度時,它會被移動到Old Generation,最后累積一定時間再移動到Permanent Generation區(qū)域
    系統(tǒng)在Young Generation、Old Generation上采用不同的回收機(jī)制,每一個Generation的內(nèi)存區(qū)域都有固定的大小,隨著新的對象陸續(xù)被分配到此區(qū)域,當(dāng)對象總的大小臨近這一級別內(nèi)存區(qū)域的閥值時,會觸發(fā)GC操作,以便騰出空間來存放其它新的對象。
    執(zhí)行GC占用的時間與Generation和Generation中的對象數(shù)量有關(guān):
    Young Generation < Old Generation < Permanent Generation
    Gener中的對象數(shù)量與執(zhí)行時間成正比

4、Young Generation GC
由于其對象存活時間短,因此基于Copying算法(掃描出存活的對象,并復(fù)制到一塊新的完全未使用的控件中)來回收,新生代采用空閑指針的方式來控制GC觸發(fā),指針保持最后一個分配的對象在Young Generation區(qū)間的位置,當(dāng)有新的對象要分配內(nèi)存時,用于檢測空間是否足夠,不夠就觸發(fā)GC。
5、Old Generation GC
由于其對象存活時間較長,比較穩(wěn)定,因此采用Mart(標(biāo)記)算法(掃描出存活的對象,然后再回收未標(biāo)記的對象,回收后對空出的空間要么合并,要么標(biāo)記出來便于下次分配,以減少內(nèi)存碎片帶來的效率損耗)來回收。
GC類型
在Android系統(tǒng)中,GC有三種類型:
kGcCauseForAlloc:分配內(nèi)存不夠引起的GC,會Stop World,由于是并發(fā)GC,其它線程都會停止,知道GC完成。
kGcCauseBackground:內(nèi)存達(dá)到一定閾值觸發(fā)的GC,由于是一個后臺GC,所以不會引起Stop World。
kGcCauseExplicit:顯示調(diào)用時進(jìn)行的GC,當(dāng)ART打開這個選項時,使用System.gc時會進(jìn)行GC。
可達(dá)性分析與GCRoots
在Java,可作為GC Roots的對象包括:
1.方法區(qū):類靜態(tài)屬性引用的對象;
2.方法區(qū):常量引用的對象
3.虛擬機(jī)棧(本地變量表)中引用的對象
4.本地方法棧JNI(Native方法)中引用的對象
Android低內(nèi)存殺進(jìn)程機(jī)制

低內(nèi)存終止守護(hù)進(jìn)程(LMK)

LMK 使用一個名為 oom_adj_score 的“內(nèi)存不足”分值來確定正在運(yùn)行的進(jìn)程的優(yōu)先級,以此決定要終止的進(jìn)程。最高得分的進(jìn)程最先被終止。后臺應(yīng)用最先被終止,系統(tǒng)進(jìn)程最后被終止。下表列出了從高到低的 LMK 評分類別。評分最高的類別,即第一行中的項目將最先被終止

image.jpeg

  • Android 進(jìn)程,高分在上,低分在下
    后臺應(yīng)用:之前運(yùn)行過且當(dāng)前不處于活動狀態(tài)的應(yīng)用,LMK將首先從具有最高oom_adj_score的應(yīng)用終止后臺應(yīng)用。
    上一個應(yīng)用:最近用過的后臺應(yīng)用,上一個應(yīng)用比后臺應(yīng)用具有更高的優(yōu)先級(得分更低),因?yàn)橄啾饶硞€后臺應(yīng)用,用戶更有可能切換到上一個應(yīng)用。
    主屏幕應(yīng)用:這是啟動器應(yīng)用,終止該應(yīng)用會使壁紙消失。
    服務(wù):服務(wù)由應(yīng)用啟動,可能包括同步或上傳到云端。
    可覺察的應(yīng)用,用戶可通過某種方式察覺到的非前臺應(yīng)用,例如運(yùn)行一個顯示小界面的搜索進(jìn)程或聽音樂。
    前臺應(yīng)用:當(dāng)前正在使用的應(yīng)用,終止前臺應(yīng)用看起來就像應(yīng)用崩潰了,可能會向用戶提示設(shè)備出了問題。
    持久性(服務(wù)):這些是設(shè)備的核心服務(wù),手機(jī)可能看起來即將重新啟動。
    原生:系統(tǒng)使用的極低級別的進(jìn)程
    設(shè)備制造商可以更改LMK的行為。
    系統(tǒng)需要進(jìn)行內(nèi)存回收時最先回收空進(jìn)程,然后是后臺進(jìn)程,以此類推最后才會回收前臺進(jìn)程(一般情況 下前臺進(jìn)程就是與用戶交互的進(jìn)程了,如果連前臺進(jìn)程都需要回收那么此時系統(tǒng)幾乎不可用了


    image.jpeg

    ActivityManagerService會對所有進(jìn)程進(jìn)行評分(存放在變量adj中),然后再講這個評分更新到內(nèi)核,由內(nèi)核去完成真正的內(nèi)存回收(lowmemorykiller,oom_killer),這里只是大概的流程,中間過程還是很復(fù)雜的。
    什么是OOM

OOM(OutOfMemoryError)內(nèi)存溢出錯誤,在常見的Crash疑難排行榜上,OOM絕對可以名列前茅并且經(jīng)久不衰,因?yàn)樗l(fā)生時的Crash堆棧信息往往不是導(dǎo)致問題的根本原因,而只是壓死駱駝的最后一根稻草。

Android 2.x系統(tǒng) GC LOG中的dalvik allocated + external allocated + 新分配的大小 >= getMemoryClass()值的時候就會發(fā)生OOM。 例如,假設(shè)有這么一段Dalvik輸出的GC LOG: GC_FOR_MALLOC free 2K, 13% free 32586K/37455K, external 8989K/10356K, paused 20ms, 那么32586+8989+(新分配23975)=65550>64M時,就會發(fā)生OOM。

Android 4.x系統(tǒng) Android 4.x的系統(tǒng)廢除了external的計數(shù)器,類似bitmap的分配改到dalvik的 java heap中申請,只要allocated + 新分配的內(nèi)存 >= getMemoryClass()的時候就會發(fā)生OOM

OOM原因分類

image.jpeg

OOM代碼分析

Android虛擬機(jī)最終拋出OutOfMemoryError的地方
/art/runtime/thread.cc
void Thread::ThrowOutOfMemoryError(const char* msg) { LOG(WARNING) << StringPrintf("Throwing OutOfMemoryError "%s"%s",
msg, (tls32_.throwing_OutOfMemoryError ? " (recursive case)" : "")); if (!tls32_.throwing_OutOfMemoryError) {
tls32_.throwing_OutOfMemoryError = true; ThrowNewException("Ljava/lang/OutOfMemoryError;", msg); tls32_.throwing_OutOfMemoryError = false;
} else {
Dump(LOG_STREAM(WARNING)); // The pre-allocated OOME has no stack, so
help out and log one.
SetException(Runtime::Current()->GetPreAllocatedOutOfMemoryError()); }
}

堆內(nèi)存分配失敗
/art/runtime/gc/heap.cc
void Heap::ThrowOutOfMemoryError(Thread* self, size_t byte_count, AllocatorType allocator_type) {
// If we're in a stack overflow, do not create a new exception. It would require running the
// constructor, which will of course still be in a stack overflow.
if (self->IsHandlingStackOverflow()) { self->SetException(
Runtime::Current()- >GetPreAllocatedOutOfMemoryErrorWhenHandlingStackOverflow());
return; }

std::ostringstream oss; size_t total_bytes_free = GetFreeMemory();

//為對象分配內(nèi)存時達(dá)到進(jìn)程的內(nèi)存上限
oss << "Failed to allocate a " << byte_count << " byte allocation with "
<< total_bytes_free << " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << "
until OOM," << " target footprint " <<
target_footprint_.load(std::memory_order_relaxed) << ", growth limit "
<< growth_limit_;
//沒有足夠大小的連續(xù)地址空間
// There is no fragmentation info to log for large-object space.
if (allocator_type != kAllocatorTypeLOS) { CHECK(space != nullptr) << "allocator_type:" << allocator_type
<< " byte_count:" << byte_count
<< " total_bytes_free:" << total_bytes_free; space->LogFragmentationAllocFailure(oss, byte_count);
} }

創(chuàng)建線程失敗
/art/runtime/thread.cc
void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
CHECK(java_peer != nullptr);
Thread* self = static_cast<JNIEnvExt*>(env)->GetSelf();
// TODO: remove from thread group?
env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer, 0);
{
std::string msg(child_jni_env_ext.get() == nullptr ?
StringPrintf("Could not allocate JNI Env: %s", error_msg.c_str()) : StringPrintf("pthread_create (%s stack) failed: %s",
PrettySize(stack_size).c_str(), strerror(pthread_create_result)));
ScopedObjectAccess soa(env);
soa.Self()->ThrowOutOfMemoryError(msg.c_str()); }

內(nèi)存三大問題
  • 1.內(nèi)存抖動
    短時間內(nèi)有大量對象創(chuàng)建和銷毀,它伴隨著頻繁的GC,內(nèi)存波動圖形呈齒裝、GC導(dǎo)致卡頓
    例如在自定義view里面onDraw方法中多次創(chuàng)建對象
    如何預(yù)防?
    1)避免在循環(huán)中創(chuàng)建對象
    2)避免在頻繁調(diào)用的方法中創(chuàng)建對象,如view的onDraw方法
    3)允許復(fù)用的情況下,使用對象池進(jìn)行緩存,如:Handler的Message單鏈表(obtain),
    Message.obtain();
    2.內(nèi)存溢出
    內(nèi)存不夠用了,垃圾回收又回收不了
    3.內(nèi)存泄漏
    這個對象已經(jīng)不再使用了,但是還被GC Roots引用,導(dǎo)致不能回收。
    程序中已動態(tài)分配的堆內(nèi)存由于某種原因程序未釋放或無法釋放,造成系統(tǒng)內(nèi)存的浪費(fèi)。
    長生命周期對象持有短生命周期對象的強(qiáng)引用,從而導(dǎo)致短生命周期對象無法被回收
Android內(nèi)存泄漏常見場景以及解決方案
  • 1.資源性對象未關(guān)閉
    對于資源性對象不再使用時,應(yīng)該立即調(diào)用它的close()函數(shù),將其關(guān)閉,然后再置為null,例如Bitmap等資源未關(guān)閉會造成內(nèi)存泄漏,此時我們應(yīng)該在Activity銷毀時及時關(guān)閉
    2.注冊對象未注銷
    例如BroadcastReceiver、EventBus未注銷造成的內(nèi)存泄漏,我們應(yīng)該在Activity銷毀時及時注銷
    3.類的靜態(tài)變量持有大數(shù)據(jù)對象
    盡量避免使用靜態(tài)變量存儲數(shù)據(jù),特別是大數(shù)據(jù)對象,建議使用數(shù)據(jù)庫存儲
    4.單例造成的內(nèi)存泄漏
    優(yōu)先使用Application的Context,如需使用Activity的Context,可以在傳入Context時使用弱引用進(jìn)行封裝,然后,在使用到的地方從弱引用中獲取Context,如果獲取不到,則直接return即可。
    5.非靜態(tài)內(nèi)部類的靜態(tài)實(shí)例
    該實(shí)例的生命周期和應(yīng)用一樣長,這就導(dǎo)致該靜態(tài)實(shí)例一直持有該Activity的引用,Activity的內(nèi)存資源不能正?;厥?。此時,我們可以將該內(nèi)部類設(shè)為靜態(tài)內(nèi)部類或?qū)?nèi)部類抽取出來封裝成一個單例,如果需要使用Context,盡量使用Application Context,如果需要使用Activity Context,就記得用完后置空讓GC可以回收,否則還是會內(nèi)存泄漏。
    6、Handler臨時性內(nèi)存泄漏
    Message發(fā)出之后存儲在MessageQueue中,在Message中存在一個target,它是Handler的一個引用,Message在Queue中存在的時間過長,就會導(dǎo)致Handler無法被回收。如果Handler是非靜態(tài)的,則會導(dǎo)致Activity或者Service不會被回收,并且消息隊列是在一個Looper線程中不斷地輪詢處理消息,當(dāng)這個Activity退出時,消息隊列中還有未處理的消息或者正在處理的消息,并且消息隊列中的Message持有Handler實(shí)例的引用,Handler又持有Activity的引用,所以導(dǎo)致該Activity的內(nèi)存資源無法及時回收,引發(fā)內(nèi)存泄漏,解決方案如下所示:
    1.使用一個靜態(tài)Handler內(nèi)部類,然后對Handler持有的對象(一般是Activity)使用弱引用,這樣在回收時,也可以回收Handler持有的對象。
    2.在Activity的Destory或者Stop時,應(yīng)該移除消息隊列中的消息,避免Looper線程的消息隊列中有待處理的消息需要處理。
    7、容器中的對象沒清理造成的內(nèi)存泄漏
    在退出程序之前,將集合里的東西clear,然后置為null,再退出程序
    8、WebView
    WebView都存在內(nèi)存泄漏的問題,在應(yīng)用中只要使用一次WebView,內(nèi)存就不會被釋放掉,我們可以為WebView開啟一個獨(dú)立的進(jìn)程,使用AIDL與應(yīng)用的主進(jìn)程進(jìn)行通信,WebView所在的進(jìn)程可以根據(jù)業(yè)務(wù)的需要選擇合適的時機(jī)進(jìn)行銷毀,達(dá)到正常釋放內(nèi)存的目的。
    9、使用ListView時造成的內(nèi)存泄漏
    在構(gòu)造Adapter時,使用緩存的convertView
    另外可參考android官網(wǎng)管理應(yīng)用內(nèi)存的部分https://developer.android.com/topic/performance/memory?hl=zh_cn
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容