從基礎到深入案例,成功收穫美團,小米offer
直擊面試
反正我是帶著這些問題往下讀的
- 說一下 JVM 執行時資料區吧,都有哪些區?分別是幹什麼的?
- Java 8 的記憶體分代改進
- 舉例棧溢位的情況?
- 調整棧大小,就能儲存不出現溢位嗎?
- 分配的棧記憶體越大越好嗎?
- 垃圾回收是否會涉及到虛擬機器棧?
- 方法中定義的區域性變數是否執行緒安全?
執行時資料區
記憶體是非常重要的系統資源,是硬碟和 CPU 的中間倉庫及橋樑,承載著作業系統和應用程式的實時執行。JVM 記憶體佈局規定了 Java 在執行過程中記憶體申請、分配、管理的策略,保證了 JVM 的高效穩定執行。不同的 JVM 對於記憶體的劃分方式和管理機制存在著部分差異。
下圖是 JVM 整體架構,中間部分就是 Java 虛擬機器定義的各種執行時資料區域。
jvm-framework
Java 虛擬機器定義了若干種程式執行期間會使用到的執行時資料區,其中有一些會隨著虛擬機器啟動而建立,隨著虛擬機器退出而銷燬。另外一些則是與執行緒一一對應的,這些與執行緒一一對應的資料區域會隨著執行緒開始和結束而建立和銷燬。
- 執行緒私有:程式計數器、棧、本地棧
- 執行緒共享:堆、堆外記憶體(永久代或元空間、程式碼快取)
下面我們就來一一解毒下這些記憶體區域,先從最簡單的入手
一、程式計數器
程式計數暫存器(Program Counter Register),Register 的命名源於 CPU 的暫存器,暫存器儲存指令相關的執行緒資訊,CPU 只有把資料裝載到暫存器才能夠執行。
這裡,並非是廣義上所指的物理暫存器,叫程式計數器(或PC計數器或指令計數器)會更加貼切,並且也不容易引起一些不必要的誤會。JVM 中的 PC 暫存器是對物理 PC 暫存器的一種抽象模擬。
程式計數器是一塊較小的記憶體空間,可以看作是當前執行緒所執行的位元組碼的行號指示器。
1.1 作用
PC 暫存器用來儲存指向下一條指令的地址,即將要執行的指令程式碼。由執行引擎讀取下一條指令。
jvm-pc-counter
(分析:進入class檔案所在目錄,執行javap -v xx.class反解析(或者通過IDEA外掛Jclasslib直接檢視,上圖),可以看到當前類對應的Code區(彙編指令)、本地變量表、異常表和程式碼行偏移量對映表、常量池等資訊。)
1.2 概述
- 它是一塊很小的記憶體空間,幾乎可以忽略不計。也是執行速度最快的儲存區域
- 在 JVM 規範中,每個執行緒都有它自己的程式計數器,是執行緒私有的,生命週期與執行緒的生命週期一致
- 任何時間一個執行緒都只有一個方法在執行,也就是所謂的當前方法。如果當前執行緒正在執行的是 Java 方法,程式計數器記錄的是 JVM 位元組碼指令地址,如果是執行 natice 方法,則是未指定值(undefined)
- 它是程式控制流的指示器,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成
- 位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令
- 它是唯一一個在 JVM 規範中沒有規定任何 OutOfMemoryError 情況的區域
:使用PC暫存器儲存位元組碼指令地址有什麼用呢?為什麼使用PC暫存器記錄當前執行緒的執行地址呢?
♂?:因為CPU需要不停的切換各個執行緒,這時候切換回來以後,就得知道接著從哪開始繼續執行。JVM的位元組碼直譯器就需要通過改變PC暫存器的值來明確下一條應該執行什麼樣的位元組碼指令。
:PC暫存器為什麼會被設定為執行緒私有的?
♂?:多執行緒在一個特定的時間段內只會執行其中某一個執行緒方法,CPU會不停的做任務切換,這樣必然會導致經常中斷或恢復。為了能夠準確的記錄各個執行緒正在執行的當前位元組碼指令地址,所以為每個執行緒都分配了一個PC暫存器,每個執行緒都獨立計算,不會互相影響。
二、虛擬機器棧
2.1 概述
Java 虛擬機器棧(Java Virtual Machine Stacks),早期也叫 Java 棧。每個執行緒在建立的時候都會建立一個虛擬機器棧,其內部儲存一個個的棧幀(Stack Frame),對應著一次次 Java 方法呼叫,是執行緒私有的,生命週期和執行緒一致。
作用:主管 Java 程式的執行,它儲存方法的區域性變數、部分結果,並參與方法的呼叫和返回。
特點:
- 棧是一種快速有效的分配儲存方式,訪問速度僅次於程式計數器
- JVM 直接對虛擬機器棧的操作只有兩個:每個方法執行,伴隨著入棧(進棧/壓棧),方法執行結束出棧
- 棧不存在垃圾回收問題
棧中可能出現的異常:
Java 虛擬機器規範允許?Java虛擬機器棧的大小是動態的或者是固定不變的
- 如果採用固定大小的 Java 虛擬機器棧,那每個執行緒的 Java 虛擬機器棧容量可以線上程建立的時候獨立選定。如果執行緒請求分配的棧容量超過 Java 虛擬機器棧允許的最大容量,Java 虛擬機器將會丟擲一個?StackOverflowError?異常
- 如果 Java 虛擬機器棧可以動態擴充套件,並且在嘗試擴充套件的時候無法申請到足夠的記憶體,或者在建立新的執行緒時沒有足夠的記憶體去建立對應的虛擬機器棧,那 Java 虛擬機器將會丟擲一個OutOfMemoryError異常
可以通過引數-Xss來設定執行緒的最大棧空間,棧的大小直接決定了函式呼叫的最大可達深度。
2.2 棧的儲存單位
棧中儲存什麼?
- 每個執行緒都有自己的棧,棧中的資料都是以棧幀(Stack Frame)的格式存在
- 在這個執行緒上正在執行的每個方法都各自有對應的一個棧幀
- 棧幀是一個記憶體區塊,是一個數據集,維繫著方法執行過程中的各種資料資訊
2.3 棧執行原理
- JVM 直接對 Java 棧的操作只有兩個,對棧幀的壓棧和出棧,遵循“先進後出/後進先出”原則
- 在一條活動執行緒中,一個時間點上,只會有一個活動的棧幀。即只有當前正在執行的方法的棧幀(棧頂棧幀)是有效的,這個棧幀被稱為當前棧幀(Current Frame),與當前棧幀對應的方法就是當前方法(Current Method),定義這個方法的類就是當前類(Current Class)
- 執行引擎執行的所有位元組碼指令只針對當前棧幀進行操作
- 如果在該方法中呼叫了其他方法,對應的新的棧幀會被創建出來,放在棧的頂端,稱為新的當前棧幀
- 不同執行緒中所包含的棧幀是不允許存在相互引用的,即不可能在一個棧幀中引用另外一個執行緒的棧幀
- 如果當前方法呼叫了其他方法,方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,接著,虛擬機器會丟棄當前棧幀,使得前一個棧幀重新成為當前棧幀
- Java 方法有兩種返回函式的方式,一種是正常的函式返回,使用 return 指令,另一種是丟擲異常,不管用哪種方式,都會導致棧幀被彈出