參考文章:
- 《Java Se11 虛擬機規范》
- 《深入理解Java虛擬機-JVM高級特性與最佳實踐 第3版》- 周志明
本文基于Java Se 11講解,
根據《Java虛擬機規范》的規定,Java虛擬機所管理的記憶體將會包括以下幾個運行時資料區域:

對于不同的虛擬機實作,在運行時資料區的實作上并不完全相同,對于常用的HotSpot虛擬機來說,它的運行時資料區如下:

主要區別在于,HotSpot使用了直接使用本地記憶體(即機器本身記憶體)的元空間(metaspace)來實作方法區,
下面針對每個具體的資料區域進行詳細的介紹,
1. 程式計數器
程式計數器(Program Counter Register)是一塊較小的記憶體空間,它可以看作是當前執行緒所執行的位元組碼的行號指示器,
JVM可以同時支持多個執行執行緒,每個Java虛擬機執行緒都有自己的pc(程式計數器)暫存器,在任何時候,每個Java虛擬機執行緒都在執行單個方法的代碼,即該執行緒的當前方法,如果該方法不是native方法,則pc暫存器包含當前正在執行的Java虛擬機指令的地址,如果執行緒當前正在執行的方法是native的,則pc暫存器的值為undefined,Java虛擬機的pc暫存器足夠寬,可以容納特定平臺上的returnAddress或native指標,
此記憶體區域是唯一一個在《Java虛擬機規范》中沒有規定任何OutOfMemoryError情況的區域,
2. Java虛擬機堆疊
與程式計數器一樣,是執行緒私有的,生命周期與執行緒相同,虛擬機堆疊描述的是Java方法執行的執行緒記憶體模型,
「虛擬機堆疊」里面的每條資料就是「堆疊幀」,在 Java 方法執行的時候則創建一個「堆疊幀」并入堆疊「虛擬機堆疊」,呼叫結束則「堆疊幀」出堆疊,
每個堆疊幀包含四個區域:
- 區域變數表:存盤了方法執行程序中需要用到的所有區域變數
- 運算元堆疊:暫存變數,通過變數的入堆疊、出堆疊等操作來執行計算
- 動態連接:翻譯符號參考為直接參考,即把一個字面量翻譯為運行時的一個地址參考
- 回傳地址
每個執行緒擁有一個「虛擬機堆疊」,每個「虛擬機堆疊」擁有多個「堆疊幀」,而堆疊幀則對應著一個方法,每個「堆疊幀」包含區域變數表、運算元堆疊、動態鏈接、方法回傳地址,方法運行結束則意味著該「堆疊幀」出堆疊,
在《Java虛擬機規范》中,對這個記憶體區域規定了兩類例外狀況:
- 如果執行緒請求的堆疊深度大于虛擬機所允許的深度,將拋出
StackOverflowError例外; - 如果Java虛擬機堆疊容量可以動態擴展(HotSpot虛擬機的堆疊容量不能動態擴展),當堆疊嘗試擴展時無法申請到足夠的記憶體,或為一個新執行緒初始化JVM堆疊時沒有足夠的記憶體時會拋出
OutOfMemoryError例外,
3. 本地方法堆疊
本地方法堆疊(Native Method Stacks)與虛擬機堆疊所發揮的作用是非常相似的,其區別只是虛擬機堆疊為虛擬機執行Java方法(也就是位元組碼)服務,而本地方法堆疊則是為虛擬機使用到的本地(Native)方法服務,
《Java虛擬機規范》對本地方法堆疊中方法使用的語言、使用方式與資料結構并沒有任何強制規定,因此具體的虛擬機可以根據需要自由實作它,甚至有的Java虛擬機(譬如Hot-Spot虛擬機)直接就把本地方法堆疊和虛擬機堆疊合二為一,與虛擬機堆疊一樣,本地方法堆疊也會在堆疊深度溢位或者堆疊擴展失敗時分別拋出StackOverflowError和OutOfMemoryError例外,
4. Java堆
所有執行緒共享,虛擬機啟動時創建,唯一的目的是用于存放物件實體和陣列,絕大部分物件實體在堆上分配記憶體,
在 Java 中,陣列也是物件,
現代垃圾收集器大部分基于分代收集理論設計,“新生代”、“老年代”這些名詞僅僅是一部分GC的設計風格,而不是《Java虛擬機規范》定義的,而從G1收集器出現之后,出現了不采用分代設計的新垃圾收集器,
JDK8之后Class物件、static變數、字串常量池都放在堆里,
static變數作為類的資訊,存盤在Class物件里,
Java 的物件可以分為基本資料型別和普通物件,普通物件會在堆上分配,對于基本資料型別,如果是區域變數,則會在堆疊上分配,其他情況,通常在在堆上分配,逃逸分析的情況下可能會在堆疊分配,
如果在Java堆中沒有記憶體完成實體分配,并且堆也無法再擴展時,Java虛擬機將會拋出OutOfMemoryError例外,
4.1 字串常量池
字串常量池是由String類維護的一個字串池,是一種池化思想的實作,是為了節省重復創建字串物件的性能開銷和記憶體空間,
每當代碼創建字串常量時,JVM會首先檢查字串常量池,如果字串已經存在池中,就回傳池中的實體參考,如果字串不在池中,就會實體化一個字串并放到池中,Java能夠進行這樣的優化是因為字串是不可變的,可以不用擔心資料沖突進行共享,
字串常量池從JDK7開始挪到了堆中,
可以通過呼叫String.intern()方法把一個字串物件放到字串常量池中,如果池中已經存在相等的物件,則會回傳已存在物件的參考;否則會把這個字串物件加入到池中,并回傳新加入的字串物件的參考,
String s = new String("hello")會創建幾個物件?
如果字串常量池中沒有"hello",則生成2個,否則只生成一個,
String s = new String("abc"); System.out.println((s.intern() == s));列印結果是什么?
列印結果為false,s指向的是堆中的物件,s.intern()回傳的是字串常量池中的物件的參考,
4.2 字面量和常量
字面量(literal) :用于表達原始碼中的一個固定值的符號(notation),如整數、浮點數及字串等,如1、0x01是整數字面量,Hello World是字串字面量,
常量:在java中,final修飾的變數也可以被稱為是常量,任何具有不變性的東西都可以稱為常量,如String物件是常量,
物件池:是Java語言層面實作的,如Integer.valueOf()(Integer i = 10也會調該方法)會使用IntegerCache的快取物件,如果使用new Integer(10)則不會使用物件池中的實體,
字串常量池:類似于物件池,但它是JVM層面的技術,字串常量池的實作是c++實作的StringTable,實際上是一個固定容量的Hashtable,每一個bucket包含一系列相同hash碼的字串,
5. 方法區
用于存盤被JVM加載的class的元資料資訊,比如類的結構、運行時的常量池、欄位、常量、方法資料、方法建構式以及介面初始化等特殊方法,還有JIT編譯器編譯后的代碼快取等資料,
JDK8之前,HotSpot采用永久代的概念實作方法區,JDK8開始廢棄了永久代的概念,改用在本地記憶體(Native Memory)中實作的元空間(Meta-space)來代替,
方法區的GC比較少出現,回收目標主要是針對常量池的回收和對型別的卸載,
根據《Java虛擬機規范》的規定,如果方法區無法滿足新的記憶體分配需求時,將拋出OutOfMemoryError例外,
5.1 運行時常量池
運行時常量池(Runtime Constant Pool)是方法區的一部分,Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池表(Constant Pool Table),用于存放編譯期生成的各種字面量與符號參考,這部分內容將在類加載后存放到方法區的運行時常量池中,
一般來說,除了保存Class檔案中描述的符號參考外,還會把由符號參考翻譯出來的直接參考也存盤在運行時常量池中,
既然運行時常量池是方法區的一部分,自然受到方法區記憶體的限制,當常量池無法再申請到記憶體時會拋出OutOfMemoryError例外,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/530540.html
標籤:其他
