1. Java運行時數據區域
Java虛擬機在執行Java程序的過程中會把它所管理的內存劃分為若干個不同的數據區域。根據虛擬機規范規定,主要分為以下幾個運行時數據區域:
- 程序計數器
- 虛擬機棧
- 本地方法棧
- 堆
- 方法區
其中方法區和堆是所有線程共享的數據區,其他幾個則是線程隔離的數據區。
1.1 程序計數器
程序計數器是一塊較小的內存空間,它可以看作是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型里,字節碼解釋器工作時就是通過這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等都需要依賴這個計數器來完成。
由于Java虛擬機的多線程是通過線程輪流切換并分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對于多核來說是一個內核)只會執行一條線程中的指令。因此,為了確保線程切換后能恢復到正確的執行位置,每個線程都需要有一個獨立的程序計數器,各個線程之間的計數器互不影響,獨立存儲,我們稱這類內存區域為"線程私有"的內存。
如果線程正在執行的是一個Java方法,這個計數器紀錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行Native方法,這個計數器值則為空。此內存區域是唯一一個在Java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域。
1.2 Java虛擬機棧
與程序計數器一樣,Java虛擬機棧也是線程私有,它的生命周期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法執行的時候都會創建一個棧幀(Stack Frame)用于存儲局部變量表、操作棧、動態鏈接、方法出口等信息。每一個方法被調用直到被執行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。
局部變量表存放了編譯期可知的各種數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它不等同于對象本身,根據不同的虛擬機實現)和returnAddress類型(指向了下一條字節碼指令的地址)。
其中64位的long和double類型的數據會占用2個局部變量空間,其余的數據類型只占據一個。局部變量表所需要的內存空間在編譯器間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量的大小。在Java虛擬機規范中,對這個區域規定了兩種異常:如果線程請求的棧深度大于虛擬機所允許的深度,則拋出StackOverflowError異常;如果虛擬機棧可以動態擴展,當擴展無法申請到足夠的內存時會拋出OutOfMemoryError異常。
1.3 本地方法棧
本地方法棧與虛擬機棧所發揮的作用是非常相似的,區別是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧為虛擬機使用到的Native方法服務。本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。
1.4 Java堆
對于大多數應用來說,Java堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊。Java堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。此區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配。
Java堆是垃圾回收器管理的主要區域。
根據Java虛擬機的規范,Java堆可以處理物理上不連續的內存空間中,只有邏輯上是連續的就可以。如果在堆中沒有內存完成實例分配,并且堆也無法擴展時,將會拋出OutOfMemoryError異常。
1.5 方法區
方法區與Java堆一樣,是各個線程共享的內存區域,它用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。
根據虛擬機規范,方法區如果無法滿足內存分配,將會產生OutOfMemoryError異常。
1.6 運行時常量池
運行時常量池是方法區的一部分。自然也會受到方法區的內存限制,當常量池無法再申請到內存時會拋出OutOfMemoryError異常。
運行時常量池用于存放編譯期產生的各種字面量和符號引用。
1.7 直接內存
直接內存并不是虛擬機運行時數據區的一部分,也不是Java虛擬機規范中定義的內存區域,但是這部分內存也被頻繁地使用,而且也可能導致OutOfMemoryError異常出現。
2. 對象訪問
以Object obj = new Object()
來講:
Object obj
將會反應到Java虛擬機棧的本地變量表中,作為一個reference類型數據出現。
new Object()
將會反應到Java堆中,形成了一塊存儲了Object類型所有實例數據值(Instance Data,對象中各個實例字段的數據)的結構化內存。另外Java堆中還必須包含能查到此對象類型數據(如對象類型、父類、實現的接口、方法等)的地址信息,這些類型數據則存在方法區。
主流的對象訪問方式有兩種:使用句柄和直接指針。
- 使用句柄
Java堆中將會劃分出來一塊內存來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中則包含了對象實例數據和類型數據各自的具體地址信息。 - 直接指針
reference中直接存儲的就是對象地址(即直接指向Java堆中的對象實例),對象實例的對象頭存放對象類型指針。
這兩種對象的訪問方式各有優勢,使用句柄訪問的最大好處就是reference中存儲的是穩定的句柄地址,在對象被移動(GC回收會移動)時只會改變句柄中的示例數據指針,而reference本身不需要被修改。
使用直接指針訪問方式最大好處就是速度更快,節省了一次指針定位的時間開銷。