JVM(三) 內(nèi)存與垃圾回收|運行時數(shù)據(jù)區(qū)(上)

本文主要介紹運行時數(shù)據(jù)區(qū)的程序計數(shù)器Java虛擬機棧部分。

目錄
?1 基本概念
??1.1 內(nèi)存
??1.2 線程私有和線程共享
??1.3 線程
???1.3.1 JVM系統(tǒng)線程
?2 程序計數(shù)器(PC寄存器)
??2.1 作用
??2.2 代碼示例
?3 虛擬機棧
??3.1 概述
???3.1.1 背景
???3.1.2 內(nèi)存中的堆與棧
???3.1.3 虛擬機棧基本內(nèi)容
???3.1.4 棧的特點
???3.1.5 棧中可能出現(xiàn)的異常
???3.1.6 設(shè)置棧的大小
??3.2 棧的存儲單位
???3.2.1 棧的存儲結(jié)構(gòu)
??3.3 局部變量表(Local Variables)
???3.3.1 局部變量表
???3.3.2 字節(jié)碼中方法內(nèi)部結(jié)構(gòu)剖析
???3.3.3 slot
???3.3.4 靜態(tài)變量與局部變量的對比
??3.4 操作數(shù)棧(Operand Stack)
??? 3.4.1 概述
???3.4.2 實例解析
???3.4.3 棧頂緩存技術(shù)ToS(Top-of-Stack Cashing)
??3.5 動態(tài)鏈接(Dynamic Linking)
???3.5.1 概述
???3.5.2 方法的調(diào)用
???3.5.3 虛方法和非虛方法
???3.5.4 動態(tài)類型語言和靜態(tài)類型語言
???3.5.5 方法重寫的本質(zhì)
???3.5.6 虛方法表
??3.6 方法返回地址(Return Address)
??3.7 一些附加信息
?4 練習題


1 基本概念

JDK8以后的運行時數(shù)據(jù)區(qū)
1.1 內(nèi)存

內(nèi)存是非常重要的系統(tǒng)資源宵膨,是硬盤和cpu的中間倉庫及橋梁皱炉,承載著操作系統(tǒng)和應(yīng)用程序的實時運行。JVM內(nèi)存布局規(guī)定了JAVA在運行過程中內(nèi)存申請、分配蝇完、管理的策略爸邢,保證了JVM的高效穩(wěn)定運行巫员。不同的jvm對于內(nèi)存的劃分方式和管理機制存在著部分差異。

1.2 線程私有和線程共享

java虛擬機定了了若干種程序運行期間會使用到的運行時數(shù)據(jù)區(qū)甲棍,其中有一些會隨著虛擬機啟動而創(chuàng)建简识,隨著虛擬機退出而銷毀(進程)赶掖。另外一些則是與線程一一對應(yīng)的,這些與線程對應(yīng)的數(shù)據(jù)區(qū)域會隨著線程開始和結(jié)束而創(chuàng)建和銷毀七扰。
如圖奢赂,灰色的區(qū)域為單獨線程私有的,紅色的為多個線程共享的颈走,即

  • 每個線程:獨立包括程序計數(shù)器膳灶、棧、本地棧
  • 線程間共享:堆立由、堆外內(nèi)存(方法區(qū)轧钓、永久代或元空間、代碼緩存)

    一般來說锐膜,jvm垃圾回收95%集中在堆區(qū)毕箍,5%集中在方法區(qū)
1.3 線程
  • 線程是一個程序里的運行單元,JVM允許一個程序有多個線程并行的執(zhí)行道盏;
  • 在HotSpot JVM而柑,每個線程都與操作系統(tǒng)的本地線程直接映射。當一個java線程準備好執(zhí)行以后荷逞,此時一個操作系統(tǒng)的本地線程也同時創(chuàng)建媒咳。java線程執(zhí)行終止后。本地線程也會回收种远。
  • 操作系統(tǒng)負責所有線程的安排調(diào)度到任何一個可用的CPU上涩澡。一旦本地線程初始化成功,它就會調(diào)用java線程中的run()方法.
1.3.1 JVM系統(tǒng)線程
  • 如果你使用jconsole或者任何一個調(diào)試工具坠敷,都能看到在后臺有許多線程在運行妙同。這些后臺線程不包括調(diào)用main方法的main線程以及所有這個main線程自己創(chuàng)建的線程;
  • 這些主要的后臺系統(tǒng)線程在HotSpot JVM里主要是以下幾個:

虛擬機線程:這種線程的操作時需要JVM達到安全點才會出現(xiàn)常拓。這些操作必須在不同的線程中發(fā)生的原因是他們都需要JVM達到安全點渐溶,這樣堆才不會變化。這種線程的執(zhí)行包括“stop-the-world”的垃圾收集弄抬,線程棧收集茎辐,線程掛起以及偏向鎖撤銷
周期任務(wù)線程:這種線程是時間周期事件的提現(xiàn)(比如中斷),他們一般用于周期性操作的調(diào)度執(zhí)行掂恕。
GC線程:這種線程對于JVM里不同種類的垃圾收集行為提供了支持
編譯線程:這種線程在運行時會降字節(jié)碼編譯成本地代碼
信號調(diào)度線程:這種線程接收信號并發(fā)送給JVM,在它內(nèi)部通過調(diào)用適當?shù)姆椒ㄟM行處理拖陆。

