Java 內存分配策略

參考來源於深入理解Android虛擬機一書。

1. Java 虛擬機棧 VM Stack

棧中的數據是以棧幀(Stack Frame)的格式存在的,虛擬機在執行每一個方法的調用時都會創建一個棧幀的數據結構,棧幀包括了方法的局部變量表(輸入參數、輸出參數、方法內的變量)、棧操作(記錄出棧、入棧的操作)、動態鏈接、方法、類文件等一些額外的附加信息。

局部變量表中存放了編譯期的基本數據類型(boolean、byte、int、char、short、float、long、double)和對象的引用(reference類型,並不是對象本身)。每一個方法的調用,其實就是對應着一個棧幀在虛擬機裏入棧出棧的過程。對於活動線程中棧頂的棧幀,稱爲當前棧,這個棧幀所關聯的方法稱爲當前方法。

虛擬機棧是線程私有的,是在線程創建時創建的,它的生命週期跟隨線程的生命週期,線程結束時釋放內存,是不需要垃圾回收的。當方法A被調用就會產生一個棧幀F1,並壓入到棧中,A方法又調用了B方法,產生棧幀F2也被壓入棧,方法執行完畢,先彈出F2,再彈出F1,遵循”先進後出”原則,線程結束,棧釋放。

這裏寫圖片描述

Java棧會拋出的異常:

  • 如果線程請求的棧深度大於虛擬機所允許的深度,會拋出StackOverflowError異常。
  • 如果無法申請到足夠的內存來實現棧的對臺擴展,或者沒有足夠的內存爲一個新線程創建Java棧,會拋出OutOfMemoryError異常。

2. Java 堆 Java Heap

Java堆用來存放由關鍵字new創建的對象和數組。在堆中分配內存,是所有線程共享的內存區域。堆在虛擬機啓動時創建。Java堆內存區域的唯一目的就是存放對象的實例,幾乎所有的對象實例都在這裏分配內存。

實際上,棧中的變量指向堆內存中的變量,這就是Java的指針。

Java堆也是垃圾回收機制管理的主要區域,因此很多時候也會內成爲”GC堆”(Garbage Collected Heap)
一個Java虛擬機實例只會存在一個堆內存,堆內存分成三部分:

  • 永久存儲區 Permanent Space :存放JDK自身攜帶的Class Interface的元數據,在此區域的數據是不會被GC的,只有關閉虛擬機纔會被釋放所佔用的內存。
  • 新生區 Young Generation Space :新生區是類誕生、成長、消亡的區域,一個類在這裏產生、應用,最後被GC回收。新生區又分爲2部分:伊甸區(Eden Space)和倖存區(Survivor Space)。所有類都在伊甸區被new出來。倖存區又分爲:倖存0區(Survivor 0 Space)和倖存1區(Survivor 1 Space)。當伊甸區內存用完時,程序又需要創建對象,Java虛擬機的垃圾回收器就會對伊甸區進行GC,將此區不再被其他對象所引用的對象進行銷燬,剩餘的對象移動到倖存0區。如果倖存0區也滿了,在對該區進行GC,然後移到倖存1區。如果倖存1區也滿了,就移動到養老區。
  • 養老區 Tenure Generation Space :養老區用於保存從新生區帥選出來的Java對象。

這裏寫圖片描述

如果堆中沒有可用內存完成類實例或數組的分配,在對象數量達到最大的堆容量限制後就會拋出OutOfMemoryError異常。

通過-Xmx和-Xms限制堆內存大小。

3. 本地方法棧 Native Method Stack

在Java中,本地方法棧中執行的不是Java語言所編寫的代碼,如C、C++。

4. 方法區 Method Area

在Java系統中,方法區在虛擬機啓動時創建,是所有線程共享的內存區域,用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼數據等。有個別名叫做Non-Heap(非堆)。

方法區的回收目標主要針對的是常量池的回收和對類的卸載。方法區的大小可以控制。

