|
堆和棧的概念可以說(shuō)是 Java 開(kāi)發(fā)底層的一大問(wèn)題了。今天和一個(gè)復(fù)旦的哥們?cè)谟懻摶緮?shù)據(jù)類(lèi)型在堆棧中的存儲(chǔ)問(wèn)題,以及明白了這個(gè)問(wèn)題對(duì)于用戶(hù)(程序員)來(lái)說(shuō)有何意義。
順便總結(jié)一下堆棧相關(guān)的知識(shí)。google 了很多,學(xué)習(xí)了很多,學(xué)習(xí) Java 堆棧知識(shí),看這篇就夠了!
- 棧內(nèi)存用來(lái)存儲(chǔ)局部變量和方法調(diào)用。
- 而堆內(nèi)存用來(lái)存儲(chǔ) Java 中的對(duì)象。無(wú)論是成員變量,局部變量,還是類(lèi)變量,它們指向的對(duì)象都存儲(chǔ)在堆內(nèi)存中。
- 異常錯(cuò)誤不同
如果棧內(nèi)存或者堆內(nèi)存不足都會(huì)拋出異常。
- 棧空間不足:java.lang.StackOverFlowError。
- 堆空間不足:java.lang.OutOfMemoryError。
- 空間大小
棧的空間大小遠(yuǎn)遠(yuǎn)小于堆的。
棧幀由三部分組成:局部變量區(qū)、操作數(shù)棧、幀數(shù)據(jù)區(qū)。局部變量區(qū)和操作數(shù)棧的大小要視對(duì)應(yīng)的方法而定,他們是按字長(zhǎng)計(jì)算的。但調(diào)用一個(gè)方法時(shí),它從類(lèi)型信息中得到此方法局部變量區(qū)和操作數(shù)棧大小,并據(jù)此分配棧內(nèi)存,然后壓入 Java 棧。
- 局部變量區(qū)
局部變量區(qū)被組織為以一個(gè)字長(zhǎng)為單位、從 0 開(kāi)始計(jì)數(shù)的數(shù)組,類(lèi)型為 short、byte 和 char 的值在存入數(shù)組前要被轉(zhuǎn)換成 int 值,而 long 和 double 在數(shù)組中占據(jù)連續(xù)的兩項(xiàng),在訪(fǎng)問(wèn)局部變量中的 long 或 double 時(shí),只需取出連續(xù)兩項(xiàng)的第一項(xiàng)的索引值即可, 如某個(gè) long 值在局部變量區(qū)中占據(jù)的索引時(shí) 3、4 項(xiàng),取值時(shí),指令只需取索引為 3 的 long 值即可。
如下代碼以及圖所示:
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;
}

- 操作數(shù)棧
和局部變量區(qū)一樣,操作數(shù)棧也被組織成一個(gè)以字長(zhǎng)為單位的數(shù)組。但和前者不同的是,它不是通過(guò)索引來(lái)訪(fǎng)問(wèn)的,而是通過(guò)入棧和出棧來(lái)訪(fǎng)問(wèn)的。可把操作數(shù)棧理解為存儲(chǔ)計(jì)算時(shí),臨時(shí)數(shù)據(jù)的存儲(chǔ)區(qū)域。下面我們通過(guò)一段簡(jiǎn)短的程序片段外加一幅圖片來(lái)了解下操作數(shù)棧的作用。
int a = 1;
int b = 98;
int c = a+b;

