-從一道面試題開始學習JVM:Java最大棧深度有多大?要想回答該問題需要對jvm記憶體模型有一定的了解,在Java體系結......https://i.postimg.cc/NjfzfVtR/jvm.jpg
有JVM的記憶體結構我們可知:
随着線程棧的大小越大,能夠支援越多的方法調用,也即是能夠存儲更多的棧幀;
局部變量表内容越多,那麼棧幀就越大,棧深度就越小。
從Java運作時資料區域我們知道,線程中的虛拟機棧結構如下:

每個棧幀包含:本地變量表,操作數棧,動态連結,傳回位址等東西。也就是說棧調用深度越大,棧幀就越多,就越耗記憶體。
下面我們用一個測試例子來說明:
有如下遞歸方法:
我們設定啟動參數
-Xms256m -Xmx256m -Xmn128m -Xss256k
輸出内容:
可以發現,棧深度為1556的時候,就報 StackOverflowError了。
接下來我們調整-Xss線程棧大小為 512k,輸出内容:
發現棧深度變味了3249,說明了:
随着線程棧的大小越大,能夠支援越多的方法調用,也即是能夠存儲更多的棧幀。
這裡我們固定設定-Xss為256k。
我們知道此時的深度為:1556。
接下來我們給方法添加參數:
為何要添加參數呢,因為添加參數之後,棧幀中的本地變量表就會增加内容,我們可以嘗試使用以下指令檢視下Class檔案的彙編指令:
javap -v StackTest.class
可以發現<code>recursiveCalls</code>方法的本地變量表的确增加了,對應方法的入參 a:
這個時候我們在執行程式看看結果:
可以發現,棧深度由原來的1556程式設計了1318。
可以得出結論:
JVM Java Virtual Machine
JDK Java Development Kit
JRE Java Runtime Environment
直接上官網上的介紹的圖檔,一目了然。
JVM有2個特别有意思的特性,語言無關性和平台無關性。
語言無關性:是指實作了Java虛拟機規範的語言對可以在JVM上運作,如Groovy,和在大資料領域比較火的語言Scala,因為JVM最終運作的是class檔案,隻要最終的class檔案複合規範就可以在JVM上運作。
平台無關性:是指安裝在不同平台的JVM會把class檔案解釋為本地的機器指令,進而實作Write Once,Run Anywhere
Java虛拟機在執行Java程式的過程中會把它所管理的記憶體劃分為若幹個不同的資料區域。這些區域都有各自的用途,以及建立和銷毀的時間,有的區域随着虛拟機程序的啟動而存在,有些區域則依賴使用者線程的啟動和結束而建立和銷毀。
Java虛拟機所管理的記憶體将會包括以下幾個運作時資料區域
其中方法區和堆是所有線程共享的資料區程式計數器,虛拟機棧,本地方法棧是線程隔離的資料區,畫一個邏輯圖
程式計數器是一塊較小的記憶體空間,它可以看作是目前線程所執行的位元組碼的行号訓示器
為什麼要記錄目前線程所執行的位元組碼的行号?直接執行完不就可以了嗎?
因為代碼是線上程中運作的,線程有可能被挂起。即CPU一會執行線程A,線程A還沒有執行完被挂起了,接着執行線程B,最後又來執行線程A了,CPU得知道執行線程A的哪一部分指令,線程計數器會告訴CPU。
虛拟機棧存儲目前線程運作方法所需要的資料,指令,傳回位址等。
虛拟機棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀用于存儲局部變量表,操作數棧,動态連結,方法出口等資訊。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛拟機棧中從入棧道出棧的過程。
局部變量表是一組變量值存儲空間,用于存放方法參數和方法内部定義的局部變量,其中存放的資料的類型是編譯期可知的各種基本資料類型、對象引用(reference)和returnAddress類型(它指向了一條位元組碼指令的位址)。也即基本基本資料類型,則存在局部變量表中,如果是引用類型。如String,局部變量表中存的是引用,而執行個體在堆中。局部變量表所需的記憶體空間在編譯期間完成配置設定,即在Java程式被編譯成Class檔案時,就确定了所需配置設定的最大局部變量表的容量。當進入一個方法時,這個方法需要在棧中配置設定多大的局部變量空間是完全确定的,在方法運作期間不會改變局部變量表的大小。來看一個例子:引用類型(new出來的對象)的資料如何存儲的,
假如methodOne方法調用methodTwo方法時, 虛拟機棧的情況如下:
當虛拟機棧無法再放下棧幀的時候,就會出現StackOverflowError。
拓展: 局部變量表的容量以變量槽(Slot)為最小機關。在虛拟機規範中并沒有明确指明一個Slot應占用的記憶體空間大小(允許其随着處理器、作業系統或虛拟機的不同而發生變化),一個Slot可以存放一個32位以内的資料類型:boolean、byte、char、short、int、float、reference和returnAddresss。reference是對象的引用類型,returnAddress是為位元組指令服務的,它執行了一條位元組碼指令的位址。對于64位的資料類型(long和double),虛拟機會以高位在前的方式為其配置設定兩個連續的Slot空間。 虛拟機通過索引定位的方式使用局部變量表,索引值的範圍是從0開始到局部變量表最大的Slot數量,對于32位資料類型的變量,索引n代表第n個Slot,對于64位的,索引n代表第n和第n+1兩個Slot。 在方法執行時,虛拟機是使用局部變量表來完成參數值到參數變量清單的傳遞過程的,如果是執行個體方法(非static),則局部變量表中的第0位索引的Slot預設是用于傳遞方法所屬對象執行個體的引用,在方法中可以通過關鍵字“this”來通路這個隐含的參數。其餘參數則按照參數表的順序來排列,占用從1開始的局部變量Slot,參數表配置設定完畢後,再根據方法體内部定義的變量順序和作用域配置設定其餘的Slot。 局部變量表中的Slot是可重用的,方法體中定義的變量,作用域并不一定會覆寫整個方法體,如果目前位元組碼PC計數器的值已經超過了某個變量的作用域,那麼這個變量對應的Slot就可以交給其他變量使用。這樣的設計不僅僅是為了節省空間,在某些情況下Slot的複用會直接影響到系統的而垃圾收集行為。
操作數棧又常被稱為操作棧,操作數棧的最大深度也是在編譯的時候就确定了。32位資料類型所占的棧容量為1, 64位資料類型所占的棧容量為2。
當一個方法開始執行時,它的操作棧是空的,在方法的執行過程中,會有各種位元組碼指令(比如:加操作、指派元算等)向操作棧中寫入和提取内容,也就是入棧和出棧操作。
Java虛拟機的解釋執行引擎稱為“基于棧的執行引擎”,其中所指的“棧”就是操作數棧。是以我們也稱Java虛拟機是基于棧的,這點不同于Android虛拟機,Android虛拟機是基于寄存器的。
基于棧的指令集最主要的優點是可移植性強,主要的缺點是執行速度相對會慢些;
由于寄存器由硬體直接提供,是以基于寄存器指令集最主要的優點是執行速度快,主要的缺點是可移植性差。
接着解釋一下操作數棧,還是比較容易了解的。假如Test.java中有如下方法,
反編譯生成的Test.class檔案,并輸出到show.txt中
show.txt的内容如下,簡單2個數相加都會用到棧,這個棧就是操作數棧。
每個棧幀都包含一個指向運作時常量池(在方法區中,後面介紹)中該棧幀所屬方法的引用,持有這個引用是為了支援方法調用過程中的動态連接配接。Class檔案的常量池中存在有大量的符号引用,位元組碼中的方法調用指令就以常量池中指向方法的符号引用為參數。這些符号引用,一部分會在類加載階段或第一次使用的時候轉化為直接引用(如final、static域等),稱為靜态解析,另一部分将在每一次的運作期間轉化為直接引用,這部分稱為動态連接配接。
當一個方法被執行後,有兩種方式退出該方法:執行引擎遇到了任意一個方法傳回的位元組碼指令或遇到了異常,并且該異常沒有在方法體内得到處理。無論采用何種退出方式,在方法退出之後,都需要傳回到方法被調用的位置,程式才能繼續執行。方法傳回時可能需要在棧幀中儲存一些資訊,用來幫助恢複它的上層方法的執行狀态。一般來說,方法正常退出時,調用者的PC計數器的值就可以作為傳回位址,棧幀中很可能儲存了這個計數器值,而方法異常退出時,傳回位址是要通過異常處理器來确定的,棧幀中一般不會儲存這部分資訊。
方法退出的過程實際上等同于把目前棧幀出站,是以退出時可能執行的操作有:恢複上層方法的局部變量表和操作數棧,如果有傳回值,則把它壓入調用者棧幀的操作數棧中,調整PC計數器的值以指向方法調用指令後面的一條指令。
本地方法棧(Native Method Stack)與虛拟機棧發揮的作用是非常相似的,他們之間的差別不過是:
虛拟機棧為虛拟機執行Java方法(也就是位元組碼)服務,
而本地方法棧則為虛拟機使用到的Native方法服務。
對于大多數應用來說,Java堆(Java Heap)是Java虛拟機鎖管理的記憶體中最大的一塊。
Java堆是所有線程共享的一塊記憶體區域,在虛拟機啟動時建立,在JVM中隻有一個。
此記憶體區域的唯一目的就是存放對象執行個體以及數組(當然,數組引用是存放在Java棧中的),幾乎所有的對象執行個體都在這裡配置設定記憶體。
這部分空間也是Java垃圾收集器管理的主要區域。
方法區(Method Area)與Java堆一樣,是各個線程共享的記憶體區域,它用于存儲以下資訊(不僅限于):
已被虛拟機加載的類的資訊(包括類的名稱、方法資訊、字段資訊)
靜态變量
常量
編譯器編譯後的代碼。
在Class檔案中除了類的字段、方法、接口等描述資訊外,還有一項資訊是常量池,用來存儲編譯期間生成的字面量和符号引用。
在方法區中有一個非常重要的部分就是運作時常量池,它是每一個類或接口的常量池的運作時表示形式,在類和接口被加載到JVM後,對應的運作時常量池就被建立出來。當然并非Class檔案常量池中的内容才能進入運作時常量池,在運作期間也可将新的常量放入運作時常量池中,比如String的intern方法。
在JVM規範中,沒有強制要求方法區必須實作垃圾回收。很多人習慣将方法區稱為“永久代”,是因為HotSpot虛拟機以永久代來實作方法區,進而JVM的垃圾收集器可以像管理堆區一樣管理這部分區域,進而不需要專門為這部分設計垃圾回收機制。不過自從JDK7之後,Hotspot虛拟機便将運作時常量池從永久代移除了。
它是JVM用來存儲對象執行個體以及數組值的區域,可以認為Java中所有通過new建立的對象的記憶體都在此配置設定,Heap中的對象的記憶體需要等待GC進行回收。
(1) 堆是JVM中所有線程共享的,是以在其上進行對象記憶體的配置設定均需要進行加鎖,這也導緻了new對象的開銷是比較大的
(2) Sun Hotspot JVM為了提升對象記憶體配置設定的效率,對于所建立的線程都會配置設定一塊獨立的空間TLAB(Thread Local Allocation Buffer),其大小由JVM根據運作的情況計算而得,在TLAB上配置設定對象時不需要加鎖,是以JVM在給線程的對象配置設定記憶體時會盡量的在TLAB上配置設定,在這種情況下JVM中配置設定對象記憶體的性能和C基本是一樣高效的,但如果對象過大的話則仍然是直接使用堆空間配置設定
(3) TLAB僅作用于新生代的Eden Space,是以在編寫Java程式時,通常多個小的對象比大的對象配置設定起來更加高效。
(4) 所有新建立的Object 都将會存儲在新生代Yong Generation中。如果Young Generation的資料在一次或多次GC後存活下來,那麼将被轉移到OldGeneration。新的Object總是建立在Eden Space。
由顔色可以看出,jdk1.8之前,堆記憶體被分為新生代,老年代,永久代,jdk1.8及以後堆記憶體被分成了新生代和老年代和元空間,元空間可以了解為直接的實體記憶體。新生代的區域又分為eden區,s0區,s1區,預設比例是8:1:1,
GC (Garbage Collection)的基本原理:将記憶體中不再被使用的對象進行回收,GC中用于回收的方法稱為收集器,由于GC需要消耗一些資源和時間,Java在對對象的生命周期特征進行分析後,按照新生代、舊生代的方式來對對象進行收集,以盡可能的縮短GC對應用造成的暫停
(1)對新生代的對象的收集稱為minor GC;
(2)對舊生代的對象的收集稱為Full GC;
(3)程式中主動調用System.gc()強制執行的GC為Full GC。
不同的對象引用類型, GC會采用不同的方法進行回收,JVM對象的引用分為了四種類型:
(1)強引用:預設情況下,對象采用的均為強引用(這個對象的執行個體沒有其他對象引用,GC時才會被回收)
(2)軟引用:軟引用是Java中提供的一種比較适合于緩存場景的應用(隻有在記憶體不夠用的情況下才會被GC)
(3)弱引用:在GC時一定會被GC回收
(4)虛引用:由于虛引用隻是用來得知對象是否被GC
https://www.cnblogs.com/dolphin0520/p/3613043.html
https://blog.csdn.net/ns_code/article/details/17565503
https://www.itzhai.com/articles/how-stack-frame-can-a-thread-hold.html
https://www.itzhai.com/articles/how-java-runtime-data-area-works.html
https://zhuanlan.zhihu.com/p/109794172
了解更多知識,關注我。