內存模型
內存模型如下圖所示
堆
堆是Java虛擬機所管理的內存最大一塊。堆是所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域唯一的目的就是存放對象實例。所有的對象實例都在這里分配內存
Java堆是垃圾收集器管理的主要區域。從內存回收的角度來看,由於現在的垃圾收集器采用的是分代收集算法。所以,java堆又分為新生代
和老年代
。從內存分配的角度來說,線程共享的java對中可能划分出多個線程私有的fenp緩沖區(Thread Local Allocation Buffer)。
可以通過 -Xms
、-Xmx
分別控制堆初始化是最小堆內存和最大堆內存大小。
虛擬機棧
與程序計數器一樣,java虛擬機棧也是線程私有的,他的生命周期與線程相同。
虛擬機棧描述的是Java方法的執行的內存模型:每個方法在執行的同時會創建一個棧楨(stack frame)
用於存儲局部變量表、操作數棧、動態鏈表、方法出口等信息
。每個方法從調用直至執行完成的過程,就對應着棧楨在虛擬機棧中入棧到出棧的過程。
虛擬機棧存儲的數據類型
- 局部變量表
存放的是編譯器可知得到各種基本數據類型boolean、byte、char、short、int、float、long、double、對象引用(refrence類型,不等同於對象本身,一個指向對象的起始內存位置的引用指針)
- 操作數棧
- 動態鏈表
- 方法出口
- ...
常見異常
在虛擬機規范中,對這個區域規定了兩種異常情況:
- 如果線程請求的棧深度大於虛擬機所允許的深度,將拋出
StackOverflowError
- 如果虛擬機棧可以動態擴展,擴展時無法申請做夠的內存,將會爬出
OutOfMemorryError
本地方法棧
與虛擬機棧發揮的作用非常類似,他們之間的區別是虛擬機棧為虛擬機執行java方法服務,而本地方法棧則為虛擬機使用到的native
方法服務。與虛擬機棧一樣,本地房發展區域也會拋出StackOverflowError
,OutOfMemorryError
異常。
方法區(1.8后該區域被廢棄)
方法區與java堆一樣,是各個線程所共享的,它用來存儲已被虛擬機加載的類信息
、常量
、靜態變量
、即時編譯后的代碼
等數據。
方法區是jvm提出的規范,而永久代
就是方法區的具體實現。
java虛擬機對方法區的限制非常寬松,可以像堆一樣不需要連續的內存可可選擇的固定大小外,還可以選擇不識閑垃圾收集,相對而言,垃圾收集行為在這邊區域是比較少出現的。
在方法區會報出 永久代內存溢出的錯誤。而java1.8為了解決這個問題,就提出了meta space(元空間)
的概念,就是為了解決永久代內存溢出的情況,一般來說,在不指定 meta space
大小的情況下,虛擬機方法區內存大小就是宿主主機的內存大小
程序計數器
程序計數器是一塊較小的內存空間,他可以看做是當前線程所執行字節碼的行號指示器。在虛擬機的概念模型里,字節碼解釋器工作時就是通過改變這個計數器的值來選擇下一條將要執行的字節碼指令。
由於JAVA虛擬機的多線程是通過多線程流轉切換並分配處理器執行時間的方式來實現的。在任一一個確定的時刻,一個處理器都只會執行一條線程中的指令。因此,為了線程切換后能恢復到正確的執行位置,每條線程都需要一個獨立的程序計數器,各個線程的計數器之間互不影響,獨立存儲,我們稱該類內存區域為線程私有
如果線程正在執行一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址。
運行時常量池
運行時常量池是方法區的一部分。Class文件除了 有類的版本、字段、方法、接口等描述信息外,還有一項是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容在類加載后進入方法區的運行時常量池。
運行時常量池相對於Class文件常量池的另外一個重要特征是具備動態性
.Java語言並不要求常量一定只有在編譯器才能產生,依舊是並非預置入Class文件中的常量池的內容才能進入方法區運行時常量池
對象創建
在語言層面上,創建對象(克隆,反序列化)通常只是一個new
關鍵字。
過程
虛擬機在遇到一條new
指令時,首先去檢查這個指令的參數是否能在常量池中定位到這個類的符號引用並且減產這個符號引用代表的類是否已經被加載、解析、和初始化國。如果沒有,那必須執行相應的類加載過程。
在類加載檢查通過后,接下來虛擬機將為新生的對象分配內存。對象所需內存的大小在類加載完成后便可完全確定。
內存分配
為對象分配空間的任務等同於把一塊確定大小的內存從java堆中划分出來。
假設java堆中內存是絕對規整的,所有用過的內存都放在一邊,空閑的放在另外一邊,中間放着一個指針最為分界點的指示器,那所分配內存就僅僅是把那個指針向空閑的空間那邊挪動一段與對象大小相等的距離,這種分配方式稱為指針碰撞
。
如果內存不是規整的,已使用的和空閑的內存區域是相互交錯的,虛擬機必須維護一個列表,記錄哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間划分給對象實例,並更新這個列表。這種分配方式是空閑列表
。
選擇哪種分配分配方式是由java堆是否規整來決定的,而java堆是否規整又是由其所采用 的垃圾收集器是否帶有壓縮規整的功能決定,因此,使用Serial
、ParNew
等帶來Compat過程的收集器時,分配算法是指針碰撞。而使用CMS
這種基於Mark-Swaeep
算法,采用的是空閑列表分配方式。
對象的內存布局
在HotSpot虛擬機中,對象在內存中的存儲布局分為3塊區域:對象頭(Header)
、實例數據(Instance Data)
、對其補充(Padding)
對象頭(Heading)
對象頭包括兩部分信息
- 用於存儲對象自身運行時數據。
如哈希碼,GC分代年齡、鎖狀態標志、偏向線程ID。這部分s數據的長度在32位和64位的虛擬機中分別為32bit和64bit。
對象的訪問定位
創建對象時為了使用對象,java程序需要通過棧上的refrence數據來操作堆上的具體對象
。由於refrence類在java虛擬機值規定了一個指向對象的引用,並沒有定義這個引用應該通過何種方式去定位、訪問堆中的具體位置,所以對象訪問方式也取決於虛擬機對這個refrence的具體實現,目前主流的訪問凡是是使用句柄
、直接指針
兩種。
句柄訪問
如果使用句柄訪問的話,那么java堆中就會划分出一塊內存來座位句柄池,refrence中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據和類型數據的各自具體的地址信息。
指針直接訪問
如果使用指針直接訪問,那么java堆對象的布局就必須考慮如何放置訪問類型數據的相關信息,而refrence中存儲的直接就是對象地址
這個兩種訪問方式各有優勢,使用句柄訪問的最大好處就是refrence中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時)只會改變句柄中對象實例指針,refrence本省不需要修改。
使用直接指針訪問的方式最大的好處就是速度更快,節省了一次指針定位的事件開銷。由於對象的訪問在java中非常頻繁,一次這類開銷積少成多也是一個比較客觀的優化。