1. 程式人生 > >Andorid 知識梳理: 效能優化基本知識

Andorid 知識梳理: 效能優化基本知識

java的記憶體區域如何劃分

有兩種說法:
- 從抽象的JVM的角度看: 堆(Heap),棧(Stacks)方法區(MethodArea),執行時常量池(RuntimeConstant Pool),本地方法棧(NativeMethod Stacks),PC Register(PC暫存器)。
- 從作業系統上的程序的角度看:堆(Heap),棧(Stacks),資料段(data segment),程式碼段(code segment)。

Heap/Stack

  • Heap:Heap記憶體的分配也叫做動態記憶體分配,java中執行環境用來分配給物件和JRE類的記憶體都在堆記憶體,C/C++有時候可以用malloc或者new來申請分配一個記憶體。在C/C++可能需要自己負責釋放(java裡面直接依賴GC機制)。
  • Stack:Stack記憶體是相對於執行緒Thread而言的, 在執行函式(方法)時,函式一些內部變數的儲存都可以放在棧上面建立,函式執行結束的時候這些儲存單元就會自動被釋放掉。棧記憶體包括分配的運算速度很快,因為內建在處理器的裡面的。當然容量有限。它儲存執行緒中方法中短期存在的變數值和對Heap中物件的引用等.

區別:堆是不連續的記憶體區域,堆空間比較靈活也特別大。
棧是一塊連續的記憶體區域,大小是由作業系統覺決定的。堆管理很麻煩,頻繁地new/remove會造成大量的記憶體碎片,這樣就會慢慢導致效率低下。對於棧的話,他先進後出,進出完全不會產生碎片,執行效率高且穩定。

我們通常說的記憶體洩露,GC,是針對Heap記憶體的

. 因為Stack記憶體在函數出棧的時候就銷燬了。
比如說這個類

public class People{
    int a = 1;    //堆
    Student s1 = new Student(); //堆
    public void XXX(){
        int b = 1;  //棧
        Student s2 = new Student();  //new出的物件存在堆中,棧記憶體中持有s2的引用
    }
}

總結:
- Heap: 儲存類物件(成員變數);空間大但不連續;易存在記憶體碎片
- Stack:儲存函式(區域性變數);空間小但連續,且運算速度快(內建處理器);棧為先進後出

java四大引用

Java中有四種強度不同的引用,從強到弱依次是:
- 強引用>弱引用>軟引用>虛引用。

它們均為java.lang.ref.Reference抽象類的實現類,實現不同則垃圾回收的處理策略不同。

如果指向一個例項的所有引用中最強的引用是強/軟/弱引用,則稱該例項處於強/軟/弱引用可達(strongly/softly/weakly reachable)的狀態(下文也用該方式描述)。一個物件的引用可達性狀態會影響GC回收的表現。

強引用(Strong Reference)

Book book = new Book();  //最常見的一種建立物件方式

GC不回收強引用可達(strongly reachable)的物件。只有在強引用被置為null時(弱化),才會在被回收。

軟引用(SoftReference)

用來描述一些還有用但並非必須的物件,如快取Cache。 當一次GC後發現記憶體不足,則會把軟引用物件回收。如果回收後仍不足,才會丟擲記憶體溢位異常。所以也可以說:軟引用物件在丟擲OutOfMemoryError之前已經回收。

弱引用(WeakReference)

被弱引用關聯的物件只能生存到下一個垃圾收集之前。當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。

WeakReference<Cacheable> weakData = new WeakReference<Cacheable>(data);
if(weakData.get()!=null){
//do what you like
}

虛引用 (PhantomReference)

虛引用的get()方法永遠返回null,這是因為虛引用的設計目的不是獲取物件例項,而是作為物件已清除(finalized)、GC準備回收記憶體(reclaim memory)的訊號。 虛引用要配合ReferenceQueue使用,虛引用構造時必須傳入一個ReferenceQueue物件。

虛引用在物件從記憶體中完全移除後插入引用佇列。通過ReferenceQueue.remove()可以獲得有新引用物件插隊的通知,靈活地進行最後的清理工作。(不推薦使用終結器finalize()方法來清理,執行時間不可靠、笨重,在終結器中建立一個強引用指向正在終結的物件會使物件復活,相關文章Finalization and Phantom References、Dalvik虛擬機器 Finalize 方法執行分析)。

記憶體洩漏

記憶體洩漏
當一個物件已經不需要再使用了,本該被回收時,而有另外一個正在使用的物件持有它的引用,從而就導致物件不能被回收。這種導致了本該被回收的物件不能被回收而停留在堆記憶體中,就產生了記憶體洩漏。記憶體洩露問題,在下篇部落格中會詳細介紹把記憶體洩露抓出來。

發生GC(回收弱引用)–> 記憶體仍然不足(回收軟應用)–> 記憶體仍不足(OutOfMemoryError)

