1.簡述
對于Java程式員來說,在虛拟機自動記憶體管理機制下,不需要關心記憶體的回收問題,但是一旦出現記憶體洩漏和溢出問題,如果不了解虛拟機是怎麼管理記憶體的,那麼問題排查工作将是一項非常艱巨的任務。本文主要從概念層面介紹虛拟機記憶體的各個區域,以及對象建立時記憶體如何配置設定。
2.運作時資料區域
Java虛拟機在執行Java程式的過程中會把它所管理的記憶體劃分為若幹個不同的區域。這些區域都有不同的用途,有的區域随着虛拟機程序的啟動而存在,有些區域則依賴于使用者線程的啟動和結束而建立和銷毀。根據Java虛拟機規範的規定,Java虛拟機所管理的記憶體包含以下幾個運作時區域
2.1.程式計數器
程式計數器(Program Counter Register)是一塊較小的記憶體空間,它可以看作是目前線程所執行的位元組碼的行号訓示器,在虛拟機概念模型裡,位元組碼解釋器工作時就是通過改變這個計數器的值來選取下一跳需要執行的位元組碼指令,分支、循環、跳轉、異常處理、線程恢複等基礎功能都需要依賴這個計數器來完成。
由于java虛拟機的多線程是通過線程輪流切換并配置設定處理器時間的方式來實作的,在任何一個确定的時刻,一個處理器都會執行一條線程的指令。是以,為了線程切換後能夠恢複到正确的執行位置,每條線程都需要一個獨立的程式計數器,線程之間的計數器互不影響,獨立存儲,我們稱這類記憶體區域為【線程私有】的記憶體。
如果線程正在執行一個java方法,這個計數器記錄的是正在執行的虛拟機位元組碼指令的位址;如果正在執行的是Native方法,這個計數器的值則為空(Undefined)。此記憶體區域是唯一一個在java虛拟機規範中沒有規定任何OutOfMemoryError情況的區域。
2.2.Java虛拟機棧
與程式計數器一樣,java虛拟機棧(java virtual machine stacks)也是線程私有的,它的生命周期與線程相同。虛拟機棧描述的是java方法執行的記憶體模型,每個方法執行的同時都會建立一個棧幀(stack frame)用于存儲局部變量表、操作數棧、動态連結、方法出口等資訊。每一個方法從調用直至執行完成的過程,就對應一個棧幀在虛拟機棧中入棧和出棧的過程。
局部變量表存放了編譯期的各種基本資料類型(boolean、byte、char、short、int、float、long、double)、對象引用和returnAddress類型(指向一條位元組碼指令位址)
64位的long和double會占據2個局部變量空間(slot),其餘類型占據1個。局部變量表所需要的記憶體空間在編譯期間完成配置設定,當進入一個方法時,這個方法需要在幀中配置設定多大的局部變量空間是完全确定的,在方法運作期間不會改變局部變量表的大小。
在Java虛拟機規範中,這個區域規定了兩種異常情況:
- 如果線程請求棧的深度大于虛拟機所允許的深度,将抛出StackOverflowError異常
- 如果虛拟機棧動态擴充時無法申請到足夠的記憶體,就會抛出OutOfMemoryError異常
總結:
關鍵詞 | 描述 |
---|---|
棧幀 | 存儲局部變量表、操作數棧、動态連結、方法出口等資訊 |
方法生命周期 | 方法從調用到完成,對應着棧幀的入棧和出棧 |
異常情況 | 此區域有2中異常情況 |
StackOverflowError異常 | 線程請求棧的深度大于虛拟機所允許的深度 |
OutOfMemoryError異常 | 虛拟機棧動态擴充時無法申請到足夠的記憶體 |
2.3.本地方法棧
本地方法棧(Native Method Stack)與虛拟機棧的作用是非常相似的,他們之間的差別是:
- 虛拟機棧為虛拟機執行方法服務
- 本地方法棧為虛拟機使用本地(Native)方法服務
- 在虛拟機規範中對本地方法棧中方法使用的語言、使用方式與資料結構并沒有強制規定,是以具體的虛拟機可以自由的實作它。
- 與虛拟機棧一樣,本地方法棧也會抛出StackOverflowError和OutOfMemoryError異常。
2.4.Java堆
對于大多數應用來說,Java堆(Java Heap)是Java虛拟機所管理的記憶體中最大的一塊,Java堆是被所有線程共享的一塊記憶體區域,在虛拟機啟動時建立。此記憶體區域唯一的目的是存放對象執行個體,幾乎所有的對象執行個體都在這裡配置設定。
Java虛拟機規範中的描述是:所有的對象執行個體以及數組都在堆上配置設定,但是随着JIT編譯器的發展和逃逸分析技術逐漸成熟,棧上配置設定、标量替換優化技術将會導緻一些微妙的變化,所有對象都配置設定在堆上也漸漸變得不是那麼絕對。
總結:
關鍵詞 | 描述 |
---|---|
Java堆作用 | 存放對象執行個體和數組 |
線程共享 | 所有線程共享的一塊記憶體區域 |
垃圾收集器 | java堆是垃圾收集器管理的主要區域 |
OutOfMemoryError | 如果堆中沒有記憶體完成執行個體配置設定,并且堆無法再擴充時,将會抛出記憶體溢出 |
2.5.方法區
方法區(Method Area)與Java堆一樣,是所有線程共享的記憶體區域,它用于存儲已被虛拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料。雖然Java虛拟機規範把方法區描述為堆的一個邏輯部分,但是它卻有一個别名叫做Non-Heap(非堆),目的應該是與Java堆區分開。
關鍵詞 | 描述 |
---|---|
Java方法區作用 | 存儲已被虛拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼 |
線程共享 | 所有線程共享 |
OutOfMemoryError | 當方法區無法滿足記憶體配置設定需求時,将抛出記憶體溢出異常 |
2.6.運作時常量池
運作時常量池(Runtime Constant Pool)是方法區的一部分。Class檔案中除了有類的版本、字段、方法、接口等描述資訊外,還有一項就是常量池(Constant Pool Table)用于存放編譯器生成的各種字面量和符号引用,這部分内容将在類加載後進入方法區的運作時常量池存放。
運作時常量池相對于Class檔案常量池的另外一個重要特征是具備動态性,java語言并不要求常量一定隻有編譯器才能産生,也就是并非預置入Class檔案常量池的内容才能進入方法區運作時常量池,運作期間也可以将新的常量放入池中,這種特性被開發人員用的比較多的地方是String類的intern()方法.
既然運作時常量是方法區的一部分,自然受到方法區記憶體的限制,當常量池無法再申請到記憶體時會抛出OutOfMemoryError異常。
關鍵詞 | 描述 |
---|---|
作用 | 存放編譯器生成的各種字面量和符号引用 |
歸屬 | Java方法區的一部分 |
特性 | 具備動态擴充的特性 |
OutOfMemoryError | 當常量池無法再申請到記憶體時會抛出OutOfMemoryError異常 |
3.直接記憶體
直接記憶體并不是虛拟機運作時資料區的一部分,也不是java虛拟機規範中定義的記憶體區域。但是這部分記憶體也被頻繁的使用,而且也可能導緻OutOfMemoryError異常出現。
在JDK1.4中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)和緩沖區(Buffer)的I/O方式,它可以使用Native函數庫直接配置設定堆外記憶體,然後通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊記憶體的引用進行操作。這樣能在一些場景中明顯提高性能,因為避免了在Java堆和Native堆之間反複複制資料。
很顯然,直接記憶體的配置設定不會受到Java堆大小的限制,但是還是會受到本機總記憶體大小及處理器尋址空間的限制。在配置虛拟機參數時,會根據實際記憶體設定-Xmx等參數資訊,卻經忽略了直接記憶體,導緻各個記憶體區域總和大于實體記憶體限制,最終導緻動态擴充時出現OutOfMemoryError異常。
4.HotPot虛拟機對象
在代碼層面上建立一個對象,對于程式員來說僅僅是一個new關鍵字而已,但在虛拟機中對象的建立是一個怎樣的過程呢?答案如下:
當虛拟機遇到一條new指令時,首先檢查這個指令的參數是否能在常量池中定位到一個類的符号引用,并且檢查這個符号引用代表的類是否已被加載、解析和初始化過。如果沒有,就先執行相應的類加載過程。
4.1.對象的記憶體配置設定
在類加載檢查通過後,接下來虛拟機将為新生對象配置設定記憶體,包括以下兩種配置設定方式:
4.1.1.指針碰撞
假設Java堆中的記憶體是完全規整的,使用中的記憶體在一邊,空閑的記憶體在另一邊,中間是一個指針作為分界點的訓示器,那麼配置設定記憶體時隻需要将指針向空閑空間一邊挪動一段與對象大小相等的距離,這種配置設定方式成為“指針碰撞”
4.1.2.空閑清單
如果Java堆中的記憶體不是規整的,已使用的記憶體和空閑記憶體互相交錯,那就沒辦法用指針碰撞來配置設定記憶體了,這時虛拟機就必須維護一個清單,上邊記錄着哪塊記憶體是可用的,配置設定的時候從清單中找到一塊足夠大的空間劃分給對象執行個體,并更新清單記錄資訊,這種配置設定方式成為“空閑清單”
總結:使用哪種配置設定方式由Java堆是否規整決定的,而Java堆是否規整又有所采用的垃圾收集器是否帶有壓縮整理功能決定的。是以,在使用Serial、ParNew等帶有Compact過程的收集器時,系統采用的配置設定算法是指針碰撞,而使用CMS這種基于Mark-Sweep算法的收集器時,系統采用的配置設定方式是空閑清單
4.2.對象的記憶體布局
對象在記憶體中存儲的布局分為3塊區域:對象頭(Header)、執行個體資料(Instance)、對齊填充(Padding)
4.2.1.對象頭(Header)
對象頭包括兩部分資訊,第一部分用于存儲對象自身的運作時資料,如哈希碼(HashCode)、GC分代年齡、鎖狀态标志、線程持有的鎖、偏向鎖ID、偏向時間戳等,這部分資料的長度在32位和64位虛拟機中分别為32bit和63bit,官方稱為“Mark Word”。但是對象存儲的資料比較多,一般都會大于32,64位Bitmap結構所能記錄的長度,考慮到虛拟機的空間效率,Mark Word被設計成一個非固定的資料結構以便在極小的空間記憶體儲盡量多的資訊,它會根據對象的狀态複用自己的存儲空間 。
存儲内容 | 标志位 | 狀态 |
---|---|---|
對象哈希碼、對象分代年齡 | 01 | 未鎖定 |
指向鎖記錄的指針 | 00 | 輕量級鎖定 |
指向重量級鎖的指針 | 10 | 膨脹(重量級鎖定) |
空,不需要記錄資訊 | 11 | GC标記 |
偏向鎖ID、偏向時間戳、對象分代年齡 | 01 | 可偏向 |
對象頭的另一部分是類型指針,即對象指向它的類中繼資料的指針,虛拟機通過這個指針來确定這個對象是哪個類的執行個體(并非所有的虛拟機實作都必須在對象中記錄這個指針);如果對象是一個Java數組,那在對象頭中還必須有一塊用于記錄數組長度的資料,因為虛拟機可以通過普通Java對象的中繼資料資訊确定Java對象的大小,但是從數組的中繼資料中卻無法确定數組的大小。
4.2.2.執行個體資料(Instance)
執行個體資料是對象真正存儲的有效資訊,也是在程式代碼中所定義的各種類型的字段内容,無論是父類繼承下來的還是子類定義的都需要記錄下來。
執行個體資料存儲順序:與虛拟機配置設定政策參數和字段在Java源碼中定義的順序有關。
預設配置設定政策:longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),從配置設定政策中可以看出,相同寬度的字段總是被配置設定到一起。
4.2.3.對齊填充(Padding)
該部分不是必然存在的,也沒有特别的含義,它僅僅起着占位符的作用。由于HotSpot VM的自動記憶體管理系統要求對象起始位址必須是8位元組的整數倍,換句話說,就是對象的大小必須是8位元組的整數倍。而對象頭部分正好是8位元組的倍數(1倍或者2倍),是以,當對象執行個體資料部分沒有對齊時,就需要通過對齊填充來補全。
4.3.對象的通路定位
建立對象是為了使用對象,java程式需要通過棧上的reference來操作堆上的具體對象。由于reference類型在Java虛拟機規範中隻規定了一個指向對象的引用,并沒有定義這個引用應該通過何種方式去定位、通路堆中的對象的具體位置,是以對象的通路方式取決于虛拟機的實作而定。目前主流的通路方式有使用句柄和直接指針兩種
4.3.1.使用句柄
如果使用句柄通路的話,那麼Java堆中将會劃分出一塊記憶體來作為句柄池,reference中存儲的就是對象的句柄位址,而句柄中包含了對象執行個體資料與類型資料各自的具體位址資訊
4.3.2.直接指針
指針通路 那麼Java堆對象的布局中就必須考慮如何放置通路類型資料的相關資訊,而reference中存儲的直接就是對象位址
總結:
使用句柄來通路的最大好處就是reference中存儲的是穩定的句柄位址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時隻會改變句柄中的執行個體資料指針,而reference本身不需要修改。
使用直接指針通路方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷,由于對象的通路在Java中非常頻繁,是以這類開銷積少成多後也是一項非常可觀的執行成本。HotSpot使用的是直接指針這種方式通路對象的。
參考資料:深入了解Java虛拟機