1. 程式人生 > >JVM相關總結

JVM相關總結

記憶體區域和記憶體溢位異常

執行時資料區域

Java虛擬機器執行時資料區

  • 程式計數器 是一塊較小、執行緒私有的的記憶體空間,可以看作是當前執行緒所執行的位元組碼的行號指示器。

    如果執行緒正在執行的是一個Java方法,計數器記錄的就是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是Native方法,計數器就為空值

    這個記憶體區域是唯一一個在Java虛擬機器規範中沒有規定OutOfMemoryError的情況

  • Java虛擬機器棧 執行緒私有的記憶體空間,生命週期與執行緒相同。

    描述的是Java方法(位元組碼)執行的記憶體模型:每個方法執行的同時都會建立一個棧幀用於儲存區域性變數、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫到執行完成對應著一個棧幀在虛擬機器棧中入棧到出站的過程

    其中區域性變量表部分存放了編譯器可知的各種基本資料型別、物件引用(reference型別)、returnAddress型別(指向了一條位元組碼指令的地址)。這些都是所執行的方法中所持有的。基本資料型別中的long和double型別的資料會佔用2個區域性變數空間(Slot),其餘的佔用1個。 區域性變量表的記憶體空間是在編譯期間完成分配,方法執行期間不會改變其大小

    這個記憶體區域有兩種異常情況: (1)StackOverflowError:如果執行緒請求的棧深度大於了虛擬機器所允許的深度,將丟擲此異常(特別是遞迴) (2)OutOfMemoryError:如果虛擬機器可以動態擴充套件,當擴充套件時無法申請到足夠的記憶體

  • 本地方法棧 作用與虛擬機器棧類似,不過虛擬機器棧是虛擬機器執行Java方法的服務,而本地方法棧是為虛擬機器使用的Native方法服務。在規範中並沒有強制規定本地方法棧中方法使用的語言、使用方式和資料結構等,可以任意實現

    這個記憶體區域有兩種異常情況:(與虛擬機器棧差不多) (1)StackOverflowError: (2)OutOfMemoryError:

  • Java堆 Java堆算是虛擬機器中記憶體最大的一塊,是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立

    唯一目的就是存放物件的例項,幾乎所有的物件例項在這分配記憶體

    Java堆是垃圾收集器管理的主要區域,因此也可以稱之為GC堆。從回收的角度看,Java堆還可以細分為新生代和老年代,再細緻一點可以分為Eden空間、From Survivor空間、To Survivor空間等。從記憶體分配角度,可以劃分出多個執行緒私有的分配緩衝區(TLAB)

    有一種異常情況: OutOfMemoryError:當堆中沒有記憶體完成例項分配且堆無法擴充套件時,丟擲

  • 方法區 與Java堆一樣,是各個執行緒共享的記憶體區域,用於儲存已被虛擬機器記載的類資訊、常量、靜態常量、即時編譯器編譯後的程式碼等資料

    在虛擬機器規範中這是Java堆的一個邏輯部分,但也叫Non-Heap(非堆)。

    一種異常情況: OutOfMemoryError:當方法區無法滿足記憶體分配需求時,丟擲

  • 執行時常量池 是方法區的一部分,用於存放類載入後進入方法區的Class檔案中的常量池資訊,存放了編譯器生成的各種字面量和符號引用

    一種異常情況: OutOfMemoryError:當常量池無法再申請到記憶體時,丟擲

  • 直接記憶體 不是虛擬機器執行時資料區的一部分,也不是Java虛擬機器規範中定義的記憶體區域,但這部分記憶體被頻繁使用

    本機直接記憶體的分配不會手Java堆的大小限制

    一種異常情況: OutOfMemoryError:在給其他區域分配記憶體時忽略了直接記憶體,導致動態擴充套件時內個記憶體區域總和大於實體記憶體限制,就丟擲

