Java虛拟機之記憶體區域,今天這篇文章來深入了解一下把
深入了解Java虛拟機之Java記憶體區域
Java 虛拟機在執行 Java 程式的時候會把它所管理的記憶體分為多個不同的區域,每個區域都有不同的作用,以及由各自的生命周期,有些随着虛拟機進行的 啟動而存在,有些區域則依賴于使用者線程的啟動或結束而建立或銷毀等。在《Java虛拟機規範(Java SE7版)》中規定,Java 記憶體分為以下一種,如圖所示:

1、程式計數器
程式計數器(Program Counter Register)是一個記憶體較小的區域,它可以被看作是目前線程所執行到的位元組碼的行号的訓示器,位元組碼解釋器在執行下一條指令,比如分支,跳轉,循環,異常處理等都需要依賴這個計數器來完成的。
由于 Java 多線程是通過不斷的切換配置設定處理器時間片的方式來實作的,在任何一個确定的時刻,一個處理器都隻會執行一條指令。也就是說,在多線程中每個線程都可以搶到 cpu 的時間片,那麼别搶去的線程會立即停止下來,直到它再一次獲得 cpu 的時間片。那麼, Java 虛拟機是如何確定再一次獲得處理器時間片的時候能夠在正确的位置上繼續執行指令?
Java 虛拟機就是通過目前線程的程式計數器保證的,因為儲存了目前線程上次執行結束的位置,是以,程式計數器是每個線程獨有的,各線程之前的程式計數器互不影響,獨立存儲,我們稱這類記憶體區域為“線程私有”的記憶體。
如果線程正在執行 Java 方法,這個計數器記錄的是正在執行的虛拟機位元組碼指令的位址,如果正在執行的是 native 方法,那麼這個計數器值則為空(Undefined);因為程式計數器記錄的是目前線程位元組碼執行指令的位址,是以它的記憶體大小是不會随着線程的執行而發生變化,是以,該記憶體區域是唯一一個在 Java虛拟機規範中沒有規定任何 OutOfMemeryError (記憶體溢出)的情況的區域。
- Java虛拟機棧
Java 虛拟機棧與程式計數器一樣都是線程私有的,它的生命周期與線程一緻,每當建立一個新的線程時都會産生一個新的的虛拟機棧,線程銷毀時虛拟機棧也随即銷毀。每個方法的執行都會建立一個棧幀用于存儲局部變量、操作數棧、動态連結清單,方法的出口等資訊。每個方法的執行到結束就對應着一個棧幀在虛拟機中入棧和出棧的過程。
我們在日常開發的過程的中經常會把 Java 記憶體分為堆記憶體和棧記憶體,這個說法是不準确的,因為 Java 記憶體區域的劃分遠比這個複雜的多,這種劃分的方法隻能說明程式員最關注的,與對象記憶體配置設定關系最密切的記憶體區域這兩塊,這裡所說的棧其實指的是虛拟機中的局部變量表。
局部變量表中存放了編譯期可知的基本資料類型(char,byte,int,boolean,short,float,long,double)、對象的引用(reference類型,指的是對象位址的引用指針或是句柄)、returnAddress類型(指向位元組碼指令的位址)。
局部變量表所需的記憶體空間在編譯期完成配置設定,當進入一個方法時,這個方法所需要的棧幀的大小就已經确定了,在運作期間不會改變局部變量表的大小。其中64位長度的 long 和 double 類型的資料會占用兩個局部變量空間(Slot),其餘的資料類型隻能占用1個。
在 Java虛拟機規範中,對虛拟機棧定義了兩種異常:第一,如果線程請求的深度大于虛拟機棧所允許的最大深度,将抛出 StackoverflowError 異常,第二,如果虛拟機棧可以動态的擴充,當擴充時無法申請到足夠的記憶體,則會抛出 OutOfMemoryError 異常。
2.1、運作時棧幀
棧幀是 Java 虛拟機用來進行方法的調用和方法的指定的資料結構,它是虛拟機運作時資料庫的 Java 虛拟機棧的棧元素。
每一個棧幀都包含局部變量表、操作數棧、動态連結、方法傳回位址和一些額外的附加資訊。
局部變量:
方法的參數和方法中聲明的局部變量都存儲在局部變量中。在 Class 編譯的時候就已經确定了的局部變量表的最大容量,聲明在方法的 Code 屬性的max_locals 資料項中。變量槽(Slot)是局部變量表的最小機關,可以存儲32位和64位的資料,64位的資料則需要兩個連續的變量槽來表示。
操作數棧:也成為操作棧,是一個後進先出的結構。方法執行過程中的算術運算或者調用其它方法的參數傳遞的時候都是在操作數棧中進行的。
動态連結:Class 檔案中存放了大量的符号引用,位元組碼中的方法調用指令就是以常量池中指向方法的符号引用作為參數。這些符号引用一部分會類第一次加載或第一次引用的時候轉化為直接引用,這種轉化稱為靜态解析。另一部分将在每一次運作期轉化為直接引用,這部分稱為動态連接配接。
傳回位址:當一個方法執行完成後的出口,有兩個情況:一是正常情況退出,會将傳回值傳遞給上一個方法的調用者,另一種是異常情況,此時是沒有傳回值的。
3.本地方法棧
本地方法棧和 Java 虛拟機棧的作用是差不多的,他們的唯一差別在于: Java 虛拟機棧為虛拟機執行 Java 方法(位元組碼)服務,而本地方法棧則為虛拟機中 Native 方法服務。在虛拟機規範中并沒有明确的規定本地方法棧使用何種語言與資料結構,可有具體的虛拟機去實作它。本地方法棧和 Java 虛拟機棧一樣,也會抛出 StackoverflowError 和 OutOfMemoryError 兩個異常。
4.Java堆
Java 堆是 Java 記憶體管理區域中最大的一塊,Java 堆是所有線程共享的一塊記憶體區域,在虛拟機啟動時建立。 Java 堆是用來存放對象,幾乎所有的 Java 對象和數組都存放在Java堆中,都在堆中配置設定空間。
Java 堆是立即回收器管理的主要區域,是以有人也會把 Java 堆稱為“GC堆”。從垃圾回收的角度來看,由于現在大多數的收集器都是采用分代收集算法,是以 Java 堆還可以細分為:新生代和老年代;再細緻一點有可以分為Eden 空間, From Survivor 空間、To Survivor 空間等;從記憶體配置設定的角度來看,線程共享的java堆中可能劃分為多個線程私有的配置設定緩沖區(TLAB)。
這樣配置設定是為了更好的回收記憶體和建立記憶體等,與存放的區域無關,都是存放 Java 對象。根據Java虛拟機規範的規定,Java 堆可以是實體上不連續的記憶體空間,隻要邏輯上連續就可以了。在實作時可以設定固定大小的空,也可以動态擴充的,不過目前流程的虛拟機都是可以動态來擴充的。如果在堆中沒有足夠的空間來配置設定執行個體對象,或者無法擴充堆空間,那麼就會抛出 OutOfMemoryError 異常。
5.方法區
方法區和 Java 堆一樣,都是線程共享的記憶體區域,它主要用于存儲已被虛拟機加載的類資訊,常量,靜态變量,即時編譯器編譯後的代碼等資料。雖然 Java 虛拟機規範把方法區描述為 Java 堆的一部分,但是它卻有一個别名叫做 Non-Heap(非堆),目的是與 Java 堆分開。
Java 虛拟機規範對方法區的限制非常的寬松,除了和 Java 堆一樣不需要實體上連續的記憶體和可以選擇固定大小空間或動态擴充為,還可以選擇不實作垃圾回收。相對而言,垃圾回收是比較少在這個區域中出現的,但并非進入到方法區的資料都能永久的存在的。這塊區域的的記憶體回收目标主要是針對常量池的回收和對類型的解除安裝,但是回收的效果并不是非常明顯,特别是對類型的解除安裝,條件非常的苛刻,但這塊記憶體的回收是非常必要的。在 SUN 公司的 BUG清單中,曾經出現過若幹個非常嚴重的 BUG ,就是由于低版本中 Hotspot 虛拟機沒有對此記憶體區域進行回收造成的。
根據 Java 虛拟機規範的規定,當方法區無法滿足記憶體配置設定的需求時,将抛出 OutOfMemoryError 異常。
6.運作時常量池
運作時常量池是屬于方法區的一部分。 Class 檔案中除了有類的版本、字段、方法、接口等描述資訊外,還有一項資訊是常量池,用于存放編譯期生成的各種字面量和符号引用,這部分的内容将在類加載完成 後進入到方法區的運作時常量池中存放。
運作時常量池相對于 Class 檔案常量池的另一個特征是具備動态性,Java 語言并不要求常量一定隻有編譯期才能産生,也就是并非預置入 Class 檔案中的常量池的内容才可以進入方法區的運作時常量池,運作期間也可以将新的常量放入到池中,比如 String 類的 intern 方法。
既然運作時常量池是存在于方法區中的,那麼在無法配置設定到足夠的記憶體是也會抛出 OutOfMemoryError 異常。