掃二維碼與項(xiàng)目經(jīng)理溝通
我們在微信上24小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
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),我們會(huì)仔細(xì)了解各客戶的需求而做出多方面的分析、設(shè)計(jì)、整合,為客戶設(shè)計(jì)出具風(fēng)格及創(chuàng)意性的商業(yè)解決方案,創(chuàng)新互聯(lián)公司更提供一系列網(wǎng)站制作和網(wǎng)站推廣的服務(wù)。
主要是分三塊:
棧與堆的區(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垃圾回收器來自動(dòng)管理。在堆內(nèi)存中頻繁的 new/delete 會(huì)造成大量內(nèi)存碎片,使程序效率降低。
對于非靜態(tài)變量的儲(chǔ)存位置,我們可以粗暴的認(rèn)為:
四種引用類型的介紹
GC 釋放對象的根本原則是該對象不再被引用(強(qiáng)引用)。那么什么是強(qiáng)引用呢?
強(qiáng)引用(Strong Reference)
我們平常用的最多的就是強(qiáng)引用,如下:
- IPhotos iPhotos = new IPhotos();
JVM 寧可拋出 OOM ,也不會(huì)讓 GC 回收具有強(qiáng)引用的對象。強(qiáng)引用不使用時(shí),可以通過 obj = null 來顯式的設(shè)置該對象的所有引用為 null,這樣就可以回收該對象了。至于什么時(shí)候回收,取決于 GC 的算法,這里不做深究。
軟引用(Soft Reference)
- SoftReference
softReference = new SoftReference<>(str);
如果一個(gè)對象只具有軟引用,那么在內(nèi)存空間足夠時(shí),垃圾回收器就不會(huì)回收它;如果內(nèi)存空間不足了,就會(huì)回收這些對象的內(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 會(huì)很頻繁,導(dǎo)致釋放軟引用的頻率也很高,這樣會(huì)降低它的使用效率。并且 3.0 以前 Bitmap 是存放在 Native Memory 中,它的釋放是不受 GC 控制的,所以使用軟引用緩存 Bitmap 可能會(huì)造成 OOM。
弱引用(Weak Reference)
- WeakReference
weakReference = new WeakReference<>(str);
與軟引用的區(qū)別在于:只具有弱引用的對象擁有更短暫的生命周期。因?yàn)樵?GC 時(shí),一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當(dāng)前內(nèi)存空間足夠與否,都會(huì)回收它的內(nèi)存。不過,由于垃圾回收器是一個(gè)優(yōu)先級很低的線程,因此不一定會(huì)很快發(fā)現(xiàn)那些只具有弱引用的對象- -。
虛引用(PhantomReference)
顧名思義,就是形同虛設(shè),與其他幾種引用都不同,虛引用并不會(huì)決定對象的生命周期,也無法通過虛引用獲得對象實(shí)例。虛引用必須和引用隊(duì)列(ReferenceQueue)聯(lián)合使用。當(dāng)垃圾回收器準(zhǔn)備回收一個(gè)對象時(shí),如果發(fā)現(xiàn)它還有虛引用,就會(huì)在回收對象的內(nèi)存之前,把這個(gè)虛引用加入到與之關(guān)聯(lián)的引用隊(duì)列中。程序可以通過判斷引用隊(duì)列中是否存在該對象的虛引用,來了解這個(gè)對象是否將要被回收。
Android的垃圾回收機(jī)制簡介
Android 系統(tǒng)里面有一個(gè) Generational Heap Memory 模型,系統(tǒng)會(huì)根據(jù)內(nèi)存中不同的內(nèi)存數(shù)據(jù)類型分別執(zhí)行不同的 GC 操作。
該模型分為三個(gè)區(qū):
大多數(shù) new 出來的對象都放到 eden 區(qū),當(dāng) eden 區(qū)填滿時(shí),執(zhí)行 Minor GC(輕量級GC),然后存活下來的對象被轉(zhuǎn)移到 Survivor 區(qū)(有 S0,S1 兩個(gè))。 Minor GC 也會(huì)檢查 Survivor 區(qū)的對象,并把它們轉(zhuǎn)移到另一個(gè) Survivor 區(qū),這樣就總會(huì)有一個(gè) Survivor 區(qū)是空的。
Old Generation
存放長期存活下來的對象(經(jīng)過多次 Minor GC 后仍然存活下來的對象) Old Generation 區(qū)滿了以后,執(zhí)行 Major GC(大型 GC)。
在Android 2.2 之前,執(zhí)行 GC 時(shí),應(yīng)用的線程會(huì)被暫停,2.3 開始添加了并發(fā)垃圾回收機(jī)制。
Permanent Generation
存放方法區(qū)。一般存放:
60 FPS
這里簡單的介紹一下幀率的概念,以便于理解為什么大量的 GC 容易引起卡頓。
App 開發(fā)時(shí),一般追求界面的幀率達(dá)到60 FPS(60 幀/秒),那這個(gè) FPS 是什么概念呢?
Android 每隔 16 ms發(fā)出 VSYNC 信號,觸發(fā)對 UI 的渲染(即每 16 ms繪制一幀),如果整個(gè)過程保持在 16 ms以內(nèi),那么就會(huì)達(dá)到 60 FPS 的流暢畫面。超過了 16 ms就會(huì)造成卡頓。那么如果在 UI 渲染時(shí)發(fā)生了大量 GC,或者 GC 耗時(shí)太長,那么就可能導(dǎo)致繪制過程超過 16 ms從而造成卡頓(FPS 下降、掉幀等),而我們大腦對于掉幀的情況十分敏銳,因此如果沒有做好內(nèi)存管理,將會(huì)給用戶帶來非常不好的體驗(yàn)。
再介紹一下內(nèi)存抖動(dòng)的概念,本文后面可能會(huì)用到這個(gè)概念。
內(nèi)存抖動(dòng)
短時(shí)間內(nèi)大量 new 對象,達(dá)到 Young Generation 的閾值后觸發(fā)GC,導(dǎo)致剛 new 出來的對象又被回收。此現(xiàn)象會(huì)影響幀率,造成卡頓。
內(nèi)存抖動(dòng)在 Android 提供的 Memory Monitor 中大概表現(xiàn)為這樣:
Android中常見的內(nèi)存泄露及解決方案
集合類泄露
如果某個(gè)集合是全局性的變量(比如 static 修飾),集合內(nèi)直接存放一些占用大量內(nèi)存的對象(而不是通過弱引用存放),那么隨著集合 size 的增大,會(huì)導(dǎo)致內(nèi)存占用不斷上升,而在 Activity 等銷毀時(shí),集合中的這些對象無法被回收,導(dǎo)致內(nèi)存泄露。比如我們喜歡通過靜態(tài) HashMap 做一些緩存之類的事,這種情況要小心,集合內(nèi)對象建議采用弱引用的方式存取,并考慮在不需要的時(shí)候手動(dòng)釋放。
單例造成的內(nèi)存泄露
單例的靜態(tài)特性導(dǎo)致其生命周期同應(yīng)用一樣長。
有時(shí)創(chuàng)建單例時(shí)如果我們需要Context對象,如果傳入的是Application的Context那么不會(huì)有問題。如果傳入的是Activity的Context對象,那么當(dāng)Activity生命周期結(jié)束時(shí),該Activity的引用依然被單例持有,所以不會(huì)被回收,而單例的生命周期又是跟應(yīng)用一樣長,所以這就造成了內(nèi)存泄露。
解決辦法一:在創(chuàng)建單例的構(gòu)造中不直接用傳進(jìn)來的context,而是通過這個(gè)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)造單例時(shí)不需要傳入 context,直接在我們的 Application 中寫一個(gè)靜態(tài)方法,方法內(nèi)通過 getApplicationContext 返回 context,然后在單例中直接調(diào)用這個(gè)靜態(tài)方法獲取 context。
非靜態(tài)內(nèi)部類造成的內(nèi)存泄露
在 Java 中,非靜態(tài)內(nèi)部類(包括匿名內(nèi)部類,比如 Handler, Runnable匿名內(nèi)部類最容易導(dǎo)致內(nèi)存泄露)會(huì)持有外部類對象的強(qiáng)引用(如 Activity),而靜態(tài)的內(nèi)部類則不會(huì)引用外部類對象。
非靜態(tài)內(nèi)部類或匿名類因?yàn)槌钟型獠款惖囊?,所以可以訪問外部類的資源屬性成員變量等;靜態(tài)內(nèi)部類不行。
因?yàn)槠胀▋?nèi)部類或匿名類依賴外部類,所以必須先創(chuàng)建外部類,再創(chuàng)建普通內(nèi)部類或匿名類;而靜態(tài)內(nèi)部類隨時(shí)都可以在其他外部類中創(chuàng)建。
Handler內(nèi)存泄露可以關(guān)注我的另一篇專門針對Handler內(nèi)存泄露的文章:鏈接
WebView 的泄漏
Android 中的 WebView 存在很大的兼容性問題,有些 WebView 甚至存在內(nèi)存泄露的問題。所以通常***這個(gè)問題的辦法是為 WebView 開啟另外一個(gè)進(jìn)程,通過 AIDL 與主進(jìn)程進(jìn)行通信, WebView 所在的進(jìn)程可以根據(jù)業(yè)務(wù)的需要選擇合適的時(shí)機(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 類將被包裝在一個(gè) Message 對象中(具體可以看 AlertController 類的 setButton 方法),而且這個(gè) Message 會(huì)在其內(nèi)部被復(fù)制一份(AlertController 類的 mButtonHandler 中可以看到),兩份 Message 中只有一個(gè)被 recycle,另一個(gè)(OnClickListener 的成員變量引用的 Message 對象)將會(huì)泄露!
解決辦法:
Drawable 引起的內(nèi)存泄露
Android 在 4.0 以后已經(jīng)解決了這個(gè)問題。這里可以跳過。
當(dāng)我們屏幕旋轉(zhuǎn)時(shí),默認(rèn)會(huì)銷毀掉當(dāng)前的 Activity,然后創(chuàng)建一個(gè)新的 Activity 并保持之前的狀態(tài)。在這個(gè)過程中,Android 系統(tǒng)會(huì)重新加載程序的UI視圖和資源。假設(shè)我們有一個(gè)程序用到了一個(gè)很大的 Bitmap 圖像,我們不想每次屏幕旋轉(zhuǎn)時(shí)都重新加載這個(gè) Bitmap 對象,最簡單的辦法就是將這個(gè) 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)時(shí)有可能引起內(nèi)存泄露,因?yàn)?,?dāng)一個(gè) Drawable 綁定到了 View 上,實(shí)際上這個(gè) View 對象就會(huì)成為這個(gè) Drawable 的一個(gè) callback 成員變量,上面的例子中靜態(tài)的 sBackground 持有 TextView 對象的引用,而 TextView 持有 Activity 的引用。當(dāng)屏幕旋轉(zhuǎn)時(shí),Activity 無法被銷毀,這樣就產(chǎn)生了內(nèi)存泄露問題。
該問題主要產(chǎn)生在 4.0 以前,因?yàn)樵?2.3.7 及以下版本 Drawable 的 setCallback 方法的實(shí)現(xiàn)是直接賦值,而從 4.0.1 開始,setCallback 采用了弱引用處理這個(gè)問題,避免了內(nèi)存泄露問題。
資源未關(guān)閉造成的內(nèi)存泄露
總結(jié)
我們不難發(fā)現(xiàn),大多數(shù)問題都是 static 造成的!
幾種內(nèi)存檢測工具的介紹
Memory Monitor
位于 Android Monitor 中,該工具可以:
Allocation Tracker
該工具用途:
使用方法:在 Memory Monitor 中有個(gè) Start Allocation Tracking 按鈕即可開始跟蹤 在點(diǎn)擊停止跟蹤后會(huì)顯示統(tǒng)計(jì)結(jié)果。
Heap Viewer
該工具用于:
使用方法: 在 Memory Monitor 中有個(gè) Dump Java Heap 按鈕,點(diǎn)擊即可,在統(tǒng)計(jì)報(bào)告左上角選按 package 分類。配合 Memory Monitor 的 initiate GC(執(zhí)行 GC)按鈕,可檢測內(nèi)存泄露等情況。
LeakCanary
重要的事情說三遍:
- for (int i = 0; i < 3; i++) {
- Log.e(TAG, "檢測內(nèi)存泄露的神器!");
- }
LeakCanary 具體使用不再贅述,自行 Google。

我們在微信上24小時(shí)期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流