HotSpot虛擬機器物件

  • 普通Java物件的建立 (1)當遇到一個new指令時,虛擬機器首先去檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被載入、解析、初始化過。如果沒有,就會先進行類載入。 (2)當類載入檢查通過後,就開始為新生物件分配記憶體。物件所需的記憶體大小在類載入完成後就完全確定。為物件分配空間的任務等同於把一塊確定大小的記憶體從Java堆中劃分出來。

    指標碰撞: 假設堆中記憶體絕對規整,所有用過的記憶體在一邊,空閒的在一邊,中間放著一個指標作為分界點的指示器,分配新的記憶體時就僅僅是把指示器這個指標想空閒空間挪動一段與物件大小相等的距離

    空閒列表: 如果堆中記憶體不規整,用過的和空閒的相互交錯,虛擬機器必須維護一個表用於記錄可用的記憶體塊,在分配的時候從列表中找到一塊足夠大的空間灰分給物件表,並更新表上的記錄

    Java堆是否規整取決於垃圾收集器是否帶有壓縮整理功能,不同的垃圾收集器分配方式不同。Serial、ParNew帶有Compact過程就採用指標碰撞,CMS基於Mark-Sweep採用空閒列表

    還需要處理的問題:併發安全,可能出現正在給物件A分配記憶體,指正還沒來得及修改,物件B又同時使用了原來的指標來分配記憶體的情況 解決方案: (a)對分配記憶體空間的動作進行同步處理:虛擬機器採用CAS配上失敗重試的方式保證更新操作的原子性 (b)把記憶體分配的動作按照執行緒劃分在不同的空間之中進行:即每個執行緒在Java堆中預先分配一小塊記憶體(本地執行緒分配緩衝區TLAB),分配記憶體時優先在自己的TLAB上分配,只有TLAB用完才進行同步鎖定

    (3)分配完成後,虛擬機器將分配到的記憶體空間都初始化為零值(不包括物件頭) (4)接著,虛擬機器對物件進行必要的設定:配置物件頭,例如這個物件是哪個類的例項、如何找到類的元資料、物件的哈戲碼、物件的GC分代年齡,是否使用物件鎖 (5)此時從虛擬機器來說,一個新物件就產生了,從Java程式來說,才剛剛開始,因為<init>方法還沒有執行,所有的欄位都為0

  • 物件的記憶體佈局 在HotSpot虛擬機器中,物件在記憶體中儲存的佈局分為如下: (1)物件頭(Header)

    物件頭包含兩部分資訊:

    第一部分用於儲存物件自身的執行時資料,如哈戲碼、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等,這部分資料的長度在32位和64位虛擬機器中分別為32bit和64bit

    HotSpot虛擬機器物件頭Mark World:

    儲存內容 標誌位 狀態
    物件哈戲碼、物件分代年齡 01 未鎖定
    指向鎖記錄的指標 00 輕量級鎖定
    指向重量級鎖的指正 10 膨脹(重量級鎖定)
    空,不需要的記錄 11 GC標記
    偏向執行緒ID、偏向時間戳、物件分代年齡 01 可偏向

    另一部分是型別指標,即物件指向他的類元資料的指標,虛擬機器通過這個指標來確定這個物件是哪個的例項

    如果物件是一個Java陣列,還有一部分用於記錄陣列長度的資料

    (2)例項資料(Instance Data) 物件真正儲存的有效資訊,也是在程式程式碼中所定義的各種型別的欄位的內容,包括父類繼承、子類定義的。

    HotSpot虛擬機器中預設相同寬度的欄位分配到一起

    (3)對齊填充(Padding) 不一定存在,沒有特別含義,僅僅是佔位符的作用

  • 物件的訪問定位 (1)使用控制代碼訪問 Java虛擬對中將會劃分一塊記憶體來作為控制代碼池,reference中儲存的就是物件的控制代碼地址,控制代碼中包含了物件例項資料與型別資料各自的具體地址資訊 控制代碼訪問

    (2)直接指標訪問 reference中儲存的直接就是物件的地址 指標訪問

    兩種方式的優缺點:控制代碼訪問最大的好處就是reference中儲存的是穩定的控制代碼地址,在物件被移動時只會改變控制代碼中的例項資料指標,reference本身不修改;直接指標訪問的最大好處就是速度更快,節省了一次指標定位的時間開銷

記憶體溢位

  • Java堆溢位 當物件數量到達最大堆的容量限制就會產生記憶體溢位異常

  • 虛擬機器棧和本地方法棧溢位 兩種異常:StackOverflowError和OutOfMemoryError 在單個執行緒下,即使記憶體無法分配,基本上只會丟擲StackOverflowError;每個執行緒的棧分配的記憶體越大越容易丟擲OutOfMemoryError 每個執行緒分配到的棧容量越大,可以建立的執行緒數量自然就越少,建立執行緒時就越容易把剩下的記憶體耗盡

    在大多數情況下,虛擬機器預設棧深度可以達到1000~2000。如果建立過多執行緒導致的記憶體溢位,在不能減少執行緒數或者更換64位機的情況下,就只能通過減少最大堆和減少棧容量來換取更多的執行緒(這樣虛擬機器棧和本地方法棧的記憶體就會更多)

  • 方法區和執行時常量池溢位 JDK1.7開始,String.intern()方法是一個Native方法,作用是:如果字串常量池中已經包含一個等於此String物件的字串,則返回代表池中這個字串的String物件,否則新增,並返回引用

    方法區溢位是一種常見的記憶體溢位異常。

  • 本機記憶體直接溢位 unsafe.allocateMemory()

垃圾回收器與記憶體分配策略

