1. 程式人生 > >JVM調優(2)之基本概念

JVM調優(2)之基本概念

資料型別

Java虛擬機器中,資料型別可以分為兩類:基本型別引用型別
基本型別的變數儲存原始值,即:他代表的值就是數值本身;
引用型別的變數儲存引用值。“引用值”代表了某個物件的引用,而不是物件本身,物件本身存放在這個引用值所表示的地址的位置。

基本型別包括:

  1. byte
  2. short
  3. int
  4. long
  5. char
  6. float
  7. double
  8. boolean
  9. returnAddress

引用型別包括

  1. 類型別
  2. 介面型別
  3. 陣列。

堆與棧

堆和棧是程式執行的關鍵,很有必要把他們的關係說清楚。
enter description here

棧是執行時的單位,而堆是儲存的單位。
棧解決程式的執行問題,即程式如何執行,或者說如何處理資料;
堆解決的是資料儲存的問題,即資料怎麼放、放在哪兒。

在Java中一個執行緒就會相應有一個執行緒棧與之對應,這點很容易理解,因為不同的執行緒執行邏輯有所不同,因此需要一個獨立的執行緒棧。而堆則是所有執行緒共享的。棧因為是執行單位,因此裡面儲存的資訊都是跟當前執行緒(或程式)相關資訊的。包括區域性變數、程式執行狀態、方法返回值等等;而堆只負責儲存物件資訊。

為什麼要把堆和棧區分出來呢?棧中不是也可以儲存資料嗎?
第一,從軟體設計的角度看,JVM棧代表了處理邏輯,而JVM堆代表了資料。這樣分開,使得處理邏輯更為清晰。分而治之的思想。這種隔離、模組化的思想在軟體設計的方方面面都有體現。

第二,JVM堆與JVM棧的分離,使得JVM堆中的內容可以被多個JVM棧共享(也可以理解為多個執行緒訪問同一個物件)。這種共享的收益是很多的。一方面這種共享提供了一種有效的資料互動方式(如:共享記憶體),另一方面,JVM堆中的共享常量和快取可以被所有JVM棧訪問,節省了空間。

第三,JVM棧因為執行時的需要,比如儲存系統執行的上下文,需要進行地址段的劃分。由於JVM棧只能向上增長,因此就會限制住JVM棧儲存內容的能力。而JVM堆不同,JVM堆中的物件是可以根據需要動態增長的,因此JVM棧和JVM堆的拆分,使得動態增長成為可能,相應JVM棧中只需記錄JVM堆中的一個地址即可。

第四,面向物件就是JVM堆和JVM棧的完美結合。其實,面向物件方式的程式與以前結構化的程式在執行上沒有任何區別。但是,面向物件的引入,使得對待問題的思考方式發生了改變,而更接近於自然方式的思考。當我們把物件拆開,你會發現,物件的屬性其實就是資料,存放在JVM堆中;而物件的行為(方法),就是執行邏輯,放在JVM棧中。我們在編寫物件的時候,其實即編寫了資料結構,也編寫的處理資料的邏輯。不得不承認,面向物件的設計,確實很美。

JVM堆中存什麼?JVM棧中存什麼?

JVM堆中存的是物件。JVM棧中存的是基本資料型別和JVM堆中物件的引用。一個物件的大小是不可估計的,或者說是可以動態變化的,但是在JVM棧中,一個物件只對應了一個4btye的引用(JVM堆JVM棧分離的好處:))。

為什麼不把基本型別放JVM堆中呢?
因為其佔用的空間一般是1~8個位元組——需要空間比較少,而且因為是基本型別,所以不會出現動態增長的情況——長度固定,因此JVM棧中儲存就夠了,如果把他存在JVM堆中是沒有什麼意義的(還會浪費空間,後面說明)。可以這麼說,基本型別和物件的引用都是存放在JVM棧中,而且都是幾個位元組的一個數,因此在程式執行時,他們的處理方式是統一的。但是基本型別、物件引用和物件本身就有所區別了,因為一個是JVM棧中的資料一個是JVM堆中的資料。最常見的一個問題就是,Java中引數傳遞時的問題。

