業精于勤荒于嬉,寫文章練習表達能力,寫代碼練習基本工。
OOM和記憶體優化總結
什麼是OOM?
OOM 即 (java.lang.OutOfMemoryError), JVM沒有足夠記憶體給對象配置設定空間,超過jvm的堆空間最大值(-Xmx參數),此異常就會被觸發,導緻應用強制被殺死。
OOM原因?
對于java程式員來說,我們一般隻管建立對象,而對象的回收,我們很少操心,是因為JVM有垃圾回收器來定期執行GC,負責回收已經引用不到的無用對象來釋放記憶體。不過,我們的應用還是出現記憶體洩露或者記憶體溢出,這也是導緻oom的兩大因素。
記憶體洩露和記憶體溢出?
- 記憶體洩露,是指之前配置設定的記憶體空間,不再使用,但也無法被垃圾回收器釋放,這種情況如果一直堆積,會導緻記憶體溢出。
- 記憶體溢出,沒有足夠的記憶體來配置設定空間了,空間不夠了。
OOM發生在哪個區域?
我們先來認識一些jvm記憶體模型:
按照JVM的規範,除了程式計數器區,其他區域都可能出現OOM。
jvm 記憶體配置設定與回收機制
我們平時寫java代碼,絕大部分的OOM都發生在堆區,是以着重了解一下堆空間對象的記憶體模型和回收機制。
- 堆空間劃分為,新生代(eden + S0 + S1), 老年代(Tenured)和永久代(Permanent),永久代java1.8已經取消,改為元空間。
- 新建立的對象基本都配置設定在新生代eden區,大對象直接配置設定到老年代。
- 一次GC後,eden區仍然存活的對象被移到S0;S0記憶體活的對象移到S1;S1記憶體活的對象被移到Tenured
- 每一次GC,對象年齡增長一歲,到達15歲(預設,可設定),晉升到老年代。
對象被回收的判斷算法
- 可達性分析 :以 GC Root 為分析的起點 , 查找對象的引用 , 如果找到一個對象 , 無法被 GC Root 直接或間接引用到 , 那麼該對象就可以被回收了 。
- 引用計數法 :給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1。
垃圾回收機制(GC)
垃圾回收算法:
- 标記-清除
- 标記-整理
- 複制
垃圾回收主要在新生代和老年代工作,在分代收集模型中(上圖),不同的分代采用不同的收集算法:
- eden和Survivor:通常是複制算法
- Tenured:通常是标記整理算法,android是CMS
Android App 的記憶體限制
- Java Heap :Java申請的記憶體,受 vm.heapsize 大小限制。
- native Heap : c/c++層申請的記憶體,不受 vm.heapsize 大小限制。
如何修改:
代碼位置 /frameworks/base/core/jni/AndroidRuntime.cpp
int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote, bool primary_zygote)
{
/*
* The default starting and maximum size of the heap. Larger
* values should be specified in a product property override.
*/
parseRuntimeOption("dalvik.vm.heapstartsize", heapstartsizeOptsBuf, "-Xms", "4m");
parseRuntimeOption("dalvik.vm.heapsize", heapsizeOptsBuf, "-Xmx", "16m");
parseRuntimeOption("dalvik.vm.heapgrowthlimit", heapgrowthlimitOptsBuf, "-XX:HeapGrowthLimit=");
}
Android 抛出OOM的源頭
堆配置設定失敗
代碼位置:/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()->GetPreAllocatedOutOfMemoryError());
return;
}
std::ostringstream oss;
size_t total_bytes_free = GetFreeMemory();
oss << "Failed to allocate a " << byte_count << " byte allocation with " << total_bytes_free
<< " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << " until OOM";
// If the allocation failed due to fragmentation, print out the largest continuous allocation.
if (total_bytes_free >= byte_count) {
space::AllocSpace* space = nullptr;
if (allocator_type == kAllocatorTypeNonMoving) {
space = non_moving_space_;
} else if (allocator_type == kAllocatorTypeRosAlloc ||
allocator_type == kAllocatorTypeDlMalloc) {
space = main_space_;
} else if (allocator_type == kAllocatorTypeBumpPointer ||
allocator_type == kAllocatorTypeTLAB) {
space = bump_pointer_space_;
} else if (allocator_type == kAllocatorTypeRegion ||
allocator_type == kAllocatorTypeRegionTLAB) {
space = region_space_;
}
if (space != nullptr) {
space->LogFragmentationAllocFailure(oss, byte_count);
}
}
self->ThrowOutOfMemoryError(oss.str().c_str());
}
常用的記憶體分析指令
adb shell dumpsys meminfo [package name]
adb shell procrank
記憶體占用排行榜,沒試成功,需要自己上傳sh。
adb shell cat /proc/meminfo
adb shell free
total = used + free
adb shell top
記憶體洩露分析工具
Memory Analyzer Tool
下載下傳位址
1,抓取記憶體申請快照hprof檔案
adb shell am dumpheap com.xxx.xxx /data/local/tmp/14_54.hprof
2,轉換為标準hprof
hprof-conv 14_54.hprof 14_54_R.hprof
hprof-conv在sdk的platform-tools目錄中
3,使用mat打開
首頁會有目前記憶體洩露的猜想,大對象,數量多的對象會被置為懷疑目标。
分析記憶體洩露
大對象分析
點選 Overview - Dominator ,對象按照占用空間大小排序
重點排查排在上面的大對象,下圖中,很明顯竟然有多個相同activity,且保持引用了大對象,無法釋放,造成記憶體洩露。
GC Root 引用鍊檢視
上圖中,選擇一項,右鍵 -> Paths to GC Roots–>exclude all phantom/weak/soft etc reference ,重點分析強引用。
可以看到context引用鍊,定位到自己的代碼裡面,本例是一個類靜态持有了Activity的context,Activity銷毀後,context無法釋放造成記憶體洩露。
直方圖比較
如果問題是記憶體随着時間緩慢增長,單個hprof看不出來,那麼就隔一段時間再去一次快照,對兩個快照檔案進行比較,看那個對象一直在增長。
之前分析問題的hprof找不到了,臨時找了兩個,上面這個圖檔僅供步驟參考,具體以實際操作為準。
Android Profile
打開Profile頁面,選擇監控的程式,點選MEMORY–>dump按鈕。
同樣在android studio上可以檢視hprof進行分析。
LeakCanary
github位址
引用
dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6'
}
調用
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
return;
}
LeakCanary.install(this);
}
}
例子
public class MainActivity extends AppCompatActivity {
private static int k = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Thread(runnable).start();
}
public Runnable runnable = new Runnable() {
@Override
public void run() {
while (true){
MainActivity.k++;
Log.d("TAG",""+k);
}
}
};
}
發生記憶體洩露,進入這個頁面直接檢視。
這個圖檔尺寸怎麼縮小?哭
記憶體洩露,記憶體溢出常見場景和解決方案總結
以下摘抄自他人總結的,在這裡做備份。
1、資源未關閉
對于資源性對象不再使用時,應該立即調用它的close()函數,将其關閉,然後再置為null。例如Bitmap
等資源未關閉會造成記憶體洩漏,此時我們應該在Activity銷毀時及時關閉。
2、注冊未登出
例如BraodcastReceiver、EventBus未登出造成的記憶體洩漏,我們應該在Activity銷毀時及時登出。
3、類的靜态變量持有大資料對象
盡量避免使用靜态變量存儲資料,特别是大資料對象,建議使用資料庫存儲。
4、單例造成的記憶體洩漏
優先使用Application的Context,如需使用Activity的Context,可以在傳入Context時使用弱引用進行封
裝,然後,在使用到的地方從弱引用中擷取Context,如果擷取不到,則直接return即可。
5、非靜态内部類的靜态執行個體
該執行個體的生命周期和應用一樣長,這就導緻該靜态執行個體一直持有該Activity的引用,Activity的記憶體資源
不能正常回收。如果需要使用Context,盡量使用Application Context。
6、Handler臨時性記憶體洩漏
Message發出之後存儲在MessageQueue中,在Message中存在一個target,它是Handler的一個引
用,Message在Queue中存在的時間過長,就會導緻Handler無法被回收。如果Handler是非靜态的,
則會導緻Activity或者Service不會被回收。并且消息隊列是在一個Looper線程中不斷地輪詢處理消息,
當這個Activity退出時,消息隊列中還有未處理的消息或者正在處理的消息,并且消息隊列中的Message
持有Handler執行個體的引用,Handler又持有Activity的引用,是以導緻該Activity的記憶體資源無法及時回
收,引發記憶體洩漏。解決方案如下所示:
1、使用一個靜态Handler内部類,然後對Handler持有的對象(一般是Activity)使用弱引用,這
樣在回收時,也可以回收Handler持有的對象。
2、在Activity的Destroy或者Stop時,應該移除消息隊列中的消息,避免Looper線程的消息隊列中
有待處理的消息需要處理。
需要注意的是,AsyncTask内部也是Handler機制,同樣存在記憶體洩漏風險,但其一般是臨時性的。對于
類似AsyncTask或是線程造成的記憶體洩漏,我們也可以将AsyncTask和Runnable類獨立出來或者使用靜
态内部類。
7、容器中的對象沒清理造成的記憶體洩漏
在退出程式之前,将集合裡的東西clear,然後置為null,再退出程式
8、WebView
WebView都存在記憶體洩漏的問題,在應用中隻要使用一次WebView,記憶體就不會被釋放掉。我們可以為
WebView開啟一個獨立的程序,使用AIDL與應用的主程序進行通信,WebView所在的程序可以根據業
務的需要選擇合适的時機進行銷毀,達到正常釋放記憶體的目的。
9、使用ListView時造成的記憶體洩漏
在構造Adapter時,使用緩存的convertView。
如有錯誤,請幫忙指出。
參考
Android記憶體優化之OOM
使用者指南 - android app性能優化大彙總(記憶體性能優化)
使用記憶體性能分析器檢視應用的記憶體使用情況
【Android 記憶體優化】記憶體抖動 ( 垃圾回收算法總結 | 分代收集算法補充 | 記憶體抖動排查 | 記憶體抖動操作 | 集合選擇 )
JVM記憶體管理:深入Java記憶體區域與OOM