下面我們從運行時數(shù)據(jù)區(qū)的組成來了解它

2 程序計數(shù)器(PC寄存器)

JVM中的程序計數(shù)寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器懊亡,寄存器存儲指令相關(guān)的現(xiàn)場信息依啰。CPU只有把數(shù)據(jù)裝載到寄存器才能夠運行。JVM中的PC寄存器是對物理PC寄存器的一種抽象模擬店枣。

2.1 作用

PC寄存器是用來存儲指向下一條指令的地址速警,也就是即將將要執(zhí)行的指令代碼叹誉。由執(zhí)行引擎讀取下一條指令。


  • 它是一塊很小的內(nèi)存空間闷旧,幾乎可以忽略不計长豁。也是運行速度最快的存儲區(qū)域
  • 在jvm規(guī)范中,每個線程都有它自己的程序計數(shù)器忙灼,是線程私有的匠襟,生命周期與線程的生命周期保持一致
  • 任何時間一個線程都只有一個方法在執(zhí)行,也就是所謂的當前方法该园。程序計數(shù)器會存儲當前線程正在執(zhí)行的java方法的JVM指令地址酸舍;或者,如果實在執(zhí)行native方法里初,則是未指定值(undefined)啃勉。
  • 它是程序控制流的指示器,分支青瀑、循環(huán)璧亮、跳轉(zhuǎn)萧诫、異常處理斥难、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計數(shù)器來完成
  • 字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取嚇一跳需要執(zhí)行的字節(jié)碼指令
  • 它是唯一 一個在java虛擬機規(guī)范中沒有規(guī)定任何OOM(OutOfMemory)情況的區(qū)域
2.2 代碼示例
public class PCRegisterTest {

    public static void main(String[] args) {
        int i = 10;
        int j = 20;
        int k = i + j;

        String s = "abc";
        System.out.println(i);
        System.out.println(k);

    }
}

查看以上代碼的字節(jié)碼指令


字節(jié)碼指令

常見問題:
①使用PC寄存器存儲字節(jié)碼指令地址有什么用呢?/ 為什么使用PC寄存器記錄當前線程的執(zhí)行地址呢帘饶?
答:因為CPU需要不停的切換各個線程哑诊,當CPU從A切換到B再切換回來以后,就得知道接著從哪開始繼續(xù)執(zhí)行及刻。JVM的字節(jié)碼解釋器需要通過改變PC寄存器的值來明確下一條應(yīng)該執(zhí)行什么樣的字節(jié)碼指令
②PC寄存器為什么會設(shè)定為線程私有镀裤?
由于CPU時間片輪詢限制,眾多線程在并發(fā)執(zhí)行過程中缴饭,任何一個確定的時刻暑劝,一個處理器或者多核處理器中的一個內(nèi)核,只會執(zhí)行某個線程中的一條指令颗搂。這樣必然導(dǎo)致經(jīng)常中斷或恢復(fù)担猛,如何保證分毫無差呢?
答:為了能夠準確地記錄各個線程正在執(zhí)行的當前字節(jié)碼指令地址丢氢,最好的辦法自然是為每一個線程都分配一個PC寄存器,這樣一來各個線程之間便可以進行獨立計算傅联,從而不會出現(xiàn)相互干擾的情況。

3 虛擬機棧

3.1 概述
3.1.1 背景

由于跨平臺性的設(shè)計疚察,java的指令都是根據(jù)棧來設(shè)計的蒸走。不同平臺CPU架構(gòu)不同,所以不能設(shè)計為基于寄存器的貌嫡。
優(yōu)點是跨平臺比驻,指令集小该溯,編譯器容易實現(xiàn),缺點是性能下降别惦,實現(xiàn)同樣的功能需要更多的指令朗伶。

3.1.2 內(nèi)存中的堆與棧
  • 棧是運行時的單位,而堆是存儲的單位步咪。即:棧解決程序的運行問題论皆,即程序如何執(zhí)行,或者說如何處理數(shù)據(jù)猾漫。堆解決的是數(shù)據(jù)存儲的問題点晴,即數(shù)據(jù)怎么放、放在哪兒悯周。
  • 一般來講粒督,對象主要都是放在堆空間的,椙菀恚空間存放局部變量(基本數(shù)據(jù)類型以及對象的引用)
