JVM棧是運行時的單位,而JVM堆是存儲的單位。
JVM棧解決程序的運行問題,即程序如何執行,或者說如何處理數據;JVM堆解決的是數據存儲的問題,即數據怎么放、放在哪兒。
在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棧的完美結合。其實,面向對象方式的程序與以前結構化的程序在執行上沒有任何區別。但是,面向對象的引入,使得對待問題的思考方式發生了改變,而更接近于自然方式的思考。當我們把對象拆開,你會發現,對象的屬性其實就是數據,存放在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堆就是一塊共享的內存。不過,正是因為JVM堆和JVM棧的分離的思想,才使得Java的垃圾回收成為可能。
Java中,JVM棧的大小通過-Xss來設置,當JVM棧中存儲數據比較多時,需要適當調大這個值,否則會出現java.lang.StackOverflowError異常。常見的出現這個異常的是無法返回的遞歸,因為此時JVM棧中保存的信息都是方法返回的記錄點。
java棧的組成元素——棧幀
棧幀由三部分組成:局部變量區、操作數棧、幀數據區。局部變量區和操作數棧的大小要視對應的方法而定,他們是按字長計算的。但調用一個方法時,它從類型信息中得到此方法局部變量區和操作數棧大小,并據此分配棧內存,然后壓入Java棧。
局部變量區:局部變量區被組織為以一個字長為單位、從0開始計數的數組,類型為short、byte和char的值在存入數組前要被轉換成int值,而long和double在數組中占據連續的兩項,在訪問局部變量中的long或double時,只需取出連續兩項的第一項的索引值即可,如某個long值在局部變量區中占據的索引時3、4項,取值時,指令只需取索引為3的long值即可。
說再多也沒用,下面就看個例子,好讓大家對局部變量區有更深刻的認識。這個圖來著《深入JVM》:
public static int runClassMethod(int i,long l,float f,double d,Object o,byte b) {
return 0;
}
public int runInstanceMethod(char c,double d,short s,boolean b) {
return 0;
}
runInstanceMethod的局部變量區第一項是個reference(引用),它指定的就是對象本身的引用,也就是我們常用的this,但是在runClassMethod方法中,沒這個引用,那是因為runClassMethod是個靜態方法。
操作數棧和局部變量區一樣,操作數棧也被組織成一個以字長為單位的數組。但和前者不同的是,它不是通過索引來訪問的,而是通過入棧和出棧來訪問的??砂巡僮鲾禇@斫鉃榇鎯τ嬎銜r,臨時數據的存儲區域。下面我們通過一段簡短的程序片段外加一幅圖片來了解下操作數棧的作用。
Int a= 100;
Int b = 98;
Int c = a+b;
從圖中可以得出:操作數棧其實就是個臨時數據存儲區域,它是通過入棧和出棧來進行操作的。
幀數據區 除了局部變量區和操作數棧外,java棧幀還需要一些數據來支持常量池解析、正常方法返回以及異常派發機制。這些數據都保存在java棧幀的幀數據區中。當JVM執行到需要常量池數據的指令時,它都會通過幀數據區中指向常量池的指針來訪問它。
除了處理常量池解析外,幀里的數據還要處理java方法的正常結束和異常終止。如果是通過return正常結束,則當前棧幀從Java棧中彈出,恢復發起調用的方法的棧。如果方法又返回值,JVM會把返回值壓入到發起調用方法的操作數棧。
為了處理java方法中的異常情況,幀數據區還必須保存一個對此方法異常引用表的引用。當異常拋出時,JVM給catch塊中的代碼。如果沒發現,方法立即終止,然后JVM用幀區數據的信息恢復發起調用的方法的幀。然后再發起調用方法的上下文重新拋出同樣的異常。
class Example3C{
public static void addAndPrint(){
double result = addTwoTypes(1,88.88);
System.out.println(result);
}
public static double addTwoTypes(int i, double d){
return i+d;
}
}
1.只有在調用一個方法時,才為當前棧分配一個幀,然后將該幀壓入棧
2 幀中存儲了對應方法的局部數據,方法執行完,對應的幀則從棧中彈出,并把返回結果存儲在調用 方法的幀的操作數棧中