无遮挡边摸边吃奶边做的视频刺激 攜程面試官尽然問我 Java 虛擬機棧!


发布日期:2022-05-19 04:29    点击次数:176


世界好,我是那個永遠 18 歲的老魔鬼~噓

從《JVM 內存區域劃分》這篇著述中,世界應該 get 到了,Java 虛擬機內存區域不错劃分為法子計數器、Java 虛擬機棧、土产货法子棧和堆。今天,我們來圍繞其中的一個區域——Java 虛擬機棧,真切地展開下。

先說明一下哈。這篇著述的標題里帶了一個“攜程面試官”,有標題黨的嫌疑。但有一說一,確實有讀者在上一篇著述里留言說,攜程面試官問他了 Java 虛擬機內存方面的知識點,是以今天的標題我就“借題發揮”了。

從“相見恨晚”這個詞中,我估摸著這名讀者在這道面試題前边折戟沉沙了。這么說吧,面試官確實喜歡問 Java 虛擬機方面的知識點,因為很能熟习出别称應聘者的真實功底,是以我规划多寫幾篇這方面的著述,但愿能給世界多一點點幫助~

Java 虛擬機以法子作為基本的執行單元,“棧幀(Stack Frame)”則是用于复古 Java 虛擬機進行法子調用和法子執行的基本數據結構。每一個棧幀中都包含了局部變量表、操作數棧、動態鏈接、法子复返地址和一些額外的附加信息(比如與調試、性高人機相關的信息)。之前的著述里有提到過這些认识,并做了一些簡單扼要的介紹,但我覺得還不夠詳細,是以這篇重點要來介紹一下棧幀中的這些认识。

1)局部變量表

局部變量表(Local Variables Table)用來保存法子中的局部變量,以及法子參數。當 Java 源代碼文献被編譯成 class 文献的時候,局部變量表的最大容量就已經確定了。

我們來看這樣一段代碼。

public 无遮挡边摸边吃奶边做的视频刺激class LocalVaraiablesTable {     private void write(int age) {         String name = "默然王二";     } } 

write() 法子有一個參數 age,一個局部變量 name。

然后用 Intellij IDEA 的 jclasslib 稽查一下編譯后的字節碼文献 LocalVaraiablesTable.class。不错看到 write() 法子的 Code 屬性中,Maximum local variables(局部變量表的最大容量)的值為 3。

按理說,局部變量表的最大容量應該為 2 才對,一個 age,一個 name,為什么是 3 呢?

當一個成員法子(非靜態法子)被調用時,第 0 個變量其實是調用這個成員法子的對象援用,也便是那個大名鼎鼎的 this。調用法子 write(18),實際上是調用 write(this, 18)。

點開 Code 屬性,稽查 LocalVaraiableTable 就不错看到詳細的信息了。

第 0 個是 this,類型為 LocalVaraiablesTable 對象;第 1 個是法子參數 age,類型為整形 int;第 2 個是法子內部的局部變量 name,類型為字符串 String。

當然了,局部變量表的大小并不是法子中悉数局部變量的數量之和,它與變量的類型和變量的作用域有關。當一個局部變量的作用域結束了,它占用的局部變量表中的位置就被接下來的局部變量取代了。

來看底下這段代碼。

public static void method() {     // ①     if (true) {         // ②         String name = "默然王二";     }     // ③     if(true) {         // ④         int age = 18;     }     // ⑤ } 
method() 法子的局部變量表大小為 1,因為是靜態法子,是以不需要添加 this 作為局部變量表的第一個元素; ②的時候局部變量有一個 name,局部變量表的大小變為 1; ③的時候 name 變量的作用域結束; ④的時候局部變量有一個 age,局部變量表的大小為 1; ⑤的時候局 age 變量的作用域結束;

關于局部變量的作用域,《Effective Java》 中的第 57 條建議:

將局部變量的作用域最小化,不错增強代碼的可讀性和可維護性,并缩小出錯的可能性。

在此,我還有一點要指示世界。為了盡可能節省棧幀耗用的內存空間,局部變量表中的槽是不错重用的,就像 method() 法子演示的那樣,這就意味著,合理的作用域有助于普及法子的性能。

局部變量表的容量以槽(slot)為最小單位,一個槽不错容納一個 32 位的數據類型(比如說 int,當然了,《Java 虛擬機規范》中沒有明確指出一個槽應該占用的內存空間大小,但我認為這樣更容易暴露),像 float 和 double 這種明確占用 64 位的數據類型會占用兩個緊挨著的槽。

來看底下的代碼。

public void solt() {     double d = 1.0;     int i = 1; } 

用 jclasslib 不错稽查到, 偷窥 亚洲 另类 图片 熟女solt() 法子的 Maximum local variables 的值為 4。

為什么等于 4 呢?帶上 this 也就 3 個呀?

稽查 LocalVaraiableTable 就显豁了,變量 i 的下標為 3,也就意味著變量 d 占了兩個槽。

2)操作數棧

