深刻理解java虛擬機學習01 - 運行時數據區域

運行時數據區域

Java虛擬機在執行Java程序的過程當中會把它所管理的內存劃分爲若干個不一樣的數據區域。這些區域有各自的用途,以及建立和銷燬的時間,有的區域替換虛擬機進程的啓動而根據《 Java虛擬機規範》的規定,Java虛擬機所管理的內存將會包括如下幾個運行時數據區域,諸如此類,而一直存在,某些區域則是依賴用戶線程的啓動和結束而創建和銷燬。 圖1所示。程序員

圖1 Java虛擬機運行時數據區

1.1程序計數器

程序計數器(Program Counter Register)是一塊較小的內存空間,它能夠選擇是當前線程所執行的字節碼的行號指示器。在Java虛擬機的概念模型裏 ,字節碼解釋器工做時就是經過改變這個計數器的值來挑選下一條須要執行的字節碼指令,它是程序控制流的指示器,分支,循環,轉換,異常處理,線程恢復等基礎功能都須要依賴這個計數器來完成。 因爲Java虛擬機的多線程是經過線程輪流切換,分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)都只會執行一條所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間的計數器互不影響,獨立存儲,咱們稱量其中的內存區域爲「線程專有」的內存。 若是線程正在執行的是一個Java方法,這個計數器記錄的正在執行的虛擬機字節碼指令的地址;若是正在執行的是本地(本機)方法,這個計數器值則應爲空(未定義)。此內存區域是惟一一個在《 Java虛擬機規範》中沒有規定任何OutOfMemoryError狀況的區域。數組

1.2 Java虛擬機棧

與程序計數器同樣,Java虛擬機棧(Java Virtual Machine Stack)也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的線程內存模型:每一個方法被執行的時候, Java虛擬機都會同步建立一個棧幀 (Stack Frame)用於存儲局部變量表,操做數棧,動態鏈接,方法出口等信息。每個方法被調用而後從新執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。 常常有人把Java內存區域籠統地劃分爲堆內存(Heap)和棧內存(Stack),這種劃分方式直接繼承自傳統的C,C ++程序的內存佈局結構,在Java語言裏就有些粗糙了,實際的內存區域劃分要比這更復雜。不過這種劃分方式的流行也間接說明了程序員最關注的,與對象內存分配關係最緊密的區域是「堆」和「棧」兩塊。其中, 「堆」在後來的筆者會專門提出,而「棧」一般就是指這裏講的虛擬機棧,或者更多的狀況下只是指虛擬機棧中局部變量表部分。 局部變量表存放了編譯期可知的各類Java虛擬機基本數據類型(布爾,字節,char,short,int,float,long,double),對象引用(引用類型,它並不等於於對象自己,多是一個指向對象起始地址的引用指針,也多是指向一個表明對象的句柄或其餘指向對象相關的位置)和returnAddress類型(指向了一個字節碼指令的地址)。 這些數據類型在局部變量表中的存儲空間以局部變量槽(Slot)來表示,其中64位長度的long和double類型的數據會佔用兩個變量槽,其他的數據類型只佔用一個。所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法須要在棧幀中分配多大的局部變量空間是徹底肯定的,在方法運行期間不會改變局部變量表的大小。請讀者注意,這裏說的「大小」是指變量槽的數量,虛擬機真正使用多大的內存空間(例如如遵循1個變量槽佔用32個比特,64個比特,或者更多)來實現一個變量槽,這是徹底由具體的虛擬機實現自行決定的事情。 在《 Java虛擬機規範》中,對這個內存區域規定了兩類異常情況:若是線程請求的棧深度大於虛擬機所容許的深度,將引起StackOverflowError異常;若是Java虛擬機棧容量能夠動態擴展 ,當棧擴展時沒法申請到足夠的內存會拋出OutOfMemoryError異常。緩存

1.3本地方法棧

