## 一些理解
堆是堆(heap),棧是棧(stack),堆棧是棧。嗯我很不喜歡“堆棧”這種叫法,容易讓新人掉坑里。JVM規范讓每個Java線程擁有自己的獨立的JVM棧,也就是Java方法的調用棧。同時JVM規范為了允許native代碼可以調用Java代碼,以及允許Java代碼調用native方法,還規定每個Java線程擁有自己的獨立的native方法棧。這倆都是JVM規范所規定的概念上的東西,并不是說具體的JVM實現真的要給每個Java線程開兩個獨立的棧。以Oracle JDK / OpenJDK的HotSpot VM為例,它使用所謂的“mixed stack”——在同一個調用棧里存放Java方法的棧幀與native方法的棧幀,所以每個Java線程其實只有一個調用棧,融合了JVM規范的JVM棧與native方法棧這倆概念。JVM里的“堆”(heap)特指用于存放Java對象的內存區域。所以根據這個定義,Java對象全部都在堆上。
要注意,這個“堆”并不是數據結構意義上的堆(Heap (data structure),一種有序的樹),而是動態內存分配意義上的堆——用于管理動態生命周期的內存區域。JVM的堆被同一個JVM實例中的所有Java線程共享。它通常由某種自動內存管理機制所管理,這種機制通常叫做“垃圾回收”(garbage collection,GC)。JVM規范并不強制要求JVM實現采用哪種GC算法。
## 堆和棧的區別
* 功能不同
棧內存用來存儲局部變量和方法調用。
而堆內存用來存儲Java中的對象。無論是成員變量,局部變量,還是類變量,它們指向的對象都存儲在堆內存中。
* 共享性不同
棧內存是線程私有的。
堆內存是所有線程共有的。
* 異常錯誤不同
如果棧內存或者堆內存不足都會拋出異常。
棧空間不足:java.lang.StackOverFlowError。
堆空間不足:java.lang.OutOfMemoryError。
* 空間大小
棧的空間大小遠遠小于堆的。
## 深入理解棧
棧幀由三部分組成:局部變量區、操作數棧、幀數據區。局部變量區和操作數棧的大小要視對應的方法而定,他們是按字長計算的。但調用一個方法時,它從類型信息中得到此方法局部變量區和操作數棧大小,并據此分配棧內存,然后壓入Java棧。
### 局部變量區
局部變量區被組織為以一個字長為單位、從0開始計數的數組,類型為short、byte和char的值在存入數組前要被轉換成int值,而long和double在數組中占據連續的兩項,在訪問局部變量中的long或double時,只需取出連續兩項的第一項的索引值即可,如某個long值在局部變量區中占據的索引時3、4項,取值時,指令只需取索引為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;
}
```

### 操作數棧
和局部變量區一樣,操作數棧也被組織成一個以字長為單位的數組。但和前者不同的是,它不是通過索引來訪問的,而是通過入棧和出棧來訪問的。可把操作數棧理解為存儲計算時,臨時數據的存儲區域。下面我們通過一段簡短的程序片段外加一幅圖片來了解下操作數棧的作用。
```
int a = 100;
int b = 98;
int c = a+b;
```

從圖中可以得出:操作數棧其實就是個臨時數據存儲區域,它是通過入棧和出棧來進行操作的。
### 幀數據區
除了局部變量區和操作數棧外,java棧幀還需要一些數據來支持常量池解析、正常方法返回以及異常派發機制。這些數據都保存在java棧幀的幀數據區中。
當JVM執行到需要常量池數據的指令時,它都會通過幀數據區中指向常量池的指針來訪問它。
除了處理常量池解析外,幀里的數據還要處理java方法的正常結束和異常終止。如果是通過return正常結束,則當前棧幀從Java棧中彈出,恢復發起調用的方法的棧。如果方法有返回值,JVM會把返回值壓入到發起調用方法的操作數棧。
為了處理java方法中的異常情況,幀數據區還必須保存一個對此方法異常引用表的引用。當異常拋出時,JVM給catch塊中的代碼。如果沒發現,方法立即終止,然后JVM用幀區數據的信息恢復發起調用的方法的幀。然后再發起調用方法的上下文重新拋出同樣的異常。
### 棧的整個結構
棧是由棧幀組成,每當線程調用一個java方法時,JVM就會在該線程對應的棧中壓入一個幀,而幀是由局部變量區、操作數棧和幀數據區組成。那在一個代碼塊中,棧到底是什么形式呢?下面是《深入JVM》中的一個例子:
```
public class Main{
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、幀中存儲了對應方法的局部數據,方法執行完,對應的幀則從棧中彈出,并把返回結果存儲在調用 方法的幀的操作數棧中。
## 從操作系統里進程的內存結構解釋
下圖是linux 中一個進程的虛擬內存分布:

圖中0號地址在最下邊,越往上內存地址越大。以32位地址操作系統為例,一個進程可擁有的虛擬內存地址范圍為0-2^32。分為兩部分,一部分留給kernel使用(kernel virtual memory),剩下的是進程本身使用, 即圖中的process virtual memory。普通Java 程序使用的就是process virtual memory。上圖中最頂端的一部分內存叫做user stack,中間有個 runtime heap, 他們的名字和數據結構里的stack 和 heap 幾乎沒啥關系。注意在上圖中,stack 是向下生長的; heap是向上生長的。當程序進行函數調用時,每個函數都在stack上有一個 call frame。比如對于以下程序,
```
public void foo(){
//do something...
println("haha"); // <<<=== 在這兒設置breakpoint 1
}
public void bar(){
foo();
}
main(){
bar();
println("hahaha"); // <<<=== 在這兒設置 breakpoint 2
}
```
當程序運行到breakponit1時,user stack 里會有三個frame:

其中 esp 和 ebp 都是寄存器。 esp 指向stack 的頂(因為stack 向下生長,esp會向下走); ebp 指向當前frame的邊界。當程序繼續執行到brekapoing 2的時候stack 大概是這樣的:

也就是說當一個函數執行結束后,它對應的call frame就被銷毀了。(其實就是esp 和 ebp分別移動,但是內存地址中的數據只有在下一次寫的時候才被覆蓋。)
說了這么多,終于該說什么東西放在stack 上什么東西放在heap 上了。最直白的解釋:
```
public void foo(){
int i = 0; // <= i 的值存在stack上,foo()的call frame 里。
Object obj = new Object(); // object 對象本身存在heap 里, foo()的call frame 里存該對象的地址。
}
```
## 常見誤區
### Java中的基本數據類型一定存儲在棧中嗎?
不一定。棧內存用來存儲局部變量和方法調用。
如果該局部變量是基本數據類型例如
:
```
int a = 1;
```
那么直接將該值存儲在棧中。
如果該局部變量是一個對象如
:
```
int[] array=new int[]{1,2};
```
那么,將引用存在棧中,而對象({1,2})存儲在堆內。
### 棧的速度比堆快嗎?
一定情況下棧的速度是比堆快的,但是快的并不明顯。畢竟都是RAM。所以這算不上堆和棧的一大區別。
## 結論
基本類型數據如果是局部變量并且非對象,那么JVM中是把值直接存入棧中的,而不是存儲一個引用對象然后借由這個對象來找到值。這其實算的上是實際運行時JVM提供的性能優化。因此基本數據類型和引用類型在棧中的存儲情況就是不一樣的了。
但是這些不一樣,對于用戶(程序員)來說是透明的。所以如果僅僅從語義的角度把基本類型看成引用類型,雖然不夠嚴謹,但是對于使用者(程序員)來說有利于理解和學習。
- java
- 設計模式
- 設計模式總覽
- 設計原則
- 工廠方法模式
- 抽象工廠模式
- 單例模式
- 建造者模式
- 原型模式
- 適配器模式
- 裝飾者模式
- 代理模式
- 外觀模式
- 橋接模式
- 組合模式
- 享元模式
- 策略模式
- 模板方法模式
- 觀察者模式
- 迭代子模式
- 責任鏈模式
- 命令模式
- 備忘錄模式
- 狀態模式
- 訪問者模式
- 中介者模式
- 解釋器模式
- 附錄
- JVM相關
- JVM內存結構
- Java虛擬機的內存組成以及堆內存介紹
- Java堆和棧
- 附錄-數據結構的堆棧和內存分配的堆區棧區的區別
- Java內存之Java 堆
- Java內存之虛擬機和內存區域概述
- Java 內存之方法區和運行時常量池
- Java 內存之直接內存(堆外內存)
- JAVA內存模型
- Java內存模型介紹
- 內存模型如何解決緩存一致性問題
- 深入理解Java內存模型——基礎
- 深入理解Java內存模型——重排序
- 深入理解Java內存模型——順序一致性
- 深入理解Java內存模型——volatile
- 深入理解Java內存模型——鎖
- 深入理解Java內存模型——final
- 深入理解Java內存模型——總結
- 內存可見性
- JAVA對象模型
- JVM內存結構 VS Java內存模型 VS Java對象模型
- Java的對象模型
- Java的對象頭
- HotSpot虛擬機
- HotSpot虛擬機對象探秘
- 深入分析Java的編譯原理
- Java虛擬機的鎖優化技術
- 對象和數組并不是都在堆上分配內存的
- 垃圾回收
- JVM內存管理及垃圾回收
- JVM 垃圾回收器工作原理及使用實例介紹
- JVM內存回收理論與實現(對象存活的判定)
- JVM參數及調優
- CMS GC日志分析
- JVM實用參數(一)JVM類型以及編譯器模式
- JVM實用參數(二)參數分類和即時(JIT)編譯器診斷
- JVM實用參數(三)打印所有XX參數及值
- JVM實用參數(四)內存調優
- JVM實用參數(五)新生代垃圾回收
- JVM實用參數(六) 吞吐量收集器
- JVM實用參數(七)CMS收集器
- JVM實用參數(八)GC日志
- Java性能調優原則
- JVM 優化經驗總結
- 面試題整理
- 面試題1
- java日志規約
- Spring安全
- OAtuth2.0簡介
- Spring Session 簡介(一)
- Spring Session 簡介(二)
- Spring Session 簡介(三)
- Spring Security 簡介(一)
- Spring Security 簡介(二)
- Spring Security 簡介(三)
- Spring Security 簡介(四)
- Spring Security 簡介(五)
- Spring Security Oauth2 (一)
- Spring Security Oauth2 (二)
- Spring Security Oauth2 (三)
- SpringBoot
- Shiro
- Shiro和Spring Security對比
- Shiro簡介
- Session、Cookie和Cache
- Web Socket
- Spring WebFlux