JVM_03 運行時數據區1-[程序計數器+虛擬機棧+本地方法棧]

完整JVM學習筆記請戳

內存與線程

01 內存

內存是很是重要的系統資源,是硬盤和cpu的中間倉庫及橋樑,承載着操做系統和應用程序的實時運行。JVM內存佈局規定了JAVA在運行過程當中內存申請、分配、管理的策略,保證了JVM的高效穩定運行。不一樣的jvm對於內存的劃分方式和管理機制存在着部分差別(對於Hotspot主要指方法區)java

jdk8之後 ,方法區就是元數據區
(圖源阿里)JDK8的元數據區+JIT編譯產物 就是JDK8之前的方法區

02 分區介紹

java虛擬機定了了若干種程序運行期間會使用到的運行時數據區,其中有一些會隨着虛擬機啓動而建立,隨着虛擬機退出而銷燬。另一些則是與縣城一一對應的,這些與線程對應的數據區域會隨着線程開始和結束而建立和銷燬。
如圖,灰色的區域爲單獨線程私有的,紅色的爲多個線程共享的,即python

  • 每一個線程:獨立包括程序計數器、棧、本地棧
  • 線程間共享:堆、堆外內存(方法區、永久代或元空間、代碼緩存)

通常來講,jvm優化95%是優化堆區,5%優化的是方法區

03 線程

  • 線程是一個程序裏的運行單元,JVM容許一個程序有多個線程並行的執行;
  • 在HotSpot JVM,每一個線程都與操做系統的本地線程直接映射。
    • 當一個java線程準備好執行之後,此時一個操做系統的本地線程也同時建立。java線程執行終止後。本地線程也會回收。
  • 操做系統負責全部線程的安排調度到任何一個可用的CPU上。一旦本地線程初始化成功,它就會調用java線程中的run()方法.

2.1 JVM系統線程

  • 若是你使用jconsole或者任何一個調試工具,都能看到在後臺有許多線程在運行。這些後臺線程不包括調用main方法的main線程以及全部這個main線程本身建立的線程;
  • 這些主要的後臺系統線程在HotSpot JVM裏主要是如下幾個:
    • 虛擬機線程L這種線程的操做時須要JVM達到安全點纔會出現。這些操做必須在不一樣的線程中發生的緣由是他們都須要JVM達到安全點,這樣堆纔不會變化。這種線程的執行包括「stop-the-world」的垃圾收集,線程棧收集,線程掛起以及偏向鎖撤銷
    • 週期任務線程:這種線程是時間週期事件的提現(好比中斷),他們通常用於週期性操做的調度執行。
    • GC線程:這種線程對於JVM裏不一樣種類的垃圾收集行爲提供了支持
    • 編譯線程:這種線程在運行時會降字節碼編譯成本地代碼
    • 信號調度線程:這種線程接收信號併發送給JVM,在它內部經過調用適當的方法進行處理。

1.程序計數器(PC寄存器)

JVM中的程序計數寄存器(Program Counter Register)中,Register的命名源於CPU的寄存器,寄存器存儲指令相關的現場信息。CPU只有把數據裝載到寄存器纔可以運行。JVM中的PC寄存器是對屋裏PC寄存器的一種抽象模擬git

1.1 做用

PC寄存器是用來存儲指向下一條指令的地址,也即將將要執行的指令代碼。由執行引擎讀取下一條指令。github

  • 它是一塊很小的內存空間,幾乎能夠忽略不計。也是運行速度最快的存儲區域
  • 在jvm規範中,每一個線程都有它本身的程序計數器,是線程私有的,生命週期與線程的生命週期保持一致
  • 任什麼時候間一個線程都只有一個方法在執行,也就是所謂的當前方法。程序計數器會存儲當前線程正在執行的java方法的JVM指令地址;或者,若是實在執行native方法,則是未指定值(undefined)。
  • 它是程序控制流的指示器,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成
  • 字節碼解釋器工做時就是經過改變這個計數器的值來選取嚇一跳須要執行的字節碼指令
  • 它是惟一一個在java虛擬機規範中沒有規定任何OOM狀況的區域

