架構師的必經之路 深入淺出JVM虛擬機

運行時數據區域

程序計數器(Programn Counter Register)

程序計數器是一塊較小的內存空間,它可以看做是當前線程所執行的字節碼的行號指示器。

由於 Java 虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個內核)都只會執行一條線程中的指令。因此,為了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各個線程之間的計數器互不影響,獨立存儲,我們稱這類內存區域為“線程私有”的內存。

如果線程正在執行的是一個 Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是 Native 方法,這個計數器值則為空(Undefined)。此內存區域是唯一一個在 Java 虛擬機規範中沒有 規定任何 OutOfMemoryError 情況的區域。

Java 虛擬機棧

與程序計數器一樣,Java 虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同。

虛擬機棧描述的是 Java 方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀(Stack Frame)用於存儲局部變量表、操作數棧、動態鏈接、方法出口的信息。每一個方法從調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。

局部變量表存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型,它不等同於對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)和 returnAddress 類型(指向了一條字節碼指令的地址)。

其中 64 位長度的 long 和 double 類型的數據會佔用 2 個局部變量空間(Slot),其餘的數據類型只佔用 1 個。局部變量表所需的內存空間在編譯期完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。

在 Java 虛擬機規範中,對這個區域規定了兩種異常狀況:如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;如果虛擬機棧可以動態擴展(當前大部分的 Java 虛擬機都可動態擴展,只不過 Java 虛擬機規範中也允許固定長度的虛擬機棧),如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常。

本地方法棧

本地方法棧(Native Method Stack)與虛擬機棧所發揮的作用是非常相似的,他們之間的區別不過是虛擬機棧為虛擬機執行 Java 方法(也就是字節碼)服務,而本地方法棧則為虛擬機使用到的 Native 方法服務。

在虛擬機規範中對本地方法棧中方法使用的語言、使用方法與數據結構並沒有強制規定,因此具體的虛擬機可以自由實現它。甚至有的虛擬機(譬如 Sun HotSpot 虛擬機)直接把本地方法棧和虛擬機棧合二為一。

與虛擬機棧一樣,本地方法棧區域也會拋出 StackOverflowError 和 OutOfMemoryError 異常。

Java 堆

對於大多數應用來說,Java 堆(Java Heap)是 Java 虛擬機所管理的內存中最大的一塊。

Java 堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這裡分配內存。這一點在 Java 虛擬機規範中的描述是:所有的對象實例以及數組都要在堆上分配,但是隨著 JIT 編譯器的發展與逃逸分析計數逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的對象都分配在堆上也漸漸變得不是那麼“絕對”了。

Java 堆是垃圾收集器管理的主要區域,因此很多時候也被稱作“GC 堆”(Garbage Collected Heap)。

根據 Java 虛擬機規範的規定,Java 堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可,就像我們的磁盤空間一樣。在實現時,既可以實現成固定大小,也可以是可擴展的,不過當前主流的虛擬機都是按照可擴展來實現的(通過 -Xmx 和 -Xms 控制)。如果在堆中沒有內存完成實例分配,並且堆也無法再擴展時,將會拋出 OutOfMemoryError 異常。

方法區

方法區(Method Area)與 Java 堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。

雖然 Java 虛擬機規範把方法區面數為堆的一個邏輯部分,但是它卻有一個別名叫 Non-Heap(非堆),目的應該是與 Java 堆區分開來。

對於習慣在 HotSpot 虛擬機上開發、部署程序的開發者來說,很多人都更願意把方法區稱為“永久代”(Permanent Generation),本質上兩者並不等價,僅僅是因為 HotSpot 虛擬機的設計團隊選擇把 GC 分代收集擴展至方法區,或者說使用永久代來實現方法區而已,這樣 HotSpot 的垃圾收集器可以像管理 Java 堆一樣管理這部分內存,能夠省去專門為方法區編寫內存管理代碼的工作。對於其他虛擬機(如 BEA JRockit、IBM J9 等)來說是不存在永久代的概念的。

對於 HotSpot 虛擬機,根據官方發佈的路線圖信息,現在也有放棄永久代並逐步改為採用 Native Memory 來實現方法區的規劃了,在目前已經發布的 JDK1.7 的 HotSpot 中,已經把原本放在永久代的字符串常量池移出。

根據 Java 虛擬機規範的規定,當方法區無法滿足內存分配需求時,將拋出 OutOfMemoryError 異常。

運行時常量池

運行時常量池(Runtime Constant Pool)是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。

Java 虛擬機對 Class 文件每一部分(自然也包括常量池)的格式都有嚴格規定,每一個字節用於存儲哪種數據都必須符合規範的要求才會被虛擬機認可、裝載和執行,但對於運行時常量池,Java 虛擬機規範沒有做任何細節的要求,不同的提供商實現的虛擬機可以按照自己的需要來實現這個內存區域。

運行時常量池相對於 Class 文件常量池的另外一個重要特徵是具備動態性,Java 語言並不要求常量一定只有編譯期才能產生,也就是並非預置如 Class 文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的便是 String 類的 intern() 方法。

既然運行時常量池是方法區的一部分,自然受到方法區內存的限制,當常量池無法再申請到內存時會拋出 OutOfMemoryError 異常。

直接內存

直接內存(Direct Memory)並不是虛擬機運行時數據區的一部分,也不是 Java 虛擬機規範中定義的內存區域。但是這部分內存也被頻繁地使用,而且也可能導致 OutOfMemoryError 異常出現。

在 JDK 1.4 中新加入了 NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的 I/O 方式,它可以使用 Native 函數庫直接分配堆外內存,然後通過一個存儲在 Java堆中的 DirectByteBuffer 對象作為這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在 Java 堆和 Native 堆中來回複製數據。

顯然,本機直接內存的分配不會受到 Java 堆大小的限制,但是,既然是內存,肯定還是會受到本機總內存(包括 RAM 以及 SWAP 區或者分頁文件)大小以及處理器尋址空間的限制。

服務器管理員在配置虛擬機參數時,會根據實際內存設置 -Xms 等參數信息,但經常忽略直接內存,使得各個內存區域總和大於物理內存限制(包括物理的和操作系統級的限制),從而導致動態擴展時出現 OutOfMemoryError 異常。

對象的內存佈局

在 HotSpot 虛擬機中,對象在內存中存儲的佈局可以分為 3 塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。

實例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄下來。這部分的存儲順序會受到虛擬機分配策略參數(FieldsAllocationStyle)和字段在Java源代碼中定義順序的影響。

HotSpot 虛擬機默認的分配策略為 longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),從分配策略中可以看出,相同寬度的字段總是被分配到一起。在滿足這個前提條件的情況下,在父類中定義的變量會出現在子類之前。如果 CompactFileds 參數值為 true(默認為 true),那麼子類之中較窄的變量也可能會插入到父類的空隙之中。

由於 HotSpot VM 的自動內存管理系統要求對象起始地址必須是 8 字節的整數倍,換句話說,就是對象的大小必須是 8 字節的整數倍。而對象頭部分正好是 8 字節的倍數(1 倍或者 2 倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。

對象的訪問定位

建立對象是為了使用對象,我們的 Java 程序需要通過棧上的 reference 數據來操作堆上的具體對象。由於 reference 類型在 Java 虛擬機規範中值規定了一個指向對象的引用,並沒有定義這個引用應該通過何種方式去定位、訪問堆中的對象的具體位置,所以對象訪問方式也是取決於虛擬機實現而定的。

目前主流的訪問方式有使用句柄和直接指針兩種。

架構師的必經之路 深入淺出JVM虛擬機

架構師的必經之路 深入淺出JVM虛擬機

架構師的必經之路 深入淺出JVM虛擬機

架構師的必經之路 深入淺出JVM虛擬機

這兩種對象訪問方式各有優勢,使用句柄來訪問的最大好處就是 reference 中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而 reference 本身不需要修改。

使用直接指針訪問方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷,由於對象的訪問在 Java 中非常頻繁,因此這類開銷積少成多後也是一項非常可觀的執行成本。

就 Sun HotSpot 而言, 它是使用第二種方式進行對象訪問(直接指針訪問),但從整個軟件開發的範圍來看,各種語言和框架使用句柄來訪問的情況也十分常見。

1、具有1-5工作經驗的,面對目前流行的技術不知從何下手,需要突破技術瓶頸的可以加群。

2、在公司待久了,過得很安逸,但跳槽時面試碰壁。需要在短時間內進修、跳槽拿高薪的可以加群。

3、如果沒有工作經驗,但基礎非常紮實,對java工作機制,常用設計思想,常用java開發框架掌握熟練的,可以加群。

4、覺得自己很牛B,一般需求都能搞定。但是所學的知識點沒有系統化,很難在技術領域繼續突破的可以加群。

5.群號:810309655 Java技術交流

6.阿里Java高級大牛直播講解知識點,分享知識,上面五大專題都是各位老師多年工作經驗的梳理和總結,帶著大家全面、科學地建立自己的技術體系和技術認知!

每天晚上8:00都有免費的課程分享,分享地址是:授課地址:https://ke.qq.com/course/260263?enter_room=1&flowToken=1000658#tuin=122f4214


分享到:


相關文章: