摘要:不能滿足被回收的條件,盡管調用也還是不能得到回收這就造成了內存泄漏。種解決單例中的內存泄漏將引用置為銷毀監聽使用弱引用將監聽器放入弱引用中從弱引用中取出回調通過第七小點就能完美的解決單例中回調引起的內存泄漏。
我們為什么要優化內存
在 Android 中我們寫的 .java 文件,最終會編譯成 .class 文件, class 又由類裝載器加載后,在 JVM 中會形成一份描述 class 結構的元信息對象,通過該元信息對象可以知道 class 的結構信息 (構造函數、屬性、方法)等。JVM 會把描述類的數據從 class 文件加載到內存,Java 有一個很好的管理內存的機制,垃圾回收機制 GC 。為什么 Java 都給我們提供了垃圾回收機制,程序有時還會導致內存泄漏,內存溢出 OOM,甚至導致程序 Crash 。接下來我們就對實際開發中出現的這些內存問題,來進行優化。
JAVA 虛擬機我們先來大概了解一下 Java 虛擬機里面運行時的數據區域有哪些,如果想深入了解 Java 虛擬機 建議可以購買<<深入理解 Java 虛擬機>> 或者直接點擊我這里的 PDF 版本 密碼: jmnf
線程獨占區
程序計數器
相當于一個執行代碼的指示器,用來確認下一行執行的地址
每個線程都有一個
沒有 OOM 的區
虛擬機棧
我們平時說的棧就是這塊區域
java 虛擬機規范中定義了 OutOfMemeory , stackoverflow 異常
本地方法棧
java 虛擬機規范中定義了 OutOfMemory ,stackoverflow 異常
注意
在 hotspotVM 中把虛擬機棧和本地方法棧合為了一個棧區
線程共享區方法區
ClassLoader 加載類信息
常量、靜態變量
編譯后的代碼
會出現 OOM
運行時常量池
public static final
符號引用類、接口全名、方法名
java 堆 (本次需要優化的地方)
虛擬機能管理的最大的一塊內存 GC 主戰場
會出現 OOM
對象實例
數據的內容
JAVA GC 如何確定內存回收隨著程序的運行,內存中的實例對象、變量等占據的內存越來越多,如果不及時進行回收,會降低程序運行效率,甚至引發系統異常。
目前虛擬機基本都是采用可達性分析算法,為什么不采用引用計數算法呢?下面就說說引用計數法是如果統計所有對象的引用計數的,再對比可達性分析算法是如何解決引用計數算法的不足。下面就來看下這 2 個算法:
引用計數算法每個對象有一個引用計數器,當對象被引用一次則計數器加一,當對象引用一次失效一次則計數器減一,對于計數器為 0 的時候就意味著是垃圾了,可以被 GC 回收。
下面通過一段代碼來實際看下
public class GCTest {
private Object instace = null;
public static void onGCtest() {
//step 1
GCTest gcTest1 = new GCTest();
//step 2
GCTest gcTest2 = new GCTest();
//step 3
gcTest1.instace = gcTest2;
//step 4
gcTest2.instace = gcTest1;
//step 5
gcTest1 = null;
//step 6
gcTest2 = null;
}
public static void main(String[] arg) {
onGCtest();
}
}
分析代碼
//step 1 gcTest1 引用 + 1 = 1
//step 2 gcTest2 引用 + 1 = 1
//step 3 gcTest1 引用 + 1 = 2
//step 4 gcTest2 引用 + 1 = 2
//step 5 gcTest1 引用 - 1 = 1
//step 6 gcTest2 引用 - 1 = 1
很明顯現在 2 個對象都不能用了都為 null 了,但是 GC 確不能回收它們,因為它們本身的引用計數不為 0 。不能滿足被回收的條件,盡管調用 System.gc() 也還是不能得到回收, 這就造成了 內存泄漏 。當然,現在虛擬機基本上都不采用此方式。
可達性分析算法
從 GC Roots 作為起點開始搜索,那么整個連通圖中額對象邊都是活對象,對于 GC Roots 無法到達的對象便成了垃圾回收的對象,隨時可能被 GC 回收。
可以作為 GC Roots 的對象
虛擬機棧正在運行使用的引用
靜態屬性 常量
JNI 引用的對象
GC 是需要 2 次掃描才回收對象,所以我們可以使用 finalize 去救活丟失的引用
@Override
protected void finalize() throws Throwable {
super.finalize();
instace = this;
}
到了這里,相信大家已經能夠弄明白這 2 個算法的區別了吧?反正對于對象之間循環引用的情況,引用計數算法無法回收這 2 個對象,而可達性是從 GC Roots 開始搜索,所以能夠正確的回收。
不同引用類型的回收狀態Object strongReference = new Object()
如果一個對象具有強引用,那垃圾回收器絕不會回收它,當內存空間不足, Java 虛擬機寧愿拋出 OOM 錯誤,使程序異常 Crash ,也不會靠隨意回收具有強引用的對象來解決內存不足的問題.如果強引用對象不再使用時,需要弱化從而使 GC 能夠回收,需要:
strongReference = null; //等 GC 來回收
還有一種情況,如果:
public void onStrongReference(){
Object strongReference = new Object()
}
在 onStrongReference() 內部有一個強引用,這個引用保存在 java 棧 中,而真正的引用內容 (Object)保存在 java 堆中。當這個方法運行完成后,就會退出方法棧,則引用對象的引用數為 0 ,這個對象會被回收。
但是如果 mStrongReference 引用是全局時,就需要在不用這個對象時賦值為 null ,因為 強引用 不會被 GC 回收。
如果一個對象只具有軟引用,則內存空間足夠,垃圾回收器就不會回收它;如果內存空間不足了,就會回收這些對象的內存,只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。
軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收器回收, java 虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。
注意: 軟引用對象是在 jvm 內存不夠的時候才會被回收,我們調用 System.gc() 方法只是起通知作用, JVM 什么時候掃描回收對象是 JVM 自己的狀態決定的。就算掃描到了 str 這個對象也不會回收,只有內存不足才會回收。
弱引用與軟引用的區別在于: 只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過由于垃圾回收器是一個優先級很低的線程,因此不一定會很快發現那些只具有弱引用的對象。
弱引用可以和一個引用隊列聯合使用,如果弱引用所引用的對象被垃圾回收,Java 虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。
可見 weakReference 對象的生命周期基本由 GC 決定,一旦 GC 線程發現了弱引用就標記下來,第二次掃描到就直接回收了。
注意這里的 referenceQueuee 是裝的被回收的對象。
@Test
public void onPhantomReference()throws InterruptedException{
String str = new String("123456");
ReferenceQueue queue = new ReferenceQueue();
// 創建虛引用,要求必須與一個引用隊列關聯
PhantomReference pr = new PhantomReference(str, queue);
System.out.println("PhantomReference:" + pr.get());
System.out.printf("ReferenceQueue:" + queue.poll());
}
虛引用顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。
虛引用主要用來跟蹤對象被垃圾回收器回收的活動。虛引用與軟引用和弱引用的一個區別在于: 虛引用必須和引用隊列 (ReferenceQueue) 聯合使用。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。
引用類型 | 調用方式 | GC | 是否內存泄漏 |
---|---|---|---|
強引用 | 直接調用 | 不回收 | 是 |
軟引用 | .get() | 視內存情況回收 | 否 |
弱引用 | .get() | 回收 | 不可能 |
虛引用 | null | 任何時候都可能被回收,相當于沒有引用一樣 | 否 |
工具很多,掌握原理方法,工具隨意挑選使用。
top/procrank meinfo Procstats DDMS MAT Finder - Activity LeakCanary LeakInspector 內存泄漏產生的原因: 一個長生命周期的對象持有一個短生命周期對象的引用,通俗點講就是該回收的對象,因為引用問題沒有被回收,最終會產生 OOM。
下面我們來利用 Profile 來檢查項目是否有內存泄漏
怎么利用 profile 來查看項目中是否有內存泄漏
在 AS 中項目以 profile 運行
在 MEMORY 界面中選擇要分析的一段內存,右鍵 export
Allocations: 動態分配對象個數
Deallocation: 解除分配的對象個數
Total count: 對象的總數
Shalow Size: 對象本身占用的內存大小
Retained Size: GC 回收能收走的內存大小
轉換 profile 文件格式
將 export 導出的 dprof 文件轉換為 Mat 的 dprof 文件
cd /d 進入到 Android sdk/platform-tools/hprof-conv.exe
//轉換命令 hprof-conv -z src des
D:AndroidAndroidDeveloper-sdkandroid-sdk-windowsplatform-tools>hprof-conv -z D: emp_ emp_6.hprof D: emp_memory6.hprod
下載 Mat 工具
打開 MemoryAnalyzer.exe 點擊左上角 File 菜單中的 Open Heap Dupm
查看內存泄漏中的 GC Roots 強引用
這里我們得知是一個 ilsLoginListener 引用了 LoginView,我們來看下代碼最后怎么解決的。
代碼中我們找到了 LoginView 這個類,發現是一個單例中的回調引起的內存泄漏,下面怎么解決勒,請看第七小點。
2種解決單例中的內存泄漏
將引用置為 null
/**
* 銷毀監聽
*/
public void unRemoveRegisterListener(){
mMessageController.unBindListener();
}
public void unBindListener(){
if (listener != null){
listener = null;
}
}
使用弱引用
//將監聽器放入弱引用中
WeakReference listenerWeakReference = new WeakReference<>(listener);
//從弱引用中取出回調
listenerWeakReference.get();
通過第七小點就能完美的解決單例中回調引起的內存泄漏。
單例
示例 :
public class AppManager {
private static AppManager sInstance;
private CallBack mCallBack;
private Context mContext;
private AppManager(Context context) {
this.mContext = context;
}
public static AppManager getInstance(Context context) {
if (sInstance == null) {
sInstance = new AppManager(context);
}
return sInstance;
}
public void addCallBack(CallBack call){
mCallBack = call;
}
}
通過上面的單列,如果 context 傳入的是 Activity , Service 的 this,那么就會導致內存泄漏。
以 Activity 為例,當 Activity 調用 getInstance 傳入 this ,那么 sInstance 就會持有 Activity 的引用,當 Activity 需要關閉的時候需要 回收的時候,發現 sInstance 還持有 沒有用的 Activity 引用,導致 Activity 無法被 GC 回收,就會造成內存泄漏
addCallBack(CallBack call) 這樣寫看起來是沒有毛病的。但是當這樣調用在看一下勒。
//在 Activity 中實現單例的回調
AppManager.getInstance(getAppcationContext()).addCallBack(new CallBack(){
@Override
public void onStart(){
}
});
這里的 new CallBack() 匿名內部類 默認持有外部的引用,造成 CallBack 釋放不了,那么怎么解決了,請看下面解決方法
解決方法:
getInstance(Context context) context 都傳入 Appcation 級別的 Context,或者實在是需要傳入 Activity 的引用就用 WeakReference 這種形式。
匿名內部類建議大家多帶帶寫一個文件或者
public void addCallBack(CallBack call){
WeakReference mCallBack= new WeakReference(call);
}
Handler
示例:
//在 Activity 中實現 Handler
class MyHandler extends Handler{
private Activity m;
public MyHandler(Activity activity){
m=activity;
}
// class.....
}
這里的 MyHandler 持有 activity 的引用,當 Activity 銷毀的時候,導致 GC 不會回收造成 內存泄漏。
解決方法:
1.使用靜態內部類 + 弱引用
2.在 Activity onDestoty() 中處理 removeCallbacksAndMessages()
@Override
protected void onDestroy() {
super.onDestroy();
if(null != handler){
handler.removeCallbacksAndMessages(null);
handler = null;
}
}
靜態變量
示例:
public class MainActivity extends AppCompatActivity {
private static Police sPolice;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (sPolice != null) {
sPolice = new Police(this);
}
}
}
class Police {
public Police(Activity activity) {
}
}
這里 Police 持有 activity 的引用,會造成 activity 得不到釋放,導致內存泄漏。
解決方法:
//1. sPolice 在 onDestory()中 sPolice = null; //2. 在 Police 構造函數中 將強引用 to 弱引用;
非靜態內部類
參考 第二點 Handler 的處理方式
匿名內部類
示例:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Thread(){
@Override
public void run() {
super.run();
}
};
}
}
很多初學者都會像上面這樣新建線程和異步任務,殊不知這樣的寫法非常地不友好,這種方式新建的子線程Thread和AsyncTask都是匿名內部類對象,默認就隱式的持有外部Activity的引用,導致Activity內存泄露。
解決方法:
//靜態內部類 + 弱引用
//多帶帶寫一個文件 + onDestory = null;
未取消注冊或回調
示例:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
registerReceiver(mReceiver, new IntentFilter());
}
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// TODO ------
}
};
}
在注冊觀察則模式的時候,如果不及時取消也會造成內存泄露。比如使用Retrofit + RxJava注冊網絡請求的觀察者回調,同樣作為匿名內部類持有外部引用,所以需要記得在不用或者銷毀的時候取消注冊。
解決方法:
//Activity 中實現 onDestory()反注冊廣播得到釋放
@Override
protected void onDestroy() {
super.onDestroy();
this.unregisterReceiver(mReceiver);
}
定時任務
示例:
public class MainActivity extends AppCompatActivity {
/**模擬計數*/
private int mCount = 1;
private Timer mTimer;
private TimerTask mTimerTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
mTimer.schedule(mTimerTask, 1000, 1000);
}
private void init() {
mTimer = new Timer();
mTimerTask = new TimerTask() {
@Override
public void run() {
MainActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
addCount();
}
});
}
};
}
private void addCount() {
mCount += 1;
}
}
當我們Activity銷毀的時,有可能Timer還在繼續等待執行TimerTask,它持有Activity 的引用不能被 GC 回收,因此當我們 Activity 銷毀的時候要立即cancel掉Timer和TimerTask,以避免發生內存泄漏。
解決方法:
//當 Activity 關閉的時候,停止一切正在進行中的定時任務,避免造成內存泄漏。
private void stopTimer() {
if (mTimer != null) {
mTimer.cancel();
mTimer = null;
}
if (mTimerTask != null) {
mTimerTask.cancel();
mTimerTask = null;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
stopTimer();
}
資源未關閉
示例:
ArrayList,HashMap,IO,File,SqLite,Cursor 等資源用完一定要記得 clear remove 等關閉一系列對資源的操作。
解決方法:
用完即刻銷毀
屬性動畫
示例:
動畫同樣是一個耗時任務,比如在 Activity 中啟動了屬性動畫 (ObjectAnimator) ,但是在銷毀的時候,沒有調用 cancle 方法,雖然我們看不到動畫了,但是這個動畫依然會不斷地播放下去,動畫引用所在的控件,所在的控件引用 Activity ,這就造成 Activity 無法正常釋放。因此同樣要在Activity 銷毀的時候 cancel 掉屬性動畫,避免發生內存泄漏。
解決方法:
@Override
protected void onDestroy() {
super.onDestroy();
//當關閉 Activity 的時候記得關閉動畫的操作
mAnimator.cancel();
}
Android 源碼或者第三方 SDK
示例:
//如果在開發調試中遇見 Android 源碼或者 第三方 SDK 持有了我們當前的 Activity 或者其它類,那么現在怎么辦了。
解決方法:
//當前是通過 Java 中的反射找到某個類或者成員,來進行手動 = null 的操作。
內存頻繁的分配與回收,(分配速度大于回收速度時) 最終產生 OOM 。
也許下面的錄屏更能解釋什么是內存抖動
可以看出當我點擊了一下 Button 內存就頻繁的創建并回收(注意看垃圾桶)。
那么我們找出代碼中具體那一塊出現問題了勒,請看下面一段錄屏
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
imPrettySureSortingIsFree();
}
});
/**
* 排序后打印二維數組,一行行打印
*/
public void imPrettySureSortingIsFree() {
int dimension = 300;
int[][] lotsOfInts = new int[dimension][dimension];
Random randomGenerator = new Random();
for (int i = 0; i < lotsOfInts.length; i++) {
for (int j = 0; j < lotsOfInts[i].length; j++) {
lotsOfInts[i][j] = randomGenerator.nextInt();
}
}
for (int i = 0; i < lotsOfInts.length; i++) {
String rowAsStr = "";
//排序
int[] sorted = getSorted(lotsOfInts[i]);
//拼接打印
for (int j = 0; j < lotsOfInts[i].length; j++) {
rowAsStr += sorted[j];
if (j < (lotsOfInts[i].length - 1)) {
rowAsStr += ", ";
}
}
Log.i("ricky", "Row " + i + ": " + rowAsStr);
}
}
最后我們之后是 onClick 中的 imPrettySureSortingIsFree() 函數里面的 rowAsStr += sorted[j]; 字符串拼接造成的 內存抖動 ,因為每次拼接一個 String 都會申請一塊新的堆內存,那么怎么解決這個頻繁開辟內存的問題了。其實在 Java 中有 2 個更好的 API 對 String 的操作很友好,相信應該有人猜到了吧。沒錯就是將 此處的 String 換成 StringBuffer 或者 StringBuilder,就能很完美的解決字符串拼接造成的內存抖動問題。
修改后
/**
* 打印二維數組,一行行打印
*/
public void imPrettySureSortingIsFree() {
int dimension = 300;
int[][] lotsOfInts = new int[dimension][dimension];
Random randomGenerator = new Random();
for(int i = 0; i < lotsOfInts.length; i++) {
for (int j = 0; j < lotsOfInts[i].length; j++) {
lotsOfInts[i][j] = randomGenerator.nextInt();
}
}
// 使用StringBuilder完成輸出,我們只需要創建一個字符串即可, 不需要浪費過多的內存
StringBuilder sb = new StringBuilder();
String rowAsStr = "";
for(int i = 0; i < lotsOfInts.length; i++) {
// 清除上一行
sb.delete(0, rowAsStr.length());
//排序
int[] sorted = getSorted(lotsOfInts[i]);
//拼接打印
for (int j = 0; j < lotsOfInts[i].length; j++) {
sb.append(sorted[j]);
if(j < (lotsOfInts[i].length - 1)){
sb.append(", ");
}
}
rowAsStr = sb.toString();
Log.i("jason", "Row " + i + ": " + rowAsStr);
}
}
這里可以看見沒有垃圾桶出現,說明內存抖動解決了。
注意: 實際開發中如果在 LogCat 中發現有這些 Log 說明也發生了 內存抖動 (Log 中出現 concurrent copying GC freed ....)
回收算法ps:我覺得這個只是為了應付面試,那么可以參考這里,我也只了解概念這里就不用在多寫了,點擊看這個帖子吧
也可以參考掘金的這一篇 GC 回收算法
標記清除算法 Mark-Sweep 復制算法 Copying 標記壓縮算法 Mark-Compact 分代收集算法 總結 (只要養成這樣的習慣,至少可以避免 90 % 以上不會造成內存異常)
數據類型: 不要使用比需求更占用空間的基本數據類型
循環盡量用 foreach ,少用 iterator, 自動裝箱也盡量少用
數據結構與算法的解度處理 (數組,鏈表,棧樹,樹,圖)
數據量千級以內可以使用 Sparse 數組 (Key為整數),ArrayMap (Key 為對象) 雖然性能不如 HashMap ,但節約內存。
枚舉優化
缺點:
每一個枚舉值都是一個單例對象,在使用它時會增加額外的內存消耗,所以枚舉相比與 Integer 和 String 會占用更多的內存
較多的使用 Enum 會增加 DEX 文件的大小,會造成運行時更多的 IO 開銷,使我們的應用需要更多的空間
特別是分 Dex 多的大型 APP,枚舉的初始化很容易導致 ANR
優化后的代碼:可以直接限定傳入的參數個數
public class SHAPE {
public static final int TYPE_0=0;
public static final int TYPE_1=1;
public static final int TYPE_2=2;
public static final int TYPE_3=3;
@IntDef(flag=true,value={TYPE_0,TYPE_1,TYPE_2,TYPE_3})
@Target({ElementType.PARAMETER,ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Model{
}
private @Model int value=TYPE_0;
public void setShape(@Model int value){
this.value=value;
}
@Model
public int getShape(){
return this.value;
}
}
static , static final 的問題
static 會由編譯器調用 clinit 方法進行初始化
static final 不需要進行初始化工作,打包在 dex 文件中可以直接調用,并不會在類初始化申請內存
基本數據類型的成員,可以全寫成 static final
字符串的拼接盡量少用 +=
重復申請內存問題
同一個方法多次調用,如遞歸函數 ,回調函數中 new 對象
不要在 onMeause() onLayout() ,onDraw() 中去刷新UI(requestLayout)
避免 GC 回收將來要重新使用的對象 (內存設計模式對象池 + LRU 算法)
Activity 組件泄漏
非業務需要不要把 activity 的上下文做參數傳遞,可以傳遞 application 的上下文
非靜態內部類和匿名內部內會持有 activity 引用(靜態內部類 或者 多帶帶寫文件)
單例模式中回調持有 activity 引用(弱引用)
handler.postDelayed() 問題
如果開啟的線程需要傳入參數,用弱引接收可解決問題
handler 記得清除 removeCallbacksAndMessages(null)
Service 耗時操作盡量使用 IntentService,而不是 Service
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/7402.html
摘要:歡迎來我的個人站點性能優化其他優化瀏覽器關鍵渲染路徑開啟性能優化之旅高性能滾動及頁面渲染優化理論寫法對壓縮率的影響唯快不破應用的個優化步驟進階鵝廠大神用直出實現網頁瞬開緩存網頁性能管理詳解寫給后端程序員的緩存原理介紹年底補課緩存機制優化動 歡迎來我的個人站點 性能優化 其他 優化瀏覽器關鍵渲染路徑 - 開啟性能優化之旅 高性能滾動 scroll 及頁面渲染優化 理論 | HTML寫法...
閱讀 1295·2021-10-08 10:04
閱讀 1922·2021-09-04 16:40
閱讀 2536·2019-08-30 13:21
閱讀 2280·2019-08-29 15:10
閱讀 2848·2019-08-29 12:35
閱讀 1189·2019-08-26 17:41
閱讀 3062·2019-08-26 17:03
閱讀 1136·2019-08-26 12:01