3.1.3 虛擬機椡篱希基本內(nèi)容
  • java虛擬機棧(Java Virtual Machine Stack),早期也叫Java棧闰挡。每個線程在創(chuàng)建時都會創(chuàng)建一個虛擬機棧锐墙,其內(nèi)部保存一個個的棧幀(Stack Frame),對應(yīng)這個一次次的java方法調(diào)用长酗。

  • 它是線程私有的

  • 生命周期和線程是一致的

  • 作用:主管java程序的運行溪北,它保存方法的局部變量(8種基本數(shù)據(jù)類型、對象的引用地址)夺脾、部分結(jié)果之拨,并參與方法的調(diào)用和返回。

3.1.4 棧的特點
  • 棧是一種快速有效的分配存儲方式咧叭,訪問速度僅次于PC寄存器(程序計數(shù)器)
  • JVM直接對java棧的操作只有兩個: ①每個方法執(zhí)行蚀乔,伴隨著進棧(入棧,壓棧)菲茬;②執(zhí)行結(jié)束后的出棧工作
  • 對于棧來說不存在垃圾回收問題吉挣,但存在OOM問題
3.1.5 棧中可能出現(xiàn)的異常

java虛擬機規(guī)范允許Java棧的大小是動態(tài)的或者是固定不變的

  • 如果采用固定大小的Java虛擬機棧,那每一個線程的java虛擬機棧容量可以在線程創(chuàng)建的時候獨立選定生均。如果線程請求分配的棧容量超過java虛擬機棧允許的最大容量听想,java虛擬機將會拋出一個 StackOverFlowError異常
/**
 * 演示棧中的異常
 */
public class StackErrorTest {
    public static void main(String[] args) {
        main(args);
    }
}
  • 如果java虛擬機棧可以動態(tài)拓展马胧,并且在嘗試拓展的時候無法申請到足夠的內(nèi)存汉买,或者在創(chuàng)建新的線程時沒有足夠的內(nèi)存去創(chuàng)建對應(yīng)的虛擬機棧,那java虛擬機將會拋出一個 OutOfMemoryError異常
3.1.6 設(shè)置棧的大小

我們可以使用參數(shù)-Xss選項來設(shè)置線程的最大椗寮梗空間蛙粘,棧的大小直接決定了函數(shù)調(diào)用的最大可達深度垫卤。

idea設(shè)置棧的大小

/**
 * 演示棧中的異常
 *
 * 默認情況下:count : 9474
 *  設(shè)置棧的大小: -Xss256k : count : 2459
 */
public class StackErrorTest {
    private static int count = 1;
    public static void main(String[] args) {
        System.out.println(count);
        count++;
        main(args);
    }
}
3.2 棧的存儲單位
3.2.1 棧的存儲結(jié)構(gòu)
  • 每個線程都有自己的棧出牧,棧中的數(shù)據(jù)都是以棧幀(Stack Frame)的格式存在
  • 在這個線程上正在執(zhí)行的每個方法都對應(yīng)各自的一個棧幀
  • 棧幀是一個內(nèi)存區(qū)塊穴肘,是一個數(shù)據(jù)集,維系著方法執(zhí)行過程中的各種數(shù)據(jù)信息
3.2.2 棧的運行原理
  • JVM直接對java棧的操作只有兩個舔痕,就是對棧幀的壓棧出棧评抚,遵循先進后出/后進先出的和原則。

  • 在一條活動線程中伯复,一個時間點上慨代,只會有一個活動的棧幀。即只有當前正在執(zhí)行的方法的棧幀(棧頂棧幀)是有效的啸如,這個棧幀被稱為當前棧幀(Current Frame),與當前棧幀對應(yīng)的方法就是當前方法(Current Frame)

  • 執(zhí)行引擎運行的所有字節(jié)碼指令只針對當前棧幀進行操作

  • 如果在該方法中調(diào)用了其他方法侍匙,對應(yīng)的新的棧幀會被創(chuàng)建出來,放在棧的頂端叮雳,成為新的當前棧幀想暗。

public class StackFrameTest {
    public static void main(String[] args) {
            StackFrameTest test = new StackFrameTest();
            test.method1();
    }

    public void method1(){
        System.out.println("method1()開始執(zhí)行...");
        method2();
        System.out.println("method1()執(zhí)行結(jié)束...");

}

    public int method2() {
        System.out.println("method2()開始執(zhí)行...");
        int i = 10;
        int m = (int) method3();
        System.out.println("method2()即將結(jié)束...");
        return i + m;
    }

    public double method3() {
        System.out.println("method3()開始執(zhí)行...");
        double j = 20.0;
        System.out.println("method3()即將結(jié)束...");
        return j;
    }

}
  • 不同線程中所包含的棧幀是不允許相互引用的,即不可能在另一個棧幀中引用另外一個線程的棧幀
  • Java方法有兩種返回函數(shù)的方式帘不,一種是正常的函數(shù)返回说莫,使用return指令;另外一種是拋出異常厌均。不管使用哪種方式唬滑,都會導(dǎo)致棧幀被彈出告唆。
3.2.3 棧的存儲結(jié)構(gòu)

每個棧幀中存儲著:

  • 局部變量表(Local Variables)
  • 操作數(shù)棧(Operand Stack)(或表達式棧)
  • 動態(tài)鏈接(Dynamic Linking)(或執(zhí)行運行時常量池的方法引用)
  • 方法返回地址(Return Adress)(或方法正常退出或者異常退出的定義)
  • 一些附加信息


3.3 局部變量表(Local Variables)
3.3.1 局部變量表
  • 局部變量表也被稱之為局部變量數(shù)組或本地變量表
  • 定義為一個數(shù)字數(shù)組棺弊,主要用于存儲方法參數(shù)和定義在方法體內(nèi)的局部變量,這些數(shù) 據(jù)類型包括各類基本數(shù)據(jù)類型擒悬、對象引用(reference)模她,以及returnAddressleixing
  • 由于局部變量表是建立在線程的棧上,是線程私有的數(shù)據(jù)懂牧,因此不存在數(shù)據(jù)安全問題
  • 局部變量表所需的容量大小是在編譯期確定下來的,并保存在方法的Code屬性的maximum local variables數(shù)據(jù)項中侈净。在方法運行期間是不會改變局部變量表的大小的
  • 方法嵌套調(diào)用的次數(shù)由棧的大小決定。一般來說僧凤,棧越大畜侦,方法嵌套調(diào)用次數(shù)越多。對一個函數(shù)而言躯保,他的參數(shù)和局部變量越多旋膳,使得局部變量表膨脹,它的棧幀就越大途事,以滿足方法調(diào)用所需傳遞的信息增大的需求验懊。進而函數(shù)調(diào)用就會占用更多的椛眯撸空間,導(dǎo)致其嵌套調(diào)用次數(shù)就會減少义图。
  • 局部變量表中的變量只在當前方法調(diào)用中有效减俏。在方法執(zhí)行時,虛擬機通過使用局部變量表完成參數(shù)值到參數(shù)變量列表的傳遞過程碱工。當方法調(diào)用結(jié)束后娃承,隨著方法棧幀的銷毀,局部變量表也會隨之銷毀怕篷。
3.3.2 字節(jié)碼中方法內(nèi)部結(jié)構(gòu)剖析


3.3.3 slot
  • 參數(shù)值的存放總是在局部變量數(shù)組的index0開始草慧,到數(shù)組長度-1的索引結(jié)束

  • 局部變量表,最基本的存儲單元是Slot(變量槽)

  • 局部變量表中存放編譯期可知的各種基本數(shù)據(jù)類型(8種)匙头,引用類型(reference)漫谷,returnAddress類型的變量。

  • 在局部變量表里蹂析,32位以內(nèi)的類型只占用一個slot(包括returnAddress類型)舔示,64位的類型(long和double)占用兩個slot
    ---byte电抚、short惕稻、char、float在存儲前被轉(zhuǎn)換為int蝙叛,boolean也被轉(zhuǎn)換為int俺祠,0表示false,非0表示true借帘;
    ---long和double則占據(jù)兩個slot蜘渣。

  • JVM會為局部變量表中的每一個slot都分配一個訪問索引,通過這個索引即可成功訪問到局部變量表中指定的局部變量值

  • 當一個實例方法被調(diào)用的時候肺然,它的方法參數(shù)和方法體內(nèi)部定義的局部變量將會按照順序被復(fù)制到局部變量表中的每一個slot上

  • 如果需要訪問局部變量表中一個64bit的局部變量值時蔫缸,訪問索引的起始位置即可。(比如:訪問long或者double類型變量)


    image.png
  • 如果當前幀是由構(gòu)造方法或者實例方法創(chuàng)建的际起,那么該對象引用this將會存放在index為0的slot處,其余的參數(shù)按照參數(shù)表順序排列拾碌。

public class LocalVariablesTest {

    private int count = 1;
    //靜態(tài)方法不能使用this
    public static void testStatic(){
        //編譯錯誤,因為this變量不存在與當前方法的局部變量表中
        System.out.println(this.count);
    }
}
this存在slot中
  • 棧幀中的局部變量表中的槽位是可以重復(fù)利用的街望,如果一個局部變量過了其作用域校翔,那么在其作用域之后申明的新的局部變量就很有可能會復(fù)用過期局部變量的槽位,從而達到節(jié)省資源的目的灾前。
3.3.4 靜態(tài)變量與局部變量的對比
  • 變量按照數(shù)據(jù)類型分:
    基本數(shù)據(jù)類型;
    引用數(shù)據(jù)類型防症;
  • 變量按照在類中聲明的位置分:
    成員變量:在使用前,都經(jīng)歷過默認初始化賦值
    --static修飾:類變量。類加載linking的準備階段給類變量默認賦值——>初始化階段給類變量顯式賦值即靜態(tài)代碼塊賦值告希;
    --不被static修飾:實例變量:隨著對象的創(chuàng)建扑浸,會在堆空間分配實例變量空間,并進行默認賦值
    局部變量:在使用前燕偶,必須要進行顯式賦值的喝噪!否則,編譯不通過
public void test5Temp(){
        int num;
        //錯誤信息:變量num未進行初始化    
        System.out.println(num);
    }

補充說明:

  • 在棧幀中指么,與性能調(diào)優(yōu)關(guān)系最為密切的部分就是局部變量表酝惧。在方法執(zhí)行時,虛擬機使用局部變量表完成方法的傳遞
  • 局部變量表中的變量也是重要的垃圾回收根節(jié)點伯诬,只要被局部變量表中直接或間接引用的對象都不會被回收
3.4 操作數(shù)棧(Operand Stack)
3.4.1 概述

椡泶剑可以使用數(shù)組或者鏈表來實現(xiàn),操作數(shù)棧就是用數(shù)組實現(xiàn)的。

  • 每一個獨立的棧幀中除了包含局部變量表以外盗似,還包含一個后進先出的操作數(shù)棧哩陕,也可以成為表達式棧
  • 操作數(shù)棧,在方法執(zhí)行過程中赫舒,根據(jù)字節(jié)碼指令悍及,往棧中寫入數(shù)據(jù)或提取數(shù)據(jù),即入棧(push)或出棧(pop)
  • 操作數(shù)棧接癌,主要用于保存計算過程的中間結(jié)果心赶,同時作為計算過程中變量臨時的存儲空間
  • 操作數(shù)棧就是jvm執(zhí)行引擎的一個工作區(qū)缺猛,當一個方法開始執(zhí)行的時候缨叫,一個新的棧幀也會隨之被創(chuàng)建出來,這個方法的操作數(shù)棧是空的
  • 每一個操作數(shù)棧都會擁有一個明確的棧深度用于存儲數(shù)值荔燎,其所需的最大深度在編譯器就定義好了耻姥,保存在方法的code屬性中,為max_stack的值湖雹。
  • 棧中的任何一個元素都是可以任意的java數(shù)據(jù)類型
    --32bit的類型占用一個棧單位深度
    --64bit的類型占用兩個棧深度單位
  • 操作數(shù)棧雖是數(shù)組但并非采用訪問索引的方式來進行數(shù)據(jù)訪問的咏闪,而是只能通過標磚的入棧push出棧pop操作來完成一次數(shù)據(jù)訪問
  • 如果被調(diào)用的方法帶有返回值的話,其返回值將會被壓入當前棧幀的操作數(shù)棧中摔吏,并更新PC寄存器中下一條需要執(zhí)行的字節(jié)碼指令。
  • 操作數(shù)棧中的元素的數(shù)據(jù)類型必須與字節(jié)碼指令的序列嚴格匹配纵装,這由編譯器在編譯期間進行驗證征讲,同時在類加載過程中的類驗證階段的數(shù)據(jù)流分析階段要再次驗證。
  • 另外橡娄,我們說Java虛擬機的解釋引擎是基于棧的執(zhí)行引擎,其中的棧指的就是操作數(shù)棧诗箍。
3.4.2 實例解析
 public void testAddOperation() {
        //byte、short挽唉、char滤祖、boolean:都以int型來保存
        byte i = 15;
        int j = 8;
        int k = i + j;
    }

結(jié)合上面代碼來看一下一個方法(棧幀)的執(zhí)行過程:
JVM指令可參考JVM指令的使用深入詳解

  • 15入棧
  • 棧頂元素15出棧筷狼,存入局部變量表索引為1的位置(this索引為0)
  • 8入棧
  • 棧頂元素8出棧,存入局部變量表表索引為2的位置
  • 分別從局部變量表中把索引為1和2的是數(shù)據(jù)取出來匠童,放到操作數(shù)棧埂材;
  • 8和15依次出棧,求和并將結(jié)果23入棧汤求;
  • 棧頂元素23出棧俏险,存入局部變量表表索引為3的位置
3.4.3 棧頂緩存技術(shù)ToS(Top-of-Stack Cashing)

基于棧式架構(gòu)的虛擬機所使用的零地址指令更加緊湊扬绪,但完成一項操作的時候必然需要使用更多的入棧和出棧指令竖独,這同時也就意味著將需要更多的指令分派(instruction dispatch)次數(shù)和內(nèi)存讀/寫次數(shù)
由于操作數(shù)是存儲在內(nèi)存中的,因此頻繁地執(zhí)行內(nèi)存讀/寫操作必然會影響執(zhí)行速度挤牛。為了解決這個問題莹痢,HotSpot JVM的設(shè)計者們提出了棧頂緩存技術(shù),將棧頂元素全部緩存在屋里CPU的寄存器中墓赴,以此降低對內(nèi)存的讀/寫次數(shù)格二,提升執(zhí)行引擎的執(zhí)行效率