1.2 代碼示例

利用javap -v xxx.class反編譯字節碼文件,查看指令等信息面試

1.3 面試常問

1.使用PC寄存器存儲字節碼指令地址有什麼用呢?/ 爲何使用PC寄存器記錄當前線程的執行地址呢? 由於CPU須要不停的切換各個線程,這時候切換回來之後,就得知道接着從哪開始繼續執行
JVM的字節碼解釋器就須要經過改變PC寄存器的值來明確下一條應該執行什麼樣的字節碼指令

2.PC寄存器爲何會設定爲線程私有
咱們都知道所謂的多線程在一個特定的時間段內指回執行其中某一個線程的方法,CPU會不停滴作任務切換,這樣必然會致使常常中斷或恢復,如何保證分毫無差呢?爲了可以準確地記錄各個線程正在執行的當前字節碼指令地址,最好的辦法天然是爲每個線程都分配一個PC寄存器,這樣一來各個線程之間即可以進行獨立計算,從而不會出現相互干擾的狀況。
因爲CPU時間片輪限制,衆多線程在併發執行過程當中,任何一個肯定的時刻,一個處理器或者多核處理器中的一個內核,只會執行某個線程中的一條指令。
這樣必然致使常常中斷或恢復,如何保證分毫無差呢?每一個線程在建立後,都會產生本身的程序計數器和棧幀,程序計數器在各個線程之間互不影響。算法

CPU時間片

CPU時間片即CPU分配各各個程序的時間,每一個線程被分配一個時間段。稱做它的時間片。
在宏觀上:咱們能夠同時打開多個應用程序,每一個程序並行不悖,同時運行。 但在微觀上:因爲只有一個CPU,一次只能處理程序要求的一部分,如何處理公平,一種方法就是引入時間片,每一個程序輪流執行。
並行與併發
並行:同一時間多個線程同時執行;
併發:一個核快速切換多個線程,讓它們依次執行,看起來像並行,其實是併發編程


* 2.虛擬機棧

2.1概述

2.1.1 背景

因爲跨平臺性的設計,java的指令都是根據棧來設計的。不一樣平臺CPU架構不一樣,因此不能設計爲基於寄存器的。
優勢是跨平臺,指令集小,編譯器容易實現,缺點是性能降低,實現一樣的功能須要更多的指令。數組

2.1.2 內存中的堆與棧

  • 棧是運行時的單位,而堆是存儲的單位
    即:棧解決程序的運行問題,即程序如何執行,或者說如何處理數據。堆解決的是數據存儲的問題,即數據怎麼放、放在哪兒。
  • 通常來說,對象主要都是放在堆空間的,是運行時數據區比較大的一塊
  • 棧空間存放 基本數據類型的局部變量,以及引用數據類型的對象的引用

2.1.3 虛擬機棧是什麼

  • java虛擬機棧(Java Virtual Machine Stack),早期也叫Java棧。 每一個線程在建立時都會建立一個虛擬機棧,其內部保存一個個的棧幀(Stack Frame),對應這個一次次的java方法調用。它是線程私有的
  • 生命週期和線程是一致的
  • 做用:主管java程序的運行,它保存方法的局部變量(8種基本數據類型、對象的引用地址)、部分結果,並參與方法的調用和返回。
    • 局部變量:相對於成員變量(或屬性)
    • 基本數據變量: 相對於引用類型變量(類,數組,接口)

2.1.4 棧的特色

  • 棧是一種快速有效的分配存儲方式,訪問速度僅次於PC寄存器(程序計數器)
  • JVM直接對java棧的操做只有兩個
    • 每一個方法執行,伴隨着進棧(入棧,壓棧)
    • 執行結束後的出棧工做
  • 對於棧來講不存在垃圾回收問題

2.1.5 棧中可能出現的異常

