Java內存分配策略程序員
Java 程序運行時的內存分配策略有三種算法
【靜態分配】靜態存儲區(方法區):主要存放靜態數據、全局 static 數據和常量。這塊內存在程序編譯時就已經分配好,而且在程序整個運行期間都存在。編程
【棧式分配】棧區:當方法被執行時,方法體內的局部變量(其中包括基礎數據類型、對象的引用)都在棧上建立,並在方法執行結束時這些局部變量所持有的內存將會自動被釋放。由於棧內存分配運算內置於處理器的指令集中,效率很高,可是分配的內存容量有限數組
【堆式分配】堆區:又稱動態內存分配,一般就是指在程序運行時直接 new 出來的內存,也就是對象的實例。這部份內存在不使用時將會由 Java 垃圾回收器來負責回收。緩存
棧與堆的區別bash
在方法體內定義的(局部變量)一些基本類型的變量和對象的引用變量都是在方法的棧內存中分配的。當在一段方法塊中定義一個變量時,Java就會在棧中爲該變量分配內存空間,當超過該變量的做用域後,該變量也就無效了,分配給它的內存空間也將被釋放掉,該內存空間能夠被從新使用。網絡
堆內存用來存放全部由new建立的對象(包括該對象其中的全部成員變量)和數組。在堆中分配的內存,將由Java垃圾回收器來自動管理。在堆中產生了一個數組或者對象後,還能夠在棧中定義一個特殊的變量,這個變量的取值等於數組或者對象在堆內存中的首地址,這個特殊的變量就是咱們上面說的引用變量。咱們能夠經過這個引用變量來訪問堆中的對象或者數組。異步
public class Sample {
int s1 = 0;
Sample mSample1 = new Sample();
public void method() {
int s2 = 1;
Sample mSample2 = new Sample();
}
}
Sample mSample3 = new Sample();
Sample 類的局部變量 s2 和引用變量 mSample2 都是存在於棧中,但 mSample2 指向的對象是存在於堆上的。
mSample3 指向的對象實體存放在堆上,包括這個對象的全部成員變量 s1 和 mSample1,而它本身存在於棧中。
複製代碼
結論:ide
局部變量的基本數據類型和引用存儲於棧中,引用的對象實體存儲於堆中。—— 由於它們屬於方法中的變量,生命週期隨方法而結束。函數
成員變量所有存儲與堆中(包括基本數據類型,引用和引用的對象實體)—— 由於它們屬於類,類對象終究是要被new出來使用的。
Java是如何管理內存
Java的內存管理就是對象的分配和釋放問題。
內存的分配是由程序完成的,在Java中,程序員須要經過關鍵字new爲每一個對象申請內存空間 (基本類型除外),全部的對象都在堆(Heap)中分配空間
對象的釋放是由 GC決定和執行的,這種收支兩條線的方法確實簡化了程序員的工做。但同時也加劇了JVM的工做。這也是Java程序運行速度較慢的緣由之一。由於,GC爲了可以正確釋放對象,GC必須監控每個對象的運行狀態,包括對象的申請、引用、被引用、賦值等,GC都須要進行監控。監視對象狀態是爲了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象再也不被引用。
爲了更好理解 GC 的工做原理,咱們能夠將對象考慮爲有向圖的頂點,將引用關係考慮爲圖的有向邊,有向邊從引用者指向被引對象。另外,每一個線程對象能夠做爲一個圖的起始頂點,例如大多程序從main進程開始執行,那麼該圖就是以 main 進程頂點開始的一棵根樹。在這個有向圖中,根頂點可達的對象都是有效對象,GC將不回收這些對象。若是某個對象(連通子圖)與這個根頂點不可達(注意,該圖爲有向圖),那麼咱們認爲這個(這些)對象再也不被引用,能夠被 GC回收。如下,咱們舉一個例子說明如何用有向圖表示內存管理。對於程序的每個時刻,咱們都有一個有向圖表示JVM的內存分配狀況。
Java使用有向圖的方式進行內存管理,能夠消除引用循環的問題,例若有三個對象,相互引用,只要它們和根進程不可達的,那麼GC也是能夠回收它們的。這種方式的優勢是管理內存的精度很高,可是效率較低。另一種經常使用的內存管理技術是使用計數器,例如COM模型採用計數器方式管理構件,它與有向圖相比,精度行低(很難處理循環引用的問題),但執行效率很高。什麼是Java中的內存泄露
在Java中,內存泄漏就是存在一些被分配的對象,這些對象有下面兩個特色,首先,這些對象是可達的,即在有向圖中,存在通路能夠與其相連;其次,這些對象是無用的,即程序之後不會再使用這些對象。若是對象知足這兩個條件,這些對象就能夠斷定爲Java中的內存泄漏,這些對象不會被GC所回收,然而它卻佔用內存。
在C++中,內存泄漏的範圍更大一些。有些對象被分配了內存空間,而後卻不可達,因爲C++中沒有GC,這些內存將永遠收不回來。在Java中,這些不可達的對象都由GC負責回收,所以程序員不須要考慮這部分的內存泄露。
經過分析,咱們得知,對於C++,程序員須要本身管理邊和頂點,而對於Java程序員只須要管理邊就能夠了(不須要管理頂點的釋放)。經過這種方式,Java提升了編程的效率。
所以,經過以上分析,咱們知道在Java中也有內存泄漏,但範圍比C++要小一些。由於Java從語言上保證,任何對象都是可達的,全部的不可達對象都由GC管理。對於程序員來講,GC基本是透明的,不可見的。雖然,咱們只有幾個函數能夠訪問GC,例如運行GC的函數System.gc(),可是根據Java語言規範定義, 該函數不保證JVM的垃圾收集器必定會執行。由於,不一樣的JVM實現者可能使用不一樣的算法管理GC。一般,GC的線程的優先級別較低。JVM調用GC的策略也有不少種,有的是內存使用到達必定程度時,GC纔開始工做,也有定時執行的,有的是平緩執行GC,有的是中斷式執行GC。但一般來講,咱們不須要關心這些。除非在一些特定的場合,GC的執行影響應用程序的性能,例如對於基於Web的實時系統,如網絡遊戲等,用戶不但願GC忽然中斷應用程序執行而進行垃圾回收,那麼咱們須要調整GC的參數,讓GC可以經過平緩的方式釋放內存,例如將垃圾回收分解爲一系列的小步驟執行,Sun提供的HotSpot JVM就支持這一特性。
一個 Java 內存泄漏的典型例子,
Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
Object o = new Object();
v.add(o);
o = null;
}
複製代碼
在這個例子中,咱們循環申請Object對象,並將所申請的對象放入一個 Vector 中,若是咱們僅僅釋放引用自己,那麼Vector仍然引用該對象,因此這個對象對GC來講是不可回收的。所以,若是對象加入到Vector後,還必須從 Vector 中刪除,最簡單的方法就是將 Vector 對象設置爲 null。
Android中常見的內存泄漏彙總
集合類若是僅僅有添加元素的方法,而沒有相應的刪除機制,致使內存被佔用。若是這個集合類是全局性的變量 (好比類中的靜態屬性,全局性的 map 等即有靜態引用或 final一直指向它),那麼沒有相應的刪除機制,極可能致使集合所佔用的內存只增不減。好比上面的典型例子就是其中一種狀況,固然實際上咱們在項目中確定不會寫這麼2B的代碼,但稍不注意仍是很容易出現這種狀況,好比咱們都喜歡經過HashMap作一些緩存之類的事,這種狀況就要多留一些心眼。
因爲單例的靜態特性使得其生命週期跟應用的生命週期同樣長,因此若是使用不恰當的話,很容易形成內存泄漏。好比下面一個典型的例子
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context;
}
public static AppManager getInstance(Context context) {
if (instance == null) {
instance = new AppManager(context);
}
return instance;
}
}
複製代碼
這是一個普通的單例模式,當建立這個單例的時候,因爲須要傳入一個Context,因此這個Context的生命週期的長短相當重要:
一、若是此時傳入的是 Application 的 Context,由於 Application 的生命週期就是整個應用的生命週期,因此這將沒有任何問題。
二、若是此時傳入的是 Activity 的 Context,當這個 Context 所對應的 Activity 退出時,因爲該 Context 的引用被單例對象所持有,其生命週期等於整個應用程序的生命週期,因此當前 Activity 退出時它的內存並不會被回收,這就形成泄漏了。
正確的方式應該改成下面這種方式:
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;
}
}
複製代碼
或者這樣寫,連 Context 都不用傳進來了:
在你的 Application 中添加一個靜態方法,getContext() 返回 Application 的 context,
context = getApplicationContext();
/**
* 獲取全局的context
* @return 返回全局context對象
*/
public static Context getContext(){
return context;
}
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager() {
this.context = MyApplication.getContext();// 使用Application 的context
}
public static AppManager getInstance() {
if (instance == null) {
instance = new AppManager();
}
return instance;
}
}
複製代碼
靜態變量存儲在方法區,它的生命週期從類加載開始,到整個進程結束。一旦靜態變量初始化後,它所持有的引用只有等到進程結束纔會釋放。好比下面這樣的狀況,在Activity中爲了不重複的建立info,將sInfo做爲靜態變量:
public class MainActivity extends AppCompatActivity {
private static Info sInfo;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (sInfo != null) {
sInfo = new Info(this);
}
}
}
class Info {
public Info(Activity activity) {
}
}
複製代碼
Info做爲Activity的靜態成員,而且持有Activity的引用,可是sInfo做爲靜態變量,生命週期確定比Activity長。因此當Activity退出後,sInfo仍然引用了Activity,Activity不能被回收,這就致使了內存泄露。
在Android開發中,靜態持有不少時候都有可能由於其使用的生命週期不一致而致使內存泄露,因此咱們在新建靜態持有的變量的時候須要多考慮一下各個成員之間的引用關係,而且儘可能少地使用靜態持有的變量,以免發生內存泄露。固然,咱們也能夠在適當的時候講靜態量重置爲null,使其再也不持有引用,這樣也能夠避免內存泄露。
非靜態內部類(包括匿名內部類)默認就會持有外部類的引用,當非靜態內部類對象的生命週期比外部類對象的生命週期長時,就會致使內存泄露。非靜態內部類致使的內存泄露在Android開發中有一種典型的場景就是使用Handler,不少開發者在使用Handler是這樣寫的:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
start();
}
private void start() {
Message msg = Message.obtain();
msg.what = 1;
mHandler.sendMessage(msg);
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == 1) {
// 作相應邏輯
}
}
};
}
複製代碼
也許有人會說,mHandler並未做爲靜態變量持有Activity引用,生命週期可能不會比Activity長,應該不必定會致使內存泄露呢,顯然不是這樣的!
熟悉Handler消息機制的都知道,mHandler會做爲成員變量保存在發送的消息msg中,即msg持有mHandler的引用,而mHandler是Activity的非靜態內部類實例,即mHandler持有Activity的引用,那麼咱們就能夠理解爲msg間接持有Activity的引用。msg被髮送後先放到消息隊列MessageQueue中,而後等待Looper的輪詢處理(MessageQueue和Looper都是與線程相關聯的,MessageQueue是Looper引用的成員變量,而Looper是保存在ThreadLocal中的)。那麼當Activity退出後,msg可能仍然存在於消息對列MessageQueue中未處理或者正在處理,那麼這樣就會致使Activity沒法被回收,以至發生Activity的內存泄露。一般在Android開發中若是要使用內部類,但又要規避內存泄露,通常都會採用靜態內部類+弱引用的方式。
public class MainActivity extends AppCompatActivity {
private Handler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mHandler = new MyHandler(this);
start();
}
private void start() {
Message msg = Message.obtain();
msg.what = 1;
mHandler.sendMessage(msg);
}
private static class MyHandler extends Handler {
private WeakReference<MainActivity> activityWeakReference;
public MyHandler(MainActivity activity) {
activityWeakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = activityWeakReference.get();
if (activity != null) {
if (msg.what == 1) {
// 作相應邏輯
}
}
}
}
}
複製代碼
mHandler經過弱引用的方式持有Activity,當GC執行垃圾回收時,遇到Activity就會回收並釋放所佔據的內存單元。這樣就不會發生內存泄露了。
上面的作法確實避免了Activity致使的內存泄露,發送的msg再也不已經沒有持有Activity的引用了,可是msg仍是有可能存在消息隊列MessageQueue中,因此更好的是在Activity銷燬時就將mHandler的回調和發送的消息給移除掉。
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
複製代碼
非靜態內部類形成內存泄露還有一種狀況就是使用Thread或者AsyncTask。 好比在Activity中直接new一個子線程Thread:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Thread(new Runnable() {
@Override
public void run() {
// 模擬相應耗時邏輯
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
複製代碼
或者直接新建AsyncTask異步任務:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
// 模擬相應耗時邏輯
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
}.execute();
}
}
複製代碼
不少初學者都會像上面這樣新建線程和異步任務,卻不知這樣的寫法很是地不友好,這種方式新建的子線程Thread和AsyncTask都是匿名內部類對象,默認就隱式的持有外部Activity的引用,致使Activity內存泄露。要避免內存泄露的話仍是須要像上面Handler同樣使用靜態內部類+弱應用的方式(代碼就不列了,參考上面Hanlder的正確寫法)。
好比咱們在Activity中註冊廣播,若是在Activity銷燬後不取消註冊,那麼這個剛播會一直存在系統中,同上面所說的非靜態內部類同樣持有Activity引用,致使內存泄露。所以註冊廣播後在Activity銷燬後必定要取消註冊。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.registerReceiver(mReceiver, new IntentFilter());
}
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// 接收到廣播須要作的邏輯
}
};
@Override
protected void onDestroy() {
super.onDestroy();
this.unregisterReceiver(mReceiver);
}
}
複製代碼
在註冊觀察則模式的時候,若是不及時取消也會形成內存泄露。好比使用Retrofit+RxJava註冊網絡請求的觀察者回調,一樣做爲匿名內部類持有外部引用,因此須要記得在不用或者銷燬的時候取消註冊。
Timer和TimerTask在Android中一般會被用來作一些計時或循環任務,好比實現無限輪播的ViewPager:
public class MainActivity extends AppCompatActivity {
private ViewPager mViewPager;
private PagerAdapter mAdapter;
private Timer mTimer;
private TimerTask mTimerTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
mTimer.schedule(mTimerTask, 3000, 3000);
}
private void init() {
mViewPager = (ViewPager) findViewById(R.id.view_pager);
mAdapter = new ViewPagerAdapter();
mViewPager.setAdapter(mAdapter);
mTimer = new Timer();
mTimerTask = new TimerTask() {
@Override
public void run() {
MainActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
loopViewpager();
}
});
}
};
}
private void loopViewpager() {
if (mAdapter.getCount() > 0) {
int curPos = mViewPager.getCurrentItem();
curPos = (++curPos) % mAdapter.getCount();
mViewPager.setCurrentItem(curPos);
}
}
private void stopLoopViewPager() {
if (mTimer != null) {
mTimer.cancel();
mTimer.purge();
mTimer = null;
}
if (mTimerTask != null) {
mTimerTask.cancel();
mTimerTask = null;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
stopLoopViewPager();
}
}
複製代碼
當咱們Activity銷燬的時,有可能Timer還在繼續等待執行TimerTask,它持有Activity的引用不能被回收,所以當咱們Activity銷燬的時候要當即cancel掉Timer和TimerTask,以免發生內存泄漏。
這個比較好理解,若是一個對象放入到ArrayList、HashMap等集合中,這個集合就會持有該對象的引用。當咱們再也不須要這個對象時,也並無將它從集合中移除,這樣只要集合還在使用(而此對象已經無用了),這個對象就形成了內存泄露。而且若是集合被靜態引用的話,集合裏面那些沒有用的對象更會形成內存泄露了。因此在使用集合時要及時將不用的對象從集合remove,或者clear集合,以免內存泄漏。
在使用IO、File流或者Sqlite、Cursor等資源時要及時關閉。這些資源在進行讀寫操做時一般都使用了緩衝,若是及時不關閉,這些緩衝對象就會一直被佔用而得不到釋放,以至發生內存泄露。所以咱們在不須要使用它們的時候就及時關閉,以便緩衝能及時獲得釋放,從而避免內存泄露。
動畫一樣是一個耗時任務,好比在Activity中啓動了屬性動畫(ObjectAnimator),可是在銷燬的時候,沒有調用cancle方法,雖然咱們看不到動畫了,可是這個動畫依然會不斷地播放下去,動畫引用所在的控件,所在的控件引用Activity,這就形成Activity沒法正常釋放。所以一樣要在Activity銷燬的時候cancel掉屬性動畫,避免發生內存泄漏。
@Override
protected void onDestroy() {
super.onDestroy();
mAnimator.cancel();
}
複製代碼
關於WebView的內存泄露,由於WebView在加載網頁後會長期佔用內存而不能被釋放,所以咱們在Activity銷燬後要調用它的destory()方法來銷燬它以釋放內存。
另外在查閱WebView內存泄露相關資料時看到這種狀況:
Webview下面的Callback持有Activity引用,形成Webview內存沒法釋放,即便是調用了Webview.destory()等方法都沒法解決問題(Android5.1以後)。
最終的解決方案是:在銷燬WebView以前須要先將WebView從父容器中移除,而後在銷燬WebView。詳細分析過程請參考這篇文章
@Override
protected void onDestroy() {
super.onDestroy();
// 先從父控件中移除WebView
mWebViewContainer.removeView(mWebView);
mWebView.stopLoading();
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.removeAllViews();
mWebView.destroy();
}
複製代碼
總結
內存泄露在Android內存優化是一個比較重要的一個方面,不少時候程序中發生了內存泄露咱們不必定就能注意到,全部在編碼的過程要養成良好的習慣。總結下來只要作到如下這幾點就能避免大多數狀況的內存泄漏: 構造單例的時候儘可能別用Activity的引用;靜態引用時注意應用對象的置空或者少用靜態引用;使用靜態內部類+軟引用代替非靜態內部類;及時取消廣播或者觀察者註冊;耗時任務、屬性動畫在Activity銷燬時記得cancel;文件流、Cursor等資源及時關閉;Activity銷燬時WebView的移除和銷燬。