JVM詳解(三)——運行時數據區


一、概述

1、介紹

  類比一下:紅框就好比內存的運行時數據區,在各自不同的位置放了不同的東西。而廚師就好比執行引擎。

  內存是非常重要的系統資源,是硬盤和CPU的中間倉庫及橋梁,承載着操作系統和應用程序的實時運行。JVM內存布局規定了Java在運行過程中內存申請、分配、管理的策略,保證了JVM的高效穩定運行。不同的JVM對於內存的划分方式和管理機制存在着部分差異(典型的不同,就是針對方法區)。結合JVM虛擬機規范,來探討一下經典的JVM內存布局。運行時數據區-詳圖:

  紅框處有變化,叫方法區,JDK7之前叫永久代,JDK8之后叫元空間。整個紅框也可以叫非堆空間。具體變化在方法區一節中會詳細闡述。
  Java虛擬機定義了若干個程序運行期間會使用到的運行時數據區,其中有一些會隨着虛擬機啟動而創建,退出而銷毀;另外一些是與線程一一對應的,會隨着線程開始而創建,結束而銷毀。
  灰色,每個線程私有:程序計數器(PC)、虛擬機棧(VMS)、本地方法棧(NMS)。
  紅色,線程共享:堆、堆外空間(方法區:永久代或叫元空間 + 代碼緩存)。

  JVM優化當中,線程里面的結構沒有太多優化的點,重點說的優化(我們講垃圾回收)指的是堆空間,當然也包括方法區(主要放類的信息)。從頻率來上說,95%的垃圾回收都集中在堆區,5%是集中在方法區。方法區,jdk8以后,又叫元空間,使用的是本地內存。本地內存還是比較大的,如果沒有進行過參數設置,方法區一般來說,不會出現溢出,因為本地內存一般比較大。
  每個JVM只有一個Runtime實例,即為運行時環境,相當於內存結構中間的那個框框:運行時環境。
  Runtime:運行時對象,一個JVM實例,就對應一個Runtime的實例。Runtime實例對象就相當於運行時數據區。整個運行時數據區對虛擬機來說只有一份。Runtime是單例的。

2、線程

  線程是程序里的一個運行單元,JVM允許一個應用有多個線程並行的執行。在HotSpot JVM里,每個線程都與操作系統的本地線程直接映射。當一個Java線程准備好執行以后,此時一個操作系統的本地線程也同時創建。Java線程執行終止后,本地線程也會回收。操作系統負責所有線程的安排調度到任何一個可用的CPU上,一旦本地線程初始化成功,它就會調用Java線程中的run()方法。
  如果程序中,都是守護線程,那么虛擬機就可以退出了。
  主要的后台系統線程在HotSpot JVM里主要是一下幾個:
  虛擬機線程:這種線程的操作是需要JVM達到安全點才會出現。這些操作必須在不同的線程中發生的原因是他們都需要JVM達到安全點,這樣堆才不會變化。這種線程的執行類型包括"stop-the-world"的垃圾收集,線程棧收集,線程掛起以及偏向鎖撤銷。
  周期任務線程:這種線程是時間周期事件的體現(比如中斷),他們一般用於周期性操作的調度執行。
  GC線程:這種線程對在JVM里不同種類的垃圾收集行為提供了支持。
  編譯線程:這種線程在運行時會將字節碼編譯成到本地代碼。
  信號調度線程:這種線程接收信息並發送給JVM,在它內部通過調用適當的方法進行處理。

二、程序計數器

1、介紹

  JVM中的程序計數寄存器(Program Counter Register),Register的命名源於CPU的寄存器,寄存器存儲指令相關的現場信息。CPU只有把數據裝載到寄存器才能能夠運行。這里,並非是廣義上所指的物理寄存器,或許將其翻譯為PC計數器(或指令計數器,也稱程序鈎子)會更貼切,並且不容易引起不必要的誤會。JVM中的PC寄存器是對物理PC寄存器的一種抽象模擬。
  作用:PC寄存器用來存儲指向下一條指令的地址,即,將要執行的指令代碼,由執行引擎讀取下一條指令。

  它是一塊很小的內存空間,幾乎可以忽略不計,也是運行速度最快的存儲區域。在JVM規范中,每個線程都有自己的程序計數器,是線程私有的,生命周期與線程的生命周期保持一致。在任何時間,一個線程都只有一個方法在執行,也就是當前方法。程序計數器會存儲當前線程正在執行的Java方法的JVM指令地址。或者,如果是在執行native方法,則是未指定值(undefined)。
  它是程序控制流的指示器,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令。它是唯一一個在Java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域。PC既沒有GC ,也沒有OOM。
  理解成游標、或者Java里集合的迭代器。用於指明當前程序到哪兒了。
  代碼示例:使用說明

 1 public class Main {
 2     public static void main(String[] args) {
 3         Main test = new Main();
 4         test.minus();
 5     }
 6 
 7     public int minus() {
 8         int c = 301;
 9         int d = 401;
10         return c - d;
11         
12         // int k = c + d;
13         // String s = "abc";
14     }
15 }
使用說明

  字節碼文件:

