導語
最近一段時間需要對專案的記憶體進行優化,因為專案比較老,程式碼經過很多手,導致應用在使用過程中有較為嚴重的記憶體洩漏,在某些情況下還會出現OOM,簡直是不能忍,所以簡單記錄一下從入門到放棄的過程,就當做是學習和總結。
#JAVA執行時記憶體區域 Java虛擬機器有一套記憶體自動管理的機制,所以程式設計師不需要也不能手動的alloc記憶體,這在很大程度上避免了記憶體洩漏的發生,但是不能百分百的避免。 在執行程式的時候會對它管理的記憶體進行劃分,在不同的區域分配不同的工作,下圖中展示了執行時各區域的基本結構,主要分為執行緒私有和共享兩類:
###1.1程式計數器 程式計數器是執行緒私有的,且是記憶體分割槽裡頭唯一一個不會造成OOM異常的一個記憶體區域。它的作用是虛擬機器位元組碼指令執行過程的管理者,通過計數器的值就可以控制下一條需要執行的指令位元組碼,它記錄的是位元組碼指令的地址,由於Java虛擬機器可以執行多執行緒,而每個處理器在每個時刻都只會執行一條執行緒,為了執行緒切換過程中保證位元組碼指令的穩定,每一條執行緒都會持有一個獨立的計數器。需要說明的是,以上我們針對的是Java方法,如果虛擬機器執行的是Native方法的話,計數器的值會為0。
###1.2虛擬機器棧 Java虛擬機器棧是執行緒私有的,它負責Java方法執行過程中的入棧、彈棧,它可能會造成兩種異常,StackOverFlowError與OutOfMemoryError,就是棧溢位與記憶體溢位。
我們經常會提到棧記憶體,這個棧記憶體指的就是虛擬機器棧,虛擬機器棧是的構成元素是棧幀,每個棧幀包含了區域性變數表,運算元棧,動態連線,方法返回地址和一些額外資訊,一個方法對應一個棧幀,方法從開始呼叫到執行結束的過程,就是一個棧幀在虛擬機器棧中入棧到彈棧的過程。
大概講一下圖中的棧幀各組成結構作用
- 1.2.1區域性變數表 區域性變數表用於存放方法中的引數和方法內部定義的區域性變數,最小組成單位是Slot,虛擬機器規範並沒有明確規定Slot佔用的記憶體空間大小為32位,但是需要每一個Slot能夠保證存放一個boolean、byte、char、short、int、float、reference(即物件型別)、returnAddress型別(指向一條位元組碼指令的地址),即能夠存放32位以下的資料,所以根據不同虛擬機器的具體實現把Slot設定為64位也是有可能的。 Java程式設計師都知道在方法內部呼叫"this"關鍵字可以引用方法所在的物件的例項引用,其實這個隱藏的引數是編譯器在此處懟進去的,如果是例項方法(非static,static方法裡面引用不了"this"關鍵字也是這裡造成的),會在區域性變數表的首位,即第0索引處儲存該方法所屬物件的例項引用。
- 1.2.2運算元棧 運算元棧用於存取方法執行過程中位元組碼指令操作的內容,算術操作和引數的傳遞都是在操作棧中進行的。 順便提一下,Java虛擬機器和Android 的Dalvik虛擬機器一個重要區別就在這個地方,Java虛擬機器是基於棧的執行引擎,這個棧就是運算元棧,它必須使用指令來載入和操作棧上資料,而Dalvik虛擬機器是基於暫存器的。
- 1.2.3動態連線 我們都知道Java有三大特性,繼承、封裝、多型,為了實現多型,Java中方法呼叫就不能直接指向方法的地址,因為這樣的話方法就會唯一確定,而多型是在執行時動態的選擇方法(比如子類實現父類方法A,只有在虛擬機器執行的時候才知道到底要執行父類還是子類的方法A),為了支援方法呼叫過程中的動態連線,棧幀中就會保留指向常量池中的符號引用,在真正方法呼叫時會以常量池中的符號來獲取直接引用(實際的方法地址),為了虛擬機器的效能著想,一部分的方法呼叫會在編譯期間就會把符號引用轉化成直接引用,這裡會分為兩種情況: 1.符合"編譯期可知,執行期不可變"的方法呼叫會在程式碼寫好和編譯完成之後就會把這些方法的符號引用轉化為直接引用,主要包括靜態方法和私有方法,這兩種方法不可能被繼承,執行期間是不可能發生改變的 2.其它的方法會把符號引用到直接引用的轉化延遲到執行期間才會進行,也稱為分派呼叫,具體的呼叫邏輯可以參考書籍《深入理解Java虛擬機器》
- 1.2.4方法返回地址 方法執行完成後,需要回到方法呼叫位置,這樣才能保證程式繼續執行,一般情況下此處會保留呼叫者的計數器值,而在方法異常退出的情況下,返回地址是通過異常處理表來處理的。
- 1.2.5附加資訊 可以把這裡當做Java虛擬機器規範為了虛擬機器的具體實現者預留的擴充套件位置,方便虛擬機器具體實現者儲存一些Java虛擬機器規範中沒有明確要求的資訊。 虛擬機器棧的深度是有規定要求的,在超過其最大值會拋StackOverFlowError異常(比如無限遞迴呼叫本方法),在一些虛擬機器棧可以動態擴充套件的虛擬機器上,在棧深度不夠用時則會擴充套件,一直到記憶體不夠用的時候丟擲OutOfMemoryError異常。
###1.3本地方法棧 本地方法棧與虛擬機器棧功能基本一樣,是執行緒私有的,兩者的主要區別在於它負責Native方法執行過程中的入棧、彈棧,而虛擬機器棧負責Java方法,它也可能會造成兩種異常,StackOverFlowError與OutOfMemoryError,就是棧溢位與記憶體溢位。 ###1.4堆 平時程式猿所討論的堆疊,棧指的是虛擬機器棧,而這個堆就是這裡講到的Java堆,它是執行緒共享的,基本上所有的物件和陣列都是在堆上分配記憶體的。它可能會造成OutOfMemoryError異常。 OOM異常一般是堆中記憶體不足造成的,由於物件不能或者未及時釋放,導致物件一直佔用堆中的記憶體,這會造成記憶體洩漏,當積累到一定程度,即堆記憶體不夠用的時候虛擬機器就會丟擲OOM異常。 Java堆也稱為"GC堆",物件的建立可以說是程式中最為頻繁的現象,Java虛擬機器會自動管理Java堆中的記憶體,在記憶體不夠用的時候虛擬機器會啟動垃圾收集器進行垃圾回收操作釋放一部分沒有被引用的物件。後面有時間的話可以講一下垃圾回收相關的知識。 ###1.5方法區 方法區跟堆一樣,屬於執行緒共享,當記憶體不足時會拋OutOfMemoryError異常。方法區邏輯上是堆的一部分,但是又會和堆區分開來,所以又稱為非堆。 在我們的開發中,有時會把方法區稱為永久代,用於儲存一些類資訊、常量、靜態變數等,從永久代這個稱謂中就可以看出在方法區中的變數存活時間比較久,因為這個區域很少會發生垃圾收集的行為,但是並非資料進入方法區後就會永久存在,方法區中的常量池是存在被垃圾回收的可能的。
- 1.5.1執行時常量池 執行時常量池是方法區的一部分,在講到虛擬機器棧-棧幀-動態連線的時候,提到過動態連線會保留指向執行時常量池的方法字元引用,從這裡可以看出執行時常量池用於存放Class檔案編譯期生成的各種字面量和符號引用,對於靜態方法和私有方法,執行時常量池在編譯期也會儲存這些方法的直接引用。