本地方法棧(Native Method Stacks)與虛擬機棧所發揮的做用是很是類似的,其區別只是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的本地(Native)方法服務。 《 Java虛擬機規範》對本地方法棧中a`方法使用的語言,使用方式與數據結構並無任何強制規定,所以具體的虛擬機能夠根據須要自由實現它,甚至有的Java虛擬機(例如如熱點與虛擬機棧同樣,本地方法棧也會在棧深度重疊或棧擴展失敗時分別引起StackOverflowError和OutOfMemoryError異常。服務器

1.4 Java堆

對於Java應用程序來講,Java堆(Java堆)是虛擬機所管理的內存中最大的一塊。Java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,Java世界裏「幾乎」全部的對象實例都在這裏分配內存。在《 Java虛擬機規範》中對Java堆的描述是:「全部的對象實例以及上面的都在堆上分配 」 ,而這裏筆者寫的「幾乎」是指從實現角度來看,經過Java語言的發展,如今已經能看到一些許諾代表往後可能出現值類型的支持,甚至只考慮如今,因爲即時編譯技術的進步,尤爲是逃逸分析技術的日漸強大,棧上分配,標量 替換優化手段已經致使一些微妙的變化悄然發生,因此說Java對象實例都分配在堆上也漸進得不是那麼絕對了。 Java堆是垃圾收集器管理的內存區域,所以一些資料中它也被稱爲「 GC堆」(Garbage Collected Heap,幸虧內部沒翻譯成「垃圾堆」)。從回收內存的角度看,因爲現代垃圾收集器大部分都是基於分代收集理論設計的,因此Java堆中常常會出現「新生代」「老年代」「永久代」「 Eden空間」「從倖存者空間」「到倖存者空間」等名詞,這些概念在本書後續章節中將會反覆登場亮相,在這裏筆者想先說明的是這些區域劃分並部分垃圾收集器的共同特性或說設計風格而已,而不是Java虛擬機具體實現的固有內存佈局,更不是《 Java虛擬機規範》裏對Java堆的進一步細緻劃分。大量數據上常常寫着替換到「 Java虛擬機的堆內存劃分了新生代,老年代,永久代,Eden,倖存者……」這樣的內容。在十年以前(以G1收集器的出現爲分界),做爲發展中國家絕對主流的HotSpot虛擬 機,它內部的垃圾收集器所有都基於「經典分代」 來設計,須要新生代,老年代收集器搭配才能發揮做用,在這種背景下,上述所說還算是不會產生太大的歧義。今天,垃圾收集器技術與十年前已不可同日而語,HotSpot裏面也出現了不採用分代設計的新垃圾收集器,再按照上面的提法就有不少須要商榷的地方了。 若是從分配內存的角度看,全部線程共享的Java堆中能夠劃分出多個線程私有的分配堆棧(線程本地分配緩衝區,TLAB),以提高對象分配時的效率。如何劃分,都不會改變Java堆中存儲內容的共性,不管是哪一個區域,存儲的都只能是對象的實例,將Java堆劃分的目的只是爲了更好地回收內存,或者替換地分配內存。在本章中,咱們僅針對內存區域的做用進行討論,Java堆中的上述多個區域的分配,回收等細節將會是下一章的主題。 根據《 Java虛擬機規範》的規定,Java堆能夠位於物理上不連續的內存空間中,但在邏輯上它應該被視爲連續的,這點就像咱們用磁盤空間去存儲文件同樣,並不要求每一個文件都連續存放。但對於大對象(典型的如數組對象),多個虛擬機要實現簡單,存儲高效的考慮,極可能會要求連續的內存空間。 Java堆既能夠被實現成固定大小的,也能夠是可擴展的,不過當前主流的Java虛擬機都是按照可擴展來實現的(經過參數-Xmx和-Xms設置)。若是在Java堆中沒有內存完成實例分配,而且堆也沒法再擴展時,Java虛擬機將會拋出OutOfMemoryError異常。數據結構

1.5方法區

方法區(Method Area)與Java堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類型信息,常量,靜態變量,即時編譯器編譯後的代碼緩存等數據。雖然Java虛擬機規範》中把方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫做「非堆」(Non-Heap),目的是與Java堆區分開來。 說到方法區,必須提一下「永久代」這個概念,尤爲是在JDK 8之前,許多Java程序員都習慣在Hotspot虛擬機上開發,部署程序,不少人都更願意把方法區稱呼爲「本質上這兩個並不是等價的,由於當時當時的熱點虛擬機設計團隊選擇把工具的分代設計擴展至方法區,或者說使用永久代來實現方法區而已,這樣經過熱點的垃圾收集器可以像管理Java堆同樣管理這部份內存,省去專門爲方法區編寫內存管理代碼的工做。可是對於其餘虛擬機實現,例如BEA JRockit ,IBM J9等而言,是不存在永久代的概念的。原則上如何實現方法區屬於虛擬機實現細節,無需《 Java虛擬機規範》管束,並不要求統一。但如今回頭來看,當年使用永久代來實現方法區的決定並非一個好主意,這種設計致使了Java應用更容易遇到 內存溢出的問題(永久代有-XX:MaxPermSize的上限,即便不設置也有大小,而J9和JRockit只要沒有觸碰到連續可用的內存的上限,例如32位系統中的4GB限制,就不會出問題),並且有極少數方法(例如String :: intern())會因永久代的緣由而致使不一樣虛擬機下有不一樣的表現。當Oracle收購BEA得到了JRockit的全部權後,準備把JRockit中的優秀功能,例如如Java Mission Control管理工具,移植到Hotpot虛擬機時,但由於另外一個對方法區實現的差別而面臨諸多困難。考慮到Hotpot將來的發展,在JDK 6的時候Hotpot開發團隊有放棄永久代,逐步從新採用本地內存(Native Memory)來實現方法區的計劃了 ,到了JDK 7的熱點,已經把本來放在永久代的串行常量池,靜態變量等移出,而到了JDK 8,終於徹底廢棄了永久代的概念,改用與JRockit,J9同樣在本地內存中實現的元空間(Meta-space)來代替,把JDK 7中永久代還剩餘的內容(主要是類型信息)所有移到元空間中。 《 Java虛擬機規範》對方法區的約束是很是寬鬆的,除了和Java堆同樣不須要連續的內存和能夠選擇固定大小或可擴展外,甚至還能夠選擇不實現垃圾收集。相對而言,垃圾收集行爲在這個區域的確是比較少出現的,但並不是數據進入了方法區就如永久代的名字同樣「永久」存在了。這區域的內存回收目標主要是針對常量池的回收和對類型的卸載,一般來講這個區域的回收效果比較難於知足,尤爲是類型的卸載,條件至關苛刻,可是這部分區域的回收有時又確實是必要的。之前Sun公司的錯誤列表中,曾出現過的若干個嚴重的Bug就是因爲低版本的HotSpot虛擬機該區域未徹底回收而致使內存泄漏。 根據《 Java虛擬機規範》的規定,若是方法區域沒法知足新的內存分配需求時,將拋出OutOfMemoryError異常。多線程

1.6運行時常量池

類文件中除了有類的版本,索引,方法,接口等描述信息外,還有某些信息是常量池表(常量池表),用於放置編譯期生成的各類字面量與符號引用,這部份內容將在類加載後存放到方法區的運行時常量池中。 Java虛擬機對於類文件每一部分(天然也包括常量池)的格式都有嚴格規定,如每個字節存儲的數據都必須符合規範上的要求才會被虛擬機承認,加載和執行,但對於運行時常量池,《 Java虛擬機規範》並無作任何細節的要求,不一樣提供商實現的虛擬機能夠按照本身的須要來實現這個內存區域,不過通常來講,除了保存類文件中描述的符號引用外,將會把由符號引用翻譯出來的直接引用也存儲在運行時常量池中 。運行時常量池相對於類文件常量池的另一個重要特徵是具備動態性,Java語言並不要求常量必定只有編譯期才能產生,而不是連續地插入類文件中常量池的內容才能進入方法區運行時常量池,運行期間也能夠將新的常量加入池中,這種特性被開發人員利用得比較多的即是字符串類的intern()方法。 既然運行時常量池是方法區的一部分,天然受到方法區內存的限制,當常量池沒法再申請到內存時會拋出OutOfMemoryError異常。函數

1.7直接內存

直接內存(Direct Memory)並非虛擬機運行時數據區的一部分,也不是《 Java虛擬機規範》中定義的內存區域。可是這部份內存也被替換了地使用,並且也可能致使OutOfMemoryError異常出現,因此咱們放到這裏一塊兒講解。 在JDK 1.4中新加入了NIO(新輸入/輸出)類,發佈了一種基於通道(Channel)與嵌套(Buffer)的I / O方式,它可使用本地函數庫直接分配堆外部內存,而後經過一個存儲在Java堆裏面的DirectByteBuffer對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯着提升性能,由於避免了在Java堆和本地堆中來回複製數據。 顯然,本機直接內存的分配不會受到Java堆大小的限制,可是,既然是內存,則確定仍是會遇到本機總內存(包括物理內存,SWAP分區或分頁文件)大小以及處理器尋址空間的限制,通常服務器管理員配置虛擬機參數時,會根據實際的內存去設置-Xmx等參數信息,但常常忽略掉直接內存,而且要避免各自內存區域總和大於物理內存限制(包括物理的和操做系統級的限制),從而致使動態擴展時出現OutOfMemoryError異常。工具

相關文章
相關標籤/搜索