JVM與字節碼—類的方法區模型

從一個類開始

咱們從一個簡單類開始提及:java

package example.classLifecicle;
public class SimpleClass {
	public static void main(String[] args) {
		SimpleClass ins = new SimpleClass();
	}
}

這是一段平凡得不能再平凡的Java代碼,稍微有點編程語言入門知識的人都能理解它表達的意思:node

  1. 建立一個名爲SimpleClass的類;
  2. 定義一個入口main方法;
  3. 在main方法中建立一個SimpleClass類實例;
  4. 退出。

什麼是Java bytecode

那麼這一段代碼是怎麼在機器(JVM)裏運行的呢?在向下介紹以前先說清幾個概念。linux

首先,Java語言和JVM徹底能夠當作2個徹底不相干的體系。雖然JVM全稱叫Java Virtual Machine,最開始也是爲了可以實現Java的設計思想而制定開發的。可是時至今日他徹底獨立於Java語言成爲一套生命力更爲強悍的體系工具。他有整套規範,根據這個規範它有上百個應用實現,其中包括咱們最熟悉的hotspot、jrockit等。還有一些知名的變種版本——harmony和android dalvik,嚴格意義上變種版本並不能叫java虛擬機,由於其並未按照jvm規範開發,可是從設計思想、API上看又有大量的類似之處。android

其次,JVM並不能理解Java語言,他所理解的是稱之爲Java bytecode的"語言"。Java bytecode從形式上來講是面向過程的,目前包含130多個指令,他更像能夠直接用於CPU計算的一組指令集。因此不管什麼語言,最後只要按照規範編譯成java bytecode(如下簡稱爲"字節碼")均可以在JVM上運行。這也是scala、groovy、kotlin等各具特點的語言雖然在語法規則上不一致,可是最終均可以在JVM上平穩運行的緣由。數據庫

Java bytecode的規範和存儲形式

前面代碼保存成 .java 文件而後用下面的命令編譯事後就能夠生成.class字節碼了:編程

$ javac SimpleClass.java #SimpleClass.class

字節碼是直接使用2進制的方式存儲的,每一段數據都定義了具體的做用。下面是SimpleClass.class 的16進制數據(使用vim + xxd打開):vim

一個 .class 文件的字節碼分爲10個部分:windows

0~4字節:文件頭,用於表示這是一個Java bytecode文件,值固定爲0xCAFEBABE。緩存

2+2字節:編譯器的版本信息。app

2+n字節:常量池信息。

2字節:入口權限標記。

2字節:類符號名稱。

2字節:父類符號名稱。

2+n字節:接口。

2+n字節:域(成員變量)。

2+n字節:方法。

2+n字節:屬性。

每一個部分的前2個字節都是該部分的標識位。

本篇的目的是說明字節碼的做用以及JVM如何使用字節碼運轉的,想要詳細瞭解2進制意義的請看這裏:http://www.javashuo.com/article/p-utpexxcn-cv.html

反彙編及字節碼解析

咱們可使用 javap 命令將字節碼反彙編成咱們容易閱讀的格式化了的指令集編碼:

$ javap -p SimpleClass.class #查看類和成員
$ javap -s SimpleClass.class #查看方法簽名
$ javap -c SimpleClass.class #反彙編字節碼
$ javap -v SimpleClass.class #返彙編查看全部信息

javap 還有不少的參數,可使用 javap --help 來了解。下面是使用javap -v 命令輸出的內容,輸出了常量池信息、方法簽名、方法描述、堆棧數量、本地內存等信息:

public class example.classLifecicle.SimpleClass
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#13         // java/lang/Object."<init>":()V
   #2 = Class              #14            // example/classLifecicle/SimpleClass
   #3 = Methodref          #2.#13         // example/classLifecicle/SimpleClass."<init>":()V
   #4 = Class              #15            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               main
  #10 = Utf8               ([Ljava/lang/String;)V
  #11 = Utf8               SourceFile
  #12 = Utf8               SimpleClass.java
  #13 = NameAndType        #5:#6          // "<init>":()V
  #14 = Utf8               example/classLifecicle/SimpleClass
  #15 = Utf8               java/lang/Object
{
  public example.classLifecicle.SimpleClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class example/classLifecicle/SimpleClass
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 5: 0
        line 6: 8
}