Java中的引數傳遞時傳值呢?還是傳引用?

要說明這個問題,先要明確兩點:

1.不要試圖與C進行類比,Java中沒有指標的概念

2.程式執行永遠都是在JVM棧中進行的,因而引數傳遞時,只存在傳遞基本型別和物件引用的問題。不會直接傳物件本身。

明確以上兩點後。Java在方法呼叫傳遞引數時,因為沒有指標,所以它都是進行傳值呼叫(這點可以參考C的傳值呼叫)。因此,很多書裡面都說Java是進行傳值呼叫,這點沒有問題,而且也簡化的C中複雜性。

但是傳引用的錯覺是如何造成的呢?在執行JVM棧中,基本型別和引用的處理是一樣的,都是傳值,所以,如果是傳引用的方法呼叫,也同時可以理解為“傳引用值”的傳值呼叫,即引用的處理跟基本型別是完全一樣的。但是當進入被呼叫方法時,被傳遞的這個引用的值,被程式解釋(或者查詢)到JVM堆中的物件,這個時候才對應到真正的物件。如果此時進行修改,修改的是引用對應的物件,而不是引用本身,即:修改的是JVM堆中的資料。所以這個修改是可以保持的了。

物件,從某種意義上說,是由基本型別組成的。可以把一個物件看作為一棵樹,物件的屬性如果還是物件,則還是一顆樹(即非葉子節點),基本型別則為樹的葉子節點。程式引數傳遞時,被傳遞的值本身都是不能進行修改的,但是,如果這個值是一個非葉子節點(即一個物件引用),則可以修改這個節點下面的所有內容。

JVM棧是程式執行最根本的東西。程式執行可以沒有JVM堆,但是不能沒有JVM棧。而JVM堆是為JVM棧進行資料儲存服務,說白了JVM堆就是一塊共享的記憶體。不過,正是因為JVM堆和JVM棧的分離的思想,才使得Java的垃圾回收成為可能。

Java中,JVM棧的大小通過-Xss來設定,當JVM棧中儲存資料比較多時,需要適當調大這個值,否則會出現java.lang.StackOverflowError異常。常見的出現這個異常的是無法返回的遞迴,因為此時JVM棧中儲存的資訊都是方法返回的記錄點。

Java棧的組成元素——棧幀

棧幀由三部分組成:

  1. 區域性變數區
  2. 運算元棧
  3. 幀資料區。

區域性變數區和運算元棧的大小要視對應的方法而定,他們是按字長計算的。但呼叫一個方法時,它從型別資訊中得到此方法區域性變數區和運算元棧大小,並據此分配棧記憶體,然後壓入Java棧。

區域性變數區:區域性變數區被組織為以一個字長為單位、從0開始計數的陣列,型別為short、byte和char的值在存入陣列前要被轉換成int值,而long和double在陣列中佔據連續的兩項,在訪問區域性變數中的long或double時,只需取出連續兩項的第一項的索引值即可,如某個long值在區域性變數區中佔據的索引時3、4項,取值時,指令只需取索引為3的long值即可。

運算元棧和區域性變數區一樣,運算元棧也被組織成一個以字長為單位的陣列。但和前者不同的是,它不是通過索引來訪問的,而是通過入棧和出棧來訪問的。可把運算元棧理解為儲存計算時,臨時資料的儲存區域。下面我們通過一段簡短的程式片段外加一幅圖片來了解下運算元棧的作用。

Int a = 99;
Int b = 100;
Int c = a+b;


從圖中可以得出:運算元棧其實就是個臨時資料儲存區域,它是通過入棧和出棧來進行操作的。

幀資料區 除了區域性變數區和運算元棧外,java棧幀還需要一些資料來支援常量池解析、正常方法返回以及異常派發機制。這些資料都儲存在java棧幀的幀資料區中。當JVM執行到需要常量池資料的指令時,它都會通過幀資料區中指向常量池的指標來訪問它。

除了處理常量池解析外,幀裡的資料還要處理java方法的正常結束和異常終止。如果是通過return正常結束,則當前棧幀從Java棧中彈出,恢復發起呼叫的方法的棧。如果方法又返回值,JVM會把返回值壓入到發起呼叫方法的運算元棧。

為了處理java方法中的異常情況,幀資料區還必須儲存一個對此方法異常引用表的引用。當異常丟擲時,JVM給catch塊中的程式碼。如果沒發現,方法立即終止,然後JVM用幀區資料的資訊恢復發起呼叫的方法的幀。然後再發起呼叫方法的上下文重新丟擲同樣的異常。
==只有在呼叫一個方法時,才為當前棧分配一個幀,然後將該幀壓入棧==
==幀中儲存了對應方法的區域性資料,方法執行完,對應的幀則從棧中彈出,並把返回結果儲存在呼叫 方法的幀的運算元棧中==

Java物件的大小

基本資料的型別的大小是固定的,這裡就不多說了。
對於非基本型別的Java物件,其大小就值得商榷。
在Java中,一個空Object物件的大小是8byte,這個大小隻是儲存堆中一個沒有任何屬性的物件的大小。看下面語句:

Object ob = new Object();

這樣在程式中完成了一個Java物件的生命,但是它所佔的空間為:4byte+8byte。4byte是上面部分所說的Java棧中儲存引用的所需要的空間。而那8byte則是Java堆中物件的資訊。因為所有的Java非基本型別的物件都需要預設繼承Object物件,因此不論什麼樣的Java物件,其大小都必須是大於8byte。
有了Object物件的大小,我們就可以計算其他物件的大小了。

Class NewObject {
    int count;
    boolean flag;
    Object ob;
}

其大小為:空物件大小(8byte)+int大小(4byte)+Boolean大小(1byte)+空Object引用的大小(4byte)=17byte。但是因為Java在對物件記憶體分配時都是以8的整數倍來分,因此大於17byte的最接近8的整數倍的是24,因此此物件的大小為24byte。

這裡需要注意一下基本型別的包裝型別的大小。因為這種包裝型別已經成為物件了,因此需要把他們作為物件來看待。包裝型別的大小至少是12byte(宣告一個空Object至少需要的空間),而且12byte沒有包含任何有效資訊,同時,因為Java物件大小是8的整數倍,因此一個基本型別包裝類的大小至少是16byte。這個記憶體佔用是很恐怖的,它是使用基本型別的N倍(N>2),有些型別的記憶體佔用更是誇張(隨便想下就知道了)。因此,可能的話應儘量少使用包裝類。在JDK5.0以後,因為加入了自動型別裝換,因此,Java虛擬機器會在儲存方面進行相應的優化。

引用型別

物件引用型別分為

  1. 強引用
  2. 軟引用
  3. 弱引用
  4. 虛引用

強引用:就是我們一般宣告物件是時虛擬機器生成的引用,強引用環境下,垃圾回收時需要嚴格判斷當前物件是否被強引用,如果被強引用,則不會被垃圾回收

軟引用:軟引用一般被做為快取來使用。與強引用的區別是,軟引用在垃圾回收時,虛擬機器會根據當前系統的剩餘記憶體來決定是否對軟引用進行回收。如果剩餘記憶體比較緊張,則虛擬機器會回收軟引用所引用的空間;如果剩餘記憶體相對富裕,則不會進行回收。換句話說,虛擬機器在發生OutOfMemory時,肯定是沒有軟引用存在的。

弱引用:弱引用與軟引用類似,都是作為快取來使用。但與軟引用不同,弱引用在進行垃圾回收時,是一定會被回收掉的,因此其生命週期只存在於一個垃圾回收週期內。

強引用不用說,我們系統一般在使用時都是用的強引用。而“軟引用”和“弱引用”比較少見。他們一般被作為快取使用,而且一般是在記憶體大小比較受限的情況下做為快取。因為如果記憶體足夠大的話,可以直接使用強引用作為快取即可,同時可控性更高。因而,他們常見的是被使用在桌面應用系統的快取。

參考:
JVM調優總結(一)– 一些概念