最近小農的朋友——小勇在找工做,開年來金三銀四,都想跳一跳,找個踏(gao)實(xin)點的工做,這不小勇也去面試了,不得不說,如今面試,各類底層各類原理,層出不窮,小勇就趕上了這麼一道面試題,由於沒有回答好,面試被PASS,讓他備受打擊,做爲大(lao)哥(si)哥(ji)的我,確定要安慰一下,究竟是什麼樣的面試題,讓小勇又一次夭折在面試的路上,好奇怪爲何要說又?簡直讓人喜極而泣,哈哈哈,言歸正傳,咱們一塊兒來看一下!java
話說小勇正襟危坐在面試官面前,這已是小勇的第五次面試了,前幾回都是石沉大海,讓小勇有點着急了,可是小勇這一次但是有備而來,以前面試不會的問題,大部分都狠狠的補習了一下,想來這一次問題應該不大。程序員
前面基礎問題小勇都回答的有模有樣的,面試官一看,基礎還算能夠,問一點有深度的吧!面試
面試官:我看你簡歷上寫的熟悉JVM,我給你下面一個題目,先來說一講a = a ++; 和a = ++a; 的運行結果各是多少?數組
public class Test1 {
public static void main(String[] args) {
int a = 88;
a = a++;
// a = ++a;
System.out.println(a);
}
}
複製代碼
小勇心想:這不是小菜一碟嗎,這我能不知道?
因而小勇輕蔑一笑說:a = a++; 輸出結果是 8 ,a = ++a; 是 9
心想我還覺得多有難度呢,就這?這種題目給我再來一個吧!瀏覽器
面試官:無動於衷,面無表情的說道,爲何結果是這樣的,你知道嗎?緩存
小勇:還真來,提升難度了,小樣有點東西啊,還好準備了,否則今天就在你這道題上坑住了。
a++ 是先計算 a 在++,在分號結束的纔會作a++運算,因此當咱們作賦值操做的時候a++ 仍是 8,因此賦值給a的時候也是8,只有當分號結束了a++纔會是9
++a 是 先計算 ++a ,無論是否在分號結束,這個時候的值就已是 9 了,因此賦值的時候,a就變成了9,輸出結果也就是9了
這下沒話說了吧!markdown
面試官摸了一下下巴,緩緩說到:這個操做在JVM內存裏面是怎樣運行的?數據結構
小勇:怎麼運行的,這個不是底層原理了嗎?劇本不是這麼發展的,這塊沒有了解過。。。。
小勇:支支吾吾說道,這個沒有了解過,不太清楚底層的實現多線程
面試官輕蔑一笑說:行,今天面試就先到這裏了,有什麼事情,人事會通知你的!函數
小勇:!$% @# &*
聽到上面小勇所講的東西以後,大概瞭解到,面試官應該是要考他關於運行時數據在內存時候的知識點,不懂就學,遇到事情不要慌,想要真正理解上面的面試題的精髓,咱們要作一些前置知識的點綴,首先咱們先來看看下面一張圖:
類生命週期:
上圖中首先將.class 文件讀取到內存,存放在方法區(Perm Gen), 最終產品是Class對象,而後檢查是否有正確數據結構,JVM爲Class的靜態變量分配內存,並設置默認初始值,把Class的二進制數據中的符號引用替換爲直接引用,JVM爲執行Class 的static 語句塊,會先初始化其父類,跑到JVM虛擬機以後呢,會進入到運行時引擎,最後在運行時引擎裏面運行,運行的時候在內存裏面是一個什麼樣的狀況,這個就是咱們要講的重點——run-time data areas
Java虛擬機運行時數據區:
程序計數器是一塊較小的內存空間, 它能夠看做是當前線程所執行的字節碼的行號指示器。因爲Java虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器都會只執行一條線程中的指令,所以爲了線程切換後都能回覆正確的執行位置,每一個線程都有一個獨立的程序計數器。若是線程正在執行的是一個java方法,這個計數器記錄的就是正在執行的虛擬機字節碼指令的地址。若是正在執行的是native方法,這個計數器值則爲空。
做用:
一、字節碼解釋器經過改變程序計數器來一次讀取指令,從而實現代碼的流程控制。好比:順序執行、選擇、循環、異常處理等
二、在多線程的狀況下,程序計數器用於記錄當前線程線程執行的位置,當線程被切換回來的時候可以知道該線程上次運行到哪裏了
特色:
Java虛擬機棧也是線程私有的,它的生命週期與線程相同,虛擬機棧描述的是Java方法執行的內存模型;每一個方法在執行的同時都會建立一個棧幀(stack frame) 用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每個方法從調用至執行完成的過程,就對應着一個棧幀在虛擬機中入棧到出棧的過程。
咱們結合一個案例來看一下:
public class TestStack {
public static void main(String[] args) {
new PlayRice().print();
}
}
class PlayRice{
public void fun(){
System.out.println("乾飯人,乾飯魂,乾飯都是人上人!!!");
}
public void print(){
fun();
}
}
複製代碼
常常有人把Java 內存區域籠統的劃分紅堆內存(Heap)和棧內存(Stack),這種劃分方式是直接繼承自傳統的 C、C++程序的內部結構,可是在Java語言裏面顯然是不合適的,Java的內存區域過度要比這兩個更復雜,不過這種劃分方式的流行也簡潔說明了程序員最關注的、對象內存分配關係最密切的區域是 堆和棧,棧一般是指虛擬機,或者更多狀況下只是指 虛擬機棧中的局部變量表的部分
局部變量表存放了編譯期可知的各類基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用
在《Java虛擬機規範中》,對這個區域規定了兩種異常情況:
1. 若是線程請求的棧深度大於虛擬機所容許的深度,將拋出 StackOverflowError
2. 若是Java虛擬機棧能夠動態擴展,當擴展時沒法申請到足夠的內存,就會拋出OutOfMemoryError異常
本地方法棧(Native Method Stack)和虛擬機棧所發揮的做用是很是類似的,他們之間的區別就是虛擬機棧爲虛擬機執行的Java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native方法服務。
在虛擬機規範中對本地方法棧中方法使用的語言、使用方式與數據結構並無強制規定,所以具體的虛擬機能夠自由實現它,甚至有的Java虛擬機(Hot-Spot虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。與虛擬機同樣,本地方法棧也會拋出 StackOverflowError 和 OutOfMemoryError 異常。
Java堆是虛擬機所管理中內存最大的一塊。Java堆是被全部線程共享的一個內存區域,在虛擬機啓動時建立。這個內存區域的惟一目的就是存放對象的實例,Java世界裏 幾乎 全部的對象實例都在這裏分配。
在《Java虛擬機規範》中對Java堆的描述是:「全部的對象實例以及數組都應當在堆上分配」。Java對是垃圾收集器管理的內存區域。從回收內存的角度看,現代的垃圾收集器大部分都是分代收集理論設計的,因此Java堆中常常會出現 「新生代、老年代、永久代、Eden、Survivor」。
根據《Java虛擬機規範》的規定,Java堆能夠處在物理上不連續的內存空間中,但在邏輯上它應該被視爲連續的,這點就像咱們用磁盤空間去存儲文件同樣,並不要求每一個文件都連續存放。但對於大對象(典型的如數組對象),多數虛擬機實現出於實現簡答、存儲高效的考慮,極可能會要求連續的內存空間。
Java堆既能夠被實現成固定大小的,也能夠是可擴展的,不過當前主流的Java虛擬機都是按照可擴展來實現的(經過參數-Xmx和-Xms設定)。若是在Java堆中沒有內存完成實例分配,而且堆也沒法再擴展時,Java虛擬機會拋出OutOfMemoryError異常。
方法區(Method Area)和Java堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類型信息、常量、靜態變量、即時編譯器編譯後的代碼緩存等數據。雖然《Java虛擬機規範》中把方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫作 「非堆」(Non-Heap),目的是與Java堆區分開來。
《Java虛擬機規範》對方法區的約束是很是高寬鬆的,除了和Java堆同樣不須要連續的內存和能夠選擇固定大小或者可擴展外,甚至還能夠選擇不實現垃圾收集,因此垃圾收集的行爲在這個區域就會比較少出現。這個區域的內存回收目標主要是針對常量池的回收和類型的卸載,可是這個區域的回收效果就比較差強人意了。
若是方法區沒法知足新的內存分配需求的時候,就會拋出 OutOfMemoryError異常。
運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池表(Constant Pool Table),用於存放編譯期生成的各類字面量與符號引用,這部份內容將在類加載後存放到方法區的運行時常量池中。
Java虛擬機對於Class文件每一部分(包括常量池)的格式都有嚴格規定,如每個字節用於存儲哪一種數據都必須符合規範上的要求才會被虛擬機承認、加載和執行,但對於運行時常量池,《Java虛擬機規範》並無任何細節的要求,不一樣提供商實現的虛擬機能夠按照本身的須要來實現,這個內存區域,不過通常來講,除了保存Class文件描述的符號引用外,還會把符號引用翻譯出來的直接引用也存儲在運行時常量池中
運行時常量池相對於Class文件常量池的另一個重要特徵是具有動態性,Java語言並不要求常量必定只有編譯器才能產生,也就是說,並不是預置入Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也能夠將新的常量放入池中,這種特性被開發人員利用的比較多就是String類的intern()方法。
既然運行時常量池是方法區的一部分,天然受到方法區內存的限制,當常量池沒法再申請到內存 時會拋出OutOfMemoryError異常。
直接內存(Direct Memory)並非虛擬機運行時數據區的一部分,也不是《Java虛擬機規範》中定義的內存區域。可是這部分也被頻繁的使用過,並且也有可能會致使OutOfMemoryError異常出現,在JDK1.4中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可使用Native函數庫直接分配堆外內存。而後經過一個存儲在Java堆裏面的DirectByteBuffer對象做爲這塊內存的引用進行操做。
從下面一張圖咱們就能夠看出,每個線程都有本身的程序計數器、Java虛擬機棧以及本地方法棧,可是他們共享的是堆以及方法區,爲何每一個線程都有本身的程序計數器?咱們在上面已經講過,就是當一個線程執行完了,CPU切換到另外一個線程去執行,當另一個線程執行完成以後切回來的時候,可以知道當前線程執行的位置。
咱們回到最開始咱們講的面試題,咱們先來看 i=i++
等於8,具體他內部是怎樣執行的呢,咱們須要看它的指令是怎麼操做的
咱們能夠用過 Jclasslib
來解析他二進制碼以後點到的main方法
首先咱們須要安裝 Jclasslib,安裝成功以下圖所示:
首先咱們須要 運行main方法 ,加載其class的內容後,點擊 view -> show Bytecode With Jclasslib
main方法裏面記錄的有兩張表:
表1:LineNumberTable 記錄是行號
表2:LocalVariabletable 是局部變量表,裏面就是方法內部使用到的變量,第一個是 args ,第二個是a,因此局部變量表,指的就是咱們當前這個方法,這個棧幀裏面用到了哪些局部變量。
接下來咱們來看一下,a = a++;中間的執行過程具體是怎麼樣的
0 bipush 88
2 istore_1
3 iload_1
4 iinc 1 by 1
7 istore_1
8 getstatic #2 <java/lang/System.out>
11 iload_1
12 invokevirtual #3 <java/io/PrintStream.println>
15 return
複製代碼
若是咱們不理解指令具體是什麼意思,咱們能夠點擊對應指令,瀏覽器直接定位這條指令的詳細說明
首先咱們來看一下 bipush 88 和 istore_1
,對應的是 int a = 88;iload+1 等於89,再把89賦值出來仍是89,
istore_1
是把咱們棧頂上的那個數出棧,放到下標值爲1的局部變量表。局部變量表下標值爲1的就是a的值,剛纔88是放到棧頂上的,如今把88彈出來放到a裏面,因此這兩句話完成以後對應的int a = 88就完成了,以下圖所示istore_1: 執行 a = a++ 操做,原先已經執行了 a++ 操做,這個時候將 a++ 中 a 賦值給 int a ,因此會將棧中的數據賦值到 局部變量表中,因此這個時候局部變量表中的數據就是88了
因此咱們最後的結果就是88
字節碼指令:
0 bipush 88
2 istore_1
3 iinc 1 by 1
6 iload_1
7 istore_1
8 getstatic #2 <java/lang/System.out>
11 iload_1
12 invokevirtual #3 <java/io/PrintStream.println>
15 return
複製代碼
bipush 88和istore_1: 這句話其實完成了 int a = 88,先將88壓棧,而後在出棧賦值到局部變量表中
iinc 1 by 1: 進行++a 操做,因此這個時候局部變量表中的數據就變成了89
iload_1: 這個時候將局部變量表中的數值壓到棧中,
istore_1: 這個時候作 a = ++a 操做,將 a的值賦值給 int a,由於在棧中的數據自己就是89,因此最後打印出來的結果就是89
補充:
當咱們設置 int a = 250 的時候,下面的值會變成 sipush,是由於 250已經超過127,他已經超過byte 所能表明的最大結果,因此看到的二進制就是sipush,s 表明 short
到這裏,你學廢了嗎?其實有時候咱們學東西,知道怎麼用,可是具體裏面的細節,就須要咱們仔細的去琢磨,有時候會很枯燥,當咱們瞭解其原理以後,會有豁然開朗的感受嗎?小農會有,大家呢?
我是牧小農,怕什麼真理無窮,進一步有進一步的歡喜,你們加油!