新聞中心
Java 中的內(nèi)存分配

公司專注于為企業(yè)提供網(wǎng)站設(shè)計(jì)制作、網(wǎng)站建設(shè)、微信公眾號開發(fā)、商城網(wǎng)站建設(shè),成都微信小程序,軟件定制制作等一站式互聯(lián)網(wǎng)企業(yè)服務(wù)。憑借多年豐富的經(jīng)驗(yàn),我們會仔細(xì)了解各客戶的需求而做出多方面的分析、設(shè)計(jì)、整合,為客戶設(shè)計(jì)出具風(fēng)格及創(chuàng)意性的商業(yè)解決方案,創(chuàng)新互聯(lián)公司更提供一系列網(wǎng)站制作和網(wǎng)站推廣的服務(wù)。
主要是分三塊:
- 靜態(tài)儲存區(qū):編譯時就分配好,在程序整個運(yùn)行期間都存在。它主要存放靜態(tài)數(shù)據(jù)和常量。
- 棧區(qū):當(dāng)方法執(zhí)行時,會在棧區(qū)內(nèi)存中創(chuàng)建方法體內(nèi)部的局部變量,方法結(jié)束后自動釋放內(nèi)存。
- 堆區(qū):通常存放 new 出來的對象。由 Java 垃圾回收器回收。
棧與堆的區(qū)別
棧內(nèi)存用來存放局部變量和函數(shù)參數(shù)等。它是先進(jìn)后出的隊(duì)列,進(jìn)出一一對應(yīng),不產(chǎn)生碎片,運(yùn)行效率穩(wěn)定高。當(dāng)超過變量的作用域后,該變量也就無效了,分配給它的內(nèi)存空間也將被釋放掉,該內(nèi)存空間可以被重新使用。
堆內(nèi)存用于存放對象實(shí)例。在堆中分配的內(nèi)存,將由Java垃圾回收器來自動管理。在堆內(nèi)存中頻繁的 new/delete 會造成大量內(nèi)存碎片,使程序效率降低。
對于非靜態(tài)變量的儲存位置,我們可以粗暴的認(rèn)為:
- 局部變量位于棧中(其中引用變量指向的對象實(shí)體存在于堆中)。
- 成員變量位于堆中。因?yàn)樗鼈儗儆陬?,該類最終被new成對象,并作為一個整體儲存在堆中。
四種引用類型的介紹
GC 釋放對象的根本原則是該對象不再被引用(強(qiáng)引用)。那么什么是強(qiáng)引用呢?
強(qiáng)引用(Strong Reference)
我們平常用的最多的就是強(qiáng)引用,如下:
- IPhotos iPhotos = new IPhotos();
JVM 寧可拋出 OOM ,也不會讓 GC 回收具有強(qiáng)引用的對象。強(qiáng)引用不使用時,可以通過 obj = null 來顯式的設(shè)置該對象的所有引用為 null,這樣就可以回收該對象了。至于什么時候回收,取決于 GC 的算法,這里不做深究。
軟引用(Soft Reference)
- SoftReference
softReference = new SoftReference<>(str);
如果一個對象只具有軟引用,那么在內(nèi)存空間足夠時,垃圾回收器就不會回收它;如果內(nèi)存空間不足了,就會回收這些對象的內(nèi)存。只要垃圾回收器沒有回收它,該對象就可以被使用。
軟引用曾經(jīng)常被用來作圖片緩存,然而谷歌現(xiàn)在推薦用 LruCache 替代,因?yàn)?LRU 更高效。
In the past, a popular memory cache implementation was a SoftReference
or WeakReference bitmap cache, however this is not recommended.
Starting from Android 2.3 (API Level 9) the garbage collector is more
aggressive with collecting soft/weak references which makes them
fairly ineffective. In addition, prior to Android 3.0 (API Level 11),
the backing data of a bitmap was stored in native memory which is not
released in a predictable manner, potentially causing an application
to briefly exceed its memory limits and crash. 原文
大致意思是:因?yàn)樵?Android 2.3 以后,GC 會很頻繁,導(dǎo)致釋放軟引用的頻率也很高,這樣會降低它的使用效率。并且 3.0 以前 Bitmap 是存放在 Native Memory 中,它的釋放是不受 GC 控制的,所以使用軟引用緩存 Bitmap 可能會造成 OOM。
弱引用(Weak Reference)
- WeakReference
weakReference = new WeakReference<>(str);
與軟引用的區(qū)別在于:只具有弱引用的對象擁有更短暫的生命周期。因?yàn)樵?GC 時,一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當(dāng)前內(nèi)存空間足夠與否,都會回收它的內(nèi)存。不過,由于垃圾回收器是一個優(yōu)先級很低的線程,因此不一定會很快發(fā)現(xiàn)那些只具有弱引用的對象- -。
虛引用(PhantomReference)
顧名思義,就是形同虛設(shè),與其他幾種引用都不同,虛引用并不會決定對象的生命周期,也無法通過虛引用獲得對象實(shí)例。虛引用必須和引用隊(duì)列(ReferenceQueue)聯(lián)合使用。當(dāng)垃圾回收器準(zhǔn)備回收一個對象時,如果發(fā)現(xiàn)它還有虛引用,就會在回收對象的內(nèi)存之前,把這個虛引用加入到與之關(guān)聯(lián)的引用隊(duì)列中。程序可以通過判斷引用隊(duì)列中是否存在該對象的虛引用,來了解這個對象是否將要被回收。
Android的垃圾回收機(jī)制簡介
Android 系統(tǒng)里面有一個 Generational Heap Memory 模型,系統(tǒng)會根據(jù)內(nèi)存中不同的內(nèi)存數(shù)據(jù)類型分別執(zhí)行不同的 GC 操作。
該模型分為三個區(qū):
- Young Generation
1.eden
2.Survivor Space
1.S0
2.S1 - Old Generation
- Permanent Generation
- Young Generation
大多數(shù) new 出來的對象都放到 eden 區(qū),當(dāng) eden 區(qū)填滿時,執(zhí)行 Minor GC(輕量級GC),然后存活下來的對象被轉(zhuǎn)移到 Survivor 區(qū)(有 S0,S1 兩個)。 Minor GC 也會檢查 Survivor 區(qū)的對象,并把它們轉(zhuǎn)移到另一個 Survivor 區(qū),這樣就總會有一個 Survivor 區(qū)是空的。
Old Generation
存放長期存活下來的對象(經(jīng)過多次 Minor GC 后仍然存活下來的對象) Old Generation 區(qū)滿了以后,執(zhí)行 Major GC(大型 GC)。
在Android 2.2 之前,執(zhí)行 GC 時,應(yīng)用的線程會被暫停,2.3 開始添加了并發(fā)垃圾回收機(jī)制。
Permanent Generation
存放方法區(qū)。一般存放:
- 要加載的類的信息
- 靜態(tài)變量
- final常量
- 屬性、方法信息
60 FPS
這里簡單的介紹一下幀率的概念,以便于理解為什么大量的 GC 容易引起卡頓。
App 開發(fā)時,一般追求界面的幀率達(dá)到60 FPS(60 幀/秒),那這個 FPS 是什么概念呢?
- 10-12 FPS 時可以感受到動畫的效果;
- 24 FPS,可以感受到平滑連貫的動畫效果,電影常用幀率(不追求 60 FPS 是節(jié)省成本);
- 60 FPS,達(dá)到最流暢的效果,對于更高的FPS,大腦已經(jīng)難以察覺區(qū)別。
Android 每隔 16 ms發(fā)出 VSYNC 信號,觸發(fā)對 UI 的渲染(即每 16 ms繪制一幀),如果整個過程保持在 16 ms以內(nèi),那么就會達(dá)到 60 FPS 的流暢畫面。超過了 16 ms就會造成卡頓。那么如果在 UI 渲染時發(fā)生了大量 GC,或者 GC 耗時太長,那么就可能導(dǎo)致繪制過程超過 16 ms從而造成卡頓(FPS 下降、掉幀等),而我們大腦對于掉幀的情況十分敏銳,因此如果沒有做好內(nèi)存管理,將會給用戶帶來非常不好的體驗(yàn)。
再介紹一下內(nèi)存抖動的概念,本文后面可能會用到這個概念。
內(nèi)存抖動
短時間內(nèi)大量 new 對象,達(dá)到 Young Generation 的閾值后觸發(fā)GC,導(dǎo)致剛 new 出來的對象又被回收。此現(xiàn)象會影響幀率,造成卡頓。
內(nèi)存抖動在 Android 提供的 Memory Monitor 中大概表現(xiàn)為這樣:
Android中常見的內(nèi)存泄露及解決方案
集合類泄露
如果某個集合是全局性的變量(比如 static 修飾),集合內(nèi)直接存放一些占用大量內(nèi)存的對象(而不是通過弱引用存放),那么隨著集合 size 的增大,會導(dǎo)致內(nèi)存占用不斷上升,而在 Activity 等銷毀時,集合中的這些對象無法被回收,導(dǎo)致內(nèi)存泄露。比如我們喜歡通過靜態(tài) HashMap 做一些緩存之類的事,這種情況要小心,集合內(nèi)對象建議采用弱引用的方式存取,并考慮在不需要的時候手動釋放。
單例造成的內(nèi)存泄露
單例的靜態(tài)特性導(dǎo)致其生命周期同應(yīng)用一樣長。
有時創(chuàng)建單例時如果我們需要Context對象,如果傳入的是Application的Context那么不會有問題。如果傳入的是Activity的Context對象,那么當(dāng)Activity生命周期結(jié)束時,該Activity的引用依然被單例持有,所以不會被回收,而單例的生命周期又是跟應(yīng)用一樣長,所以這就造成了內(nèi)存泄露。
解決辦法一:在創(chuàng)建單例的構(gòu)造中不直接用傳進(jìn)來的context,而是通過這個context獲取Application的Context。代碼如下:
- public class AppManager {
- private static AppManager instance;
- private Context context;
- private AppManager(Context context) {
- this.context = context.getApplicationContext();// 使用Application 的context
- }
- public static AppManager getInstance(Context context) {
- if (instance != null) {
- instance = new AppManager(context);
- }
- return instance;
- }
- }
第二種解決方案:在構(gòu)造單例時不需要傳入 context,直接在我們的 Application 中寫一個靜態(tài)方法,方法內(nèi)通過 getApplicationContext 返回 context,然后在單例中直接調(diào)用這個靜態(tài)方法獲取 context。
非靜態(tài)內(nèi)部類造成的內(nèi)存泄露
在 Java 中,非靜態(tài)內(nèi)部類(包括匿名內(nèi)部類,比如 Handler, Runnable匿名內(nèi)部類最容易導(dǎo)致內(nèi)存泄露)會持有外部類對象的強(qiáng)引用(如 Activity),而靜態(tài)的內(nèi)部類則不會引用外部類對象。
非靜態(tài)內(nèi)部類或匿名類因?yàn)槌钟型獠款惖囊?,所以可以訪問外部類的資源屬性成員變量等;靜態(tài)內(nèi)部類不行。
因?yàn)槠胀▋?nèi)部類或匿名類依賴外部類,所以必須先創(chuàng)建外部類,再創(chuàng)建普通內(nèi)部類或匿名類;而靜態(tài)內(nèi)部類隨時都可以在其他外部類中創(chuàng)建。
Handler內(nèi)存泄露可以關(guān)注我的另一篇專門針對Handler內(nèi)存泄露的文章:鏈接
WebView 的泄漏
Android 中的 WebView 存在很大的兼容性問題,有些 WebView 甚至存在內(nèi)存泄露的問題。所以通常***這個問題的辦法是為 WebView 開啟另外一個進(jìn)程,通過 AIDL 與主進(jìn)程進(jìn)行通信, WebView 所在的進(jìn)程可以根據(jù)業(yè)務(wù)的需要選擇合適的時機(jī)進(jìn)行銷毀,從而達(dá)到內(nèi)存的完整釋放。
AlertDialog 造成的內(nèi)存泄露
- new AlertDialog.Builder(this)
- .setPositiveButton("Baguette", new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- MainActivity.this.makeBread();
- }
- }).show();
DialogInterface.OnClickListener 的匿名實(shí)現(xiàn)類持有了 MainActivity 的引用;
而在 AlertDialog 的實(shí)現(xiàn)中,OnClickListener 類將被包裝在一個 Message 對象中(具體可以看 AlertController 類的 setButton 方法),而且這個 Message 會在其內(nèi)部被復(fù)制一份(AlertController 類的 mButtonHandler 中可以看到),兩份 Message 中只有一個被 recycle,另一個(OnClickListener 的成員變量引用的 Message 對象)將會泄露!
解決辦法:
- Android 5.0 以上不存在此問題;
- Message 對象的泄漏無法避免,但是如果僅僅是一個空的 Message 對象,將被放入對象池作為后用,是沒有問題的;
- 讓 DialogInterface.OnClickListener 對象不持有外部類的強(qiáng)引用,如用 static 類實(shí)現(xiàn);
- 在 Activity 退出前 dismiss dialog
Drawable 引起的內(nèi)存泄露
Android 在 4.0 以后已經(jīng)解決了這個問題。這里可以跳過。
當(dāng)我們屏幕旋轉(zhuǎn)時,默認(rèn)會銷毀掉當(dāng)前的 Activity,然后創(chuàng)建一個新的 Activity 并保持之前的狀態(tài)。在這個過程中,Android 系統(tǒng)會重新加載程序的UI視圖和資源。假設(shè)我們有一個程序用到了一個很大的 Bitmap 圖像,我們不想每次屏幕旋轉(zhuǎn)時都重新加載這個 Bitmap 對象,最簡單的辦法就是將這個 Bitmap 對象使用 static 修飾。
- private static Drawable sBackground;
- @Override
- protected void onCreate(Bundle state) {
- super.onCreate(state);
- TextView label = new TextView(this);
- label.setText("Leaks are bad");
- if (sBackground == null) {
- sBackground = getDrawable(R.drawable.large_bitmap);
- }
- label.setBackgroundDrawable(sBackground);
- setContentView(label);
- }
但是上面的方法在屏幕旋轉(zhuǎn)時有可能引起內(nèi)存泄露,因?yàn)椋?dāng)一個 Drawable 綁定到了 View 上,實(shí)際上這個 View 對象就會成為這個 Drawable 的一個 callback 成員變量,上面的例子中靜態(tài)的 sBackground 持有 TextView 對象的引用,而 TextView 持有 Activity 的引用。當(dāng)屏幕旋轉(zhuǎn)時,Activity 無法被銷毀,這樣就產(chǎn)生了內(nèi)存泄露問題。
該問題主要產(chǎn)生在 4.0 以前,因?yàn)樵?2.3.7 及以下版本 Drawable 的 setCallback 方法的實(shí)現(xiàn)是直接賦值,而從 4.0.1 開始,setCallback 采用了弱引用處理這個問題,避免了內(nèi)存泄露問題。
資源未關(guān)閉造成的內(nèi)存泄露
- BroadcastReceiver,ContentObserver 之類的沒有解除注冊
- Cursor,Stream 之類的沒有 close
- ***循環(huán)的動畫在 Activity 退出前沒有停止
- 一些其他的該 release 的沒有 release,該 recycle 的沒有 recycle… 等等。
總結(jié)
我們不難發(fā)現(xiàn),大多數(shù)問題都是 static 造成的!
- 在使用 static 時一定要小心,關(guān)注該 static 變量持有的引用情況。在必要情況下使用弱引用的方式來持有一些引用
- 在使用非靜態(tài)內(nèi)部類時也要注意,畢竟它們持有外部類的引用。(使用 RxJava 的同學(xué)在 subscribe 時也要注意 unSubscribe)
- 注意在生命周期結(jié)束時釋放資源
- 使用屬性動畫時,不用的時候請停止(尤其是循環(huán)播放的動畫),不然會產(chǎn)生內(nèi)存泄露(Activity 無法釋放)(View 動畫不會)
幾種內(nèi)存檢測工具的介紹
- Memory Monitor
- Allocation Tracker
- Heap Viewer
- LeakCanary
Memory Monitor
位于 Android Monitor 中,該工具可以:
- 方便的顯示內(nèi)存使用和 GC 情況
- 快速定位卡頓是否和 GC 有關(guān)
- 快速定位 Crash 是否和內(nèi)存占用過高有關(guān)
- 快速定位潛在的內(nèi)存泄露問題(內(nèi)存占用一直在增長)
- 但是不能準(zhǔn)確的定位問題
Allocation Tracker
該工具用途:
- 可以定位代碼中分配的對象類型、大小、時間、線程、堆棧等信息
- 可以定位內(nèi)存抖動問題
- 配合 Heap Viewer 定位內(nèi)存泄露問題(可以找出來泄露的對象是在哪創(chuàng)建的等等)
使用方法:在 Memory Monitor 中有個 Start Allocation Tracking 按鈕即可開始跟蹤 在點(diǎn)擊停止跟蹤后會顯示統(tǒng)計(jì)結(jié)果。
Heap Viewer
該工具用于:
- 顯示內(nèi)存快照信息
- 每次 GC 后收集一次信息
- 查找內(nèi)存泄露的利器
使用方法: 在 Memory Monitor 中有個 Dump Java Heap 按鈕,點(diǎn)擊即可,在統(tǒng)計(jì)報告左上角選按 package 分類。配合 Memory Monitor 的 initiate GC(執(zhí)行 GC)按鈕,可檢測內(nèi)存泄露等情況。
LeakCanary
重要的事情說三遍:
- for (int i = 0; i < 3; i++) {
- Log.e(TAG, "檢測內(nèi)存泄露的神器!");
- }
LeakCanary 具體使用不再贅述,自行 Google。
本文標(biāo)題:Android內(nèi)存泄露總結(jié)(附內(nèi)存檢測工具)
轉(zhuǎn)載注明:http://www.dlmjj.cn/article/dpscich.html


咨詢
建站咨詢