java虛擬機規範容許Java棧的大小是動態的或者是固定不變的緩存

  • 若是採用固定大小的Java虛擬機棧,那每個線程的java虛擬機棧容量能夠在線程建立的時候獨立選定。若是線程請求分配的棧容量超過java虛擬機棧容許的最大容量,java虛擬機將會拋出一個 StackOverFlowError異常
/**
 * 演示棧中的異常
 */
public class StackErrorTest {
    public static void main(String[] args) {
        main(args);
    }
}
複製代碼
  • 若是java虛擬機棧能夠動態拓展,而且在嘗試拓展的時候沒法申請到足夠的內存,或者在建立新的線程時沒有足夠的內存去建立對應的虛擬機棧,那java虛擬機將會拋出一個 OutOfMemoryError異常

2.1.6設置棧的內存大小

咱們可使用參數-Xss選項來設置線程的最大棧空間,棧的大小直接決定了函數調用的最大可達深度。 (IDEA設置方法:Run-EditConfigurations-VM options 填入指定棧的大小-Xss256k)安全

/**
 * 演示棧中的異常
 *
 * 默認狀況下:count 10818
 * 設置棧的大小: -Xss256k count 1872
 */
public class StackErrorTest {
    private static int count = 1;
    public static void main(String[] args) {
        System.out.println(count);
        count++;
        main(args);
    }
}
複製代碼

2.2 棧的存儲結構和運行原理

2.2.1

  • 每一個線程都有本身的棧,棧中的數據都是以棧幀(Stack Frame)的格式存在
  • 在這個線程上正在執行的每一個方法都對應各自的一個棧幀
  • 棧幀是一個內存區塊,是一個數據集,維繫着方法執行過程當中的各類數據信息
  • JVM直接對java棧的操做只有兩個,就是對棧幀的壓棧和出棧,遵循先進後出/後進先出的和原則。
  • 在一條活動線程中,一個時間點上,只會有一個活動的棧幀。即只有當前正在執行的方法的棧幀(棧頂棧幀)是有效的,這個棧幀被稱爲當前棧幀(Current Frame),與當前棧幀對應的方法就是當前方法(Current Frame)
  • 執行引擎運行的全部字節碼指令只針對當前棧幀進行操做
  • 若是在該方法中調用了其餘方法,對應的新的棧幀會被建立出來,放在棧的頂端,成爲新的當前棧幀。
  • 不一樣線程中所包含的棧幀是不容許相互引用的,即不可能在另外一個棧幀中引用另一個線程的棧幀
  • 若是當前方法調用了其餘方法,方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,接着,虛擬機會丟棄當前棧幀,使得前一個棧幀從新成爲當前棧幀
  • Java方法有兩種返回函數的方式,一種是正常的函數返回,使用return指令;另一種是拋出異常。無論使用哪一種方式,都會致使棧幀被彈出。

/**
 * 棧幀
 */
public class StackFrameTest {
    public static void main(String[] args) {
        StackFrameTest test = new StackFrameTest();
        test.method1();
        //輸出 method1()和method2()都做爲當前棧幀出現了兩次,method3()一次
//        method1()開始執行。。。
//        method2()開始執行。。。
//        method3()開始執行。。。
//        method3()執行結束。。。
//        method2()執行結束。。。
//        method1()執行結束。。。
    }

    public void method1(){
        System.out.println("method1()開始執行。。。");
        method2();
        System.out.println("method1()執行結束。。。");
    }

    public int method2(){
        System.out.println("method2()開始執行。。。");
        int i = 10;
        int m = (int) method3();
        System.out.println("method2()執行結束。。。");
        return i+m;
    }

    public double method3(){
        System.out.println("method3()開始執行。。。");
        double j = 20.0;
        System.out.println("method3()執行結束。。。");
        return j;
    }

}
複製代碼

2.2.2 棧幀的內部結構

每一個棧幀中存儲着:

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

2.3 局部變量表(Local Variables)