判斷物件已死

  • 引用計數演算法 給物件新增一個引用計數器,每當有地方引用它時,計數+1,引用失效時,計數-1,;任何時刻計數為0的物件是不可能再被使用的,但是無法解決物件間迴圈引用的問題,所以大部分Java虛擬機器並沒採取這種用法

  • 可達性分析演算法 現在主流採用的演算法 通過一系列的“GC Roots”物件作為起始點,從這些節點開始向下搜尋,搜尋的路徑成為引用鏈,當一個物件到GC Roots物件沒有任何引用鏈相連,則說明此物件是不可用的 可達性分析演算法

    Java語言中,可以作為GC Roots物件有: (1)虛擬機器棧中引用的物件 (2)方法區中類靜態屬性引用的物件 (3)方法區中常量引用的物件 (4)本地方法棧中JNI引用的物件

  • 引用 (1)強引用

    強引用是使用最普遍的引用。如果一個物件具有強引用,那垃圾回收器絕不會回收它。 如:Object o=new Object(); //強引用

    當記憶體空間不足,Java虛擬機器寧願丟擲OutOfMemoryError錯誤,使程式異常終止,也不會靠隨意回收具有強引用的物件來解決記憶體不足的問題。如果不使用時,要通過如下方式來弱化引用,如下: o=null; //幫助垃圾收集器回收此物件 顯式地設定o為null,或超出物件的生命週期範圍,則gc認為該物件不存在引用,這時就可以回收這個物件

    舉個ArrayList的實現原始碼

    private transient Object[] elementData;
    public void clear() {
        modCount++;
        // Let gc do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;
        size = 0;
    }
    

    在ArrayList類中定義了一個私有的變數elementData陣列,在呼叫方法清空陣列時可以看到為每個陣列內容賦值為null。不同於elementData=null,強引用仍然存在,避免在後續呼叫 add()等方法新增元素時進行重新的記憶體分配。使用如clear()方法中釋放記憶體的方法對陣列中存放的引用型別特別適用,這樣就可以及時釋放記憶體

    (2)軟引用

    是用來描述一些還有用但非必需的物件 如果一個物件只具有軟引用,則記憶體空間足夠,垃圾回收器就不會回收它;如果記憶體空間不足了,就會回收這些物件的記憶體。二次回收後還是記憶體不夠才會丟擲記憶體溢位異常。只要垃圾回收器沒有回收它,該物件就可以被程式使用。軟引用可用來實現記憶體敏感的快取記憶體

    在JDK 1.2後提供了SoftReference來實現軟引用

    軟引用在實際中有重要的應用,例如瀏覽器的後退按鈕。按後退時,這個後退時顯示的網頁內容是重新進行請求還是從快取中取出呢?這就要看具體的實現策略了。 (a)如果一個網頁在瀏覽結束時就進行內容的回收,則按後退檢視前面瀏覽過的頁面時,需要重新構建 (b)如果將瀏覽過的網頁儲存到記憶體中會造成記憶體的大量浪費,甚至會造成記憶體溢位,這時候就可以使用軟引用

    Browser prev = new Browser();  // 獲取頁面進行瀏覽
    SoftReference sr = new SoftReference(prev); // 瀏覽完畢後置為軟引用
    if(sr.get() != null) {
        rev = (Browser) sr.get();  // 還沒有被回收器回收,直接獲取
    } else {
        prev = new Browser();  // 由於記憶體吃緊,所以對軟引用的物件回收了
        sr = new SoftReference(prev);  // 重新構建
    }
    

    軟引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果軟引用所引用的物件被垃圾回收器回收,Java虛擬機器就會把這個軟引用加入到與之關聯的引用佇列中

    (3)弱引用

    也是用來描述非必須物件,但是強度比軟引用更弱一些,被弱引用關聯的物件只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,不管記憶體是否足夠,都會回收掉關聯的物件。

    在JDK 1.2後,提供了WeakReference來實現弱引用

    引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果弱引用所引用的物件被垃圾回收,Java虛擬機器就會把這個弱引用加入到與之關聯的引用佇列中。當你想引用一個物件,但是這個物件有自己的生命週期,你不想介入這個物件的生命週期,這時候你就是用弱引用。這個引用不會在物件的垃圾回收判斷中產生任何附加的影響。比如說Thread中儲存的ThreadLocal的全域性對映,因為我們的Thread不想在ThreadLocal生命週期結束後還對其造成影響,所以應該使用弱引用,這個和快取沒有關係,只是為了防止記憶體洩漏所做的特殊操作

    (4)虛引用

    也成為幽靈引用或者幻影引用,是最弱的一種引用關係。 一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項。 為一個物件設定虛引用關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知

    在JDK 1.2後,提供了PhantomReference來實現虛引用

    虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用佇列 (ReferenceQueue)聯合使用。當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件的記憶體後,把這個虛引用加入到與之關聯的引用佇列中。程式可以通過判斷引用佇列中是否已經加入了虛引用,來了解被引用的物件是否被垃圾回收。如果程式發現某個虛引用已經被加入到引用佇列,那麼就可以在所引用的物件的記憶體回收後採取必要的行動。由於Object.finalize()方法的不安全性、低效性,常常使用虛引用完成物件回收後的資源釋放工作。當你建立一個虛引用時要傳入一個引用佇列,如果引用佇列中出現了你的虛引用,說明它已經被回收,那麼你可以在其中做一些相關操作,主要是實現細粒度的記憶體控制。比如監視快取,當快取被回收後才申請新的快取區。

  • 是否回收,判斷死亡 即使在可達性分析演算法中不可達物件也不是“非死不可”,這時只是一個標記階段,真正判斷回收至少還需要經歷兩次標記過程: (1)如果物件進行可達性分析後,沒有與GC Roots的引用鏈,那麼會被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行finalize()方法。當物件沒有覆蓋finalize()方法或者finalize()方法已經被虛擬機器呼叫過,虛擬機器將這兩種情況都視為“沒有必要執行”(finalize只能執行一次,不管是手動還是系統呼叫)。 (2)如果物件被判定有必要執行finalize()方法,那麼會講物件放置在F-Queue佇列中,並在稍後由一個虛擬機器自動建立的、低優先順序的Finalizer執行緒去執行(所謂的執行是虛擬機器會出發該方法,但並不承諾等待它執行結束) (3)finalize()方法是物件逃脫死亡的最後一次機會,稍後GC會對F-Queue中的物件進行第二次小規模的標記,如果在finalize()中從新與引用鏈連上,就會被移除“即將回收”的集合

  • 回收方法區 Java虛擬機器規範中不要求在方法區實現垃圾收集

    永久代(方法區)的垃圾收集主要是兩部分: (1)廢棄常量 與回收堆中的物件類似

    (2)無用的類 必須滿足以下條件才能判定為無用的類

    該類所有的例項都已被回收,Java堆中不存在該類的任何例項 載入該類的ClassLoader已經被回收 該類對應的方法java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法

    判定為無用的類後也只是可以回收,並不是一定回收

垃圾收集演算法

標記擦除演算法

分為標記和清除兩個階段,首先標記所有需要回收的物件,在標記完成後統一回收所有被標記的物件 標記過程就是物件判定死亡過程 標記擦除演算法

  • 主要不足: (1)效率問題:標記和清除的效率都不高 (2)空間問題:標記清除後會產生大量不連續的記憶體碎片,碎片太多導致分配大記憶體時沒有連續的記憶體就不得不觸發新一次的回收

複製演算法

將記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當一塊記憶體用完後,就將存活的物件複製到另一塊上面,然後再把已使用過的記憶體空間一次清理掉。 這樣使得每一次都是對整個半區進行記憶體回收,記憶體分配時就不用考慮碎片的情況了,只要一動棧頂指正,按順序分配即可 複製演算法

  • 不足: 記憶體縮小了一半,代價太高

商業虛擬機器基本採用這種回收演算法

標記整理演算法

針對老年代的演算法,標記後,不是直接清理,而是讓所有存活的物件都向一端移動,然後直接清理掉邊界以外的記憶體 標記整理演算法

分代收集演算法

根據物件存活週期的不同將記憶體劃分為幾塊,一般是把Java堆分為新生代和老年代,然後根據不同的特點採用最恰當的演算法 新生代中採用複製演算法,老年代中採用標記-清理或標記-整理演算法

HotSpot的演算法實現

  • 列舉根節點 HotSpot使用一組OopMap的資料結構來記錄哪些地方有物件引用、物件內偏移量上的資料型別,便於在列舉根節點時提高速度(列舉根節點幾乎是所有的虛擬機器在GC要做的事情)

  • 安全點 HotSpot只會在特定的位置(特定的指令)去生成OopMap,這些特定的位置就叫安全點。也就是程式執行時並非在所有地方都停頓下來開始GC,只有在到達安全點時才暫停去GC

    安全點的選定:指令序列複用,會讓程式長時間執行的程式碼。例如方法呼叫、迴圈跳轉、異常條狀等會產生安全點

    如何在GC發生時讓所有的執行緒都到達安全點: (1)搶斷式中斷:不需要執行緒的程式碼主動配合,GC時,首先把所有的執行緒中斷,如果發現有執行緒沒有到達安全點,就恢復讓它到達安全點(基本不採用了) (2)主動式中斷:當GC需要中斷執行緒時,不直接對執行緒操作,僅僅簡單設定一個標誌,各個執行緒執行時主動去輪詢這個標誌,發現中斷標誌為真時就自己中斷掛起;輪詢標誌的地方和安全點重合

  • 安全區域 在一段程式碼中,引用關係不會發生變化。這個區域的任意地方開始GC都是安全的

垃圾收集器

HotSpot

  • Serial收集器 單執行緒的收集器,只會使用一個CPU或一條收集執行緒去GC,並且在進行GC時必須暫停其他所有的工作執行緒,知道GC結束 在Client模式下預設新生代收集器

  • ParNew收集器 是Serial收集器的多執行緒版本,使用多條執行緒進行GC,除此外並無啥區別 在Server模式下是首選的新生代收集器,因為只有它能和CMS配合工作

  • Parallel Scavenge收集器 一個新生代收集器,使用複製演算法,並行的多執行緒收集器 也稱為“吞吐量優先”收集器,因為它的目標就是達到一個可控制的吞吐量

  • Serial Old收集器 Serial的老年代收集器,單執行緒,使用“標記-整理”演算法

  • Parallel Old收集器 Parallel Scavenge的老年代收集器,使用多執行緒和“標記-整理”演算法

  • CMS收集器 一種以最短回收停頓時間為目標的收集器,GC過程是與使用者執行緒一起併發執行的,使用“標記-清除”演算法

    GC過程: (1)初始標記 (2)併發標記:耗時較長 (3)重新標記 (4)併發清楚:耗時較長

    缺點: (1)對CPU資源非常敏感 (2)無法處理浮動垃圾:標記過後,GC無法在當次回收的的就是浮動垃圾 (3)採用“標記-清除”會導致大量的空間碎片

  • G1收集器 面向服務端的垃圾收集器,最牛逼的一個(未來),將記憶體化整為零

    特點: (1)並行與併發:充分利用多CPU、多核,來縮短停頓時間 (2)分代收集 (3)空間整合:G1整體看似基於“標記-整理”,但在區域性是基於“複製”演算法,不會留下空間碎片 (4)可停頓的預測

    過程: (1)初始標記 (2)併發標記 (3)最終標記 (4)篩選回收

記憶體分配和回收策略

物件主要分配在新生代的Eden區上,如果有本地執行緒緩衝,將按執行緒優先在TLAB上分配,少數情況可能會直接分配在老年代中

  • 物件優先在Eden分配 大多數情況下,物件在新生代Eden區分配,當Eden不夠時,會觸發一次Minor GC(新生代GC),也可能直接將新物件直接分配到老年代

  • 大物件直接進入老年代 需要大量連續記憶體空間的Java物件(長字串、陣列)很容易導致記憶體還有不少空間就提前觸發GC,所以可以直接在老年代分配

  • 長期存活的物件將進入老年代 每個物件有一個物件年齡計數器(經過的GC次數,仍存活),當這個年齡到達一定的次數(預設15),這個物件就進入老年代

  • 動態物件年齡判定 如果在Survivor空間中相同年齡所有物件的大小總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代,無須達到最大限制次數(比如年齡為5,但是有多個,並且佔用記憶體很大,它們的總和大於了Survivor的一半,那麼它們中大於或等於5的都可以直接進入老年代)

  • 控制元件分配擔保 在新生代GC之前,會先檢查老年代最大可用的連續空間是否大於新生代所有物件總空間,如果成立,那麼新生代GC是安全的,如果不成立,虛擬機器會檢視是否允許擔保失敗。如果允許,就繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,如果大於,則嘗試進行一次新生代GC,如果小於,則不允許冒險,就進行一次老年代GC(Full GC)

類檔案結構(class檔案)

image.png Java虛擬機器只與Class檔案(二進位制)所關聯

Class類檔案結構

任何一個Class檔案都對應著唯一一個類或介面的定義資訊,但類或介面不一定都得定義在檔案裡

Class檔案是一組以8位位元組為基礎單位的二進位制流,各個資料專案嚴格按順序緊湊地排列在檔案中,中間沒有新增任何分隔符。當遇到佔用8位位元組以上空間的資料會按照高位在前的方式分割成若干個8位位元組

包含兩種資料型別:無符號和表 無符號數屬於基本的資料型別,u1、u2、u4、u8分別代表1、2、4、8個位元組的無符號數,可以用於描述數字、索引引用、數量值或按照UTF-8編碼構成的字串值 表是由多個無符號數或者其他表作為資料項構成的複合資料型別,以_info結尾,整個Class檔案本質上是一張表

Class檔案格式:

型別 名稱 數量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count - 1
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attributes_count 1
attribute_info attributes attributes_count
  • 魔數 每個Class檔案的頭4個位元組成為魔數,也就是magic,唯一作用是確定這個檔案是為一個能被虛擬機器接受的Class檔案。值為:0XCAFEBABE(咖啡寶貝?) 很多檔案儲存都會使用魔數來進行身份識別,如gif、jpeg

  • Class檔案的版本 第5、第6個位元組是次版本號(minor_version) 第7、第8個位元組是主版本號(major_version)

    Class檔案版本號:

    編譯器版本 -target引數 十六進位制版本號 十進位制版本號
    JDK 1.1.8 不能帶target引數 00 03 00 2D 45.3
    JDK 1.2.2 不帶(預設target 1.1) 00 03 00 2D 45.3
    JDK 1.2.2 target 1.2 00 00 00 2E 46.0
    JDK 1.3.1_19 不帶(預設1.1) 00 03 00 2D 45.3
    JDK 1.4.2_19 target 1.3 00 00 00 2F 47.0
    JDK 1.4.2_10 不帶(預設1.2) 00 00 00 2E 46.0
    JDK 1.4.2_10 1.4 00 00 00 30 48.0
    JDK 1.5.0_11 不帶(預設1.5) 00 00 00 31 49.0
    JDK 1.5.0_11 -target 1.4 -source 1.4 00 00 00 30 48.0
    JDK 1.6.0_01 不帶(預設1.6) 00 00 00 32 50.0
    JDK 1.6.0_01 1.5 00 00 00 31 49.0
    JDK 1.6.0_01 -target 1.4 -source 1.4 00 00 00 30 48.0
    JDK 1.7.0 不帶(預設1.7) 00 00 00 33 51.0
    JDK 1.7.0 1.6 00 00 00 32 50.0
    JDK 1.7.0 -target 1.4 -source 1.4 00 00 00 30 48.0
  • 常量池 constant_pool,Class檔案中的資源倉庫,與其他專案關聯最多,佔用Class檔案空間最大的資料專案之一,第一個出現表型別資料專案

    由於常量池中常量的數量不定,所以先用一個u2型的constant_pool_count表示常量池容量計數值,從1開始(只有這個值是從1開始,其餘都是從0開始)

    常量池中主要存放字面量和符號引用: (1)字面量:接近於Java語言的常量概念,如文字字串、宣告為final的常量值 (2)符號引用:編譯原理概念,包括:類和介面的全限定名、欄位的名稱和描述符、方法的名稱和描述符

    常量池中的每一個常量都是一張表 常量池的專案型別:

    常量型別 標誌 專案結構 型別 描述
    CONSTANT_Utf8_info 1 UTF-8編碼的字串
    tag u1 值為1,標誌位
    length u2 UTF-8編碼的字串佔用的位元組數
    bytes u1 長度為length的UTF-8編碼的字串
    CONSTANT_Integer_info 3 整型字面量
    tag u1 值為3
    bytes u4 按照高位在前儲存的int值
    CONSTANT_Float_info 4 浮點型字面量
    tag u1 值為4
    bytes u4 按照高位在前儲存的float值
    CONSTANT_Long_info 5 長整型字面量
    tag u1 值為5
    bytes u8 按照高位在前儲存的long值
    CONSTANT_Double_info 6 雙精度浮點型字面量
    tag u1 值為6
    bytes u8 按照高位在前儲存的double值
    CONSTANT_Class_info 7 類或介面的符號引用
    tag u1 值為7
    name_index u2 一個索引值,指向常量池中一個CONSTANT_Utf8_info型別常量,此常量代表了這個類(或介面)的全限定名
    CONSTANT_String_info 8 字元床型別字面量
    tag u1 值為8
    index u2 指向字串字面量的索引
    CONSTANT_Fieldref_info 9 欄位的符號引用
    tag u1 值為9
    index u2 指向宣告欄位的類或者介面描述符CONSTANT_Class_info的索引項
    index u2 指向欄位描述符CONSTANT_NameAndType的索引項
    CONSTANT_Methodref_info 10 類中方法的符號引用
    tag u1 值為10
    index u2 指向宣告方法的類描述符CONSTANT_Class_info的索引項
    index u2 指向名稱及型別描述符CONSTANT_NameAndType的索引項
    CONSTANT_InterfaceMethodref_info 11 介面中方法的符號引用
    tag u1 值為11
    index u2 指向宣告方法的介面描述符CONSTANT_Class_info的索引項
    index u2 指向名稱及型別描述符CONSTANT_NameAndType_info的索引項
    CONSTANT_NameAndType_info 12 欄位或方法的部分符號引用
    tag u1 值為12
    index u2 指向該欄位或方法名稱常量項的索引
    index u2 指向該欄位或方法名稱描述符常量項的索引
    CONSTANT_MethodHandle_info 15 表示方法控制代碼
    tag u1 值為15
    reference_kind u1 值必須在1~9之間,決定了方法控制代碼的型別。方法控制代碼型別的值表示方法控制代碼的位元組碼行為
    reference_index u2 值必須是對常量池的有效索引
    CONSTANT_MethodType_info 16 標識方法型別
    tag u1 值為16
    descriptor_index u2 值必須是對常量池的有效索引,常量池在該索引處必須是CONSTANT_Utf8_info結構,表示方法的描述符
    CONSTANT_InvokeDynamic_info 18 表示一個動態方法呼叫點
    tag u1 值為18
    bootstrap_method_attr_index u2 值必須是對當前Class檔案中引導方法表中的bootstrap_methods[]陣列的有效索引
    name_and_type_index u2 值必須是對當前常量池的有效索引,常量池也該在索引處的項必須是CONSTANT_NameAndType_indo結構,表示方法名和方法描述符
  • 訪問標誌 在常量池後面緊接著2個位元組表示訪問標誌,用於識別一些類或者介面層次的訪問資訊,包括這個Class是類還是介面、是否定義為public型別、是否定義為abstract型別、如果是類是否宣告為final等

    標誌名稱 標誌值 含義
    ACC_PUBLIC 0x0001 是否為public型別
    ACC_FINAL 0x0010 是否被宣告為final,只有類可設定
    ACC_SUPER 0x0020 是否允許使用invokespecial位元組碼指令的新語意
    ACC_INTERFACE 0x0200 標識這是一個介面
    ACC_ABSTRACT 0x0400 是否為abstract型別,對於類或介面為真
    ACC_SYNTHETIC 0x1000 標識這個類並非由使用者程式碼產生
    ACC_ANNOTATION 0x2000 標識這是一個註解
    ACC_ENUM 0x4000 標識只是一個列舉
  • 類索引、父類索引、介面索引集合 類索引(this_class)、父類索引(super_class)都是一個u2型別,介面索引集合(interfaces)是一組u2型別的集合,入口第一項表示集合大小

    Class檔案通過這三項來確定類的繼承關係

  • 欄位表集合 field_info(欄位表)用於描述介面或類中宣告的變數,欄位包括類級變數以及例項級變數,但不包括在方法記憶體宣告的區域性變數

    欄位表結構:

    型別 名稱 數量
    u2 access_flags 1
    u2 name_index 1
    u2 descriptor_index 1
    u2 attributes_count 1
    attribute_info attributes attributes_count

    欄位修飾符access_flags與類中的access_flags專案類似,都是一個u2的資料型別,包括是否為public、private、protected、static、final、volatile、transient、enum、是否由編譯器自動產生

    name_index代表欄位的簡單名稱,指沒有型別和引數修飾的方法或欄位名稱,對應常量池的引用

    descriptor_index代表欄位和方法的描述符,對應常量池的引用

  • 方法表集合 同欄位表結構一樣,包括了access_flags、name_index、descriptor_index、attributes

    訪問標誌access_flags中沒有了volatile和transient關鍵字的標誌,多了synchronized、native、strictfp、abstract關鍵字的標誌

  • 屬性表集合 Class檔案、欄位表、方法表都會攜帶自己的屬性表集合,用於表述某些場景專有的資訊 屬性表集合不再要求各個屬性表具有嚴格的順序

    虛擬機器規範預定義的屬性:

    屬性名稱 使用位置 含義
    Code 方法表 Java程式碼編譯成的位元組碼指令
    ConstantValue 欄位表 final關鍵字定義的常量值
    Deprecated 類、方法表、欄位表 被宣告為deprecated的方法和欄位
    Exceptions 方法表 方法丟擲的異常
    EnclosingMethod 類檔案 僅當一個類為區域性類或者匿名類時才能擁有這個屬性

    主要看看Code屬性:Java程式方法體中的程式碼經過Javac編譯處理後,最終變為位元組碼指令儲存在Code屬性內

類載入機制

虛擬機器把描述類的資料從Class檔案中載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別

類的載入、連線和初始化過程都是在執行期完成的

類載入的時機

類或介面從被載入到虛擬機器記憶體中開始到卸載出記憶體為止,整個生命週期包括:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、解除安裝(Unloading),其中驗證、準備、解析3個部分稱為連線(Linking) 類載入過程 載入、驗證、準備、初始化、解除安裝5個階段的順序是確定的,載入過程必須按照這個順序。但解析不一定:在某些情況下可以在初始化之後再開始(為了支援動態繫結或晚期繫結)

5種情況必須初始化(初始化之前載入、驗證、準備已經執行完):

  1. 遇到new、setstatic、putstatic、invokestatic這4條位元組碼指令時,如果類或介面沒有進行初始化,則需要先進行初始化 常見場景:使用new例項化物件、讀取或設定一個類的靜態欄位(被final修飾、已在編譯器把結果放入常量池的靜態欄位除外)、呼叫一個類的靜態方法時
  2. 使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果沒有初始化,觸發初始化
  3. 當初始化一個類的時候,如果發現其父類還沒有初始化,則先出發父類進行初始化
  4. 當虛擬機器啟動時,使用者需要指定一個要執行的類(包含main函式的ActivityThread),虛擬機器會先初始化這個主類
  5. 當使用JDK 1.7的動態語言支援時,如果一個java.lang.invoke.MethodHanlde例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼,並且這個方法控制代碼所對應的類沒有進行初始化,則先觸發初始化

