記憶體管理是開發者必須掌握的基本功,不然程式總是會在各種難以捉摸的錯誤中崩潰,一些語言,例如C、C++開發者們自己申請記憶體,使用完自己釋放,但是不當的程式碼書寫習慣往往導致記憶體洩露,引用空指標等等錯誤,而Java藉助於虛擬機器幫我們完成了許多工作,使開發者從記憶體管理的深坑中爬出來了,但是由於隔著這層虛擬機器,出現問題時的應對策略更顯功力,需要對虛擬機器記憶體管理機制的深入瞭解。
這篇文章只是粗淺的介紹下虛擬機器記憶體區域的大概分佈,讓初學者在腦海中有個大概印象,而印象的開始則藉助於下面的一幅圖:
Java記憶體區域分為大的兩個區域,一部分是執行緒共享的,另一部分則是每個執行緒所獨有的。剛開始瞭解程式設計時,大致就有印象,棧記憶體存在於方法體中,定義的那些變數什麼的都是棧記憶體,方法結束就沒了,堆記憶體則是使用new關鍵字申請出來的。當然這只是粗線的認識,下面一塊塊的說上圖中的記憶體分佈。
程式計數器
類似於CPU中的PC暫存器,用於存放下一條指令的地址,但是虛擬機器不使用CPU的程式計數器,而是自己在記憶體裡設立一片區域模擬CPU的程式計數器。改變計數器的值來選取下一條需要執行的位元組碼指令,包括分支、迴圈、跳轉、異常、執行緒恢復等基礎功能都依賴於計數器。
Java的每個執行緒都有其獨立的計數器,計數器之間互不影響,這樣當多執行緒操作時,一個掛起的執行緒在恢復時,仍然能夠從計數器中恢復之前執行到的地方,繼續執行。所以說程式計數器也是執行緒隔離的。執行Java方法時,計數器中存放的是虛擬機器位元組碼的地址,而執行Native方法時則是空(undefined)。該區域不會產生OutOfMemoryError。
虛擬機器棧
Java虛擬機器棧也是執行緒私有的,它的生命週期等同於執行緒的生命週期。虛擬機器棧描述的是Java方法執行時的記憶體模型。當方法執行時,會建立一個棧幀,用於儲存方法執行期間所用到的資料結構,包含區域性變數表,運算元棧,動態連結,方法出口等資訊。
當一個方法執行時,一個包含以上元素的棧幀入棧,當方法退出時,棧幀出棧。一般我們都會知道記憶體區分為堆區和棧區,實際上也是個粗淺的分法,棧指的就是虛擬機器棧,而其中最重要的部分就是區域性變數表。
Java的垃圾收集器是不會去回收棧上的內容的,因為棧上的內容總是隨著方法的結束自動釋放。區域性變數表包含著各種編譯期已知的基本資料型別、物件引用和returnAddress。基本資料型別就是Java的8大基本資料型別(boolean,byte,char, short, int, float, long, double),物件引用,你可以把它當成指向實際物件地址的指標或者一個代表物件的控制程式碼,returnAddress則是一條位元組碼指令的地址。當進入一個方法時,它所需要分配的空間在編譯期就是已知的了。
在虛擬機器棧中可能會報以下兩種異常:
- StackOverflowError: 執行緒請求的棧深度大於所允許的深度
- OutOfMemoryError: 大多數虛擬機器棧是可以動態擴充套件的,如果無法申請到足夠記憶體,就會丟擲。
本地方法棧
區別於虛擬機器棧執行的是Java方法,本地方法棧則是虛擬機器使用的Native方法服務,我們在看一些庫的原始碼時正常定位到最後就是用Native方法實現的,但是在虛擬機器規範裡對本地方法使用的語言,使用方式進行硬性規定,所以虛擬機器可以任意實現它。HotSpot中本地方法棧和虛擬機器棧是合在一起的。
堆
至少從學習C語言時,我們就聽說malloc方法會在堆上分配空間。Java的堆上也是分配例項物件的。虛擬機器規範上講,基本所有的物件例項和陣列都分配的堆上,例如上面棧上物件引用指向的物件,都是在堆上分配的。但是隨著編譯器技術的發展,所有物件都在堆上分配就不是那麼純粹了。
堆也是垃圾收集(GC)的主陣地。現代收集器基本都採用了分代收集演算法,所以堆也可以劃分為新生代和老年代,再細緻點還有Eden空間,From Survivor空間和To Survivor空間。由於虛擬機器實現了自動垃圾收集,所以在Java中,堆中new出的物件是不需要手動釋放的。
我們可以通過-Xmx
和-Xms
控制堆的預設大小,如果在堆中再也申請不到記憶體,則會丟擲OutOfMemoryError
異常。
方法區
方法區也是各個執行緒間共享的區域,一般儲存已被載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。一般很多人也把方法區稱為永久代。其實僅僅是HotSpot團隊把GC分代收集也擴充套件到方法區了,讓垃圾收集器可以一塊回收方法區的記憶體,但是其它的虛擬機器實現沒有這麼做,也不存在永久代的,HotSpot自身也已經在JDK1.7上移除了永久代中的常量池。
相對而言,垃圾收集在方法區是不怎麼出現的。這個區域主要回收的是常量池和型別的解除安裝,但是實際上對二者的回收發生的條件極為苛刻,很少發生收集。
執行時常量池是方法區的一部分,Class檔案中包含常量池資訊,用於存放編譯期生成的各種字面量和符號引用,這部分在類載入後進入方法區的執行時常量池。這部分可以參考我的:深入理解JVM類檔案格式