從圖中可以得出:操作數(shù)棧其實(shí)就是個(gè)臨時(shí)數(shù)據(jù)存儲(chǔ)區(qū)域,它是通過(guò)入棧和出棧來(lái)進(jìn)行操作的。
- 幀數(shù)據(jù)區(qū)
除了局部變量區(qū)和操作數(shù)棧外,java 棧幀還需要一些數(shù)據(jù)來(lái)支持常量池解析、正常方法返回以及異常派發(fā)機(jī)制。這些數(shù)據(jù)都保存在 java 棧幀的幀數(shù)據(jù)區(qū)中。
當(dāng) JVM 執(zhí)行到需要常量池?cái)?shù)據(jù)的指令時(shí),它都會(huì)通過(guò)幀數(shù)據(jù)區(qū)中指向常量池的指針來(lái)訪(fǎng)問(wèn)它。
除了處理常量池解析外,幀里的數(shù)據(jù)還要處理 java 方法的正常結(jié)束和異常終止。如果是通過(guò) return 正常結(jié)束,則當(dāng)前棧幀從 Java 棧中彈出,恢復(fù)發(fā)起調(diào)用的方法的棧。如果方法又返回值,JVM 會(huì)把返回值壓入到發(fā)起調(diào)用方法的操作數(shù)棧。
為了處理 java 方法中的異常情況,幀數(shù)據(jù)區(qū)還必須保存一個(gè)對(duì)此方法異常引用表的引用。當(dāng)異常拋出時(shí),JVM 給 catch 塊中的代碼。如果沒(méi)發(fā)現(xiàn),方法立即終止,然后 JVM 用幀區(qū)數(shù)據(jù)的信息恢復(fù)發(fā)起調(diào)用的方法的幀。然后再發(fā)起調(diào)用方法的上下文重新拋出同樣的異常。
- 只有在調(diào)用一個(gè)方法時(shí),才為當(dāng)前棧分配一個(gè)幀,然后將該幀壓入棧
- 幀中存儲(chǔ)了對(duì)應(yīng)方法的局部數(shù)據(jù),方法執(zhí)行完,對(duì)應(yīng)的幀則從棧中彈出,并把返回結(jié)果存儲(chǔ)在調(diào)用 方法的幀的操作數(shù)棧中
一、Java 中的基本數(shù)據(jù)類(lèi)型一定存儲(chǔ)在棧中嗎?
不一定。棧內(nèi)存用來(lái)存儲(chǔ)局部變量和方法調(diào)用。
如果該局部變量是基本數(shù)據(jù)類(lèi)型例如
那么直接將該值存儲(chǔ)在棧中。
如果該局部變量是一個(gè)對(duì)象如
int[] array=new int[]{1,2};
那么將引用存在棧中而對(duì)象 ({1,2}) 存儲(chǔ)在堆內(nèi)。
二、棧的速度比堆快嗎?
參考《Pro .NET Performance》可知:
Contrary to popular belief, there isn’t that much of a difference between stacks and heaps in a .NET process. Stacks and heaps are nothing more than ranges of addresses in virtual memory, and there is no inherent advantage in the range of addresses reserved to the stack of a particular thread compared to the range of addresses reserved for the managed heap. Accessing a memory location on the heap is neither faster nor slower than accessing a memory location on the stack. There are several considerations that might, in certain cases, support the claim that memory access to stack locations is faster, overall, than memory access to heap locations. Among them:
- On the stack, temporal allocation locality (allocations made close together in time) implies spatial locality (storage that is close together in space). In turn, when temporal allocation locality implies temporal access locality (objects allocated together are accessed together), the sequential stack storage tends to perform better with respect to CPU caches and operating system paging systems.
- Memory density on the stack tends to be higher than on the heap because of the reference type overhead (discussed later in this chapter). Higher memory density often leads to better performance, e.g., because more objects fit in the CPU cache.
- Thread stacks tend to be fairly small – the default maximum stack size on Windows is 1MB, and most threads tend to actually use only a few stack pages. On modern systems, the stacks of all application threads can fit into the CPU cache, making typical stack object access extremely fast. (Entire heaps, on the other hand, rarely fit into CPU caches.)
With that said, you should not be moving all your allocations to the stack! Thread stacks on Windows are limited, and it is easy to exhaust the stack by applying injudicious recursion and large stack allocations.
即一定情況下棧的速度是比堆快的,但是快的并不明顯。畢竟都是 RAM。所以這算不上堆和棧的一大區(qū)別。
回到最初,我和復(fù)旦哥們的討論。基本類(lèi)型數(shù)據(jù)如果是局部變量并且非對(duì)象那么 JVM 中是把值直接存入棧中的而不是存儲(chǔ)一個(gè)引用對(duì)象然后借由這個(gè)對(duì)象來(lái)找到值。這其實(shí)算的上是實(shí)際運(yùn)行時(shí) JVM 提供的性能優(yōu)化。因此基本數(shù)據(jù)類(lèi)型和引用類(lèi)型在棧中的存儲(chǔ)情況就是不一樣的了。
但是這些不一樣,對(duì)于用戶(hù)(程序員)來(lái)說(shuō)是透明的。所以如果僅僅從語(yǔ)義的角度把基本類(lèi)型看成引用類(lèi)型,雖然不夠嚴(yán)謹(jǐn),但是對(duì)于使用者(程序員)來(lái)說(shuō)有利于理解和學(xué)習(xí)。
|