在方法區無法滿足內存分配需求時,將會拋出OutOfMemoryError異常,在異常後面跟隨的信息提示是”PermGen Space”,說明運行時常量池屬於方法區(HotSpot虛擬機永久代)的一部分。

通過-XX:PermSize和-XX:MaxPermSize限制方法區大小。

5. 運行時常量池 Runtime Constant Pool

運行時常量池是方法區的一部分。在Class文件中,除了有類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池(Constant Pool Table),用於存放編譯期間生成的各種字面量和符號引用,這部分內容將在類加載後,存放到方法區的運行時常量池中。

類的常量池在該類的Java Class文件被加載Java 虛擬機成功地加載時創建。

運行時常量池和Class文件的常量池的區別:

  • Class文件的常量池在編譯期生成,在運行期被裝載。
  • 運行時常量池具備動態性,在運行期間也可以將新的常量放入運行時常量池中。Native方法String.intern()就可以向運行時常量池中添加內容,該方法的作用是:如果池中已經包含了等於此String對象的字符串,則返回池中代表這個字符串的String對象;否則,將此String對象的字符串添加到常量池中,並且返回此String對象的引用。

運行時常量池受到方法區內存的限制,當運行時常量池無法在申請到內存時,就會拋出OutOfMemoryError異常。

在裝載Class文件時,如果Class文件的常量池的創建需要比方法區中需要更多的內存時,也會拋出OutOfMemoryError異常。

6. 直接內存 Direct Memory

直接內存並不是虛擬機運行時數據區的一部分,從JDK1.4開始,加入NIO(new Input/Output),可以通過Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆裏面的DirectByteBuffer對象作爲這塊內存的引用進行操作。

本機直接內存的分配不會受到Java堆大小的限制,但是會受到本機總內存(RAM、SWAP區或者分頁文件)的大小及處理器尋址空間的限制。如果超出了物理內存的限制,會拋出OutOfMemoryError異常。

通過-XX:MaxDirectMemorySize限制直接內存大小。

7. 對象訪問

Object obj = new Object();

如果這段代碼出現在方法中,那”Object obj”就會反映到Java棧中局部變量表中,作爲一個reference類型數據出現。而”new Object()”就會反映到Java堆中,形成一塊存儲了Object類型所有數據值的結構化內存。另外,Java堆中還必須包含能查找到此對象類型數據的地址信息,這些類型數據存儲在方法區中。

由於reference類型在Java虛擬機只規定了一個指向對象的引用,並沒有規定通過哪種方式去查找Java堆中對象的具體位置。不同的虛擬機實現的方式不同,主流又2種:使用句柄和直接指針。

  • 如果使用句柄訪問方式,Java堆中將會劃分出一塊內存來作爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據的地址信息和對象類型數據的地址信息。

這裏寫圖片描述

  • 如果使用直接指針訪問方式,reference中直接存儲的就是對象地址。(Sun HotSpot使用的是直接指針的訪問方式)。使用直接指針訪問的好處是速度快,節省了一次指針定位的時間開銷。

這裏寫圖片描述

8. 內存泄漏 Memory Leak

一般我們常說的內存泄漏是指堆內存的泄露。堆內存是指程序從堆中分配的,大小任意的(內存塊的大小可以在程序運行期決定),使用完後必須顯示釋放的內存。

內存泄漏有四種:

  • 常發性內存泄漏:泄露代碼唄執行多次,很頻繁的發生。
  • 偶發性內存泄漏:特定環境下的纔會發生的內存泄漏。
  • 一次性內存泄漏:內存泄漏的代碼只會背執行一次,如Singleton類的泄露。
  • 隱式內存泄漏:程序在運行過程中分配的內存,直到程序結束時才釋放內存。嚴格的說,並沒有發生內存泄露,但是因爲不及時的釋放內存,可能最終會導致系統內存耗盡。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章