同局部變量表一樣,操作數棧(Operand Stack)的最大深度也在編譯的時候就確定了,被寫入到了 Code 屬性的 maximum stack size 中。當一個法子剛開始執行的時候,操作數棧是空的,在法子執行過程中,會有各種字節碼指示往操作數棧中寫入和取出數據,也便是入棧和出棧操作。

來看底下這段代碼。

public class OperandStack {     public void test() {         add(1,2);     }      private int add(int a, int b) {         return a + b;     } } 

OperandStack 類共有 2 個法子,test() 法子中調用了 add() 法子,傳遞了 2 個參數。用 jclasslib 不错看到,test() 法子的 maximum stack size 的值為 3。

這是因為調用成員法子的時候會將 this 和悉数參數壓入棧中,調用完畢后 this 和參數都會逐一出棧。通過 「Bytecode」 面板不错稽查到對應的字節碼指示。

aload_0 用于將局部變量表中下標為 0 的援用類型的變量,也便是 this 加載到操作數棧中;

iconst_1 用于將整數 1 加載到操作數棧中; iconst_2 用于將整數 2 加載到操作數棧中; invokevirtual 用于調用對象的成員法子; pop 用于將棧頂的值出棧; return 為 void 法子的复返指示。

再來看一下 add() 法子的字節碼指示。

iload_1 用于將局部變量表中下標為 1 的 int 類型變量加載到操作數棧上(下標為 0 的是 this); iload_2 用于將局部變量表中下標為 2 的 int 類型變量加載到操作數棧上; iadd 用于 int 類型的加法運算; ireturn 為复返值為 int 的法子复返指示。

操作數中的數據類型必須與字節碼指示匹配,以上头的 iadd 指示為例,該指示只可用于整形數據的加法運算,它在執行的時候,棧頂的兩個數據必須是 int 類型的,不成出現一個 long 型和一個 double 型的數據進行 iadd 号令相加的情況。

3)動態鏈接

每個棧幀都包含了一個指向運行時常量池中該棧幀所屬法子的援用,持有這個援用是為了复古法子調用過程中的動態鏈接(Dynamic Linking)。

來看底下這段代碼。

public class DynamicLinking {     static abstract class Human {        protected abstract void sayHello();     }          static class Man extends Human {         @Override         protected void sayHello() {             System.out.println("须眉哭吧哭吧不是罪");         }     }          static class Woman extends Human {         @Override         protected void sayHello() {             System.out.println("山下的女人是老虎");         }     }      public static void main(String[] args) {         Human man = new Man();         Human woman = new Woman();         man.sayHello();         woman.sayHello();         man = new Woman();         man.sayHello();     } } 

世界對 Java 重寫有了解的話,應該能看懂這段代碼的根由。Man 類和 Woman 類繼承了 Human 類,况且重寫了 sayHello() 法子。來看一下運行結果:

数据显示,美国、日本、德国、澳大利亚、韩国是新增确诊病例数最多的五个国家。法国、俄罗斯、德国、意大利、泰国是新增死亡病例数最多的五个国家。

在2020年8月至2021年4月期间,密歇根大学环境健康科学和全球公共卫生教授理查德·奈泽尔及其同事在封闭的校园内的不同地点使用气泵和拭子采集样本。他们总共收集了250多个空气样本,其中1.6%的样本新冠病毒检测结果呈阳性。而在500多个表面样本中,老司机午夜精品视频资源1.4%呈阳性。

美国加利福尼亚州索尔克生物研究所的研究员马努吉安表示,每天保证至少12个小时不进食,可以让我们的消化系统得到充分休息。威斯康星大学医学与公共卫生学院副教授安德森称,限制热量摄入可以降低人体内的炎症水平。安德森认为,间歇性断食更符合身体的进化方式:“它让身体得到休息,因此能够储存食物,并将能量送到需要的地方,从而触发身体机制释放能量。”

通报称,接防疫部门线索,在对风险人员李某某(男,69岁)排查过程中,该人不如实反映个人行程。经警方调查,5月2日,李某某前往房山区与朋友聚餐。5月4日北京健康宝弹窗提示,其与疫情风险地区、点位、人员等有时空关联,需要进行风险排查。社区工作人员对其电话和当面核实,李某某均隐瞒了前往房山的行程。5月5日,其还前往市场多个摊位购物。该人隐瞒行程的行为,导致相关风险人员和点位未被及时纳入有效管控。5月6日,李某某及其1名亲属确诊。目前,李某某涉及的10余个风险点位被封控,17名密接人员被隔离。5月8日,朝阳公安分局依法对李某某刑事立案调查。

