Java虚拟机2:Java内存区域及对象
幾個計算機的概念
為以后寫文章考慮,也為鞏固自己的知識和一些基本概念,這里要理清楚幾個計算機中的概念。
1、計算機存儲單位
從小到大依次為位Bit、字節(jié)Byte、千字節(jié)KB、兆M、千兆GB、TB,相鄰單位之間都是1024倍,1024為2的10次方,即:
- 1Byte?= 8bit
- 1K = 1024Byte
- 1M = 1024K
- 1G = 1024M
- 1T = 1024G
2、計算機存儲元件
寄存器:中央處理器CPU的一部分,是計算機中讀寫速度最快的存儲元件,但是容量很少
內(nèi)存:屬于獨立的一個部件,是和CPU溝通的橋梁,用于存放CPU中的運算數(shù)據(jù)以及與外部存儲器交換的數(shù)據(jù)。盡管在今天,對內(nèi)存的讀寫速度已經(jīng)很快了,但是由于寄存器是在CPU上的,所以對于內(nèi)存的讀寫速度和對于寄存器的讀寫速度上還是有幾個數(shù)量級的差距。但是沒辦法,對于內(nèi)存的讀寫I/O操作是很難消除的,寄存器數(shù)量有限,不可能通過寄存器來完成所有的運算任務(wù)
3、內(nèi)核空間和用戶空間
連接內(nèi)存和寄存器的是地址總線,地址總線的寬度影響了物理地址的索引范圍,因為總線寬度決定了處理器一次可以從寄存器或內(nèi)存中獲取多少個Bit,同時也決定了處理器最大可以尋址的地址空間。比如32位CPU的系統(tǒng),可尋址范圍為0x00000000~0xFFFFFFFF,即232=4294967296個內(nèi)存位置,每個內(nèi)存位置1個字節(jié),即32位CPU系統(tǒng)可以有4GB的內(nèi)存空間。不過應(yīng)用程序是不可以完全使用這些地址空間的,因為這些地址空間被劃分為了內(nèi)核空間和用戶空間,程序只能使用用戶空間的內(nèi)存。內(nèi)核空間主要是指操作系統(tǒng)運行時所使用的用于程序調(diào)度、虛擬內(nèi)存的使用或者鏈接硬件資源的程序邏輯。區(qū)分內(nèi)核空間和用戶空間的目的主要是從系統(tǒng)的穩(wěn)定性的角度考慮的。Windows 32操作系統(tǒng)默認內(nèi)核空間和用戶空間的比例是1:1,即2G內(nèi)核空間、2G內(nèi)存空間,32位Linux系統(tǒng)中默認比例則是1:3,即1G內(nèi)核空間,3G內(nèi)存空間。
4、字長
CPU的主要技術(shù)指標之一,指的是CPU一次能并行處理二進制的位數(shù)(Bit)。通常稱處理字長為8位數(shù)據(jù)的CPU為8位CPU,32位CPU就是在同一時間內(nèi)處理字長為32位的二進制數(shù)據(jù)。不過目前雖然CPU大多是64位的,但還是以32位字長運行
?
前言
說到Java內(nèi)存區(qū)域,可能很多人第一反應(yīng)是“堆棧”。首先堆棧不是一個概念,而是兩個概念,堆和棧是兩塊不同的內(nèi)存區(qū)域,簡單理解的話,堆是用來存放對象而棧是用來執(zhí)行程序的。其次,堆內(nèi)存和棧內(nèi)存的這種劃分方式比較粗糙,這種劃分方式只能說明大多數(shù)程序員最關(guān)注的、與對象內(nèi)存分配關(guān)系最密切的內(nèi)存區(qū)域是這兩塊,Java內(nèi)存區(qū)域的劃分實際上遠比這復(fù)雜。對于Java程序員來說,在虛擬機自動內(nèi)存管理機制的幫助下,不再需要為每一個new操作去配對delete/free代碼,不容易出現(xiàn)內(nèi)存泄露和內(nèi)存溢出問題。但是,也正是因為Java把內(nèi)存控制權(quán)交給了虛擬機,一旦出現(xiàn)內(nèi)存泄露和內(nèi)存溢出的問題,就難以排查,因此一個好的Java程序員應(yīng)該去了解虛擬機的內(nèi)存區(qū)域以及會引起內(nèi)存泄露和內(nèi)存溢出的場景。
?
運行時數(shù)據(jù)區(qū)域
Java虛擬機(JVM)內(nèi)部定義了程序在運行時需要使用到的內(nèi)存區(qū)域,從http://images.blogjava.net/blogjava_net/nkjava/jvmstructure.png拷貝一張圖下來
之所以要劃分這么多區(qū)域出來是因為這些區(qū)域都有自己的用途,以及創(chuàng)建和銷毀的時間。有些區(qū)域隨著虛擬機進程的啟動而存在,有的區(qū)域則依賴用戶線程的啟動和結(jié)束而銷毀和建立。圖中綠色部分就是所有線程之間共享的內(nèi)存區(qū)域,而白色部分則是線程運行時獨有的數(shù)據(jù)區(qū)域,從這個分類角度來看一下這幾個數(shù)據(jù)區(qū)。
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í)行完畢的過程,就對應(yīng)著一個棧幀在虛擬機中入棧到出棧的過程。棧的大小和具體JVM的實現(xiàn)有關(guān),通常在256K~756K之間。
(3)NATIVE METHOD STACK,方法棧
和虛擬機棧起的作用一樣,只不過方法棧為虛擬機使用到的Native方法服務(wù)。虛擬機規(guī)范并沒有對這個區(qū)域有什么強制規(guī)定,因此我們使用的HotSpot虛擬機,就干脆沒有這塊區(qū)域了,它和虛擬機棧是一起的。
2、線程間共享的內(nèi)存區(qū)域
(1)HEAP,堆
大多數(shù)應(yīng)用,堆都是Java虛擬機所管理的內(nèi)存中最大的一塊,它在虛擬機啟動時創(chuàng)建,此內(nèi)存唯一的目的就是存放對象實例。由于現(xiàn)在垃圾收集器采用的基本都是分代收集算法,所以堆還可以細分為新生代和老年代,再細致一點還有Eden區(qū)、From Survivior區(qū)、To Survivor區(qū),這個后面都會講到的。
(2)METHOD AREA,方法區(qū)
這塊區(qū)域用于存儲虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù),虛擬機規(guī)范是把這塊區(qū)域描述為堆的一個邏輯部分的,但實際它應(yīng)該是要和堆區(qū)分開的。從上面提到的分代收集算法的角度看,HotSpot中,方法區(qū)≈永久代。不過JDK 7之后,我們使用的HotSpot應(yīng)該就沒有永久代這個概念了,會采用Native Memory來實現(xiàn)方法區(qū)的規(guī)劃了。
(3)RUNTIME CONSTANT POOL,運行時常量池
上面的圖中沒有畫出來,因為它是方法區(qū)的一部分。Class文件中除了有類的版本信息、字段、方法、接口等描述信息外,還有一項信息就是常量池,用于存放編譯期間生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中,另外翻譯出來的直接引用也會存儲在這個區(qū)域中。這個區(qū)域另外一個特點就是動態(tài)性,Java并不要求常量就一定要在編譯期間才能產(chǎn)生,運行期間也可以在這個區(qū)域放入新的內(nèi)容,String.intern()方法就是這個特性的應(yīng)用。
3、直接內(nèi)存
想想還是把這塊加上。直接內(nèi)存并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機規(guī)范中定義的內(nèi)存區(qū)域。但是這部分內(nèi)存也被頻繁地使用,而且也可能導(dǎo)致內(nèi)存溢出問題。JDK1.4中新增加了NIO,引入了一種基于通道與緩沖區(qū)的I/O方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在Java堆和Native堆中來回復(fù)制數(shù)據(jù)。顯然,本機直接內(nèi)存的分配不會受到Java堆大小的限制,但是,既然是內(nèi)存,肯定還是會受到本機總內(nèi)存(包括RAM、SWAP區(qū))大小以及處理器尋址空間的限制。
?
對象創(chuàng)建
Java是一門面向?qū)ο蟮恼Z言,Java程序運行過程中無時無刻都有對象被創(chuàng)建出來。在語言層面上,創(chuàng)建對象(克隆、反序列化)就是一個new關(guān)鍵字而已,但是虛擬機層面上卻不是如此。看一下在虛擬機層面上創(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這種基于標記-清除算法的,虛擬機采用這種分配方式。
另外一個問題及時保證new對象時候的線程安全性。因為可能出現(xiàn)虛擬機正在給對象A分配內(nèi)存,指針還沒有來得及修改,對象B又同時使用了原來的指針來分配內(nèi)存的情況。虛擬機采用了CAS配上失敗重試的方式保證更新更新操作的原子性和TLAB兩種方式來解決這個問題。
3、內(nèi)存分配結(jié)束,虛擬機將分配到的內(nèi)存空間都初始化為零值(不包括對象頭)。這一步保證了對象的實例字段在Java代碼中可以不用賦初始值就可以直接使用,程序能訪問到這些字段的數(shù)據(jù)類型所對應(yīng)的零值。
4、對對象進行必要的設(shè)置,例如這個對象是哪個類的實例、如何才能找到類的元數(shù)據(jù)信息、對象的哈希碼、對象的GC分代年齡等信息,這些信息存放在對象的對象頭中。
5、執(zhí)行<init>方法,把對象按照程序員的意愿進行初始化,這樣一個真正可用的對象才算完全產(chǎn)生出來。
以上這部分內(nèi)容,如果有下載OpenJDK的源代碼的話,可以通過參考hotspot/src/share/vm/interpreter/bytecodeInterpreter.cpp文件,從1939行開始。1939行的代碼是CASE(_new):{...},意思是當代碼中遇見new這個關(guān)鍵字,虛擬機做的事情。實際虛擬機可能并不是執(zhí)行的這段代碼,但是通過這段代碼來了解new對象的時候虛擬機的運作過程基本上是沒問題的。
?
對象定位方式
建立對象是為了使用對象,Java程序需要通過棧上的reference(引用)數(shù)據(jù)來操作堆上的具體對象。比如我們寫了一句
Object obj = new Object()
而new Object()之后其實有兩部分內(nèi)容,一部分是類數(shù)據(jù)(比如代表類的Class對象)、一部分是實例數(shù)據(jù)
由于reference在Java虛擬機規(guī)范中只是一個指向?qū)ο髇ew Object()的引用obj,并沒有規(guī)定obj應(yīng)該通過何種方式去定位、訪問堆中對象的具體位置,所以對象訪問方式也是取決于虛擬機而定的。主流方式有兩種:
1、句柄訪問。Java堆中劃分出一塊句柄池,obj指向的是對象的句柄地址,句柄中則包含了類數(shù)據(jù)的地址和實例數(shù)據(jù)的地址
2、指針訪問。對象中存儲所有的實例數(shù)據(jù)和類數(shù)據(jù)的地址,obj指向的是這個對象
HotSpot虛擬機采用的是后者,不過前者的對象訪問方式也是十分常見的。
總結(jié)
以上是生活随笔為你收集整理的Java虚拟机2:Java内存区域及对象的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iOS之 NSTimer(一)
- 下一篇: Toast的基本用法 吐司打印