只有主動引用(上面5種情況)才能觸發初始化,被動引用不可以。

  1. 對於靜態欄位,只有直接定義這個欄位的類才會被初始化,通過其子類來引用父類中定一個的靜態欄位,只會觸發父類的初始化而不會觸發子類的初始化,但載入、驗證是否被觸發就跟虛擬機器相關了
  2. 通過陣列定義類引用類,不會觸發此類的初始化 MyClass[] mycls = new MyClass[10];
  3. 常量在編譯階段會存入呼叫類的常量池中,本質上並沒有直接引用到定義常量的類(常量傳播優化,儲存到引用的類的常量池),因此不會觸發定義常量的類的初始化,直接呼叫常量不會觸發類的初始化

介面基本與類相同,但是沒有static靜態語句塊,但編譯器會為介面生成<clinit>()類構造器來初始化介面中定義的成員變數,介面在初始化的時候不會要求父介面的都完成了初始化,只有使用父介面的時候,父接口才會初始化

類載入的過程

  • 載入

    1. 通過一個類的全限定名來獲取定義此類的二進位制位元組流
    2. 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構
    3. 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口

    非陣列類的載入(獲取類的二進位制位元組流的動作)可以使用系統提供的引導類載入器來完成,也可以通過用於自定義的類載入器完成

    陣列類由虛擬機器直接建立的,不通過載入器,建立遵循規則: (1)如果陣列的元件型別(去掉一個維度的型別)是引用型別(如MyClass[]),那就遞迴採用載入器去載入,陣列類將在載入該元件型別的類載入器的類名稱空間上被標識 (2)如果陣列的元件型別不是引用型別(如int[]),Java虛擬機器會把這個陣列類標記為與引導類載入器關聯 (3)陣列類的可見性與它的元件型別的可見性一致,如果元件型別不是引用型別,可見性則預設public

    載入完成後,虛擬機器外部的二進位制位元組流就按照虛擬機器所需的格式儲存在方法區之中,然後在記憶體中例項化一個java.lang.Class類的物件,這個物件將作為程式訪問方法區的外部介面

    載入階段和連線階段是交叉進行的,載入階段尚未完成,連線階段可能已經開始

  • 驗證 驗證是連線的第一步,確保Class檔案的位元組流中包含的資訊符號當前虛擬機器的要求且不會危害虛擬機器自身的安全

    1. 檔案格式驗證 驗證位元組流是否符合Class檔案格式的規範,且能被當前虛擬機器處理 驗證點: (1)是否以魔數0xCAFEBABE開頭 (2)主次版本號是否在當前虛擬機器處理範圍內 (3)常量池的常量中有不被支援的常量型別(常量tag標誌) (4)指向常量的各種索引值是否有指向不存在的常量或不符合型別的常量 (5)CONSTANT_Utf8_info型的常量是否有不符合UTF8編碼的資料 (6)Class檔案中各個部分及檔案本身是否有被刪除的或附加的其他資訊 …

    2. 元資料驗證 對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合Java語言規範 驗證點: (1)這個類是否有父類 (2)這個類的父類是否繼承了不允許繼承的類(final修飾的類) (3)如果這個類不是抽象類,是否實現了父類或介面中要求實現的所有方法 (4)類中的欄位、方法是否與父類產生矛盾 …

    3. 位元組碼驗證 是整個驗證過程最複雜的階段,通過資料流和控制流分析,確定程式語義是合法的、符合邏輯的 如果一個類方法體的位元組碼沒有通過位元組碼驗證,那肯定有問題,但如果通過了,也不能說明一定安全

    4. 符號引用驗證 在虛擬機器將符號引用轉化為直接引用的時候驗證,這個轉化動作在連線的第三階段——解析階段發生 是對類自身以外的資訊進行匹配性校驗 檢驗點: (1)符號引用中通過字串描述的全限定名是否恁找到對應的類 (2)在指定類中是否存在符合方法的欄位描述符以及簡單名稱所描述的方法和欄位 (3)符號引用中的類、欄位、方法的訪問性是否可被當前類訪問 …

  • 準備 正式為類變數分配記憶體並設定類變數初始值的階段,在方法區中進行記憶體分配。 進行分配的僅包括類變數(static修飾的變數)而不包括例項變數,例項變數將會在物件例項化時伴隨對戲那個一起分配在堆中,這裡的初值是資料型別的零值。 如果類欄位的欄位屬性表中存在ConstantValue屬性,那就在這個階段進行初始化賦值,如public static final int value = 123;就會在準備階段直接賦值為123而不是零值

  • 解析 虛擬機器將常量池內的符號引用替換為直接引用的過程 符號引用:以一組符號來描述所引用的目標,符號可以是任何形式的字面量。引用的目標不一定已經載入到記憶體中 直接引用:可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制代碼

    主要針對以下符號引用解析:

    1. 類或介面的解析 假設當前程式碼所處的類為D,符號引用N解析為一個類或介面C的直接引用; (1)如果C不是一個數組型別,那虛擬機器將會把代表N的全限定名傳遞給D的類載入器去載入C (2)如果C是一個數組型別,並且陣列的元素型別為物件,也就是N的描述符會是類似“|[Ljava/land/Integer”的形式,就會按照(1)載入陣列元素型別;如果N的描述符如前面所假設的形式,需要載入的元素型別就是“java.lang.Integer”,接著由虛擬機器生成一個代表此陣列維度和元素的陣列物件 (3)如果(1)(2)沒有任何異常,那麼C在虛擬機器中實際上就是一個類或介面了,但在解析完成之前之前還要進行符號引用驗證,確認D是否具備對C的訪問許可權

    2. 欄位解析 首先將對欄位表內class_index項中索引的CONSTANT_Class_info符號引用進行解析,也就是欄位所屬的類或介面的符號引用,C解析成功後,對C後續欄位的搜尋 (1)如果C本身就包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位的直接引用,查詢結束 (2)否則,如果在C中實現了介面,將會按照繼承關係從下往上遞迴搜尋各個介面和它的父介面,如果介面中包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位的直接引用,查詢結束 (3)否則,如果C不是java.lang.Object的話,將會按照繼承關係從下往上遞迴搜尋其父類,如果在父類中包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位的直接引用,查詢結束 (4)否則查詢失敗

      查詢過程成功返回了引用後,還將對這個欄位進行許可權驗證

    3. 類方法解析 第一個步驟與欄位解析的第一個步驟一樣,也需要先解析出方法表類的class_index項中索引的方法所屬的類或介面的符號引用,解析成功後進行類方法搜尋 (1)類方法和介面方法符號引用的常量型別定義是分開的,如果在類方法表中發現class_index中索引的C是個介面,那就丟擲異常 (2)如果通過了(1),在類C中查詢是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回方法的直接引用,查詢結束 (3)否則,在類C的父類中遞迴查詢是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回方法的直接引用,查詢結束 (4)否則,在類C實現的介面列表及它們的父介面中遞迴查詢是否有簡單名稱和描述符都與目標想匹配的方法,如果存在,則說明類C是一個抽象類,查詢結束,丟擲異常 (5)否則,宣告方法查詢失敗,丟擲異常

    4. 介面方法解析 先解析出介面方法表的class_index項中索引的方法所屬的類或介面的符號引用,如果解析成功,依然用C表示這個介面,進行後續的介面方法搜尋 (1)如果在介面方法表中發現class_index中的索引C是個類不是介面,直接丟擲異常 (2)否則,在介面C中查詢是否有簡單名稱和符號引用都與目標相匹配的方法,如果有則返回方法的直接引用,查詢結束 (3)否則,在介面C的父介面中遞迴查詢,知道java.lang.Object類為止,看是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回方法的直接引用,查詢結束 (4)否則,宣告查詢失敗

  • 初始化 類載入過程的最後一步。 在這個階段,根據程式猿通過程式制定的主觀計劃去初始化類變數和其他資源,初始化階段是執行類構造器<clinit>()方法(不是例項構造器,平時所說的是例項構造器)的過程

    <clinit>()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊中的語句合併產生的,編譯器收集的順序是由語句在原始檔中出現的順序決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變數,定義在它之後的變數前面的靜態語句塊可以賦值但不能訪問

    <clinit>()方法與類的建構函式(類的例項建構函式<init>)不同,它不需要顯式地呼叫父類構造器,虛擬機器會保證在子類的<clinit>()方法執行之前,父類的<clinit>()方法執行完畢,因此虛擬機器上第一個執行的類是Object

    <clinit>()方法對類或介面來說並不是必需的,如果一個類中沒有靜態語句塊,也沒有對變數的賦值操作,那麼編譯器可以不為這個類生產<clinit>()

    介面中不能使用靜態語句塊,但仍然有變數初始化的賦值操作,因此介面和類一樣都會生成<clinit>()方法,但執行介面的<clinit>()方法不需要先執行父介面的<clinit>()方法,只有當父介面中定一個的變數使用時,父接口才會初始化。介面的實現類在初始化的時候也一樣不會執行介面的<clinit>()方法

    虛擬機器會保證一個類的<clinit>()方法在多執行緒環境中被正確地枷鎖、同步,如果多個執行緒同時初始化一個類,那麼只會有一個執行緒去執行這個類的<clinit>()方法,其他執行緒都需要阻塞等待,知道活動執行緒執行<clinit>()方法完畢

類載入器

  • 類與類載入器 類載入器用於實現類的載入動作。每一個類載入器都有一個獨立的類名稱空間(比較兩類是否“相等”,只有在這兩個類是由同一個類載入器載入的前提下才有意義,否則即使兩個類來源於同一個Class檔案,但類載入器不同,必定不相等)

  • 雙親委派模型 兩種類載入器:一種是啟動類載入器,用C++實現,是虛擬機器自身的一部分呢;另一種是所有其他的類載入器,由Java語言實現,獨立於虛擬機器外部,且全繼承自抽象類java.lang.ClassLoader

    三種系統提供的類載入器: (1)啟動類載入器:負責將存放在<JAVA_HOME>\lib目錄中的或者被-Xbootclasspath引數所指定的路徑中的且是虛擬機器識別的類庫載入到虛擬機器記憶體中。無法被Java程式直接引用 (2)擴充套件類載入器:負責載入<JAVA_HOME>\lib\ext目錄中的或者被java.ext.dirs系統變數所指定的路徑中的所有類庫。開發者可以直接使用 (3)應用程式類載入器:是ClassLoader的getSystemClassLoader()方法的返回值,一般也稱為系統類載入器

    類載入器之間的層次模型就是雙親委派模型,除了頂層的啟動類載入器外,其餘的類載入器都應當有自己的父類載入器 雙親委派模型

    雙親委派模型的工作過程:如果一個類載入器收到了類載入的請求,它首先不會自