2022年5月9日0—24时,新增本土新冠肺炎确诊病例234例,含156例由既往无症状感染者转为确诊病例。新增治愈出院847例。

须眉哭吧哭吧不是罪 山下的女人是老虎 山下的女人是老虎 

這個運行結果很好暴露,man 的援用類型為 Human,但指向的是 Man 對象,woman 的援用類型也為 Human,但指向的是 Woman 對象;之后,man 又指向了新的 Woman 對象。

從面向對象編程的角度,從多態的角度,我們對運行結果是很好暴露的,但站在 Java 虛擬機的角度,它是怎样判斷 man 和 woman 該調用哪個法子的呢?

用 jclasslib 看一下 main 法子的字節碼指示。

第 1 行:new 指示創建了一個 Man 對象,并將對象的內存地址壓入棧中。 第 2 行:dup 指示將棧頂的值復制一份并壓入棧頂。因為接下來的指示 invokespecial 會耗尽掉一個當前類的援用,是以需要復制一份。 第 3 行:invokespecial 指示用于調用構造法子進行驱动化。 第 4 行:astore_1,Java 虛擬機從棧頂彈出 Man 對象的援用,然后將其存入下標為 1 局部變量 man 中。 第 5、6、7、8 行的指示和第 1、2、3、4 行類似,不同的是 Woman 對象。 第 9 行:aload_1 指示將第局部變量 man 壓入操作數棧中。 第 10 行:invokevirtual 指示調用對象的成員法子 sayHello(),顾惜此時的對象類型為 com/itwanger/jvm/DynamicLinking$Human。 第 11 行:aload_2 指示將第局部變量 woman 壓入操作數棧中。 第 12 行同第 10 行。

顾惜,從字節碼的角度來看,man.sayHello()(第 10 行)和 woman.sayHello()(第 12 行)的字節碼是统统换取的,但我們都暴露,這兩句指示最終執行的目標法子并不换取。

究竟發生了什么呢?

還得從 invokevirtual 這個指示著手,看它是怎样實現多態的。根據《Java 虛擬機規范》,invokevirtual 指示在運行時的明白過程不错分為以下幾步:

①、找到操作數棧頂的元素所指向的對象的實際類型,記作 C。

②、若是在類型 C 中找到與常量池中的面容符匹配的法子,則進行訪問權限校驗,若是通過則复返這個法子的径直援用,查找結束;否則复返 java.lang.IllegalAccessError 異常。

③、否則,按照繼承關系從下往上一次對 C 的各個父類進行第二步的搜索和驗證。

④、若是始終沒有找到合適的法子,則拋出 java.lang.AbstractMethodError 異常。

也便是說,invokevirtual 指示在第一步的時候就確定了運行時的實際類型,是以兩次調用中的 invokevirtual 指示并不是把常量池中法子的符號援用明白到径直援用上就結束了,還會根據法子秉承者的實際類型來選擇法子版块,這個過程便是 Java 重寫的本質。我們把這種在運行期根據實際類型確定法子執行版块的過程稱為動態鏈接。

4)法子复返地址

當一個法子開始執行后,唯一兩種姿色不错退出這個法子:

往时退出,可能會有复返值傳遞給上層的法子調用者,法子是否有复返值以及复返值的類型根據法子复返的指示來決定,像之前提到的 ireturn 用于复返 int 類型,return 用于 void 法子;還有其他的一些,lreturn 用于 long 型,freturn 用于 float,dreturn 用于 double,areturn 用于援用類型。

異常退出,法子在執行的過程中遭遇了異常,况且沒有获取妥善的處理,這種情況下,是不會給它的上層調用者复返任何值的。

無論是哪種姿色退出,在法子退出后,都必須复返到法子领先被調用時的位置,法子期间繼續執行。一般來說,法子往时退出的時候,PC 計數器的值會作為复返地址,棧幀中很可能會保存這個計數器的值,異常退出時則不會。

法子退出的過程實際上等同于把當前棧幀出棧,因此接下來可能執行的操作有:恢復上層法子的局部變量表和操作數棧,把复返值(若是有的話)壓入調用者棧幀的操作數棧中,調整 PC 計數器的值,找到下一條要執行的指示等。

本文轉載自微信公眾號「默然王二」,不错通過以下二維碼關注。轉載本文請聯系默然王二公眾號。