3.5 動態(tài)鏈接(Dynamic Linking)
3.5.1 概述
  • 每一個棧幀內(nèi)部都包含一個指向運行時常量池或該棧幀所屬方法的引用。包含這個引用的目的就是為了支持當前方法的代碼能夠?qū)崿F(xiàn)動態(tài)鏈接竣蹦。比如invokedynamic指令
  • 在Java源文件被編譯成字節(jié)碼文件中時顶猜,所有的變量和方法引用都作為符號引用(symbolic Refenrence)保存在class文件的常量池里。比如:描述一個方法調(diào)用了另外的其他方法時痘括,就是通過常量池中指向方法的符號引用來表示的长窄,那么動態(tài)鏈接的作用就是為了將這些符號引用轉(zhuǎn)換為調(diào)用方法的直接引用
  • 常量池的作用纲菌,就是為了提供一些符號和常量挠日,便于指令的識別


    動態(tài)鏈接與常量池
3.5.2 方法的調(diào)用

在JVM中,將符號引用轉(zhuǎn)換為調(diào)用方法的直接引用與方法的綁定機制相關(guān)

概念 定義 對應(yīng)的綁定機制
靜態(tài)鏈接 被調(diào)用的目標方法在編譯期可知翰舌,且運行期保持不變 早期綁定
動態(tài)鏈接 編譯期無法被確定下來嚣潜,在程序運行期將調(diào)用方法的符號引用轉(zhuǎn)換為直接引用 晚期綁定

綁定是一個字段、方法或者類在符號引用被替換為直接引用的過程椅贱,這僅僅發(fā)生一次懂算。

隨著高級語言的橫空出世,類似于java一樣的基于面向?qū)ο蟮木幊陶Z言如今越來越多庇麦,盡管這類編程語言在語法風格上存在一定的差別计技,但是它們彼此之間始終保持著一個共性,那就是都支持封裝山橄,集成和多態(tài)等面向?qū)ο筇匦钥迕剑热贿@一類的編程語言具備多態(tài)特性,那么自然也就具備早期綁定和晚期綁定兩種綁定方式。
Java中任何一個普通的方法其實都具備虛函數(shù)的特征睡雇,它們相當于C++語言中的虛函數(shù)(C++中則需要使用關(guān)鍵字virtual來顯式定義)萌衬。如果在Java程序中不希望某個方法擁有虛函數(shù)的特征時,則可以使用關(guān)鍵字final來標記這個方法它抱。

3.5.3 虛方法和非虛方法

子類對象的多態(tài)性使用前提:①類的繼承關(guān)系②方法的重寫

  • 如果方法在編譯器就確定了具體的調(diào)用版本秕豫,這個版本在運行時是不可變的。這樣的方法稱為非虛方法
  • 靜態(tài)方法抗愁、私有方法馁蒂、final方法、實例構(gòu)造器蜘腌、父類方法都是非虛方法
  • 其他方法稱為虛方法

虛擬機中提供了以下幾條方法調(diào)用指令:

  • 普通調(diào)用指令:
    1.invokestatic:調(diào)用靜態(tài)方法沫屡,解析階段確定唯一方法版本;
    2.invokespecial:調(diào)用方法撮珠、私有及父類方法沮脖,解析階段確定唯一方法版本;
    3.invokevirtual調(diào)用所有虛方法芯急;
    4.invokeinterface:調(diào)用接口方法勺届;
  • 動態(tài)調(diào)用指令:
    5.invokedynamic:動態(tài)解析出需要調(diào)用的方法,然后執(zhí)行 .

前四條指令固化在虛擬機內(nèi)部娶耍,方法的調(diào)用執(zhí)行不可人為干預(yù)免姿,而invokedynamic指令則支持由用戶確定方法版本。其中invokestatic指令和invokespecial指令調(diào)用的方法稱為非虛方法榕酒,其余的(final修飾的除外)稱為虛方法胚膊。

/**
 * 解析調(diào)用中非虛方法、虛方法的測試
 */
class Father {
    public Father(){
        System.out.println("Father默認構(gòu)造器");
    }

    public static void showStatic(String s){
        System.out.println("Father show static"+s);
    }

    public final void showFinal(){
        System.out.println("Father show final");
    }

    public void showCommon(){
        System.out.println("Father show common");
    }

}

public class Son extends Father{
    public Son(){
        super();
    }

    public Son(int age){
        this();
    }

    public static void main(String[] args) {
        Son son = new Son();
        son.show();
    }

    //不是重寫的父類方法想鹰,因為靜態(tài)方法不能被重寫
    public static void showStatic(String s){
        System.out.println("Son show static"+s);
    }

    private void showPrivate(String s){
        System.out.println("Son show private"+s);
    }

    public void show(){
        //invokestatic
        showStatic(" 大頭兒子");
        //invokestatic
        super.showStatic(" 大頭兒子");
        //invokespecial
        showPrivate(" hello!");
        //invokespecial
        super.showCommon();
        //invokevirtual 因為此方法聲明有final 不能被子類重寫紊婉,所以也認為該方法是非虛方法
        showFinal();
        //虛方法如下
        //invokevirtual
        showCommon();//沒有顯式加super,被認為是虛方法辑舷,因為子類可能重寫showCommon
        info();

        MethodInterface in = null;
        //invokeinterface  不確定接口實現(xiàn)類是哪一個 需要重寫
        in.methodA();

    }

    public void info(){

    }

}

interface MethodInterface {
    void methodA();
}

  • 關(guān)于invokedynamic指令
    JVM字節(jié)碼指令集一直比較穩(wěn)定喻犁,一直到j(luò)ava7才增加了一個invokedynamic指令,這是Java為了實現(xiàn)【動態(tài)類型語言】支持而做的一種改進何缓。但是java7中并沒有提供直接生成invokedynamic指令的方法肢础,需要借助ASM這種底層字節(jié)碼工具來產(chǎn)生invokedynamic指令。直到Java8的Lambda表達式的出現(xiàn),invokedynamic指令在java中才有了直接生成方式
3.5.4 動態(tài)類型語言和靜態(tài)類型語言
  • 動態(tài)類型語言和靜態(tài)類型語言兩者的卻別就在于對類型的檢查是在編譯期還是在運行期,滿足前者就是靜態(tài)類型語言魔策,反之則是動態(tài)類型語言滞造。
  • 直白來說,靜態(tài)語言是判斷變量自身的類型信息;動態(tài)類型預(yù)言師判斷變量值的類型信息波材,變量沒有類型信息股淡,變量值才有類型信息,這是動態(tài)語言的一個重要特征。
  • Java是靜態(tài)類型語言(盡管lambda表達式為其增加了動態(tài)特性)廷区,js唯灵,python是動態(tài)類型語言.
3.5.5 方法重寫的本質(zhì)
  • 找到操作數(shù)棧的第一個元素所執(zhí)行的對象的實際類型,記作C隙轻。
  • 如果在類型C中找到與常量中的描述符合簡單名稱都相符的方法埠帕,則進行訪問權(quán)限校驗,如果通過則返回這個方法的直接引用玖绿,查找過程結(jié)束敛瓷;如果不通過,則返回java.lang.IllegalAccessError異常斑匪。
  • 否則呐籽,按照繼承關(guān)系從下往上依次對c的各個父類進行第二步的搜索和驗證過程。
  • 如果始終沒有找到合適的方法蚀瘸,則拋出java.lang.AbstractMethodError異常狡蝶。
    不兼容的改變。
3.5.6 虛方法表
  • 在面向?qū)ο缶幊讨兄瑫茴l繁期使用到動態(tài)分派贪惹,如果在每次動態(tài)分派的過程中都要重新在累的方法元數(shù)據(jù)中搜索合適的目標的話就可能影響到執(zhí)行效率。因此寂嘉,為了提高性能奏瞬,jvm采用在類的方法區(qū)建立一個虛方法表(virtual method table)(非虛方法不會出現(xiàn)在表中)來實現(xiàn)。使用索引表來代替查找垫释。
  • 每個類中都有一個虛方法表丝格,表中存放著各個方法的實際入口。
  • 那么虛方法表什么時候被創(chuàng)建棵譬? 虛方法表會在類加載的鏈接解析階段被創(chuàng)建 并開始初始化显蝌,類的變量初始值準備完成之后,jvm會把該類的方發(fā)表也初始化完畢订咸。


3.6 方法返回地址(Return Address)
  • 存放調(diào)用該方法的PC寄存器的值曼尊。

  • 一個方法的結(jié)束,有兩種方式:
    --正常執(zhí)行完成
    --出現(xiàn)未處理的異常脏嚷,非正常退出

  • 無論通過哪種方式退出骆撇,在方法退出后都返回到該方法被調(diào)用的位置。方法正常退出時父叙,調(diào)用者的pc計數(shù)器的值作為返回地址神郊,即調(diào)用該方法的指令的下一條指令的地址肴裙。而通過異常退出時,返回地址是要通過異常表來確定涌乳,棧幀中一般不會保存這部分信息蜻懦。

  • 本質(zhì)上,方法的退出就是當前棧幀出棧的過程夕晓。此時宛乃,需要恢復(fù)上層方法的局部變量表、操作數(shù)棧蒸辆、將返回值壓入調(diào)用者棧幀的操作數(shù)棧征炼、設(shè)置PC寄存器值等,讓調(diào)用者方法繼續(xù)執(zhí)行下去躬贡。

  • 正常完成出口和異常完成出口的區(qū)別在于:通過異常完成出口退出的不會給他的上層調(diào)用者產(chǎn)生任何的返回值谆奥。

  • 當一個方法開始執(zhí)行后,只要兩種方式可以退出這個方法:
    ①執(zhí)行引擎遇到任意一個方法返回的字節(jié)碼指令(return)逗宜,會有返回值傳遞給上層的方法調(diào)用者雄右,簡稱正常完成出口;
    a.一個方法在正常調(diào)用完成之后究竟需要使用哪一個返回指令還需要根據(jù)方法返回值的實際數(shù)據(jù)類型而定
    b.在字節(jié)碼指令中纺讲,返回指令包含ireturn(當返回值是boolena擂仍、byte、char熬甚、short和int類型時使用)逢渔、lreturn、freturn乡括、dreturn以及areturn肃廓,另外還有一個return指令供聲明為void的方法、實例初始化方法诲泌、類和接口的初始化方法使用

②在方法執(zhí)行的過程中遇到了異常(Exception)盲赊,并且這個異常沒有在方法內(nèi)進行處理,也就是只要在本方法的異常表中沒有搜素到匹配的異常處理器敷扫,就會導(dǎo)致方法退出哀蘑,簡稱異常完成出口
方法執(zhí)行過程中拋出異常時的異常處理,存儲在一個異常處理表葵第,方便在發(fā)生異常的時候找到處理異常的代碼绘迁。


3.7 一些附加信息

棧幀中還允許攜帶與java虛擬機實現(xiàn)相關(guān)的一些附加信息。例如卒密,對程序調(diào)試提供支持的信息缀台。(不一定有)

4 練習題

①舉例棧溢出的情況?(StackOverflowError)

遞歸調(diào)用等哮奇,通過-Xss設(shè)置棧的大刑鸥睛约;

②調(diào)整棧的大小,就能保證不出現(xiàn)溢出么依疼?

不能 如遞歸無限次數(shù)肯定會溢出痰腮,調(diào)整棧大小只能保證溢出的時間晚一些

③分配的棧內(nèi)存越大越好么而芥?

不是 會擠占其他線程的空間

④垃圾回收是否會涉及到虛擬機棧律罢?

不會

運行時數(shù)據(jù)區(qū) 是否存在Error 是否存在 GC
程序計數(shù)器 ? ?
本地方法棧 ? ?
jvm虛擬機棧 ? ?
? ?
方法區(qū) ? ?

⑤方法中定義的局部變量是否線程安全?

要具體情況具體分析

/**
 * 方法中定義的局部變量是否線程安全棍丐?具體情況具體分析
 *
 * 何為線程安全误辑?
 *     如果只有一個線程可以操作此數(shù)據(jù),則是線程安全的歌逢。
 *     如果有多個線程操作此數(shù)據(jù)巾钉,則此數(shù)據(jù)是共享數(shù)據(jù)。如果不考慮同步機制的話秘案,會存在線程安全問題
 *
 * StringBuffer是線程安全的砰苍,StringBuilder不是
 */
public class StringBuilderTest {

    //s1的聲明方式是線程安全的
    public static void method1(){
        StringBuilder s1 = new StringBuilder();
        s1.append("a");
        s1.append("b");
    }

    //stringBuilder的操作過程:是不安全的,因為method2可以被多個線程調(diào)用
    public static void method2(StringBuilder stringBuilder){
        stringBuilder.append("a");
        stringBuilder.append("b");
    }

    //s1的操作:是線程不安全的 有返回值阱高,可能被其他線程共享
    public static StringBuilder method3(){
        StringBuilder s1 = new StringBuilder();
        s1.append("a");
        s1.append("b");
        return s1;
    }

    //s1的操作:是線程安全的 赚导,StringBuilder的toString方法是創(chuàng)建了一個新的String,s1在內(nèi)部消亡了
    public static String method4(){
        StringBuilder s1 = new StringBuilder();
        s1.append("a");
        s1.append("b");
        return s1.toString();
    }

    public static void main(String[] args) {
        StringBuilder s = new StringBuilder();
        new Thread(()->{
            s.append("a");
            s.append("b");
        }).start();

        method2(s);

    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末赤惊,一起剝皮案震驚了整個濱河市吼旧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌未舟,老刑警劉巖圈暗,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異裕膀,居然都是意外死亡员串,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門昼扛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來寸齐,“玉大人,你說我怎么就攤上這事野揪》梅蓿” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵斯稳,是天一觀的道長海铆。 經(jīng)常有香客問我,道長挣惰,這世上最難降的妖魔是什么卧斟? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任殴边,我火速辦了婚禮,結(jié)果婚禮上珍语,老公的妹妹穿的比我還像新娘锤岸。我一直安慰自己,他們只是感情好板乙,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布是偷。 她就那樣靜靜地躺著,像睡著了一般募逞。 火紅的嫁衣襯著肌膚如雪蛋铆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天放接,我揣著相機與錄音刺啦,去河邊找鬼。 笑死纠脾,一個胖子當著我的面吹牛玛瘸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播苟蹈,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼糊渊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了汉操?” 一聲冷哼從身側(cè)響起再来,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎磷瘤,沒想到半個月后芒篷,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡采缚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年针炉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片扳抽。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡篡帕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出贸呢,到底是詐尸還是另有隱情镰烧,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布楞陷,位于F島的核電站怔鳖,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏固蛾。R本人自食惡果不足惜结执,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一度陆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧献幔,春花似錦懂傀、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至铸敏,卻和暖如春缚忧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背杈笔。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留糕非,地道東北人蒙具。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像朽肥,于是被迫代替她去往敵國和親禁筏。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345