場景:
- 非靜態內部類的靜態例項
由於內部類預設持有外部類的引用,而靜態例項屬於類。所以,當外部類被銷燬時,內部類仍然持有外部類的引用,致使外部類無法被GC回收。因此造成記憶體洩露。

public class TestAct extends AppCompatActivity {
    private static InnerClass mInnerClass = null;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.act_test);
        if (mInnerClass == null) {
            mInnerClass = new InnerClass();
        }
    }
    class InnerClass {
    }
}

解決:將該內部類設定為靜態內部類(靜態內部類不會持有外部類的引用)

  • 類的靜態變數持有大資料物件
    靜態變數長期維持到大資料物件的引用,阻止垃圾回收。

  • 資源物件未關閉
    資源性物件如Cursor、Stream、Socket,Bitmap,應該在使用後及時關閉。未在finally中關閉,會導致異常情況下資源物件未被釋放的隱患。

  • 註冊物件未反註冊
    我們常常寫很多的Listener,未反註冊會導致觀察者列表裡維持著物件的引用,阻止垃圾回收。

  • Handler臨時性記憶體洩露
    Handler通過傳送Message與主執行緒互動,Message發出之後是儲存在MessageQueue中的,有些Message也不是馬上就被處理的。

  • Context洩露
    這個太多了,不細說,單利模式寫的不恰當就屬於這種。

Garbage Collector(垃圾回收器)

垃圾回收機制有好幾套演算法,java語言規範沒有明確的說明JVM 使用哪種垃圾回收演算法,但是任何一種垃圾回收演算法一般要做兩件基本事情:(1)發現無用的資訊物件;(2)回收將無用物件佔用的記憶體空間。使該空間可被程式再次使用。

演算法

1. 引用計數法(Reference Counting Collector)

當一個物件被建立時,且將該物件例項分配給一個變數,該變數計數設定為1。當任何其它變數被賦值為這個物件的引用時,計數加1(a = b,則b引用的物件例項的計數器+1),但當一個物件例項的某個引用超過了生命週期或者被設定為一個新值時,物件例項的引用計數器減1。任何引用計數器為0的物件例項可以被當作垃圾收集。當一個物件例項被垃圾收集時,它引用的任何物件例項的引用計數器減1。

2. tracing演算法(Tracing Collector) 或 標記-清除演算法(mark and sweep)

2.1. 根搜尋演算法

根搜尋演算法是從離散數學中的圖論引入的,程式把所有的引用關係看作一張圖,從一個節點GC ROOT開始,尋找對應的引用節點,找到這個節點以後,繼續尋找這個節點的引用節點,當所有的引用節點尋找完畢之後,剩餘的節點則被認為是沒有被引用到的節點,即無用的節點。

2.2. 標記-清除演算法分析

標記-清除演算法採用從根集合進行掃描,對存活的物件物件標記,標記完畢後,再掃描整個空間中未被標記的物件,進行回收,如上圖所示。標記-清除演算法不需要進行物件的移動,並且僅對不存活的物件進行處理,在存活物件比較多的情況下極為高效,但由於標記-清除演算法直接回收不存活的物件,因此會造成記憶體碎片.

2.3. generation演算法(Generational Collector)
- 年輕代(Young Generation)

1.所有新生成的物件首先都是放在年輕代的。年輕代的目標就是儘可能快速的收集掉那些生命週期短的物件。

2.新生代記憶體按照8:1:1的比例分為一個eden區和兩個survivor(survivor0,survivor1)區。一個Eden區,兩個 Survivor區(一般而言)。大部分物件在Eden區中生成。回收時先將eden區存活物件複製到一個survivor0區,然後清空eden區,當這個survivor0區也存放滿了時,則將eden區和survivor0區存活物件複製到另一個survivor1區,然後清空eden和這個survivor0區,此時survivor0區是空的,然後將survivor0區和survivor1區交換,即保持survivor1區為空, 如此往復。

3.當survivor1區不足以存放 eden和survivor0的存活物件時,就將存活物件直接存放到老年代。若是老年代也滿了就會觸發一次Full GC,也就是新生代、老年代都進行回收

4.新生代發生的GC也叫做Minor GC,MinorGC發生頻率比較高(不一定等Eden區滿了才觸發)

  • 年老代(Old Generation)

1.在年輕代中經歷了N次垃圾回收後仍然存活的物件,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命週期較長的物件。

2.記憶體比新生代也大很多(大概比例是1:2),當老年代記憶體滿時觸發Major GC即Full GC,Full GC發生頻率比較低,老年代物件存活時間比較長,存活率標記高。

  • 持久代(Permanent Generation)

用於存放靜態檔案,如Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者呼叫一些class,例如Hibernate 等,在這種時候需要設定一個比較大的持久代空間來存放這些執行過程中新增的類。