2、常見問題

  使用PC寄存器存儲字節碼指令地址有什么用呢?(為什么使用PC寄存器記錄當前線程的執行地址呢?)
  因為CPU需要不停的在A、B、C各個線程之間切換,切換回來以后,需要知道接着從哪開始繼續執行。JVM的字節碼解釋器就需要通過改變PC寄存器的值來明確下一條應該執行什么樣的字節碼指令。

  PC寄存器為什么會是線程私有?
  所謂的多線程,在一個特定的時間段只會執行其中某一個線程的方法,CPU不停的做切換,這樣必然導致經常中斷或恢復,為了能夠准確的記錄各個線程正在執行的當前字節碼指令地址,最好的辦法就是每個線程都分配一個PC寄存器,這樣各個線程之間便可以進行獨立計算,從而不會出現相互干擾的情況。
  由於CPU時間片限制,眾多線程在並發執行過程中,任何一個確定的時刻,一個處理器或多核處理器中的一個內核,只會執行某個線程中的一條指令。每個線程在創建后,都會產生自己的程序計數器和棧幀,程序計數器在各個線程之間互不影響。
  這個很容器理解,不各自一份的話,在切換的時候,線程1執行到哪一條指令不就丟失了嘛。

3、CPU時間片

  CPU時間片,即CPU分配給各個程序的時間,每個線程被分配一個時間片段,稱作時間片。
  在宏觀上:我們可以同時打開多個應用程序,每個程序並行不悖,同時運行。
  在微觀上:由於只有一個CPU,一次只能處理程序要求的一部分,通過CPU時間片輪詢的方式。

三、虛擬機棧

1、介紹

  棧和寄存器:由於跨平台性的設計,Java的指令都是根據棧來設計的。不同平台CPU架構不同,所以不能設計為基於寄存器的。優點是跨平台,指令集小,編譯器容易實現,缺點是性能下降,實現同樣的功能需要更多的指令。
  內存中的棧與堆:棧是運行時的單位,而堆是存儲的單位。棧管運行,堆管存儲。棧解決程序的運行問題,即程序如何執行,或者說如何處理數據。堆解決數據存儲的問題,即數據怎么放,放在哪兒。
  Java虛擬機棧,早期也叫Java棧,每個線程在創建時都會創建一個虛擬機棧,其內部保存一個個的棧幀,對應着一次次的Java方法調用,是線程私有的。生命周期與線程一致,一個線程,對應一個Java虛擬機棧。
  作用:主管Java程序的運行,它保存了方法的局部變量(8種基本數據類型、對象的引用地址)、部分結果、並參與方法的調用和返回。

  主體的數據都在堆中放,對象主要在堆中放。方法內的局部變量,是放在棧空間中的。指的基本數據類型,要是引用數據類型,在棧空間,只是放了對象的引用。

  每一個方法,跟一個棧幀都是一一對應的關系。很顯然:目前方法B,是在這個棧的棧頂,把棧頂的這個方法,叫做當前方法。當方法B執行完以后,綠框就出棧了,它一出棧,方法A就變成了當前方法。一次次的方法調用,就對應着一個個棧幀的入棧出棧操作。虛擬機棧是隨着線程的創建而創建的,自然也隨着線程的消亡而消亡。當主線程(main方法)的methodA執行完以后,主線程就結束了,當前這個虛擬機棧就結束了。
  優點:棧是一種快速有效的分配存儲方式,訪問速度僅次於程序計數器。JVM直接對Java棧的操作只有兩個:每個方法執行,伴隨着入棧,執行結束后,伴隨着出棧。對於棧來說,不存在垃圾回收問題。

  棧的常見異常?
  Java虛擬機規范允許Java棧的大小是動態的或者固定不變的。
  如果是固定大小的,每一個線程的Java虛擬機棧容量可以在線程創建的時候獨立選定,若線程請求分配的棧容量超過Java虛擬機棧允許的最大容量,Java虛擬機將會拋出一個StackOverflowError異常。
  如果是動態擴展的,則在嘗試擴展的時候無法申請到足夠的內存,或者在創建新的線程時沒有足夠的內存去創建對應的虛擬機棧,那Java虛擬機將會拋出一個OutOfMemoryError異常。
  代碼示例:

1 // 方法的遞歸調用.棧溢出
2 public class Main {
3     public static void main(String[] args) {
4         main(args);
5     }
6 }
7 
8 // Exception in thread "main" java.lang.StackOverflowError

  如何設置棧的大小?
  使用參數-Xss來設置線程的最大棧空間,棧的大小直接決定了函數調用的最大可達深度。
  代碼示例:

 1 public class Main {
 2     private static int count = 1;
 3 
 4     public static void main(String[] args) {
 5         System.out.println(count);
 6         count++;
 7         main(args);
 8     }
 9 }
10 
11 // 未設置棧大小.默認1024k
12 // 11420
13 // Exception in thread "main" java.lang.StackOverflowError
14 
15 // 設置棧大小 -Xss256k
16 // 2463
17 // Exception in thread "main" java.lang.StackOverflowError

2、棧的存儲單位(棧幀)

  每個線程都有自己的棧,棧中的數據都是以棧幀(Stack Frame)的格式存在。在這個線程上的每個方法都各自對應一個棧幀。棧幀是一個內存區塊,是一個數據集,維系着方法執行過程中的各種數據信息。方法和棧幀是一一對應的關系。一個方法的執行就對應一個棧幀的入棧。方法的結束,這個棧幀就會出棧。
  棧的運行原理:JVM直接對Java棧的操作只有兩個,就是對棧幀的入棧和出棧。在一條活動線程中,一個時間點上,只會有一個活動的棧幀,即只有當前正在執行的方法的棧幀(棧頂棧幀)是有效的,這個棧幀被稱為當前棧幀,與當前棧幀相對應的方法就是當前方法,定義這個方法的類就是當前類。
  執行引擎運行的所有字節碼指令只針對當前棧幀進行操作。如果在該方法中調用了其他方法,對應的新的棧幀會被創建出來,放在棧的頂端,成為新的當前幀。運行原理:

  不同線程中所包含的棧幀是不允許存在相互引用的,即不可能在一個棧幀之后引用另外一個線程的棧幀。如果當前方法調用了其他方法,方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,接着,虛擬機會丟棄當前棧幀,使得前一個棧幀重新成為當前棧幀。Java方法有兩種返回方式,一種是正常函數返回,使用return指令;另外一種是拋出異常(未捕獲)。不管使用哪種方式,都會導致棧幀被彈出。
  棧的內部結構:局部變量表,操作數棧,動態鏈接(指向運行時常量池的方法引用),方法返回地址(或方法正常退出或者異常退出的定義),一些附加信息。

 

  棧的大小:一個固定大小的棧,滿了就會報stackoverflow,那么一個棧,到底能放多少棧幀呢?
  如果棧的大小是固定的,那么取決於棧幀的大小。棧幀小就能放的多,棧幀大就放的少。棧幀的大小主要取決於局部變量表,操作數棧。而一個棧幀的大小,又影響棧中能存放棧幀的個數。以及大概什么時候會出現異常。

3、局部變量表

  局部變量表也稱為局部變量數組或本地變量表,是一個一維的數組。定義為一個數字數組,主要用於存儲方法形參和方法的局部變量,包括基本數據類型、對象引用,以及returnAddress類型。這些在編譯期可知。
  由於局部變量表是建立在線程的棧上,是線程私有的,因此不存在數據安全問題。
  局部變量表所需的容量大小是在編譯期就確定了,並保存在方法的code屬性的maximum local variable數據項中,在方法運行期間是不會改變的。
  棧幀中,局部變量表的長度,所有的方法,在編譯完以后,就確定了。
  方法嵌套調用的次數由棧的大小決定。局部變量表中的變量只在當前方法調用中有效。在方法執行時,虛擬機通過使用局部變量表完成參數值到參數變量列表的傳遞過程。當方法調用結束后,隨着方法棧幀的銷毀,局部變量表也隨之銷毀。皮之不存毛將焉附。
  代碼示例:局部變量表大小

 1 // 局部變量表的大小在編譯期就確定
 2 public class Main {
 3     public static void main(String[] args) {
 4         Main test = new Main();
 5         int num = 10;
 6         test.methodA();
 7     }
 8 
 9     public void methodA() {
10 
11     }
12 }

  解析字節碼文件(截取部分):

  Start PC:0、8、11 ,變量作用域的起始位置。
  length:16,指的長度(偏移量),不是終止位置。
  StartPC + length = 16(code length)

  slot:槽

  參數值的存放在局部變量數組的 index0~length-1。局部變量表,最基本的存儲單元是slot(變量槽)。在局部變量表里,32位以內的類型只占用一個slot,64位的類型(long、double)占用兩個slot。byte、short、char、boolean在存儲前被轉換為int,0表示false,非0表示true。
  JVM為局部變量表每一個slot分配一個訪問索引,通過這個索引訪問。當一個方法被調用的時候,方法形參和方法的局部變量將按順序被復制到每一個slot上。若需要訪問一個64位的局部變量,只需要使用前一個索引即可。
  若當前棧幀是由構造方法或實例方法創建的,那么index0將會存放對象引用this,其余的參數按順序排列。slot-槽:

  slot的重復利用
  棧幀中的局部變量表中的槽是可以重用的。如果一個局部變量過了其作用域,那么在其后聲明的新的變量就會復用過期的槽位,從而達到節省資源的目的。
  代碼示例:

 1 package com.lx.jvm.day01;
 2 
 3 public class Main {
 4     public static void main(String[] args) {
 5 
 6     }
 7 
 8     public void test() {
 9         int a = 100;
10         {
11             int b = 200;
12             b = a + 300;
13         }
14 
15         int c = a + 400;
16     }
17 }

  局部變量表:

  可以看到:變量c使用的是之前變量b銷毀的slot的位置,索引為2。且變量b的作用域也不同。slot索引0 放的this。
  變量的分類

  總結
  在棧幀中,與性能調優關系最為密切的部分就是前面提到的局部變量表。在方法執行時,虛擬機使用局部變量表完成方法的傳遞。
  局部變量表中的變量也是重要的垃圾回收根節點,只要被局部變量表中直接或間接引用的對用都不會被回收。

  局部變量表中的變量,如果不存在了(這個指針不存在了)。 那這邊(堆空間)的垃圾就需要被回收。這就涉及到一個性能調優的問題。
  比如:我們說棧溢出了,內存不夠了。棧當中,占據空間比較大的,是局部變量表。局部變量表越大,棧幀就越大。整個棧里,能夠嵌套的個數就越少。

4、操作數棧

  操作數棧,在方法執行過程中,根據字節碼指令,往棧中寫入,提取數據,即入棧,出棧。某些字節碼指令將值壓入操作數棧,其余的字節碼指令將操作數取出,使用后再把結果壓棧。比如:執行復制,交換,求和等操作。
  棧:可以使用數組或鏈表來實現。操作數棧和局部變量表,都是用數組實現的。

  操作數棧,主要用於保存計算過程的中間結果,同時作為計算過程中變量的臨時存儲空間。操作數棧就是JVM執行引擎的一個工作區,當一個方法剛開始執行的時候,一個新的棧幀也隨之被創建,這個方法的操作數棧是空的。每一個操作數棧都擁有一個明確的棧深度用於存儲數值,其所需的最大深度在編譯期就確定了,保存在方法的code屬性中的max_stack的值。
  棧中的元素可以是Java任意數據類型,32位占用一個棧單位,64位占用兩個。操作數棧並非采用訪問索引的方式來進行數據訪問的,而是只能通過標准的出棧,入棧操作來完成一次數據訪問。
  如果被調用的方法帶有返回值,其返回值將會被壓入當前棧幀的操作數棧中,並更新PC寄存器中下一條執行的字節碼指令。操作數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,這由編譯器在編譯期間進行驗證,同時在類加載過程中的類檢驗階段的數據流分析階段要再次驗證。因為在編譯期,需要確定操作棧的大小,而不同類型需要的棧大小是不同的。
  另外,我們說Java虛擬機的解釋引擎是基於棧的執行引擎,其中的棧指的就是操作數棧。
  操作數棧是用數組實現。而我們知道,數組一旦被創建,其長度就是確定的!那么,上面提到的"這個方法的操作數棧是空的",此時,數組被創建了,這個數組的長度是多少呢?就是下面提到的 max_stack。
  代碼示例:代碼追蹤示例:

 1 public void test() {
 2     // byte、short、char、boolean,都是以int型來保存
 3     byte i = 15;
 4     int j = 8;
 5     int k = i + j;
 6 
 7     // int m = 800;
 8 }
 9 
10 // 對應的 bytecode
11  0 bipush 15
12  2 istore_1
13  3 bipush 8
14  5 istore_2
15  6 iload_1
16  7 iload_2
17  8 iadd
18  9 istore_3
19 10 return

  動態過程:

  iadd操作(字節碼指令)是從操作數棧中,彈出 8 和 15 ,由執行引擎解析為機器指令,交給CPU執行后,得出結果為23,並把執行結果壓棧。

 1 // 再看一下這個的字節碼指令
 2 public void test() {
 3     byte i = 15;
 4     int j = 8;
 5     int k = i + j;
 6     
 7     int m = this.test1();
 8 }
 9 
10 public int test1() {
11     return 20 + 30;
12 }

  aload_0:獲取上一個棧幀返回的結果,並保存在操作數棧中。

  棧頂緩存技術:

  前面介紹過,基於棧式架構的虛擬機所使用的零地址指令更加緊湊,但完成一項操作需要使用更多的入棧,出棧指令,這同時也就意味着將需要更多的指令分派次數和內存讀寫次數。
  由於操作數是存儲在內存中的,因此頻繁的執行內存讀寫操作必然會影響執行速度。為了解決這個問題,HotSpot JVM的設計者們提出了棧頂緩存技術,將棧頂元素全部緩存在物理CPU的寄存器中,以此降低對內存的讀寫次數,從而提升執行引擎的執行效率。寄存器,指令更少,執行速度快。

5、動態鏈接

  動態鏈接(或指向運行時常量池的方法引用):每一個棧幀內部包含一個指向運行時常量池中該棧幀所屬方法的引用。包含這個引用的目的就是為了支持當前方法的代碼能夠實現動態鏈接(Dynamic Linking),比如:invokedynamic指令。
  在字節碼文件中,所有的變量和方法引用都作為符號引用保存在class文件的常量池里。動態鏈接的作用就是為將這些符號引用轉換為調用方法的直接引用。

  運行時常量池,就是上面的constant pool。在java.exe之后,就把constant pool,存在方法區,運行時常量池這個位置。
  為什么需要常量池?
  作用:為了提供一些符號和常量,便於指令的識別。一個在字節碼文件里,一個在運行時方法區中,可以使得字節碼文件比較小,不能什么都在字節碼文件都寫明,比如父類Object等等。

6、方法返回地址

  存放調用該方法的PC寄存器的值。
  方法退出后,都返回到該方法被調用的位置。方法正常退出時,調用者的PC寄存器的值作為返回地址,即調用該方法的指令的下一條指令的地址。異常退出時,返回地址要通過異常表來確定,棧幀中一般不會保存這部分信息。
  當一個方法開始執行后,只有兩種方式可以退出這個方法:
  正常完成出口:執行引擎遇到任意一個方法返回的字節碼指令(return),會有返回值傳遞給上層的方法調用者,簡稱正常完成出口。
  異常完成出口:在方法執行的過程中遇到了異常(Exception),並且這個異常沒有在方法內進行處理,也就是只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會導致方法退出,簡稱異常完成出口。
  方法執行過程中拋出異常時的異常處理,存儲在一個異常處理表,方便在發生異常的時候找到處理異常的代碼。
  本質上,方法的退出就是當前棧幀出棧的過程。此時,需要恢復上層方法的局部變量表、操作數棧、將返回值壓入調用者棧幀的操作數棧、設置PC寄存器值等,讓調用者方法繼續執行下去。
  正常返回和異常返回的區別在於:通過異常完成出口退出的不會給他的上層調用者產生任何的返回值。

7、一些附加信息

  棧幀中還允許攜帶與Java虛擬機實現相關的一些附加信息,比如:對程序調試提供支持的信息。

四、方法的調用

1、方法的綁定機制

  在JVM中,將符號引用轉換為調用方法的直接引用與方法的綁定機制相關。
  靜態鏈接:當一個字節碼文件被裝載進JVM內部時,如果被調用的目標方法在編譯期可知,且運行期保持不變,這種情況,將調用方法的符號引用轉換為直接引用的過程稱為靜態鏈接。
  動態鏈接:如果被調用的目標方法在編譯期不可確定,即,只能夠在程序運行期將調用方法的符號引用轉換為直接引用,由於這種引用轉換過程具備動態性,稱為動態鏈接。
  對應的方法的綁定機制為:早期綁定和晚期綁定。綁定是一個字段、方法或類在符號引用被替換為直接引用的過程,這僅僅發生一次。
  早期綁定:指被調用的目標方法如果在編譯期可知,且運行期保持不變時,可將這個方法與所屬的類型進行綁定,這樣一來,由於明確了被調用的目標方法究竟是哪一個,因此也就可以使用靜態鏈接的方式將符號引用轉換為直接引用。
  晚期綁定:指如果被調用的方法在編譯期無法確定下來,只能夠在程序運行期根據實際的類型綁定相關的方法,這種綁定方式稱為晚期綁定。
  代碼示例:早期綁定和晚期綁定

 1 interface Huntable {
 2     void hunt();
 3 }
 4 
 5 class Animal {
 6     public void eat() {
 7         System.out.println("動物進食");
 8     }
 9 }
