日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關咨詢
選擇下列產(chǎn)品馬上在線溝通
服務時間:8:30-17:00
你可能遇到了下面的問題
關閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
【死磕JVM】一道面試題引發(fā)的“棧幀”!?。?/div>

【死磕JVM】一道面試題引發(fā)的“棧幀”!!!

作者:牧小農(nóng) 2021-03-16 05:44:26

云計算

虛擬化 通過朋友面試遇到的面試題,今天就這個問題給大家詳細介紹一下關于運行時數(shù)據(jù)在內(nèi)存時候的知識點。

成都創(chuàng)新互聯(lián)公司擁有一支富有激情的企業(yè)網(wǎng)站制作團隊,在互聯(lián)網(wǎng)網(wǎng)站建設行業(yè)深耕10余年,專業(yè)且經(jīng)驗豐富。10余年網(wǎng)站優(yōu)化營銷經(jīng)驗,我們已為1000+中小企業(yè)提供了網(wǎng)站制作、做網(wǎng)站解決方案,按需設計網(wǎng)站,設計滿意,售后服務無憂。所有客戶皆提供一年免費網(wǎng)站維護!

前言

最近小農(nóng)的朋友——小勇在找工作,開年來金三銀四,都想跳一跳,找個踏(gao)實(xin)點的工作,這不小勇也去面試了,不得不說,現(xiàn)在面試,各種底層各種原理,層出不窮,小勇就遇上了這么一道面試題,因為沒有回答好,面試被PASS,讓他備受打擊,作為大(lao)哥(si)哥(ji)的我,肯定要安慰一下,到底是什么樣的面試題,讓小勇又一次夭折在面試的路上,好奇怪為什么要說又?簡直讓人喜極而泣,哈哈哈,言歸正傳,我們一起來看一下!

話說小勇正襟危坐在面試官面前,這已經(jīng)是小勇的第五次面試了,前幾次都是石沉大海,讓小勇有點著急了,但是小勇這一次可是有備而來,之前面試不會的問題,大部分都狠狠的補習了一下,想來這一次問題應該不大。

前面基礎問題小勇都回答的有模有樣的,面試官一看,基礎還算可以,問一點有深度的吧!

