摘要:運行時數(shù)據(jù)區(qū)域之所以要劃分這么多區(qū)域出來是因為這些區(qū)域都有自己的用途,以及創(chuàng)建和銷毀的時間。,運行時常量池它是方法區(qū)的一部分。直接內(nèi)存直接內(nèi)存并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是虛擬機規(guī)范中定義的內(nèi)存區(qū)域。
前言
說到JAVA內(nèi)存區(qū)域,可能很多人第一反應是“堆?!?。
首先,堆棧不是一個概念,而是兩個概念,堆和棧是兩塊不同的內(nèi)存區(qū)域,簡單理解的話,堆是用來存放對象而棧是用來運行程序的。
其次,堆內(nèi)存和棧內(nèi)存的這種劃分方式比較粗糙,這種劃分方式只能說明大多數(shù)程序員最關注的、與對象內(nèi)存分配關系最密切的內(nèi)存區(qū)域是這兩塊,Java內(nèi)存區(qū)域劃分實際上遠比這復雜。
對于Java程序員來說,在虛擬機自動內(nèi)存管理機制的幫助下,不再需要為每一個new操作去配對deleted/free代碼,不容易出現(xiàn)內(nèi)存泄漏和內(nèi)存溢出問題。
但是,也正是因為Java把內(nèi)存控制權交給了虛擬機,一旦出現(xiàn)內(nèi)存泄漏和內(nèi)存溢出的問題,就難以排查,因此一個好的Java程序員應該去了解虛擬機的內(nèi)存區(qū)域以及會引起內(nèi)存泄漏和內(nèi)存溢出的場景。
之所以要劃分這么多區(qū)域出來是因為這些區(qū)域都有自己的用途,以及創(chuàng)建和銷毀的時間。有些區(qū)域隨著虛擬機進程的啟動而存在,有的區(qū)域則依賴用戶線程的啟動和結(jié)束而銷毀和建立。
1.線程獨有的內(nèi)存區(qū)域
(1) PROGRAM COUNTER REGISTER,程序計數(shù)器
這塊內(nèi)存區(qū)域很小,它是當前線程所執(zhí)行的字節(jié)碼的行號指示器,字節(jié)碼解釋器通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令。Java方法這個計數(shù)器才有值,如果執(zhí)行的是一個Native方法,那這個計數(shù)器是空的。
(2) JAVA STACK,虛擬機棧
生命周期和線程周期相同。每個方法執(zhí)行的同時都會創(chuàng)建一個棧幀,用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息,每一個方法從調(diào)用至執(zhí)行完畢的過程,就對應一個棧幀在虛擬機棧中入棧到出棧的過程。棧的大小和具體JVM的實現(xiàn)有關,通常在256K~756Kz之間。
(3) NATIVE METHOD STACK,方法棧
和虛擬機棧起的作用一樣,只不過方法棧為虛擬機使用到的Native方法服務。虛擬機規(guī)范并沒有對這個區(qū)域有什么強制規(guī)定,因此我們使用的HotSpot虛擬機,就干脆沒有這塊區(qū)域了,它和虛擬機棧是一起的。
2.線程間共享的內(nèi)存區(qū)域
(1) HEAP,堆
大多數(shù)應用,堆都是Java虛擬機所管理的內(nèi)存中最大的一塊,它在虛擬機啟動時創(chuàng)建,此內(nèi)存唯一目的就是存放對象實例。由于現(xiàn)在垃圾收集器采用的基本都是分代收集算法,所以堆還可以細分為新生代和老年代,再細致一點還有Eden區(qū)、From Survivor區(qū)、To Survivor區(qū)。
(2) METHOD AREA,方法區(qū)
這塊區(qū)域用于存儲虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù),虛擬機規(guī)范是把這塊區(qū)域描述為堆的一個邏輯部分的,但實際它應該是要和堆區(qū)分開的。從上面提到的分代收集算法的角度來看,HotSpot中,方法區(qū)≈永久代。不過JDK7之后,我們使用的HotSpot應該就沒有永久代這個概念了,會采用Native Memory來實現(xiàn)方法區(qū)的規(guī)劃了。
(3) RUNTIME CONSTANT POOL,運行時常量池
它是方法區(qū)的一部分。Class文件中除了有類的版本信息、字段、方法、接口等描述信息外,還有一項信息就是常量池,用于存放編譯期間生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中,另外翻譯出來的直接引用也會存儲在這個區(qū)域中。
(在JVM中,類從被加載到虛擬機內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期包括:加載、驗證、準備、解析、初始化、使用和卸載7個階段。而解析階段即是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程。
在Java中,一個java類將會編譯成一個class文件。在編譯時,java類并不知道所引用的類的實際地址,因此只能使用符號引用來代替。而解析階段即是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程,翻譯出來的直接引用也是存儲在方法區(qū)的運行時常量池中。)
3.直接內(nèi)存
直接內(nèi)存并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機規(guī)范中定義的內(nèi)存區(qū)域。但是這部分內(nèi)存也被頻繁地使用,而且也可能導致內(nèi)存溢出的問題。JDK1.4中新增加了NIO,引入了一種基于通道與緩沖區(qū)的I/O方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在Java堆和Native堆中來回復制數(shù)據(jù)。顯然本機直接內(nèi)存的分配不會受到Java堆大小的限制,但是,既然是內(nèi)存,肯定還是會受到本機總內(nèi)存大小及處理器尋址空間的限制。
我們來看一下在虛擬機層面上創(chuàng)建對象的步驟:
1.虛擬機遇到一條new指令,首先去檢查這個指令的參數(shù)能否在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已經(jīng)被加載、解析和初始化。如果沒有,那么必須執(zhí)行類的初始化過程。
2.類加載檢查通過后,虛擬機為新生對象分配內(nèi)存。對象所需內(nèi)存大小在類加載完成后便可以完全確定,為對象分配空間無非是從Java堆中劃分出一個確定大小的內(nèi)存而已。這個地方會有兩個問題:
(1) 如果內(nèi)存是規(guī)整的,那么虛擬機將采用的是指針碰撞法來為對象分配內(nèi)存。意思是所有用過的內(nèi)存在一邊,空閑的內(nèi)存在另一邊,中間放著一個指針作為分界點指示器,分配內(nèi)存就僅僅是把指針向空閑那邊挪動一段與對象大小相等的距離罷了。如果垃圾收集器選擇的是Serial、ParNew這種基于壓縮算法的,虛擬機采用這種分配方式。
(2) 如果內(nèi)存不是規(guī)整的,已使用的內(nèi)存和未使用的內(nèi)存相互交錯,那么虛擬機將采用的是空閑列表法來為對象分配內(nèi)存。意思是虛擬機維護了一個列表記錄了哪些內(nèi)存是可用的,再分配的時候從列表中找到一塊足夠大的空間劃分給實例,并更新列表上的內(nèi)容。如果垃圾收集器選擇是CMS這種基于標記-清除(Mark-Sweep)算法的,虛擬機采用這種分配方式。
簡言之,
垃圾收集器選擇的是Serial、ParNew這種基于Compact(壓縮)算法的,虛擬機采用指針碰撞法為對象分配內(nèi)存;
垃圾收集器選擇是CMS這種基于Mark-Sweep(標記-清除)算法的,虛擬機采用空閑列表法為對象分配內(nèi)存。
另外一個問題即是保證new對象時候的線程安全。因為可能出現(xiàn)虛擬機正在給對象A分配內(nèi)存,指針還沒有來得及修改,對象B又同時使用了原來的指針來分配內(nèi)存的情況。虛擬機采用了CAS配上失敗重試和TLAB兩種方式保證更新操作的原子性來解決這個問題。
(每個線程在Java堆中預先分配一小塊內(nèi)存,成為本地線程分配緩沖區(qū)——TLAB,線程內(nèi)部需要分配內(nèi)存時直接在TLAB上分配就行,避免了線程沖突。只有當緩沖區(qū)的內(nèi)存用光需要重新分配內(nèi)存的時候才會進行CAS操作分配更大的內(nèi)存空間。虛擬機是否使用TLAB,可以通過-XX:+/-UseTLAB參數(shù)來進行配置,但是JDK5及以后的版本默認是啟用TLAB的)
3.內(nèi)存分配結(jié)束,虛擬機講分配到的內(nèi)存空間都初始化為零值(不包括對象頭)。這一步保證了對象的實例字段在Java代碼中可以不用賦初始值就可以直接使用,程序能訪問到這些字段的數(shù)據(jù)類型所對應的零值。
4.對對象進行必要的配置,例如這個對象是哪個類的實例、如何才能找到類的元數(shù)據(jù)信息、對象的哈希嗎、對象的GC分代年齡等信息,這些信息存放在對象的對象頭中。
5.執(zhí)行
建立對象是為了使用對象,我們的Java程序需要通過棧上的reference數(shù)據(jù)來操作對上的具體對象。
比如,Object obj = new Object();而new Object()之后其實有兩部分內(nèi)容:一部分是類數(shù)據(jù)(方法區(qū))、一部分是實例數(shù)據(jù)(堆)。
由于reference類型在Java虛擬機規(guī)范里面只規(guī)定了是一個指向?qū)ο蟮囊?,并沒有定義這個引用應該通過什么方式去定位、訪問到堆中的對象的具體位置,對象訪問方式也是取決于虛擬機實現(xiàn)而定的。主流的訪問方式有使用句柄和直接指針兩種。
1.使用句柄
在Java堆中將會劃分出一塊內(nèi)存來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例與類型數(shù)據(jù)的具體各自的地址信息,
2.使用直接指針
使用直接指針訪問的話,Java堆對象的布局中就必須考慮如何放置訪問類型數(shù)據(jù)的相關信息,reference中存儲的直接就是對象地址,
文章版權歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/69081.html
摘要:下面的截圖內(nèi)容來自從規(guī)范我們可以看到,規(guī)范要求的運行時數(shù)據(jù)區(qū)域有程序計數(shù)器虛擬機棧堆方法區(qū)本地方法棧運行時常量池這及部分。查了一下,還是沒有查到官方對于運行時數(shù)據(jù)區(qū)域的說明,但是許多博客都指出將字符串常量池移動到了堆中。 不少java程序員一提JVM運行時數(shù)據(jù)區(qū)域,就會說堆和棧,當然也有java程序員給出方法區(qū)、虛擬機棧、本地方法棧、堆、程序計數(shù)器這個答案,但是還有人給出永久代、虛擬機...
摘要:框架說明開發(fā)者都知道會執(zhí)行字節(jié)碼。但是可能大多數(shù)人都不知道一個事實是的實現(xiàn),它分析字節(jié)碼,解釋并執(zhí)行代碼。執(zhí)行引擎字節(jié)碼加載到運行時數(shù)據(jù)區(qū)后,會被執(zhí)行引擎執(zhí)行。解釋器更快的解釋字節(jié)碼,但是執(zhí)行非常慢。垃圾收集收集并移除不再被使用的對象。 JVM框架說明 java開發(fā)者都知道JRE(Java Runtime Environment)會執(zhí)行字節(jié)碼。但是可能大多數(shù)人都不知道一個事實:JRE是...
摘要:對字節(jié)碼文件進行解釋執(zhí)行,把字節(jié)碼翻譯成相關平臺上的機器指令。使用命令可對字節(jié)碼文件以及配置文件進行打包可對一個由多個字節(jié)碼文件和配置文件等資源文件構(gòu)成的項目進行打包。和不存在永久代這種說法。 Java技術體系 從廣義上講,Clojure、JRuby、Groovy等運行于Java虛擬機上的語言及其相關的程序都屬于Java技術體系中的一員。如果僅從傳統(tǒng)意義上來看,Sun官方所定義的Jav...
摘要:方法區(qū)在實際內(nèi)存空間站可以是不連續(xù)的。這一規(guī)定,可以說是給了虛擬機廠商很大的自由。但是值得注意的是,堆其實還未每一個線程單獨分配了一塊空間,這部分空間在分配時是線程獨享的,在使用時是線程共享的。 在我的博客中,之前有很多文章介紹過JVM內(nèi)存結(jié)構(gòu),相信很多看多我文章的朋友對這部分知識都有一定的了解了。 那么,請大家嘗試著回答一下以下問題: 1、JVM管理的內(nèi)存結(jié)構(gòu)是怎樣的? 2、不同的...
摘要:編譯器只需面向,生成能理解的代碼或字節(jié)碼文件。源文件經(jīng)編譯器,編譯成字節(jié)碼程序,通過將每一條指令翻譯成不同平臺機器碼,通過特定平臺運行。漲見識,字節(jié)碼執(zhí)行過程分析。解決辦法減少默認棧的容量來換取更多的線程支持。 前言 JVM是java的核心和基礎,在java編譯器和os平臺之間的虛擬處理器。它是一種基于下層的操作系統(tǒng)和硬件平臺并利用軟件方法來實現(xiàn)的抽象的計算機,可以在上面執(zhí)行java的...
閱讀 2241·2021-11-19 09:40
閱讀 2260·2021-10-09 09:43
閱讀 3355·2021-09-06 15:00
閱讀 2876·2019-08-29 13:04
閱讀 2834·2019-08-26 11:53
閱讀 3626·2019-08-26 11:46
閱讀 2377·2019-08-26 11:38
閱讀 445·2019-08-26 11:27