2.3.1 概述

  • 局部變量表也被稱之爲局部變量數組或本地變量表
  • 定義爲一個數字數組,主要用於存儲方法參數和定義在方法體內的局部變量這些數據類型包括各種基本數據類型、對象引用(reference),以及returnAddressleixing
  • 因爲局部變量表是創建在線程的棧上,是線程私有的數據,所以不存在數據安全問題
  • 局部變量表所需的容量大小是在編譯期肯定下來的,並保存在方法的Code屬性的maximum local variables數據項中。在方法運行期間是不會改變局部變量表的大小的
  • 方法嵌套調用的次數由棧的大小決定。通常來講,棧越大,方法嵌套調用次數越多。對一個函數而言,他的參數和局部變量越多,使得局部變量表膨脹,它的棧幀就越大,以知足方法調用所需傳遞的信息增大的需求。進而函數調用就會佔用更多的棧空間,致使其嵌套調用次數就會減小。
  • 局部變量表中的變量只在當前方法調用中有效。在方法執行時,虛擬機經過使用局部變量表完成參數值到參數變量列表的傳遞過程。當方法調用結束後,隨着方法棧幀的銷燬,局部變量表也會隨之銷燬。

利用javap命令對字節碼文件進行解析查看局部變量表,如圖:

也能夠在IDEA 上安裝jclasslib byte viewcoder插件查看字節碼信息,以main()方法爲例

2.3.2 變量槽slot的理解與演示

  • 參數值的存放老是在局部變量數組的index0開始,到數組長度-1的索引結束
  • 局部變量表,最基本的存儲單元是Slot(變量槽)
  • 局部變量表中存放編譯期可知的各類基本數據類型(8種),引用類型(reference),returnAddress類型的變量。
  • 在局部變量表裏,32位之內的類型只佔用一個slot(包括returnAddress類型),64位的類型(long和double)佔用兩個slot。
    • byte、short、char、float在存儲前被轉換爲int,boolean也被轉換爲int,0表示false,非0表示true;
    • long和double則佔據兩個slot。
  • JVM會爲局部變量表中的每個slot都分配一個訪問索引,經過這個索引便可成功訪問到局部變量表中指定的局部變量值
  • 當一個實例方法被調用的時候,它的方法參數和方法體內部定義的局部變量將會按照順序被複制到局部變量表中的每個slot上
  • 若是須要訪問局部變量表中一個64bit的局部變量值時,只須要使用籤一個索引便可。(好比:訪問long或者double類型變量)
  • 若是當前幀是由構造方法或者實例方法建立的,那麼該對象引用this將會存放在index爲0的slot處,其他的參數按照參數表順序排列。
public class LocalVariablesTest {

    private int count = 1;
    //靜態方法不能使用this
    public static void testStatic(){
        //編譯錯誤,由於this變量不存在與當前方法的局部變量表中!!!
        System.out.println(this.count);
    }
}
複製代碼

2.3.3 slot的重複利用

棧幀中的局部變量表中的槽位是能夠重複利用的,若是一個局部變量過了其做用域,那麼在其做用域以後申明的新的局部變量就頗有可能會複用過時局部變量的槽位,從而達到節省資源的目的。

private void test2() {
        int a = 0;
        {
            int b = 0;
            b = a+1;
        }
        //變量c使用以前以及經銷燬的變量b佔據的slot位置
        int c = a+1;
    }
複製代碼

2.3.4 靜態變量與局部變量的對比及小結

變量的分類:

  • 按照數據類型分:
    • ①基本數據類型;
    • ②引用數據類型;
  • 按照在類中聲明的位置分:
    • ①成員變量:在使用前,都經歷過默認初始化賦值
      • static修飾:類變量:類加載linking的準備階段給類變量默認賦值——>初始化階段給類變量顯式賦值即靜態代碼塊賦值;
      • 不被static修飾:實例變量:隨着對象的建立,會在堆空間分配實例變量空間,並進行默認賦值
    • ②局部變量:在使用前,必需要進行顯式賦值的!不然,編譯不經過 補充:
  • 在棧幀中,與性能調優關係最爲密切的部分就是局部變量表。在方法執行時,虛擬機使用局部變量表完成方法的傳遞
  • 局部變量表中的變量也是重要的垃圾回收根節點,只要被局部變量表中直接或間接引用的對象都不會被回收

2.4 操做數棧(Operand Stack)

棧 :可使用數組或者鏈表來實現

  • 每個獨立的棧幀中除了包含局部變量表之外,還包含一個後進先出的操做數棧,也能夠成爲表達式棧
  • 操做數棧,在方法執行過程當中,根據字節碼指令,往棧中寫入數據或提取數據,即入棧(push)或出棧(pop)
    • 某些字節碼指令將值壓入操做數棧,其他的字節碼指令將操做數取出棧,使用他們後再把結果壓入棧。(如字節碼指令bipush操做)
    • 好比:執行復制、交換、求和等操做

2.4.1 概述

  • 操做數棧,主要用於保存計算過程的中間結果,同時做爲計算過程當中變量臨時的存儲空間。
  • 操做數棧就是jvm執行引擎的一個工做區,當一個方法開始執行的時候,一個新的棧幀也會隨之被建立出來,這個方法的操做數棧是空的
  • 每個操做數棧都會擁有一個明確的棧深度用於存儲數值,其所需的最大深度在編譯器就定義好了,保存在方法的code屬性中,爲max_stack的值。
  • 棧中的任何一個元素都是能夠任意的java數據類型
    • 32bit的類型佔用一個棧單位深度
    • 64bit的類型佔用兩個棧深度單位
  • 操做數棧並不是採用訪問索引的方式來進行數據訪問的,而是隻能經過標磚的入棧push和出棧pop操做來完成一次數據訪問
  • 若是被調用的方法帶有返回值的話,其返回值將會被壓入當前棧幀的操做數棧中,並更新PC寄存器中下一條須要執行的字節碼指令。
  • 操做數棧中的元素的數據類型必須與字節碼指令的序列嚴格匹配,這由編譯器在編譯期間進行驗證,同時在類加載過程當中的類驗證階段的數據流分析階段要再次驗證。
  • 另外,咱們說Java虛擬機的解釋引擎是基於棧的執行引擎,其中的棧指的就是操做數棧。
    結合上圖結合下面的圖來看一下一個方法(棧幀)的執行過程
    ①15入棧;②存儲15,15進入局部變量表
    ③壓入8;④存儲8,8進入局部變量表;

⑤從局部變量表中把索引爲1和2的是數據取出來,放到操做數棧;⑥iadd相加操做,8和15出棧

⑦iadd操做結果23入棧;⑧將23存儲在局部變量表索引爲3的位置上

2.4.2 i++ 和 ++i的區別

2.4.3 棧頂緩存技術ToS(Top-of-Stack Cashing)

  • 基於棧式架構的虛擬機所使用的零地址指令更加緊湊,但完成一項操做的時候必然須要使用更多的入棧和出棧指令,這同時也就意味着將須要更多的指令分派(instruction dispatch)次數和內存讀/寫次數
  • 因爲操做數是存儲在內存中的,所以頻繁地執行內存讀/寫操做必然會影響執行速度。爲了解決這個問題,HotSpot JVM的設計者們提出了棧頂緩存技術,將棧頂元素所有緩存在屋裏CPU的寄存器中,以此下降對內存的讀/寫次數,提高執行疫情的執行效率

2.5 動態連接(Dynamic Linking)

  • 每個棧幀內部都包含一個指向運行時常量池或該棧幀所屬方法的引用。包含這個引用的目的就是爲了支持當前方法的代碼可以實現動態連接。好比invokedynamic指令
  • 在Java源文件被編譯成字節碼文件中時,全部的變量和方法引用都做爲符號引用(symbolic Refenrence)保存在class文件的常量池裏。好比:描述一個方法調用了另外的其餘方法時,就是經過常量池中指向方法的符號引用來表示的,那麼動態連接的做用就是爲了將這些符號引用轉換爲調用方法的直接引用。

爲何須要常量池呢
常量池的做用,就是爲了提供一些符號和常量,便於指令的識別。

2.5.1方法的調用

** 在JVM中,將符號引用轉換爲調用方法的直接引用與方法的綁定機制相關 **

  • 靜態連接
    當一個 字節碼文件被裝載進JVM內部時,若是被調用的目標方法在編譯期可知,且運行期保持不變時。這種狀況下將調用方法的符號引用轉換爲直接引用的過程稱之爲靜態連接。
  • 動態連接
    若是被調用的方法在編譯期沒法被肯定下來,也就是說,只可以在程序運行期將調用方法的符號引用轉換爲直接引用,因爲這種引用轉換過程具有動態性,所以也就被稱之爲動態連接。

對應的方法的綁定機制爲:早起綁定(Early Binding)和晚期綁定(Late Bingding)。綁定是一個字段、方法或者類在符號引用被替換爲直接引用的過程,這僅僅發生一次。

  • 早期綁定
    早期綁定就是指被調用的目標方法若是在編譯期可知,且運行期保持不變時,便可將這個方法與所屬的類型進行綁定,這樣一來,因爲明確了被調用的目標方法到底是哪個,所以也就可使用靜態連接的方式將符號引用轉換爲直接引用。
  • 晚期綁定
    若是被調用的方法在編譯期沒法被肯定下來,只可以在程序運行期根據實際的類型綁定相關的方法,這種綁定方式也就被稱之爲晚期綁定。

隨着高級語言的橫空出世,相似於java同樣的基於面向對象的編程語言現在愈來愈多,儘管這類編程語言在語法風格上存在必定的差異,可是它們彼此之間始終保持着一個共性,那就是都支持封裝,集成和多態等面向對象特性,既然這一類的編程語言具有多態特性,那麼天然也就具有早期綁定和晚期綁定兩種綁定方式。
Java中任何一個普通的方法其實都具有虛函數的特徵,它們至關於C++語言中的虛函數(C++中則須要使用關鍵字virtual來顯式定義)。若是在Java程序中不但願某個方法擁有虛函數的特徵時,則可使用關鍵字final來標記這個方法。

2.5.2虛方法和非虛方法

子類對象的多態性使用前提:①類的繼承關係②方法的重寫

非虛方法

  • 若是方法在編譯器就肯定了具體的調用版本,這個版本在運行時是不可變的。這樣的方法稱爲非虛方法
  • 靜態方法、私有方法、final方法、實例構造器、父類方法都是非虛方法
  • 其餘方法稱爲虛方法
虛擬機中提供瞭如下幾條方法調用指令:

普通調用指令:
1.invokestatic:調用靜態方法,解析階段肯定惟一方法版本;
2.invokespecial:調用方法、私有及弗雷方法,解析階段肯定惟一方法版本;
3.invokevirtual調用全部虛方法;
4.invokeinterface:調用接口方法;
動態調用指令:
5.invokedynamic:動態解析出須要調用的方法,而後執行 .
前四條指令固化在虛擬機內部,方法的調用執行不可人爲干預,而invokedynamic指令則支持由用戶肯定方法版本。其中invokestatic指令和invokespecial指令調用的方法稱爲非虛方法,其他的(final修飾的除外)稱爲虛方法。

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

    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();
    }

    //不是重寫的父類方法,由於靜態方法不能被重寫
    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  不肯定接口實現類是哪個 須要重寫
        in.methodA();

    }

    public void info(){

    }

}

interface MethodInterface {
    void methodA();
}
複製代碼
關於invokedynamic指令
  • JVM字節碼指令集一直比較穩定,一直到java7才增長了一個invokedynamic指令,這是Java爲了實現【動態類型語言】支持而作的一種改進
  • 可是java7中並無提供直接生成invokedynamic指令的方法,須要藉助ASM這種底層字節碼工具來產生invokedynamic指令.直到Java8的Lambda表達式的出現,invokedynamic指令的生成,在java中才有了直接生成方式
  • Java7中增長的動態語言類型支持的本質是對java虛擬機規範的修改,而不是對java語言規則的修改,這一塊相對來說比較複雜,增長了虛擬機中的方法調用,最直接的受益者就是運行在java憑條的動態語言的編譯器
