本文主要介紹Java虛擬機的內(nèi)存分布以及對象的創(chuàng)建過程。
一、Java虛擬機的內(nèi)存分布文章開始前讀者需要了解Java虛擬機的運行時數(shù)據(jù)區(qū)是怎樣劃分的。如下圖所示:

程序計數(shù)器是一塊較小的內(nèi)存空間,它可以看作是當前線程所執(zhí)行的字節(jié)碼的行號指示器。
由于Java虛擬機的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實現(xiàn)的,所以在任何一個確定的時刻,一個處理器(對于多核處理器來說是一個內(nèi)核)都只會執(zhí)行一條線程中的指令。因此,為了線程切換后能夠恢復到正確的執(zhí)行位置,每條線程都需要有一個獨立的程序計數(shù)器。
各條線程之間計數(shù)器互不影響,獨立存儲,我們稱這類內(nèi)存區(qū)域為“線程私有”的內(nèi)存。
熟悉C++的讀者應該了解“現(xiàn)場保護”這個概念,雖然“現(xiàn)場保護”對應的是函數(shù)調(diào)用,而這里的程序計數(shù)器對應的是線程之間的切換,但筆者認為它們的作用有點相似:都是為了線程或函數(shù)執(zhí)行完后能夠恢復到正確的位置。
2、Java虛擬機棧(Java Virtual Machine Stacks)我們平時所說的棧區(qū)就是Java虛擬機棧。
與程序計數(shù)器一樣,Java虛擬機棧也是線程私有的,生命周期與線程相同。
虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧楨(Stack Frame)用于存儲該方法的信息,如局部變量表、操作數(shù)棧、方法的出口等信息。
每個方法從調(diào)用直至執(zhí)行完成的過程,就對應著一個棧楨入棧到出棧的過程。
局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,不是對象本身)和returnAddress類型(指向了一條字節(jié)碼指令的位置)。
Java虛擬機規(guī)范中,對這個區(qū)域規(guī)定了兩種異常狀況:StackOverflowError異常和OutOfMemoryError異常。
3、Java堆(Java Heap)Java堆是Java虛擬機所管理的內(nèi)存中最大的一塊,該內(nèi)存是被所有線程共享的,在虛擬機啟動時創(chuàng)建。此內(nèi)存區(qū)域存在的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內(nèi)存。既然Java堆中存放了幾乎所有的實例,那么這里自然就成了垃圾收集器管理的主要區(qū)域。
當堆中沒有內(nèi)存完成了實例分配,并且堆也無法再擴張時,將會拋出OutOfMemoryError異常。
4、方法區(qū)(Method Area)方法區(qū)與Java堆一樣,是各個線程共享的內(nèi)存區(qū)域。
它用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。
方法區(qū)還包括了運行時常量池。
當方法區(qū)無法滿足內(nèi)存分配需求時,將拋出OutOfMemoryError異常。
5、本地方法棧(Native Method Stack)本地方法棧與虛擬機棧的作用非常相似,它們的區(qū)別不過是虛擬機棧為虛擬機執(zhí)行Java方法(也就是字節(jié)碼)服務,而本地方法棧則為了虛擬機使用到的Native方法服務。
在虛擬機規(guī)范中隊本地方法棧方法使用的語言、使用的算法與數(shù)據(jù)結(jié)構(gòu)并沒有強制規(guī)定,因此具體的虛擬機可以自由實現(xiàn)它。甚至有的虛擬機(如 Sun HotSpot 虛擬機)直接就把本地方法棧和虛擬機棧合二為一。
該數(shù)據(jù)區(qū)拋出的異常和虛擬機棧相同:都會拋出StackOverflowError和OutOfMemoryError異常。
6、運行時常量池(Runtime Constant Pool)運行時常量池屬于方法區(qū)的一部分。雖然Java虛擬機沒有單獨為運行時常量池劃分內(nèi)存空間,但是由于該區(qū)域在平時開發(fā)中也是一個重要的部分,所以也為它單獨列一個條目。
Class文件中除了有類的版本、字段、方法、接口的描述信息外,還有一項信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量的引用,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中存放。
運行時常量池相對于Class文件常量池的另外一個重要特征是具有動態(tài)性,Java語言并不要求常量一定只有編譯期才能產(chǎn)生,也就是說并非預置入Class文件中常量池的內(nèi)容才能進入方法區(qū)運行時常量池,運行期間也可能將新的常量放入池中,如String類的intern()方法。
既然運行時常量池是方法區(qū)的一部分,自然受到方法區(qū)內(nèi)存的限制,當常量池無法再申請到內(nèi)存是會拋出OutOfMemoryError異常。
二、對象的創(chuàng)建過程Java是一門面向?qū)ο蟮恼Z言,在Java程序運行過程中無時無刻都會有對象被創(chuàng)建出來。在語言層面上通產(chǎn)僅僅是一個new關鍵字,而在虛擬機中可就沒有這么簡單了:
虛擬機遇到一條new指令時,首先將去檢查這個指令的參數(shù)是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否被加載、解析和初始化過。如果沒有,那必須先執(zhí)行相應的類加載過程。 在類加載檢查通過后,接下來虛擬機將為新生對象在Java堆中分配內(nèi)存(對象所需的內(nèi)存大小在內(nèi)加載完成后便可完全確定)。接下來,虛擬機要對對象進行一些必要的設置,如這個對象是哪個類的實例、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭(Object Header)之中。
在上面工作都完成之后,從虛擬機的視角來看:一個新的對象已經(jīng)產(chǎn)生了,但從Java程序的視角來看:對象創(chuàng)建才剛剛開始——<init>方法還沒有執(zhí)行,所有的字段都還為零。一般來說執(zhí)行new指令之后會接著執(zhí)行<init>方法,把對象按照程序員的意愿進行初始化。這樣一個真正可用的對象才算完全產(chǎn)生出來。
本文章系筆者的第一篇文章,文章中難免有錯誤和不足的地方,還請大家指正。
新聞熱點
疑難解答