下面是關於字節碼格式的描述:

public class example.classLifecicle.SimpleClass

這一段表示這個類的符號。

flags: ACC_PUBLIC, ACC_SUPER

該類的標記。例如是不是public類等等,實際上就是將一些Java關鍵字轉譯成對應的Java bytecode。

Constant pool:

constant pool: 以後的內容一直到 { 符號,都是咱們所說的"常量池"。在對java類進行編譯以後就會產生這個常量池。一般咱們所說的類加載,就是加載器將字節碼描述的常量信息轉換成實際存儲在運行時常量池中的一些內存數據(固然每一個方法中的指令集也會隨之加載到方法指向的某個內存空間中)。

"#1"能夠理解爲常量的ID。能夠把常量池看做一個Table,每個ID都指向一個常量,而在使用時都直接用"#1"這樣的ID來引用常量。

常量池中的包含了運行這個類中方法全部須要用到的全部常量信息,Methodref、Class、Utf八、NameAndType等表示常量的類型,後面跟隨的參數表示這個常量的引用位置或者數值。

{}:

常量池以後的{}之間是方法。每個方法分爲符號(名稱)、標記、描述以及指令集。descriptor:描述。flags:入口權限標記。Code:指令集。

Code中,stack表示這一段指令集堆棧的最大深度, locals表示本地存儲的最大個數, args_size表述傳入參數的個數。

字節碼如何驅動機器運行

在往下說以前,先說下JVM方法區的內容。方法區顧名思義就是存儲各類方法的地方。可是從實際應用來看,以Hotspot爲例——方法區在實現時一般分爲class常量池、運行常量池。在大部分書籍中,運行時常量池被描述爲包括類、方法的全部描述信息以及常量數據(詳情請看這裏的介紹)。

對於機器來講並不存在什麼類的感念的。到了硬件層面,他所能瞭解的內容就是:1)我要計算什麼(cpu),2)我要存儲什麼(緩存、主存、磁盤等,咱們統稱內存)?

按照分層模型來講JVM只是一個應用進程,是不可能直接和機器打交道的(這話也不是絕對的,有些虛擬機還真直接看成操做系統在特有硬件設備上用)。在JVM到硬件之間還隔着一層操做系統,在本地運行時是直接調用操做系統接口的(windows和linux都是C/C++)。不過爲了JVM虛擬機更高效,字節碼設計爲更接近機器邏輯行爲的方式來運行。否則也不必弄一個字節碼來轉譯Java語言,像nodejs用的V8引擎那樣實時編譯Javascript不是更直接?這也是過去C/C++唾棄Java效率低下,到了現在Java反而去吐槽其餘解釋型編譯環境跑得慢的緣由(不過這也不見得100%正確。好比某些狀況下Java在JVM上處理JSON不見得比JavaScript在nodejs上快,並且寫起代碼來也挺費勁的)。

咱們回到硬件計算和存儲的問題。CPU的計算過程實質上就是操做系統的線程不斷給CPU傳遞指令集。線程就像傳送帶同樣,把一系列指令排好隊而後一個一個交給CPU去處理。每個指令告訴CPU幹一件事,而幹事的先後總得有個依據(輸入)和結果(輸出),這就是各類緩存、內存、磁盤的做用——提供依據、保存結果。JVM線程和操做系統線程是映射關係(mapping),而JVM的堆(heap)和非堆(Non-heap)就是一個內存管理的模型。因此咱們跳出分層的概念,將字節碼理解爲直接在驅動cpu和內存運行的彙編碼更容易理解。