動態類型語言和靜態類型語言
  • 動態類型語言和靜態類型語言二者的卻別就在於對類型的檢查是在編譯期仍是在運行期,知足前者就是靜態類型語言,反之則是動態類型語言。
  • 直白來講 靜態語言是判斷變量自身的類型信息;動態類型預言師判斷變量值的類型信息,變量沒有類型信息,變量值纔有類型信息,這是動態語言的一個重要特徵
  • Java是靜態類型語言(儘管lambda表達式爲其增長了動態特性),js,python是動態類型語言.

2.5.3方法重寫的本質

  • 1 找到操做數棧的第一個元素所執行的對象的實際類型,記做C。
  • 2.若是在類型C中找到與常量中的描述符合簡單名稱都相符的方法,則進行訪問權限校驗,若是經過則返回這個方法的直接引用,查找過程結束;若是不經過,則返回java.lang.IllegalAccessError異常。
  • 3.不然,按照繼承關係從下往上依次對c的各個父類進行第二步的搜索和驗證過程。
  • 4.若是始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError異常。 IllegalAccessError介紹 程序視圖訪問或修改一個屬性或調用一個方法,這個屬性或方法,你沒有權限訪問。通常的,這個會引發編譯器異常。這個錯誤若是發生在運行時,就說明一個類發生了不兼容的改變。

2.5.4 虛方法表

  • 在面向對象編程中,會很頻繁期使用到動態分派,若是在每次動態分派的過程當中都要從新在累的方法元數據中搜索合適的目標的話就可能影響到執行效率。所以,爲了提升性能,jvm採用在類的方法區創建一個虛方法表(virtual method table)(非虛方法不會出如今表中)來實現。使用索引表來代替查找。
  • 每一個類中都有一個虛方法表,表中存放着各個方法的實際入口。
  • 那麼虛方法表何時被建立? 虛方法表會在類加載的連接階段被建立 並開始初始化,類的變量初始值準備完成以後,jvm會把該類的方發表也初始化完畢。

2.6 方法返回地址(Return Address)

  • 存放調用該方法的PC寄存器的值。
  • 一個方法的結束,有兩種方式:
    • 正常執行完成
    • 出現未處理的異常,非正常退出
  • 不管經過哪一種方式退出,在方法退出後都返回到該方法被調用的位置。方法正常退出時,調用者的pc計數器的值做爲返回地址,即調用該方法的指令的下一條指令的地址。而經過異常退出時,返回地址是要經過異常表來肯定,棧幀中通常不會保存這部分信息。
  • 本質上,方法的退出就是當前棧幀出棧的過程。此時,須要恢復上層方法的局部變量表、操做數棧、將返回值也如調用者棧幀的操做數棧、設置PC寄存器值等,讓調用者方法繼續執行下去。
  • 正常完成出口和異常完成出口的區別在於:經過異常完成出口退出的不會給他的上層調用者產生任何的返回值。

當一個方法開始執行後,只要兩種方式能夠退出這個方法: 一、執行引擎遇到任意一個方法返回的字節碼指令(return),會有返回值傳遞給上層的方法調用者,簡稱正常完成出口;

  • 一個方法在正常調用完成以後究竟須要使用哪個返回指令還須要根據方法返回值的實際數據類型而定
  • 在字節碼指令中,返回指令包含ireturn(當返回值是boolena、byte、char、short和int類型時使用)、lreturn、freturn、dreturn以及areturn,另外還有一個return指令供聲明爲void的方法、實例初始化方法、類和接口的初始化方法使用

二、在方法執行的過程當中遇到了異常(Exception),而且這個異常沒有在方法內進行處理,也就是隻要在本方法的異常表中沒有搜素到匹配的異常處理器,就會致使方法退出,簡稱異常完成出口
方法執行過程當中拋出異常時的異常處理,存儲在一個異常處理表,方便在發生異常的時候找處處理異常的代碼。