10 
11 
12 class Dog extends Animal implements Huntable {
13 
14     @Override
15     public void hunt() {
16         System.out.println("狗捕耗子");
17     }
18 
19     @Override
20     public void eat() {
21         System.out.println("狗吃骨頭");
22     }
23 }
24 
25 
26 class Cat extends Animal implements Huntable {
27 
28     public Cat() {
29         // 表現為:早期綁定
30         super();
31     }
32 
33     public Cat(String name) {
34         // 表現為:早期綁定
35         this();
36     }
37 
38     @Override
39     public void hunt() {
40         System.out.println("貓捕耗子");
41     }
42 
43     @Override
44     public void eat() {
45         // 表現為:早期綁定
46         super.eat();
47         System.out.println("貓吃魚");
48     }
49 }
50 
51 
52 public class Test {
53 
54     public static void main(String[] args) {
55 
56     }
57 
58     public void showAnimal(Animal animal) {
59         // 表現為:晚期綁定
60         animal.eat();
61     }
62 
63     public void showHunt(Huntable huntable) {
64         // 表現為:晚期綁定
65         huntable.hunt();
66     }
67 }
早期綁定和晚期綁定

  隨着高級語言的橫空出世,類似於Java一樣的基於面向對象的編程語言如今越來越多,盡管這類編程語言在語法風格上存在一定的差別,但是它們彼此之間始終保持一個共性,那就是都支持封裝,繼承和多態等面向對象特性,既然這一類的編程語言具備多態特性,那么自然也就具備早期綁定和晚期綁定兩種綁定方式。
  Java中任何一個普通的方法其實都具備虛函數的特征,它們相當於C++語言中的虛函數(C++中則需要使用關鍵字virtual來顯示定義)。如果在Java程序中不希望某個方法擁有虛函數的特征時,則可以使用關鍵字final來修飾。

2、虛方法與非虛方法

  非虛方法:如果方法在編譯期就確定了具體的調用版本,這個版本在運行時是不可變的,這樣的方法稱為非虛方法。表現為早期綁定或靜態鏈接。靜態方法、私有方法、final 方法、實例構造器、父類方法都是非虛方法。
  虛方法:其他方法稱為虛方法。