最後,咱們回到方法區(Method Area)這個規範概念。CPU只關心一堆指令,而JVM中全部的指令都是放置在方法區中的。JVM的首要任務是把這些指令有序的組織起來,按照編程好的邏輯將指令一個一個交給CPU去運行。而CPU都是靠線程來組織指令運算的,因此JVM中每一個線程都有一個線程棧,經過他將指令組織起來一個一個的交給CPU去運算——這就是計數器(Counter Register,用以指示當前應該執行什麼字節碼指令)、線程棧(Stacks,線程的運算模型——先進後出) 和 棧幀(Stacks Frame,方法執行的本地變量) 的概念。因此不管多複雜的設計,方法區能夠簡單的理解爲:有序的將指令集組織起來,並在使用的時候能夠經過某些方法找到對應的指令集合

解析常量池

先看 SimpleClass 字節碼中常量池中的一些數據,上圖中每個方框表示一個常量。方框中第一行的 #1 表示當前常量的ID,第二行 Methodref 表示這個這個常量的類型,第三行 #4,#13 表示常量的值。

咱們從 #1 開始跟着每一個常量的值向下延伸能夠展開一根以 Utf8 類型做爲葉節點的樹,每個葉節點都是一個值。全部的方法咱們均可以經過樹的方式展開獲得下面的查詢字段:

class = java/lang/Object //屬於哪一個類
method = "<init>" //方法名稱
params = NaN //參數
return = V //返回類型

全部的方法都會以 package.class.name:(params)return 的形式存儲在方法區中,經過上面的參數很快能夠定位到方法,例如  java.lang.Object."<init>":()V,這裏"<init>"是構造方法專用的名稱。

解析方法中的指令集

方法除了用於定位的標識符外就是指令集,下面解析main方法的指令集:

0: new           #2                  // class example/classLifecicle/SimpleClass
3: dup
4: invokespecial #3                  // Method "<init>":()V
7: astore_1
8: return

1))new 表示新建一個ID爲#2的對象即SimpleClass(#2->#15="example/classLifecicle/SimpleClass")。此時JVM會在堆上建立一個能放置SimpleClass類的空間並將引用地址返回寫到棧頂。這裏僅僅完成在堆中分配空間,沒執行初始化。

2)dup表示複製棧頂數據。此時棧中有2個指向同一內存區域的SimpleClass引用。

3)invokespecial #3表示執行#3的方法。經過解析常量池#3就是SimpleClass的構造方法。此後會將SimpleClass構造方法中的指令壓入棧中執行。

4)接下來來是SimpleClass的構造方法部分: a)aload_0 表示將本地內存的第一個數據壓入棧頂,本地內存的第一個數據就是this。b)invokespecial #1 表示執行 Object 的構造方法。c)退出方法。這樣就完成了實例的構造過程。

5)完成上述步驟後,線程棧上還剩下一個指向SimpleClass實例的引用,astore_1 表示將引用存入本地緩存第二個位置。

6)return -> 退出 main 方法。

方法區結構

那麼在方法區中全部的類是如何組織存放的呢?

咱們用一個關係型數據庫常的結構就能夠解釋他。在數據庫中咱們經常使用的對象有3個——表、字段、數據。每個類對應的字節碼咱們均可以當作會生成2張數據庫表——常量池表、方法表。經過字節碼的解析,在內存中產生了以下結構的表:

常量池表:example.classLifecicle.SimpleClass_Constant

id type value
#1 Methodref #4,#13
…… ……
#4 Class #15
#15 Utf8 java/lang/Object

方法表:example.classLifecicle.SimpleClass_Method

name params return flag code
<init>     NaN V static,public ……
…  …… …… …… ……

而後在運行過程當中當計數器遇到 invokespecial #3 這樣的指令時就會根據指令後面的ID去本類的常量表中查詢並組裝數據。當組裝出 class = java/lang/Object、method = "<init>"、params = NaN、return = V這樣的數據後,就會去名爲java.lang.Object的表中根據 method、params、return 字段的數據查詢對應的code,找到後爲該code建立一個本地內存,隨後線程計數器逐個執行code中的指令。

這裏僅僅用關係型數據庫表的概念來解釋方法區中如何將指令執行和字節碼對應起來,真正的JVM運行方式比這複雜得多。不過這樣很容易理解方法區究竟是怎麼一回事。

相關文章
相關標籤/搜索