天天看點

JVM記憶體模型1記憶體模型2 HotSpot虛拟機對象探秘

對于JAVA程式員來說,在虛拟機自動記憶體管理機制的幫助下,不需要為每一個對象去寫free/delete代碼。因為java程式員把記憶體控制的權限交給了JAVA虛拟機,是以,一旦出現記憶體洩漏和記憶體溢出方面的問題,如果不了解虛拟機是怎樣使用記憶體的,那麼我們就不能很好的排查錯誤(摘自深入了解java虛拟機)

1記憶體模型

java虛拟機在執行java程式的過程中會把它所管理的記憶體劃分成若幹個不同的區域,如下:

JVM記憶體模型1記憶體模型2 HotSpot虛拟機對象探秘

1.1程式計數器

        程式計數器是一塊兒較小的記憶體空間,它可以看作是目前線程所執行的位元組碼的行号訓示器。在虛拟機的概念模型裡,位元組碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、循環、跳轉、異常處理、線程恢複等基礎功能都需要依賴這個計數器來完成。 如果線程正在執行的是一個java方法,這個計數器記錄的是正在執行的虛拟機位元組碼指令的位址;如果正在執行的是Native方法,這個計數器值則為空。

        此記憶體區域是惟一一個在java虛拟機規範中沒有規定任何OutOfMemoryErrror情況的區域

1.2Java虛拟機棧

        與程式計數器一樣,Java虛拟機棧也是線程私有的,它的生命周期和線程相同。虛拟機棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時會建立一個棧幀(Stack Frame)用于存儲局部變量表,操作數棧,動态連結,方法出口等資訊。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛拟機棧中入棧和出棧的過程。局部變量表存放了編譯期預知的各種基本資料類型,對象引用,和returnAddress類型(指向了一條位元組碼指令的位址)。

        long和double資料類型會占兩個局部變量空間(Slot),局部變量表所需的記憶體空間在編譯期間完成配置設定,當進入一個方法時,這個方法需要在幀中配置設定多大的局部變量空間是完全确定的,方法運作期間不會改變局部變量表的大小。

        在java虛拟機規範裡,在這個區域規定了兩種異常情況:如果線程請求的棧的深度大于虛拟機所允許的深度,将抛出StackOverflowError異常;如果虛拟機棧可以動态擴充(目前大部分虛拟機都可動态擴充,隻不過java虛拟機規範中也允許固定長度的java虛拟機棧),但無法申請到足夠的記憶體時,就會抛出OutOfMemoryError異常。

1.3本地方法棧

        本地方法棧發揮的作用和java虛拟機棧非常相似。它們的差別是,虛拟機棧為虛拟機執行Java方法(也就是位元組碼)服務,而本地方法棧則為虛拟機使用到的Native方法服務。虛拟機規範中對本地方法棧中方法使用的語言,使用方式和資料結構并沒有強制的規定,是以虛拟機可以自由的使用它。有的虛拟機(hotspot)直接就把本地方法棧和虛拟機棧直接合二為一。

        本地方法棧也會抛出StackOverflowError和OutOfMemoryError異常

1.4Java堆

        對于大多說應用來說,Java堆是虛拟機所管理的記憶體中的最大一塊兒區域。Java堆是被所有線程共享的區域,在虛拟機啟動的時候被建立。此記憶體的唯一目的就是存放對象的執行個體,幾乎所有對象的記憶體的執行個體都在想都在這裡配置設定記憶體。這一點在java虛拟機規範中的描述:所有的對象執行個體以及數組都要在棧上配置設定,但是随着JIT編譯器的發展與逃逸分析技術逐漸成熟,棧上配置設定,标量替換優化技術将會導緻一些微妙的變化發生,所有的對象都配置設定在堆上也漸漸變得不是那麼絕對了。

        java堆是垃圾回收器管理的主要區域,從記憶體回收的角度來看,由于現在的垃圾回收器都采用的分代回收算法,是以java堆還可以細分為:新生代和老年代;再細緻一點還可分為Eden空間,From Survivor空間,To Survivor空間。從記憶體配置設定的角度來看,線程共享的java堆中可能劃分出多個線程私有的配置設定緩沖區(Thread Local Allocation Buffer, TLAB)。無論如何劃分,都與存儲的内容無關,無論哪個區域,存放的都是對象的執行個體,進一步劃分是為了更好的回收記憶體,或者更快的配置設定記憶體。

        根據java虛拟機規範的規定,java堆可以處于實體上不連續的記憶體空間中,隻要邏輯上是連續的即可。在實作時,既可以是固定大小的,也可以是可擴充的,目前的虛拟機都是按照可擴充的方式來實作的(通過-Xmx 和 -Xms控制)。

        如果在堆内沒有記憶體完成執行個體配置設定,并且堆也沒變法再擴充時,就會抛出OutOfMemoryError異常