2.7 一些附加信息

棧幀中還容許攜帶與java虛擬機實現相關的一些附加信息。例如,對程序調試提供支持的信息。(不少資料都忽略了附加信息)

2.8 虛擬機棧的5道面試題

1.舉例棧溢出的狀況?(StackOverflowError)

  • 遞歸調用等,經過-Xss設置棧的大小;

2.調整棧的大小,就能保證不出現溢出麼?

  • 不能 如遞歸無限次數確定會溢出,調整棧大小隻能保證溢出的時間晚一些

3.分配的棧內存越大越好麼?

  • 不是 會擠佔其餘線程的空間

4.垃圾回收是否會涉及到虛擬機棧?

  • 不會
內存區塊 Error GC
程序計數器
本地方法棧
jvm虛擬機棧
方法區

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

  • 要具體狀況具體分析
/**
 * 面試題:
 * 方法中定義的局部變量是否線程安全?具體狀況具體分析
 *
 * 何爲線程安全?
 *     若是隻有一個線程能夠操做此數據,則斃是線程安全的。
 *     若是有多個線程操做此數據,則此數據是共享數據。若是不考慮同步機制的話,會存在線程安全問題
 *
 * StringBuffer是線程安全的,StringBuilder不是
 */
public class StringBuilderTest {

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

    //stringBuilder的操做過程:是不安全的,由於method2能夠被多個線程調用
    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方法是建立了一個新的String,s1在內部消亡了
    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);

    }

}
複製代碼

3 本地方法棧

  • Java虛擬機棧用於管理Java方法的調用,而本地方法棧用於管理本地方法的調用
  • 本地方法棧,也是線程私有的。
  • 容許被實現成固定或者是可動態拓展的內存大小。(在內存溢出方面是相同的)
    • 若是線程請求分配的棧容量超過本地方法棧容許的最大容量,Java虛擬機將會拋出一個StackOverFlowError異常。
    • 若是本地方法棧能夠動態擴展,而且在嘗試擴展的時候沒法申請到足夠的內存,或者在建立新的線程時沒有足夠的內存去建立對應的本地方法棧,那麼java虛擬機將會拋出一個OutOfMemoryError異常。
  • 本地方法是使用C語言實現的
  • 它的具體作法是Native Method Stack中登記native方法,在Execution Engine執行時加載本地方法庫。
  • 當某個線程調用一個本地方法時,它就進入了一個全新的而且再也不受虛擬機限制的世界。它和虛擬機擁有一樣的權限
    • 本地方法能夠經過本地方法接口來 訪問虛擬機內部的運行時數據區
    • 它甚至能夠直接使用本地處理器中的寄存器
    • 直接從本地內存的堆中分配任意數量的內存
  • 並非全部的JVM都支持本地方法。由於Java虛擬機規範並無明確要求本地方法棧的使用語言、具體實現方式、數據結構等。若是JVM產品不打算支持native方法,也能夠無需實現本地方法棧。
  • 在hotSpot JVM中,直接將本地方法棧和虛擬機棧合二爲一。


JVM學習代碼及筆記(陸續更新中...)

【代碼】
github.com/willShuhuan…
【筆記】
JVM_01 簡介
JVM_02 類加載子系統
JVM_03 運行時數據區1- [程序計數器+虛擬機棧+本地方法棧]
JVM_04 本地方法接口
JVM_05 運行時數據區2-堆
JVM_06 運行時數據區3-方法區
JVM_07 運行時數據區4-對象的實例化內存佈局與訪問定位+直接內存
JVM_08 執行引擎(Execution Engine)
JVM_09 字符串常量池StringTable
JVM_10 垃圾回收1-概述+相關算法
JVM_11 垃圾回收2-垃圾回收相關概念
JVM_12 垃圾回收3-垃圾回收器

相關文章
相關標籤/搜索