子類對象的多態性的使用前提:①類的繼承關系,②方法的重寫。

  虛擬機中提供了以下幾條方法調用指令:
  普通調用指令:

  invokestatic:調用靜態方法,解析階段確定唯一方法版本。
  invokespecial:調用<init>方法、私有及父類方法,解析階段確定唯一方法版本。
  invokevirtual:調用所有虛方法。
  invokeinterface:調用接口方法。

  動態調用指令:

  invokedynamic:動態解析出需要調用的方法,然后執行。

  前四條指令固化在虛擬機內部,方法的調用執行不可人為干預,而invokedynamic指令則支持由用戶確定方法版本。其中invokestatic指令和invokespecial指令調用的方法稱為非虛方法,其余的(final修飾的除外)稱為虛方法。
  代碼示例:4種指令

 1 public class Main {
 2     public static void main(String[] args) {
 3         Son so = new Son();
 4         so.show();
 5     }
 6 }
 7 
 8 class Son extends Father {
 9     public Son() {
10         // invokespecial
11         super();
12     }
13 
14     public Son(int age) {
15         // invokespecial
16         this();
17     }
18 
19     // 不是重寫的父類的靜態方法,因為靜態方法不能被重寫!
20     public static void showStatic(String str) {
21         System.out.println("son " + str);
22     }
23 
24     private void showPrivate(String str) {
25         System.out.println("son private" + str);
26     }
27 
28     public void show() {
29         // invokestatic
30         showStatic("atguigu.com");
31         // invokestatic
32         super.showStatic("good!");
33         // invokespecial
34         showPrivate("hello!");
35         // invokespecial
36         super.showCommon();
37 
38         // 因為此方法聲明有final,不能被子類重寫,所以也認為此方法是非虛方法。
39         // invokevirtual
40         showFinal();
41 
42         // 虛方法如下:
43         // invokevirtual
44         showCommon();
45         info();
46 
47         MethodInterface in = null;
48         // invokeinterface
49         in.methodA();
50     }
51 
52     public void info() {
53 
54     }
55 
56     public void display(Father f) {
57         f.showCommon();
58     }
59 
60 }
61 
62 class Father {
63     public Father() {
64         System.out.println("father的構造器");
65     }
66 
67     public static void showStatic(String str) {
68         System.out.println("father " + str);
69     }
70 
71     public final void showFinal() {
72         System.out.println("father show final");
73     }
74 
75     public void showCommon() {
76         System.out.println("father 普通方法");
77     }
78 }
79 
80 interface MethodInterface {
81     void methodA();
82 }
4種指令

  invokedynamic指令:
  JVM字節碼指令集一直比較穩定,一直到Java7中才增加了一個invokedynamic指令,這是Java為了實現【動態類型語言】支持而做的一種改進。
  但是在Java7中並沒有提供直接生成invokedynamic指令的方法,需要借助ASM這種底層字節碼工具來產生invokedynamic指令。直到Java8的Lambda表達式的出現,invokedynamic指令的生成,在Java中才有了直接的生成方式。
  Java7中增加的動態語言類型支持的本質是對Java虛擬機規范的修改,而不是對Java語言規則的修改,這一塊相對來講比較復雜,增加了虛擬機中的方法調用,最直接的受益者就是運行在Java平台的動態語言的編譯器。
  動態類型語言和靜態類型語言:動態類型語言和靜態類型語言的區別就在於對類型的檢查是在編譯期還是運行期,滿足前者就是靜態類型語言,反之就是動態類型語言。再直白一點就是,靜態類型語言是判斷變量自身的類型信息;動態類型語言是判斷變量值的類型信息,變量沒有類型,變量值才有類型信息,這是動態語言的一個重要特征。
  Java語言是靜態類型語言。

  Java:String info = "haha"; // info = haha;
  JS:var name = "hehe"; var name = 10;

  代碼示例:體會invokedynamic

 1 public class Main {
 2     public static void main(String[] args) {
 3         Lambda lambda = new Lambda();
 4 
 5         // invokedynamic
 6         Func func = s -> {
 7             return true;
 8         };
 9 
10         lambda.lambda(func);
11 
12         // invokedynamic
13         lambda.lambda(s -> {
14             return true;
15         });
16     }
17 }
18 
19 class Lambda {
20 
21     public void lambda(Func func) {
22         return;
23     }
24 
25 }
26 
27 @FunctionalInterface
28 interface Func {
29     public boolean func(String str);
30 }
體會

3、方法重寫的本質

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

4、虛方法表

  在面向對象編程中,會很頻繁的使用到動態分派。如果在每次動態分派的過程中都要重新在類的方法元數據中搜索合適的目標的話就可能影響到執行效率。因此,為了提高性能,JVM采用在類的方法區建立一個虛方法表(virtual method table)(非虛方法不會出現在表中)來實現。使用索引表來代替查找。
  每個類中都有一個虛方法表,表中存放着各個方法的實際入口。
  那么虛方法表什么時候被創建?
  虛方法表會在類加載的鏈接階段被創建並初始化,類的變量初始值准備完成之后,JVM會把該類的方法表也初始化完畢。

  代碼示例:虛方法表

 1 interface Friendly {
 2     void sayHello();
 3 
 4     void sayGoodbye();
 5 }
 6 
 7 class Cat implements Friendly {
 8     public void eat() {
 9     }
10 
11     @Override
12     public void sayHello() {
13     }
14 
15     @Override
16     public void sayGoodbye() {
17     }
18 
19     @Override
20     protected void finalize() {
21     }
22 
23     @Override
24     public String toString() {
25         return "Cat";
26     }
27 }
28 
29 class Dog {
30 
31     public void sayHello() {
32     }
33 
34     @Override
35     public String toString() {
36         return "Dog";
37     }
38 
39 }
40 
41 class CockerSpaniel extends Dog implements Friendly {
42 
43     @Override
44     public void sayHello() {
45         super.sayHello();
46     }
47 
48     @Override
49     public void sayGoodbye() {
50     }
51 }
虛方法表

  Dog虛方法表:

  CockerSpaniel虛方法表:

  Cat虛方法表:

五、本地方法棧