面試官:我看你簡歷上寫的熟悉JVM,我給你下面一個題目,先來講一講a = a ++; 和a = ++a; 的運行結(jié)果各是多少?

  
 
 
 
  1. public class Test1 { 
  2.    public static void main(String[] args) { 
  3.        int a = 88; 
  4.        a = a++; 
  5. //       a = ++a; 
  6.        System.out.println(a); 
  7.   } 

小勇心想:這不是小菜一碟嗎,這我能不知道?于是小勇輕蔑一笑說:a = a++; 輸出結(jié)果是 88 ,a = ++a; 是 9心想我還以為多有難度呢,就這?這種題目給我再來一個吧!

面試官:無動于衷,面無表情的說道,為什么結(jié)果是這樣的,你知道嗎?

小勇:還真來,提高難度了,小樣有點東西啊,還好準備了,不然今天就在你這道題上坑住了。a++ 是先計算 a 在++,在分號結(jié)束的才會做a++運算,所以當我們做賦值操作的時候a++ 還是 88,所以賦值給a的時候也是88,只有當分號結(jié)束了a++才會是9++a 是 先計算 ++a ,不管是否在分號結(jié)束,這個時候的值就已經(jīng)是 9 了,所以賦值的時候,a就變成了9,輸出結(jié)果也就是9了這下沒話說了吧!

面試官摸了一下下巴,緩緩說到:這個操作在JVM內(nèi)存里面是怎樣運行的?

小勇:怎么運行的,這個不是底層原理了嗎?劇本不是這么發(fā)展的,這塊沒有了解過。。。。小勇:支支吾吾說道,這個沒有了解過,不太清楚底層的實現(xiàn)

面試官輕蔑一笑說:行,今天面試就先到這里了,有什么事情,人事會通知你的!

小勇:!$%@#&*

不懂就學

聽到上面小勇所講的東西之后,大概了解到,面試官應該是要考他關于運行時數(shù)據(jù)在內(nèi)存時候的知識點,不懂就學,遇到事情不要慌,想要真正理解上面的面試題的精髓,我們要做一些前置知識的點綴,首先我們先來看看下面一張圖:類生命周期:

上圖中首先將.class 文件讀取到內(nèi)存,存放在方法區(qū)(Perm Gen), 最終產(chǎn)品是Class對象,然后檢查是否有正確數(shù)據(jù)結(jié)構(gòu),JVM為Class的靜態(tài)變量分配內(nèi)存,并設置默認初始值,把Class的二進制數(shù)據(jù)中的符號引用替換為直接引用,JVM為執(zhí)行Class 的static 語句塊,會先初始化其父類,跑到JVM虛擬機之后呢,會進入到運行時引擎,最后在運行時引擎里面運行,運行的時候在內(nèi)存里面是一個什么樣的情況,這個就是我們要講的重點——run-time data areas

運行時數(shù)據(jù)區(qū)

Java虛擬機運行時數(shù)據(jù)區(qū):

1.1 程序計數(shù)器

程序計數(shù)器是一塊較小的內(nèi)存空間, 它可以看作是當前線程所執(zhí)行的字節(jié)碼的行號指示器。由于Java虛擬機的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實現(xiàn)的,在任何一個確定的時刻,一個處理器都會只執(zhí)行一條線程中的指令,因此為了線程切換后都能回復正確的執(zhí)行位置,每個線程都有一個獨立的程序計數(shù)器。如果線程正在執(zhí)行的是一個java方法,這個計數(shù)器記錄的就是正在執(zhí)行的虛擬機字節(jié)碼指令的地址。如果正在執(zhí)行的是native方法,這個計數(shù)器值則為空。

作用:

1、字節(jié)碼解釋器通過改變程序計數(shù)器來一次讀取指令,從而實現(xiàn)代碼的流程控制。比如:順序執(zhí)行、選擇、循環(huán)、異常處理等 2、在多線程的情況下,程序計數(shù)器用于記錄當前線程線程執(zhí)行的位置,當線程被切換回來的時候能夠知道該線程上次運行到哪里了

特點:

  1. 是一塊較小的內(nèi)存空間
  2. 線程私有,每一條線程都有一個程序計數(shù)器
  3. 是唯一不會出現(xiàn) OutOfMemoryError的內(nèi)存區(qū)域
  4. 生命周期隨著線程的創(chuàng)建而創(chuàng)建,隨著線程的結(jié)束而結(jié)束

1.2 Java虛擬機棧

Java虛擬機棧也是線程私有的,它的生命周期與線程相同,虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型;每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀(stack frame) 用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。每一個方法從調(diào)用至執(zhí)行完成的過程,就對應著一個棧幀在虛擬機中入棧到出棧的過程。

我們結(jié)合一個案例來看一下:

  
 
 
 
  1. public class TestStack { 
  2.  
  3.    public static void main(String[] args) { 
  4.            new PlayRice().print(); 
  5.   } 
  6.  
  7. class PlayRice{ 
  8.  
  9.    public void fun(){ 
  10.        System.out.println("干飯人,干飯魂,干飯都是人上人?。?!"); 
  11.   } 
  12.  
  13.    public void print(){ 
  14.        fun(); 
  15.   } 

 經(jīng)常有人把Java 內(nèi)存區(qū)域籠統(tǒng)的劃分成堆內(nèi)存(Heap)和棧內(nèi)存(Stack),這種劃分方式是直接繼承自傳統(tǒng)的 C、C++程序的內(nèi)部結(jié)構(gòu),但是在Java語言里面顯然是不合適的,Java的內(nèi)存區(qū)域過分要比這兩個更復雜,不過這種劃分方式的流行也簡潔說明了程序員最關注的、對象內(nèi)存分配關系最密切的區(qū)域是 堆和棧,棧通常是指虛擬機,或者更多情況下只是指 虛擬機棧中的局部變量表的部分 局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對象引用

在《Java虛擬機規(guī)范中》,對這個區(qū)域規(guī)定了兩種異常狀況: 1. 如果線程請求的棧深度大于虛擬機所允許的深度,將拋出 StackOverflowError 2. 如果Java虛擬機??梢詣討B(tài)擴展,當擴展時無法申請到足夠的內(nèi)存,就會拋出OutOfMemoryError異常

1.3 本地方法棧

本地方法棧(Native Method Stack)和虛擬機棧所發(fā)揮的作用是非常相似的,他們之間的區(qū)別就是虛擬機棧為虛擬機執(zhí)行的Java方法(也就是字節(jié)碼)服務,而本地方法棧則為虛擬機使用到的 Native方法服務。

在虛擬機規(guī)范中對本地方法棧中方法使用的語言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有強制規(guī)定,因此具體的虛擬機可以自由實現(xiàn)它,甚至有的Java虛擬機(Hot-Spot虛擬機)直接就把本地方法棧和虛擬機棧合二為一。與虛擬機一樣,本地方法棧也會拋出 StackOverflowError 和 OutOfMemoryError 異常。

1.4 堆

Java堆是虛擬機所管理中內(nèi)存最大的一塊。Java堆是被所有線程共享的一個內(nèi)存區(qū)域,在虛擬機啟動時創(chuàng)建。這個內(nèi)存區(qū)域的唯一目的就是存放對象的實例,Java世界里 幾乎 所有的對象實例都在這里分配。

在《Java虛擬機規(guī)范》中對Java堆的描述是:“所有的對象實例以及數(shù)組都應當在堆上分配”。Java對是垃圾收集器管理的內(nèi)存區(qū)域。從回收內(nèi)存的角度看,現(xiàn)代的垃圾收集器大部分都是分代收集理論設計的,所以Java堆中經(jīng)常會出現(xiàn) “新生代、老年代、永久代、Eden、Survivor”。

根據(jù)《Java虛擬機規(guī)范》的規(guī)定,Java堆可以處在物理上不連續(xù)的內(nèi)存空間中,但在邏輯上它應該被視為連續(xù)的,這點就像我們用磁盤空間去存儲文件一樣,并不要求每個文件都連續(xù)存放。但對于大對象(典型的如數(shù)組對象),多數(shù)虛擬機實現(xiàn)出于實現(xiàn)簡答、存儲高效的考慮,很可能會要求連續(xù)的內(nèi)存空間。

Java堆既可以被實現(xiàn)成固定大小的,也可以是可擴展的,不過當前主流的Java虛擬機都是按照可擴展來實現(xiàn)的(通過參數(shù)-Xmx和-Xms設定)。如果在Java堆中沒有內(nèi)存完成實例分配,并且堆也無法再擴展時,Java虛擬機會拋出OutOfMemoryError異常。

1.5 方法區(qū)

方法區(qū)(Method Area)和Java堆一樣,是各個線程共享的內(nèi)存區(qū)域,它用于存儲已被虛擬機加載的類型信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼緩存等數(shù)據(jù)。雖然《Java虛擬機規(guī)范》中把方法區(qū)描述為堆的一個邏輯部分,但是它卻有一個別名叫做 “非堆”(Non-Heap),目的是與Java堆區(qū)分開來。

《Java虛擬機規(guī)范》對方法區(qū)的約束是非常高寬松的,除了和Java堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴展外,甚至還可以選擇不實現(xiàn)垃圾收集,所以垃圾收集的行為在這個區(qū)域就會比較少出現(xiàn)。這個區(qū)域的內(nèi)存回收目標主要是針對常量池的回收和類型的卸載,但是這個區(qū)域的回收效果就比較差強人意了。

如果方法區(qū)無法滿足新的內(nèi)存分配需求的時候,就會拋出 OutOfMemoryError異常。

1.6 運行時常量池

運行時常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池表(Constant Pool Table),用于存放編譯期生成的各種字面量與符號引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運行時常量池中。

Java虛擬機對于Class文件每一部分(包括常量池)的格式都有嚴格規(guī)定,如每一個字節(jié)用于存儲哪種數(shù)據(jù)都必須符合規(guī)范上的要求才會被虛擬機認可、加載和執(zhí)行,但對于運行時常量池,《Java虛擬機規(guī)范》并沒有任何細節(jié)的要求,不同提供商實現(xiàn)的虛擬機可以按照自己的需要來實現(xiàn),這個內(nèi)存區(qū)域,不過一般來說,除了保存Class文件描述的符號引用外,還會把符號引用翻譯出來的直接引用也存儲在運行時常量池中

運行時常量池相對于Class文件常量池的另外一個重要特征是具備動態(tài)性,Java語言并不要求常量一定只有編譯器才能產(chǎn)生,也就是說,并非預置入Class文件中常量池的內(nèi)容才能進入方法區(qū)運行時常量池,運行期間也可以將新的常量放入池中,這種特性被開發(fā)人員利用的比較多就是String類的intern()方法。

既然運行時常量池是方法區(qū)的一部分,自然受到方法區(qū)內(nèi)存的限制,當常量池無法再申請到內(nèi)存 時會拋出OutOfMemoryError異常。

1.7 直接內(nèi)存

直接內(nèi)存(Direct Memory)并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是《Java虛擬機規(guī)范》中定義的內(nèi)存區(qū)域。但是這部分也被頻繁的使用過,而且也有可能會導致OutOfMemoryError異常出現(xiàn),在JDK1.4中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存。然后通過一個存儲在Java堆里面的DirectByteBuffer對象作為這塊內(nèi)存的引用進行操作。

1.8 小結(jié)

從下面一張圖我們就可以看出,每一個線程都有自己的程序計數(shù)器、Java虛擬機棧以及本地方法棧,但是他們共享的是堆以及方法區(qū),為什么每個線程都有自己的程序計數(shù)器?我們在上面已經(jīng)講過,就是當一個線程執(zhí)行完了,CPU切換到另一個線程去執(zhí)行,當另外一個線程執(zhí)行完成之后切回來的時候,能夠知道當前線程執(zhí)行的位置。

理解面試題

我們回到最開始我們講的面試題,我們先來看 i=i++等于88,具體他內(nèi)部是怎樣執(zhí)行的呢,我們需要看它的指令是怎么操作的我們可以用過 Jclasslib來解析他二進制碼之后點到的main方法

1.1 安裝 Jclasslib

首先我們需要安裝 Jclasslib,安裝成功如下圖所示:

1.2 查看字節(jié)碼

首先我們需要 運行main方法 ,加載其class的內(nèi)容后,點擊 view -> show Bytecode With Jclasslib

main方法里面記錄的有兩張表:

表1:LineNumberTable 記錄是行號表2:LocalVariabletable 是局部變量表,里面就是方法內(nèi)部使用到的變量,第一個是 args ,第二個是a,所以局部變量表,指的就是我們當前這個方法,這個棧幀里面用到了哪些局部變量。

a = a++;

接下來我們來看一下,a = a++;中間的執(zhí)行過程具體是怎么樣的

  
 
 
 
  1. 0 bipush 88 
  2. 2 istore_1 
  3. 3 iload_1 
  4. 4 iinc 1 by 1 
  5. 7 istore_1 
  6. 8 getstatic #2  
  7. 11 iload_1 
  8. 12 invokevirtual #3  
  9. 15 return 

如果我們不理解指令具體是什么意思,我們可以點擊對應指令,瀏覽器直接定位這條指令的詳細說明

首先我們來看一下 bipush 88 和 istore_1,對應的是 int a = 88;iload+1 等于89,再把89賦值出來還是89,

  • bipush 88 是指 push byte 放到棧中,88當成一個byte值,會自動擴展成Int類型,把它放到棧中,88放在局部變量表,輸入結(jié)果是88。
  • 第二條指令istore_1是把我們棧頂上的那個數(shù)出棧,放到下標值為1的局部變量表。局部變量表下標值為1的就是a的值,剛才88是放到棧頂上的,現(xiàn)在把88彈出來放到a里面,所以這兩句話完成之后對應的int a = 88就完成了,如下圖所示:

iload_1: 的意思是 從局部變量加載int(load int from local variable) ,就是從局部變量表中 拿值,之后放到棧里面,如下圖所示:

iinc 1 by 1: 執(zhí)行 a++ 操作,將局部變量表中 數(shù)值為88的進行+1 操作,所以就是 89了,

istore_1: 執(zhí)行 a = a++ 操作,原先已經(jīng)執(zhí)行了 a++ 操作,這個時候?qū)?a++ 中 a 賦值給 int a ,所以會將棧中的數(shù)據(jù)賦值到 局部變量表中,所以這個時候局部變量表中的數(shù)據(jù)就是88了。

所以我們最后的結(jié)果就是88。

a = ++a;

字節(jié)碼指令:

  
 
 
 
  1. 0 bipush 88 
  2. 2 istore_1 
  3. 3 iinc 1 by 1 
  4. 6 iload_1 
  5. 7 istore_1 
  6. 8 getstatic #2  
  7. 11 iload_1 
  8. 12 invokevirtual #3  
  9. 15 return 

bipush 88和istore_1: 這句話其實完成了 int a = 88,先將88壓棧,然后在出棧賦值到局部變量表中。

iinc 1 by 1: 進行++a 操作,所以這個時候局部變量表中的數(shù)據(jù)就變成了89。

iload_1: 這個時候?qū)⒕植孔兞勘碇械臄?shù)值壓到棧中,

istore_1: 這個時候做 a = ++a 操作,將 a的值賦值給 int a,因為在棧中的數(shù)據(jù)本身就是89,所以最后打印出來的結(jié)果就是89。

補充:當我們設置 int a = 250 的時候,下面的值會變成 sipush,是因為 250已經(jīng)超過127,他已經(jīng)超過byte 所能代表的最大結(jié)果,所以看到的二進制就是sipush,s 代表 short。

總結(jié)

到這里,你學廢了嗎?其實有時候我們學東西,知道怎么用,但是具體里面的細節(jié),就需要我們仔細的去琢磨,有時候會很枯燥,當我們了解其原理之后,會有豁然開朗的感覺嗎?小農(nóng)會有,你們呢?

我是牧小農(nóng),怕什么真理無窮,進一步有進一步的歡喜,大家加油!


網(wǎng)站欄目:【死磕JVM】一道面試題引發(fā)的“棧幀”?。?!
標題鏈接:http://www.dlmjj.cn/article/dhgijgi.html