自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

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

云計(jì)算 虛擬化
通過(guò)朋友面試遇到的面試題,今天就這個(gè)問(wèn)題給大家詳細(xì)介紹一下關(guān)于運(yùn)行時(shí)數(shù)據(jù)在內(nèi)存時(shí)候的知識(shí)點(diǎn)。

[[387780]]

前言

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

話說(shuō)小勇正襟危坐在面試官面前,這已經(jīng)是小勇的第五次面試了,前幾次都是石沉大海,讓小勇有點(diǎn)著急了,但是小勇這一次可是有備而來(lái),之前面試不會(huì)的問(wèn)題,大部分都狠狠的補(bǔ)習(xí)了一下,想來(lái)這一次問(wèn)題應(yīng)該不大。

前面基礎(chǔ)問(wèn)題小勇都回答的有模有樣的,面試官一看,基礎(chǔ)還算可以,問(wèn)一點(diǎn)有深度的吧!

面試官:我看你簡(jiǎn)歷上寫的熟悉JVM,我給你下面一個(gè)題目,先來(lái)講一講a = a ++; 和a = ++a; 的運(yùn)行結(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.   } 

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

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

小勇:還真來(lái),提高難度了,小樣有點(diǎn)東西啊,還好準(zhǔn)備了,不然今天就在你這道題上坑住了。a++ 是先計(jì)算 a 在++,在分號(hào)結(jié)束的才會(huì)做a++運(yùn)算,所以當(dāng)我們做賦值操作的時(shí)候a++ 還是 88,所以賦值給a的時(shí)候也是88,只有當(dāng)分號(hào)結(jié)束了a++才會(huì)是9++a 是 先計(jì)算 ++a ,不管是否在分號(hào)結(jié)束,這個(gè)時(shí)候的值就已經(jīng)是 9 了,所以賦值的時(shí)候,a就變成了9,輸出結(jié)果也就是9了這下沒(méi)話說(shuō)了吧!

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

小勇:怎么運(yùn)行的,這個(gè)不是底層原理了嗎?劇本不是這么發(fā)展的,這塊沒(méi)有了解過(guò)。。。。小勇:支支吾吾說(shuō)道,這個(gè)沒(méi)有了解過(guò),不太清楚底層的實(shí)現(xiàn)

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

小勇:!$%@#&*

不懂就學(xué)

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


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

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

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


1.1 程序計(jì)數(shù)器

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

作用:

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

特點(diǎn):

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

1.2 Java虛擬機(jī)棧

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

 

我們結(jié)合一個(gè)案例來(lái)看一下:

  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語(yǔ)言里面顯然是不合適的,Java的內(nèi)存區(qū)域過(guò)分要比這兩個(gè)更復(fù)雜,不過(guò)這種劃分方式的流行也簡(jiǎn)潔說(shuō)明了程序員最關(guān)注的、對(duì)象內(nèi)存分配關(guān)系最密切的區(qū)域是 堆和棧,棧通常是指虛擬機(jī),或者更多情況下只是指 虛擬機(jī)棧中的局部變量表的部分 局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對(duì)象引用

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

1.3 本地方法棧

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

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

1.4 堆

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

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

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

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

1.5 方法區(qū)

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

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

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

1.6 運(yùn)行時(shí)常量池

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

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

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

既然運(yùn)行時(shí)常量池是方法區(qū)的一部分,自然受到方法區(qū)內(nèi)存的限制,當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存 時(shí)會(huì)拋出OutOfMemoryError異常。

1.7 直接內(nèi)存

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

1.8 小結(jié)

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


理解面試題

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

1.1 安裝 Jclasslib

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

1.2 查看字節(jié)碼

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

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

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

 

a = a++;

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

  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 <java/lang/System.out
  7. 11 iload_1 
  8. 12 invokevirtual #3 <java/io/PrintStream.println> 
  9. 15 return 

如果我們不理解指令具體是什么意思,我們可以點(diǎn)擊對(duì)應(yīng)指令,瀏覽器直接定位這條指令的詳細(xì)說(shuō)明

首先我們來(lái)看一下 bipush 88 和 istore_1,對(duì)應(yīng)的是 int a = 88;iload+1 等于89,再把89賦值出來(lái)還是89,

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

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

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

 

istore_1: 執(zhí)行 a = a++ 操作,原先已經(jīng)執(zhí)行了 a++ 操作,這個(gè)時(shí)候?qū)?a++ 中 a 賦值給 int a ,所以會(huì)將棧中的數(shù)據(jù)賦值到 局部變量表中,所以這個(gè)時(shí)候局部變量表中的數(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 <java/lang/System.out
  7. 11 iload_1 
  8. 12 invokevirtual #3 <java/io/PrintStream.println> 
  9. 15 return 

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


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


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


istore_1: 這個(gè)時(shí)候做 a = ++a 操作,將 a的值賦值給 int a,因?yàn)樵跅V械臄?shù)據(jù)本身就是89,所以最后打印出來(lái)的結(jié)果就是89。


補(bǔ)充:當(dāng)我們?cè)O(shè)置 int a = 250 的時(shí)候,下面的值會(huì)變成 sipush,是因?yàn)?250已經(jīng)超過(guò)127,他已經(jīng)超過(guò)byte 所能代表的最大結(jié)果,所以看到的二進(jìn)制就是sipush,s 代表 short。

總結(jié)

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

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

 

責(zé)任編輯:姜華 來(lái)源: 牧小碼農(nóng)
相關(guān)推薦

2021-06-09 07:56:51

JvmJVM面試題Java

2015-09-02 14:09:19

面試題程序設(shè)計(jì)

2017-03-10 09:33:16

JavaScript類型

2024-10-11 17:09:27

2011-06-14 09:12:03

JavaScript

2011-05-23 11:27:32

面試題面試java

2018-03-06 15:30:47

Java面試題

2009-08-11 10:12:07

C#算法

2023-02-04 18:24:10

SeataJava業(yè)務(wù)

2009-08-11 14:59:57

一道面試題C#算法

2021-05-31 07:55:44

smartRepeatJavaScript函數(shù)

2009-08-11 15:09:44

一道面試題C#算法

2017-11-21 12:15:27

數(shù)據(jù)庫(kù)面試題SQL

2022-04-08 07:52:17

CSS面試題HTML

2023-08-01 08:10:46

內(nèi)存緩存

2021-10-28 11:40:58

回文鏈表面試題數(shù)據(jù)結(jié)構(gòu)

2022-02-08 18:09:20

JS引擎解析器

2011-03-02 10:58:16

SQL server入門面試題

2017-09-13 07:15:10

Python讀寫文件函數(shù)

2021-03-27 10:59:45

JavaScript開(kāi)發(fā)代碼
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)