1、介紹

  Java虛擬機棧用於管理Java方法的調用,而本地方法棧用於管理本地方法的調用。本地方法棧,線程私有的。
  允許被實現成固定或者可動態擴展的內存大小(在內存溢出方面是相同的):
  (1)如果線程請求分配的棧容量超過本地方法棧允許的最大容量,Java虛擬機將會拋出一個StackOverflowError異常。
  (2)如果本地方法棧可以動態擴展,並且在嘗試擴展的時候無法申請到足夠的內存,或者在創建新的線程時沒有足夠內存去創建對應的本地方法棧,那么Java虛擬機將會拋出一個OutOfMemoryError異常。
  本地方法是用C語言實現的。它的具體做法是Native Method Stack中登記native方法,在執行引擎執行時加載本地方法庫。
  當某個線程調用一個本地方法時,它就進入了一個全新的並且不再受虛擬機限制的世界。它和虛擬機擁有同樣的權限。
  (1)本地方法可以通過本地方法接口來訪問虛擬機內部的運行時數據區。
  (2)它甚至可以直接使用本地處理器中的寄存器。
  (3)直接從本地內存的堆中分配任意數量的內存。
  並不是所有的JVM都支持本地方法。因為Java虛擬機規范並沒有明確要求本地方法棧的使用語言、具體實現方式、數據結構等。如果JVM產品不打算支持native方法,也無需實現本地方法棧。
  在HotSpot JVM中,直接將本地方法棧和虛擬機棧合二為一了。
  代碼示例:舉例

 1 System.currentTimeMillis();
 2 
 3 new Thread().start();
 4 
 5 // 源碼
 6 public static native long currentTimeMillis();
 7 
 8 public synchronized void start() {
 9 
10     if (threadStatus != 0)
11         throw new IllegalThreadStateException();
12     group.add(this);
13 
14     boolean started = false;
15     try {
16         start0();
17         started = true;
18     } finally {
19         try {
20             if (!started) {
21                 group.threadStartFailed(this);
22             }
23         } catch (Throwable ignore) {
24             /* do nothing. If start0 threw a Throwable then
25               it will be passed up the call stack */
26         }
27     }
28 }
29 
30 private native void start0();
源碼

六、本地方法接口

1、介紹

  這個模塊並不屬於運行時數據區。簡單地講,一個Native Method就是一個Java調用非Java代碼的接口。該方法的實現由非Java語言實現,比如C。這個特征並非Java所特有,很多其他的編程語言都有這一機制,比如在C++中,你可以用extern "C"告知C++編譯器去調用一個C的函數。
  "A native method is a Java method whose implementation is provided by non-java code"
  在定義一個native method時,並不提供實現體(有些像定義一個Java interface),其實現體由非Java語言在外面實現。
  本地接口的作用是融合不同的編程語言為Java所用,它的初衷是融合C/C++程序。
  代碼示例:

 1 public native void method1(int x);
 2 
 3 public native static long method2();
 4 
 5 private native synchronized float method3(Object o);
 6 
 7 native void method4(int[] x) throws Exception;
 8 
 9 // 反例,會報編譯錯誤
10 public abstract native void method5();

  標識符native可以與所有其他的Java標識符連用,但是abstract除外。

2、為什么要使用Native Method?

  Java使用起來非常方便,然而有些層次的任務用Java實現起來不容易,或者對程序的效率很在意時,問題就來了。
  與Java環境外交互:有時Java應用需要與Java外面的環境交互,這是本地方法存在的主要原因。Java需要與一些底層系統,如操作系統或某些硬件交換信息時,本地方法正是這樣一種交流機制,它為我們提供了一個非常簡潔的接口,而且我們無需去了解Java應用之外的繁瑣的細節。
  與操作系統交互:JVM支持Java語言本身和運行時庫,它是Java程序賴以生存的平台,它由一個解釋器(解釋字節碼)和一些連接到本地代碼的庫組成。然而不管怎樣,它畢竟不是一個完整的系統,它經常依賴於一些底層系統的支持。這些底層系統常常是強大的操作系統。通過使用本地方法,得以用Java實現了jre的與底層系統的交互,甚至JVM的一些部分就是用C寫的。還有,如果使用一些Java語言本身沒有提供封裝的操作系統的特性時,也需要使用本地方法。
  Sun's Java:Sun的解釋器是用C實現的,這使得它能像一些普通的C一樣與外部交互。jre大部分是用Java實現的,它也通過一些本地方法與外界交互。例如:類java.lang.Thread的setPriority()方法是用Java實現的,但是它實現調用的是該類中的本地方法setPriority0()。這個本地方法是用C實現的,並被植入JVM內部。
  現狀:目前該方法使用的越來越少了,除非是與硬件有關的應用,比如通過Java程序驅動打印機或者Java系統管理生產設備,在企業級應用中已經比較少見。因為現在的異構領域間的通信很發達,比如可以使用Socket通信,也可以使用Web Service等等。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM