性能優化之內存優化

一、內存模型

Java內存模型:Java程序在運行時內存的模型。而Java代碼是運行在Java虛擬機之上的,所以Java內存模型,也就是指Java虛擬機的運行時內存模型。

Java內存模型分為線程私有和共享數據區兩大類,其中線程私有的數據區包含程序計數器PC、虛擬機棧、本地方法棧,所有線程共享的數據區包含Java堆、方法區,在方法區內有一個常量池。通常說的堆是指Java堆,棧是指虛擬機棧。

1、程序計數器PC

程序計數器PC是一塊較小的內存空間,可以看作所執行字節碼的行號指示器。字節碼解釋器就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,比如循環、跳轉、異常處理等等這些基礎功能都需要依賴這個計數器來完成。

當線程正在執行一個Java方法時,PC計數器記錄的是正在執行的虛擬機字節碼的地址;當線程正在執行的一個Native方法時,PC計數器則為空(Undefined)。

異常:不會拋出異常。

2、虛擬機棧

虛擬機棧描述的是java方法執行的內存模型,它的生命週期與線程相同。

每個方法(不包含native方法)執行的同時都會創建一個棧幀 用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。

Java虛擬機規範規定該區域有兩種異常:

· StackOverFlowError:當線程請求棧深度超出虛擬機棧所允許的深度時拋出 (遞歸函數)

· OutOfMemoryError:當Java虛擬機動態擴展到無法申請足夠內存時拋出 (OOM)

3、本地方法棧

本地方法棧和虛擬機棧差不多,前者是為虛擬機使用到的Native方法提供內存空間。有些虛擬機的實現直接把本地方法棧和虛擬機棧合二為一,比如主流的HotSpot虛擬機。

異常:Java虛擬機規範規定該區域可拋出StackOverFlowError和OutOfMemoryError。

4、Java堆

Java堆,是Java虛擬機管理的最大的一塊內存,也是GC的主戰場,所以可以叫它gc堆(垃圾堆),裡面存放的是幾乎所有的對象實例和數組數據。

異常:Java虛擬機規範規定該區域可拋出OutOfMemoryError。

5、方法區

方法區主要存放的是已被虛擬機加載的類信息、常量、靜態變量、編譯器編譯後的代碼等數據。

異常:Java虛擬機規範規定該區域可拋出OutOfMemoryError。

6、運行時常量池

運行時常量池是方法區的一部分,用於存放編譯器生成的各種字面量和符號引用。運行時常量池除了編譯期產生的Class文件的常量池,還可以在運行期間,將新的常量加入常量池,比較String類的intern()方法。

  1. 符號引用:編譯語言層面的概念,包括以下3類:

  1. 類和接口的全限定名

  2. 字段的名稱和描述符

  3. 方法的名稱和描述符

異常:Java虛擬機規範規定該區域可拋出OutOfMemoryError。

7、變量

局部變量的基本數據類型和引用存儲於棧中,引用的對象實體存儲於堆中。

——因為它們屬於方法中的變量,生命週期隨方法而結束。

成員變量全部存儲在堆中(包括基本數據類型,引用和引用的對象實體)

——因為它們屬於類,類對象終究是要被new出來使用的。

二、垃圾回收

內存的分配是由程序完成的,而內存的釋放是由垃圾收集器(Garbage Collection,GC)完成的。

1、垃圾判斷機制

GC只會回收死去的對象,那怎麼判斷對象是否存活呢?

引用計數

引用計數是垃圾收集器中的早期策略。

引用計數:當一個對象被引用時,變量計數+1,不再引用時,變量計數-1。當變量技術為0時,就會被GC回收。

優點:引用計數收集器可以很快的執行,交織在程序運行中。對程序需要不被長時間打斷的實時環境比較有利。

缺點:無法檢測出循環引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數永遠不可能為0。

可達性分析

可達性分析:遍歷所有的GC ROOT及其子節點,並添加標記。沒有添加標記的對象就是無用的對象,可以被GC回收。

java中可作為GC Root的對象有

  1. 虛擬機棧(本地變量表)中正在運行使用的引用

  2. 方法區中靜態屬性引用的對象

  3. 方法區中常量引用的對象

  4. 本地方法棧JNI中引用的對象(Native對象)

回收流程

  1. GC第一次掃描,通過可達性算法分析對象是否GC Root強引用,即是否有用。

  2. 無用的對象會調用finalize()

  3. GC第二次掃描,調用finalize()後仍然無用的對象,會被GC回收。

2、垃圾回收算法

垃圾判定後就要進行回收,那怎麼進行回收呢?

常用的垃圾回收算法:

  1. 標記-清除算法 Mark-Sweep

  2. 標記-整理算法 Mark-Compact

  3. 複製算法 Copying

  4. 分代收集算法,JVM、DVM使用的就是分代收集算法。

垃圾回收是內存抖動形成的原因,所以一個好的垃圾回收算法很重。

標記-清除算法

標記-清除算法分為標記和清除兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收被標記的對象。

標記-清除算法不會進行對象的移動,直接回收不存活的對象,因此會造成內存碎片。內存碎片會導致內存不可用,從而造成OOM。

標記-整理算法

標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。

標記-壓縮算法雖然緩解的內存碎片問題,但是它也引用了額外的開銷,比如說額外的空間來保存遷移地址,需要遍歷多次堆內存等。

複製算法

複製算法,它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。

複製收集算法在對象存活率較高時就要執行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況。

分代收集算法

分代收集算法會根據內存的劃分實現不同的收集算法。JVM還是DVM就是採用這種算法。

Java堆被分為新生代、老年代和永久代,新生代又被進一步劃分為Eden和Survivor區, Survivor由From Space和To Space組成。

新生代

新建的對象首先分配到新生代的Eden區,當Eden滿時,會把存活的對象轉移到兩個Survivor中的一個,當一個Survivor滿了的時候會把不滿足晉升的對象複製到另一個Survivor。

晉升的意思是對象每經歷一次Minor GC (新生代中的gc),年齡+1,年齡達到設置的一個閥值後,被放入老年代。

兩個Survivor的目的是避免碎片。如果只有一個Survivor,那Survivor被執行一次gc之後,可能對象是A+B+C。經歷一次GC後B被回收。則會A| |C,造成碎片。

在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,所以一般選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。

老年代

用於存放新生代中經過N次垃圾回收仍然存活的對象。

老年代的垃圾回收稱為Major GC。整堆包括新生代與老年代的垃圾回收稱之為Full GC。

老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記-清除算法或標記-整理算法來進行回收。

永久代

主要存放所有已加載的類信息,方法信息,常量池等等。

該區域的對象不需要回收。

3、垃圾收集器

垃圾收集器是垃圾回收的執行者。

Serial串行收集器

Serial串行收集器是單線程的,使用複製算法回收垃圾。在進行垃圾收集時,必須暫停其他所有的工作線程,直到收集結束才能繼續執行。

是client版本的java的默認新生代收集器。

優點:簡單高效;

缺點:需要暫停線程;

ParNew 收集器

ParNew 收集器是多線程版的Serial串行收集器,使用複製算法。是server版本的虛擬機中首選的新生代收集器。

Parallel Scavenge收集器

Parallel Scavenge收集器相比ParNew 收集器,只有吞吐量不同:吞吐量更高。吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),決定CPU的利用率。

Serial Old收集器

Serial Old收集器是老年代版本的串行收集器,使用標記整理算法。

Parallel Old收集器

Serial Old收集器是老年代版本的並行收集器,使用標記整理算法。

CMS 收集器

Concurrent Mark Sweep收集器是一種以獲得最短回收停頓事件為目標的收集器,也稱為併發低停頓收集器或低延遲垃圾收集器,使用標記清除算法。

可以分為4個步驟:

  1. 初始標記(CMS initial mark)

    僅標記一下GC Roots能直接關聯到的對象,速度很快,但需要"Stop The World"。

  2. 併發標記(CMS concurrent mark)

    進行GC Roots 追蹤的過程,剛才產生的集合中標記出存活對象。

    由於應用程序也在運行,並不能保證可以標記出所有的存活對象;。

  3. 重新標記(CMS remark)

    為了修正併發標記期間因用戶程序繼續運作而導致標記變動的那一部分對象的標記記錄;

    需要"Stop The World",且停頓時間比初始標記稍長,但遠比並發標記短;

    採用多線程並行執行來提升效率;

  4. 併發清除(CMS concurrent sweep)

    回收所有的垃圾對象。

優點:併發收集

低停頓。

缺點:

  1. 造成CPU資源緊張:會比其他收集器多開線程。

  2. 無法處理浮動垃圾:用戶線程正在進行。

  3. 大量內存碎片:來源“標記—清除”算法。

G1收集器

Garbage-First收集器是當今收集器技術發展最前沿的成果之一,是一款面向服務端應用的垃圾收集器。

G1收集器和 CMS差不多,但是G1的採集範圍是整個堆(新生代老生代)。他把內存堆分成多個大小相等的獨立區域,在最後的篩選回收的時候根據這些區域的回收價值和成本決定是否回收掉內存。

Android的收集器

Dalvik虛擬機(4.4之前)主要使用標記清除算法,也可以選擇使用拷貝算法。

ART(4.4以後) 有多個不同的 GC 方案,這些方案包括運行不同垃圾回收器。默認方案是 CMS。

三、內存洩漏

內存洩漏:不再使用的對象被GC Root持有強引用,導致GC無法回收。內存洩漏是針對堆內存。

內存分析工具:Android Profiler、MAT、LeakCanary。


分享到:


相關文章: