方法棧
方法棧并不是某一個 JVM 的內存空間,而是我們描述方法被調用過程的一個邏輯概念。
在同一個線程內,T1()調用T2():
- T1()先開始,T2()后開始;
- T2()先結束,T1()后結束。
堆和棧概述
從英文單詞角度來說
- 棧:stack
- 堆:heap
從數據結構角度來說
- 棧和堆一樣:都是先進后出,后進先出的數據結構
從 JVM 內存空間結構角度來說
- 棧:通常指 Java 方法棧,存放方法每一次執行時生成的棧幀。
- 堆:JVM 中存放對象的內存空間。包括新生代、老年代、永久代等組成部分。?
?棧幀
棧幀存儲的數據
方法在本次執行過程中所用到的局部變量、動態鏈接、方法出口等信息。棧幀中主要保存3 類數據:
本地變量(Local Variables):輸入參數和輸出參數以及方法內的變量。
棧操作(Operand Stack):記錄出棧、入棧的操作。
棧幀數據(Frame Data):包括類文件、方法等等。
棧幀的結構
- 局部變量表:方法執行時的參數、方法體內聲明的局部變量
- 操作數棧:存儲中間運算結果,是一個臨時存儲空間
- 幀數據區:保存訪問常量池指針,異常處理表
棧幀工作機制
當一個方法 A 被調用時就產生了一個棧幀 F1,并被壓入到棧中,
A 方法又調用了 B 方法,于是產生棧幀 F2 也被壓入棧,
B 方法又調用了 C 方法,于是產生棧幀 F3 也被壓入棧,
……
C 方法執行完畢后,彈出 F3 棧幀;
B 方法執行完畢后,彈出 F2 棧幀;
A 方法執行完畢后,彈出 F1棧幀;
……
遵循“先進后出”或者“后進先出”原則。
圖示在一個棧中有兩個棧幀:
棧幀 2 是最先被調用的方法,先入棧,
然后方法 2 又調用了方法 1,棧幀 1 處于棧頂的位置,
棧幀 2 處于棧底,執行完畢后,依次彈出棧幀 1 和棧幀 2,
線程結束,棧釋放。
每執行一個方法都會產生一個棧幀,保存到棧的頂部,頂部棧就是當前方法,該方法執行完畢后會自動將此棧幀出棧。
典型案例
請預測下面代碼打印的結果:34
int n = 10;
n += (n++) + (++n);
System.out.println(n);
實際執行結果:32
使用 javap 命令查看字節碼文件內容:
D:\record-video-original\day03\code>javap -c Demo03JavaStackExample.class
Compiled from "Demo03JavaStackExample.java"
public class Demo03JavaStackExample{
public Demo03JavaStackExample();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>: ()V
4: returnpublic static void main(java.lang.String[]);
Code:
0: bipush 10
2: istore_1
3: iload_1
4: iload_1
5: iinc 1, 1
8: iinc 1, 1
11: iload_1
12: iadd
13: iadd
14: istore_1
15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_1
19: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
22: return
}
內存執行過程分析:
棧溢出異常
異常名稱
java.lang.StackOverflowError
異常產生的原因
下面的例子是一個沒有退出機制的遞歸:
public class StackOverFlowTest {public static void main(String[] args) {methodInvokeToDie();}public static void methodInvokeToDie() {methodInvokeToDie();}}
拋出的異常信息:
Exception in thread "main" java.lang.StackOverflowError at com.atguigu.jvm.test.StackOverFlowTest.methodInvokeToDie(StackOverFlowTest.java:10) at com.atguigu.jvm.test.StackOverFlowTest.methodInvokeToDie(StackOverFlowTest.java:10) at com.atguigu.jvm.test.StackOverFlowTest.methodInvokeToDie(StackOverFlowTest.java:10) at com.atguigu.jvm.test.StackOverFlowTest.methodInvokeToDie(StackOverFlowTest.java:10) at com.atguigu.jvm.test.StackOverFlowTest.methodInvokeToDie(StackOverFlowTest.java:10)
原因總結:方法每一次調用都會在棧空間中申請一個棧幀,來保存本次方法執行時所需要用到的數據。但是一個沒有退出機制的遞歸調用,會不斷申請新的空間,而又不釋放空間,這樣遲早會把當前線程在棧內存中自己的空間耗盡。
棧空間的線程私有驗證
提出問題
某一個線程拋出『棧溢出異常』,會導致其他線程也崩潰嗎?從以往的經驗中我們判斷應該是不會,下面通過代碼來實際驗證一下。
代碼
new Thread(()->{while(true) {try {TimeUnit.SECONDS.sleep(2);System.out.println(Thread.currentThread().getName() + " working");} catch (InterruptedException e) {e.printStackTrace();}}
}, "thread-01").start();new Thread(()->{while(true) {try {TimeUnit.SECONDS.sleep(2);// 遞歸調用一個沒有退出機制的遞歸方法methodInvokeToDie();System.out.println(Thread.currentThread().getName() + " working");} catch (InterruptedException e) {e.printStackTrace();}}
}, "thread-02").start();new Thread(()->{while(true) {try {TimeUnit.SECONDS.sleep(2);System.out.println(Thread.currentThread().getName() + " working");} catch (InterruptedException e) {e.printStackTrace();}}
}, "thread-03").start();
結論:02 線程拋異常終止后,01 和 03 線程仍然能夠繼續正常運行,說明 02 拋異常并沒有影響到 01 和 03,說明線程對棧內存空間的使用方式是彼此隔離的。每個線程都是在自己獨享的空間內運行,反過來也可以說,這個空間是當前線程私有的。
堆空間
堆空間工作機制
- 新創建的對象會被放在Eden區
- 當Eden區中已使用的空間達到一定比例,會觸發Minor GC
- 每一次在Minor GC中沒有被清理掉的對象就成了幸存者
- 幸存者對象會被轉移到幸存者區
- 幸存者區分成from區和to區
- from區快滿的時候,會將仍然在使用的對象轉移到to區
- 然后from和to這兩個指針彼此交換位置
口訣:復制必交換,誰空誰為to
- 如果一個對象,經歷15次GC仍然幸存,那么它將會被轉移到老年代
- 如果幸存者區已經滿了,即使某個對象尚不到15歲,仍然會被移動到老年代
- 最終效果:
- Eden區主要是生命周期很短的對象來來往往
- 老年代主要是生命周期很長的對象,例如:IOC容器對象、線程池對象、數據庫連接池對象等等
- 幸存者區作為二者之間的過渡地帶
- 關于永久代:
- 從理論上來說屬于堆
- 從具體實現上來說不屬于堆
永久代在各個JDK版本之間的演變
永久代 | 常量池 | |
---|---|---|
≤JDK1.6 | 有 | 在方法區 |
=JDK1.7 | 有,但開始逐步“去永久代” | 在堆 |
≥JDK1.8 | 無 | 在元空間 |
方法區、元空間、永久代之間關系
堆、棧、方法區之間關系
堆溢出異常
異常名稱
java.lang.OutOfMemoryError,也往往簡稱為 OOM。
異常信息
- Java heap space:針對新生代、老年代整體進行Full GC后,內存空間還是放不下新產生的對象
- PermGen space:方法區中加載的類太多了(典型情況是框架創建的動態類太多,導致方法區溢出)
我們可以參考下面的控制臺日志打印:
[GC (Allocation Failure) 4478364K->4479044K(5161984K), 4.3454766 secs] [Full GC (Ergonomics) 4479044K->3862071K(5416448K), 39.3706285 secs] [Full GC (Ergonomics) 4410423K->4410422K(5416448K), 27.7039534 secs] [Full GC (Ergonomics) 4629575K->4621239K(5416448K), 24.9298221 secs] [Full GC (Allocation Failure) 4621239K->4621186K(5416448K), 29.0616791 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3210) at java.util.Arrays.copyOf(Arrays.java:3181) at java.util.ArrayList.grow(ArrayList.java:261) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227) at java.util.ArrayList.add(ArrayList.java:458) at com.atguigu.jvm.test.JavaHeapTest.main(JavaHeapTest.java:16)
小練習
測試代碼
查看下面程序在每個步驟中內存的狀態:
public class Review {// 靜態變量,類變量public static Review review = new Review();public void showMessage() {// 局部變量Review reviewLocal = new Review();}// 程序入口public static void main(String[] args) {// 局部變量Review reviewMain = new Review();// 通過局部變量調用對象的方法reviewMain.showMessage();// 手動 GCSystem.gc();}
}
各狀態分析
棧和堆的區別
堆和棧的區別主要體現在以下幾個方面。
- 內存分配方式
棧(stack)和堆(heap)都是內存中的一段區域,但它們的內存分配方式是不同的。棧是由程序自動創建和釋放的,通常用于存儲函數調用時的臨時變量、函數的返回地址等信息。而堆則是由程序員手動申請和釋放的,通常用于存儲程序中需要動態分配的內存(如動態數組、對象等)。
- 內存管理方式
棧的內存分配是按照“后進先出”的原則進行的,即最后一個進入棧的變量最先被釋放。因此,棧中的內存管理是由系統自動完成的,程序員不需要過多考慮內存的分配和釋放問題。堆的內存管理則需要程序員自行負責,使用完畢后必須手動釋放,否則會導致內存泄漏或其他問題。
- 內存大小
棧的容量較小,一般只有幾百KB到幾MB的空間,具體容量由操作系統和編譯器決定。相對而言,堆用于存儲較大的數據結構,大小一般比棧要大得多,可以動態擴展內存空間。但是,因為堆需要手動管理內存,如果不及時釋放,會導致內存泄漏,進而影響系統性能。
- 訪問速度
因為棧的內存分配是系統自動完成的,所以訪問速度相對堆更快。棧中的數據直接存放在系統內存中,而訪問堆中的數據需要通過指針進行間接訪問,會造成一定的時間損耗。此外,在多線程環境下,由于棧的線程獨享,所以不會發生競爭問題。而堆則需要考慮多線程并發訪問時的同步和互斥機制。
- 應用場景
棧適合用于存儲局部變量和函數調用,主要用于內存的臨時分配;而堆適合用于存儲需要動態分配和管理的數據結構,如動態數組、字符串、對象等。在實際開發中,應該根據具體的應用場景選擇合適的內存分配方式。