1.5方法區

        方法區(Method Area)與java堆一樣,是各個線程共享的記憶體區域,它用于存儲已被虛拟機加載的類資訊、常量、靜态變量、即使編譯器邊以後的代碼等資料。雖然java虛拟機把方法區描述為堆的一個邏輯部分,但它還有一個别名叫做Non-Heap(非堆),目的應該是與java堆區分開。

        垃圾回收器擴充至方法區,hotspot垃圾回收器可以像管理java堆記憶體一樣管理這部分記憶體,在hotspot虛拟機上開發的人,習慣的把這塊兒區域成為永久代。java虛拟機規範對方法區的限制非常寬松,除了和java堆一樣不需要連續的記憶體和可以固定大小或者可擴充外,還可以選擇不實作垃圾回收。相對而言,垃圾回收器的行為在這一塊兒很少出現,但并發資料一旦進入方法區就如永久代名字一樣永久的存在了。這區域的記憶體回收目标主要是針對常量池的回收和對類型的解除安裝。一般來說,這個區域的回收難令人滿意,尤其是類型的解除安裝,條件相當苛刻。

        根據java虛拟機規範的規定,如果方法區無法滿足記憶體配置設定時,抛出OutOfMemoryError異常。

1.6運作時常量池

        運作時常量池是方法區的一部分。Class檔案中除了有類的版本,字段,方法,接口等描述資訊外,還有一項資訊是常量池。用于存放編譯器生成的各種字面量和符号引用,這部分内容将在類加載後進入方法區的運作時常量池中存放。

        java虛拟機對class沒一部分(自然也包括常量池)的格式都有嚴格規定,每一個位元組用于存儲哪種資料都必須符合規範上的要求才會被虛拟機認可,裝載和執行,但對于運作時常量池,java虛拟機規範沒有做任何細節的要求,不同的提供商實作的虛拟機可以按照自己的需要來實作這個記憶體區域。不過,一般來說,除了報春Class檔案中描述的符号引用外,還會把翻譯出來的直接引用也存儲在運作時常量池中。

        運作時常量池相對于Class檔案常量池和另外一個重要特性是具備動态性,java語言并不要求常量一定隻有編譯期才能産生,也就是并非預置入Class檔案中常量池的内容才能進入方法區運作時常量池,運作期間也可以将新的變量放入池中,這種特性被開發人員利用的較多的就是String類的intern()方法。既然運作時常量池是方法區的一部分,自然收到方法區記憶體的限制。

        當常量池中無法再申請到記憶體時會抛出OutOfMemoryError異常。

1.7直接記憶體

        直接記憶體并不是java虛拟機記憶體資料的一部分,也不是java虛拟機規範中定義的記憶體區域。伺服器管理者在配置虛拟機參數時,會根據實際記憶體設定-Xms等參數資訊,但經常忽略到直接記憶體,使得各個記憶體區域綜合大于實體記憶體限制,導緻OutOfMemoryError異常。

2 HotSpot虛拟機對象探秘

2.1對象的建立

        虛拟機遇到一條new指令時,首先将去檢查這個指令的參數是否能在常量池中定位到這個類的引用,并且檢查這個符号引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。

        在類加載檢查通過後,接下來虛拟機将為新生對象配置設定記憶體。對象所需記憶體的大小在類加載完成後便可完全确定,為對象配置設定空間的任務等同于把一塊兒确定大小的記憶體從JAVA堆中配置設定出來。有兩種配置設定方式,一種稱為指針碰撞,另一種稱為空閑清單。選擇哪種配置設定方式由java堆是否規整決定,而java堆是否規整又由所采用的垃圾回收期是否帶有壓縮整理功能決定。是以,在使用Serial、parNewd等帶Compact過程的回收器時,系統采用的配置設定算法是指針碰撞,而使用CMS這種基于Mark-Sweep算法的回收器時,通常采用空閑清單。

        除了如何劃分可用空間外,還有另外一個需要考慮的問題就是對象建立在虛拟機中是非常頻繁的行為,即使是僅僅修改一個指針所指向的位置,在并發情況下也并不是線程安全的,可能出現正在給對象A配置設定記憶體,指針還沒來得及修改,對象B又同時使用了原來的指針來配置設定記憶體的情況。解決這個問題有兩種方案,一種是對配置設定記憶體空間的動作進行同步處理--實際上虛拟機采用CAS配上失敗重試的方式保證更新操作的原子性;另一種是把内從配置設定的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先配置設定已小塊兒記憶體,成為本地線程配置設定緩存(Thread Local Allocation Buffer,TLAB)。那個線程要配置設定記憶體,就在哪個線程的TLAB上配置設定,隻有TLAB用完并配置設定新的TLAB時,才需要同步鎖定。虛拟機是否使用TLAB,可以通過-XX:+/-UseTLAB參數來設定。

        記憶體配置設定完成後,虛拟機需要将配置設定到的記憶體空間都初始化為零值(不包括對象頭),如果使用TLAB,這一工作過程也可以提前至TLAB配置設定時進行。這一步操作保證了對象的執行個體字段在java代碼中可以不指派就直接使用,程式能通路到這些字段的資料類型所對應的零值。

        接下來,虛拟機要對對象進行必要的設定,例如這個對象是哪個類的實力,如何才能找到這個類的中繼資料資訊,對象的哈希嗎,對象的GC分代年齡等資訊。這些資訊存放在對象頭中。根據虛拟機目前的運作狀态的不同,如是否啟用偏向鎖等,對象頭會有不同的設定方式。稍後詳細介紹。

        在上面的工作完成以後,從虛拟機的角度來說,一個新的對象就已經産生了,但從java程式的視角來看,對象的建立才剛剛開始--<init>方法還沒有執行,所有的字段都還為零。是以,一般來說(由位元組碼中是否跟随invokespecial指令所決定),執行new指令之後會接着執行<init>方法,把對象按照程式員的醫院進行初始化,這樣一個可用的對象才算完全産生出來

2.2對象的記憶體布局

       在HotSpot虛拟機中,對象在記憶體中存儲的布局可以分為3塊兒區域:對象頭(Header)、執行個體資料(Instance Data)和對齊填充(Padding)。

       HotSpot虛拟機的對象頭包括兩部分資訊,第一部分用于存儲對象自身的運作時資料,如哈希嗎,GC分代年齡,鎖狀态标志,線程持有的鎖,偏向線程ID,偏向時間戳。對象頭的另外一部分是類型指針,即對象指向它的類中繼資料的指針,虛拟機通過這個指針來确定這個對象是哪個類的執行個體。如果對象是一個java數組,那在對象頭中還必須有一塊兒用于記錄數組長度的資料,因為虛拟機可以通過普通java對象的中繼資料資訊确定java對象的大小,但是從數組的中繼資料中無法确定數組的大小。

       執行個體資料部分是對象真正存儲的有效資訊,也是在程式代碼中所定義的各種類型的字段内容。無論是從父類內建下來的,還是在子類中定義的,都需要記錄起來。

       第三部分對齊填充并不是必然存在的,也沒有特别的含義,它僅僅起着占位符的作用。由于HotSpot VM的自動記憶體管理系統要求對象的起始位址必須是8位元組的整數倍。也就是說,對象的大小必須是8位元組的正如倍。而對象頭部分正好是8位元組的倍數,是以,對象執行個體資料部分沒有對其時,就需要通過對齊填充來補全。

2.3對象的通路定位

       建立對象是為了使用對象,我們的java程式需要通過棧上的reference資料來操作堆上的具體對象。由于reference類型在java虛拟機規範中隻規定了一個指向對象的引用,并沒有定義這個這個應用應該通過何種方式去定位,通路堆中的對象的具體位置,是以對象通路方式也取決于虛拟機實作而定的。目前主流的通路方式有使用句柄和直接指針兩種。

        如果使用句柄通路的話,那麼java堆中将會劃分出一塊兒記憶體在作為句柄池,reference中儲存的就是對象句柄的位址,而句柄中包含了對象執行個體資料與類型資料各自的具體位址資訊。如下圖

JVM記憶體模型1記憶體模型2 HotSpot虛拟機對象探秘

         如果使用直接指針通路,那麼java堆對象的布局中就必須考慮如何放置通路類型資料的相關資訊,而reference中存儲的直接就是對象位址。

JVM記憶體模型1記憶體模型2 HotSpot虛拟機對象探秘

         這兩種對象通路方式各有優勢,使用句柄來通路的最大好處就是reference中存儲的是穩定的句柄位址,在對象被移動(垃圾回收時移動對象是非常普遍的行為)時隻會改變句柄中的執行個體資料指針,而reference本身不需要修改。

         使用直接指針通路的最大好處就是速度更快,它節省了一次指針定位的時間開銷,對于對象的通路在java中非常頻繁,是以這類開銷積少成多後也是一項非常客觀的執行成本。HotSpot是用第二種方式通路對象的。