深入理解 Jvm 讀書筆記(一)

Jvm 內存管理,GC,類文件架構相關

知識包括:

  • jvm內存管理
    • jvm運行時數據區劃分
    • jvm層對象的創建過程
    • 對象的內存佈局
    • 對象的訪問定位
  • 垃圾收集器與內存分配策略
    • 對象已死的判定及引用分類
    • 對象死亡判定
    • 垃圾收集算法 與hotspot算法實現
    • 垃圾收集器
    • 內存分配策略
  • 類文件結構
    • class文件的結構 無符號數 與 表
    • 字節碼指令簡介

自動內存管理

jvm 運行時數據區 (JVM棧,本地方法棧,程序計數器,堆,方法區)

  • 線程私有

    • Jvm棧 (JVM Stack)

      • 生命週期與線程相同;
      • 描述的是java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀(Stack Frame)用於存儲局部變量表,操作數棧,動態鏈接,方法出口等;
      • 局部變量表 存放編譯期可知的各種基本數據類型(boolean,byte,short,int,long,float,double,char),對象引用(reference類型,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)和returnAddress類型(指向一條字節碼指令的地址);
      • 局部變量表 64位長度的long和double類型的數據會佔用2個局部變量空間(Slot),其餘數據類型只佔用1個;局部變量表所需的內存空間在編譯期間完成分配,方法運行期間不會改變局部變量表大小,進入一個方法時,這個方法在棧幀中分配多大的空間已經是確定的;
      • jvm 規定兩種異常: 線程請求的棧深度大於虛擬機所允許的深度,拋出StackOverflowError異常; 線程擴展時無法申請到足夠的內存,拋出OutOfMemoryError異常;
    • 本地方法棧 (Native Method Stack)

      • 與Jvm棧的區別是Jvm棧爲執行java方法服務,此爲使用的Native方法服務;
    • 程序計數器 (Program counter Register)

      • 當前線程所執行的字節碼的行號指示器
      • 線程執行java方法, 計數器記錄的是正在執行的虛擬機字節碼指令的地址; 線程執行native方法,計數器則爲Undefined,
      • 是唯一一個在jvm中沒有規定任何OOM情況的區域;
  • 線程共享

    • java 堆 (Heap)
      • JVM啓動時創建,目的爲存放對象實例;
      • GC管理的主要區域,分代收集算法;
      • 線程共享的堆中可劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer);
      • 可以拋出 OutOfMemoryError異常;
    • 方法區 (Method Area)
      • 用於存儲已被JVM加載的類信息,常量,靜態變量,即時編譯器編譯後的代碼等數據;
      • 可以拋出 OutOfMemoryError 異常;
      • 運行時常量池(Runtime Constant Pool) 方法區的一部分,存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放;
        • 具備動態性,不一定是編譯期才能產生,也就是並非預置於Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的變量放入池中,利用最多的就是String的intern()方法;
      • 可以拋出 OutOfMemoryError異常;
    • 直接內存 (Direct Memory)
      • 不是Jvm運行時數據區一部分;可拋出OutOfMemoryError異常;
      • Nio(New Input/Output), 引入基於通道Channel和緩衝區Buffer的I/O方式; 可使用native函數庫分配堆外內存,通過存儲在java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作,避免java堆和native堆中來回複製數據;

對象的創建 (JVM層面的對象創建)

  • new指令 檢查;檢查這個指令的參數是否能在常量池中定位一個類的符號引用,檢查這個符號引用代表的類是否已被加載,解析,初始化過,如果沒有執行類的類加載過程;
  • 新生對象分配內存;對象所需大小在類加載完成後即可完全確定;
    • 指針碰撞: 分配內存將指針向空閒空間那邊挪動一段與對象大小相等的距離;
    • 空閒列表: jvm維護一個記錄可用內存的列表,分配時從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄;
    • 併發情況下的分配內存分兩種方案:
      • jvm採用CAS(compare and swap)加上失敗重試的方式保證更新操作的原子性;
      • 將內存分配的動作按照線程劃分在不同的空間中,每個線程在java堆中預先分配一小塊內存,即(Thread Local Allocation Buffer ,TLAB),那個線程需要分配內存,就在那個線程的tlab上分配,只有tlab用完並分配新的tlab時,才需要同步鎖定;
  • 初始化零值(不包括對象頭);
  • 設置對象頭信息(Object Header); 從jvm角度看一個新的對象已經產生了,但從java程序看,對象創建剛剛開始(方法還沒有執行,所有字段都爲零,只有new指令之後接着執行方法,真正可用的對象纔算完全產生出來;)

對象的內存佈局 (對象頭,實例數據,對齊填充)

  • 對象頭 (Header)
    • 存儲對象自身的運行時數據,哈希嗎,GC分代年齡,鎖狀態標誌,線程持有的鎖,偏向時間戳等,長度爲在32位和64位jvm中分別爲32bit和64bit;
    • 類型指針,對象指向它的類元數據的指針,jvm通過這個指針確定這個對象是哪個類的實例,非必須;
    • 如果是java數組,還有記錄數組長度的數據;
  • 實例數據
    • 對象真正存儲的信息;
    • 存儲順序收到虛擬機分配策略參數(fieldsAllocationStyle)和字段在java源碼中定義順序的影響; 默認順序爲 longs/doubles,ints,shorts/chars,bytes/booleans,oops;
  • 對齊填充
    • 佔位符,hotSpot vm要求對象起始地址必須是8字節的整數倍,對象的大小必須是8字節的整數倍;

對象的訪問定位

通過jvm棧上的reference數據來操作堆上的具體數據,reference類型 在jvm規範中定義了一個指向對象的引用; 對象訪問方式取決於jvm :

  • 句柄訪問;

    • java堆中劃分出一塊內存作爲句柄池,reference中存儲的是對象的句柄地址;句柄中包含了對象實例數據和類型數據的具體地址信息;
    • 優點在於reference存儲的是穩點的句柄地址,垃圾回收時只會改變句柄中的實例數據指針,reference本身不需要改變;
  • 直接指針;

    • java堆對象的佈局中放置對象的類型數據,reference存儲的直接就是對象地址;
    • 優點在於速度更快,節省一次指針定位的時間開銷;

垃圾收集器與內存分配策略

程序計數器,jvm棧,本地方法棧都是線程私有的,每一個棧幀中分配多少內存已經在類結構確定下來就已知了,棧中的棧幀隨着方法的進入和退出,內存得以回收;而堆內存和方法區不一樣,GC主要針對這部分內存;

對象已死判定

  • 引用計數算法

    • 對象中添加引用計數器,無法解決循環引用問題;
  • 可達性分析算法

    • 通過一系列的GC Roots對象作爲起始點,從這些節點開始向下搜索,搜索走過的路徑就是引用鏈(Reference Chain) ,當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的;
    • 可作爲GC Roots 的對象包括下面幾種:
      • jvm棧(棧幀中的本地變量表) 中引用的對象;
      • 方法區中類靜態屬性,常量引用的對象;
      • 本地方法棧中jni(native方法)引用的對象;

引用分類

  • 強引用 StrongReference

    • 類似var obj = Object()這類的引用,垃圾回收器永遠不會回收掉被引用的對象;
  • 軟引用 SoftReference

    • 有用但非必須對象;
    • 在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二次回收;
  • 弱引用 WeakReference

    • 非必須對象;
    • 只能生存到下一次垃圾收集發生之前,當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象;
  • 虛引用 PhantomReference

    • 最弱的引用關係,一個對象是否有虛引用,完全不會對其生存時間構成影響;
    • 無法通過虛引用來取得一個對象實例;
    • 爲一個對象設置虛引用的唯一目的就是能在這個對象被收集器回收時收到一個系統通知;

對象的生存死亡

真正宣告一個對象,至少要經歷兩次標記過程;

  • 對象在經過可達性分析後發現沒有與GCRoots相連接的引用鏈,那它會被第一次標記,並且經過一次篩選,篩選條件爲 此對象是否有必要進行finalize方法;
    • 當對象沒有覆蓋finalize方法或finalize已經被jvm調用過,jvm視爲沒有必要執行,則直接進行GC;
    • 如果jvm視爲有必要執行finalize方法,會將此對象放置在一個叫做F-Queue隊列之中,稍後有一個jvm自動建立的低優先級的Finalizer線程去執行它(觸發,不承諾等待它結束);稍後GC將對F-Queue中的對象進行第二次小規模的標記,
      • 如果對象在finalize中重新與引用鏈上的任何一個對象建立關聯,那麼在第二次標記時移除出"即將回收"的集合;
      • 如果對象在此時還沒有建立關聯,則被真的回收了;

方法區的回收

方法區的回收效率較低,主要回收兩部分內容: 廢棄常量無用的類;

判斷一個類是無用的類,jvm可以對無用的類進行回收;

  • 該類的所有實例都已經被回收,java堆中不存在該類實例;
  • 加載該類的ClassLoader已經被回收;
  • 改類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法;

垃圾收集算法

  • 標記-清除算法 Mark-Sweep

    • 首先標記處所有需要回收的對象,在標記完成後統一回收所有被標記的對象;
    • 不足: 效率低下;清除後空間產生大量不連續的內存碎片;
  • 複製算法 Copying

    • 將可用內存劃分爲容量大小相等的兩塊,每次只使用其中一塊,當着一塊用完之後,就將還存活的對象複製到另一塊內存上面,然後把已使用的內存空間一次清理掉;
    • 每次都是對整個半區進行回收,運行高效,內存縮小一半代價高;
    • 具體比例分配不需要1:1分配,內存可分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和一塊Survivor空間;當回收時,將Eden和Survivor中還存活的對象一次性的複製到另外一塊Survivor空間上,最後清理掉Eden和剛纔使用的Survivor空間;
    • HotSpot默認Eden和Survivor的比例爲8:1,只有10%的內存會被’浪費’,當Survivor空間不夠用時,需要依賴其他(老年代)內存進行分配擔保 Handle Promotion;
      • 內存的分配擔保,如果另外一塊Survivor空間沒有足夠的空間存放上一次新生代收集下來的存活對象,這些對象直接通過分配擔保進入老年代;
  • 標記-整理算法 Mark-Compact

    • 主要針對於老年代,類似於標記清除算法,但後續步驟不是直接對對象進行清理,而是讓存活的對象都向一邊移動,然後直接清理掉端邊界以外的內存;
  • 分代收集算法 Generational Collection

    • 根據對象存活週期的不同將內存劃分爲幾塊,一般是將java堆分爲新生代和老年代,根據各個年代的特點採用最適當的收集算法;
      • 在新生代,每次垃圾回收只有少量存活,選用複製算法;
      • 在老年代,因爲對象存活率高,沒有額外空間對它進行分配擔保,必須使用’標記-清理’或’標記-整理’算法;

HotSpot的算法實現

  • 枚舉根節點

    • gc時需要進行可達性分析,可作爲GCRoots的節點主要是全局性的引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中,這項工作必須在一個能確保一致性的快照中進行

      • 一致性 指在整個分析期間整個執行系統看起來就像是凍結在某個時間點上,不可以出現分析過程中對象引用關係還在不斷變化的情況,該點不滿足的話分析結果準確性就無法得到保證; 導致GC進行時必須停頓所有的java執行線程 (Stop The World) ,即時在號稱不會停頓的CMS 收集器中,枚舉根節點也是需要停頓的;
    • jvm 通過一組稱爲OopMap的數據結構得知哪些地方存放着對象引用;

      • 在類加載完成的時候,hotspot把對象內什麼偏移量上是什麼類型的數據計算出來,在jit編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用; GC在掃描時就可以直接得知這些信息;
  • 安全點 SafePoint

    • 在oopmap的協助下,hotspot可以快速完成GcRoots的枚舉,但是oopmap內容變化的指令非常多,可能導致引用關係變化;
    • hotspot並沒有爲每條指令都生成OopMap,只會在特定的位置記錄這些信息,這些位置稱爲安全點; 即程序在執行時並非在所有的地方都能停頓下來GC,只有到達安全點時才能暫停;
    • 安全點的選定 是以程序’是否具有讓程序長時間執行的特徵’爲標準;'長時間’的最明顯特徵是指令序列複用,例如 方法調用,循環調用,異常跳轉等;具有這些功能的指令纔會產生SafePoint;
    • 在GC發生時,讓所有線程(不包括執行jni調用的線程)都跑到最近的安全點在停頓下來,兩種類型:
      • 搶先式中斷 deprecated
        • Gc時,首先把所有的線程全部中斷,如果發現有線程中斷的地方不在安全點上,就恢復線程,跑到安全點上;
      • 主動式中斷
        • Gc時需要中斷線程時,不直接對線程操作,僅僅簡單的設置一個標誌(可讀,不可讀),各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就自己中斷掛起;輪詢標誌的地方和安全點是重合的,另外再加上創建對象需要分配內存的地方;
  • 安全區域 SafeRegion

    • safepoint 機制保證了程序執行時,在不太長的時間內就會遇到可進入GC的safepoint; 但是線程處於sleep或者blocked狀態,線程無法響應jvm的中斷請求,到安全地地方去中斷掛起;
    • 安全區域指在一段代碼片段之中,引用關係不會發生變化;在這個區域中的任意地方開始GC都是安全的;
    • 在線程執行到SafeRegion中的代碼時,首先標誌進入到safeRegion,當jvm發起GC時,不用管標識自己爲safeRegion狀態的線程了; 在線程要離開safeRegion時,要檢查系統是否已經完成了根節點枚舉(或者Full GC),如果完成了,線程就繼續執行;否則它就必須等待直到收到可以安全離開safeRegion的信號爲止;

垃圾收集器

可組合的垃圾收集器

  • Serial 收集器

    • 單線程收集器,進行垃圾回收時必須暫停其他的工作線程;
    • 新生代採用複製算法,老年代採用標記整理算法,都是暫停所有用戶線程;
    • Serial / Serial Old 收集器
  • ParNew 收集器

    • serial的多線程的版本;
    • 只能它能與CMS收集器(收集老年代,新生代只能選擇PaNew或者Serial中一個)配合工作;

	並行(Parallel): 多條垃圾收集線程並行工作,此時用戶線程仍然處於等待狀態;

	併發(Coucurrent): 用戶線程與垃圾收集線程同時執行(但不一定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行在另一個cpu上;

  • Parallel Scavenge 收集器

    • 是一個新生代收集器,複製算法的收集器,並行多線程收集器;
    • 吞吐量優先; 關注點不同;CMS關注點是儘可能的縮短垃圾收集時用戶線程的停頓時間;Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量;
      • 吞吐量(Throughput): cpu用於運行用戶代碼的時間與cpu總消耗時間得比值; 吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 +垃圾收集時間)
      • 高吞吐量可高效率利用cpu時間,儘快完成運算任務,主要適合在後臺運行而不需要太多交互的任務; 停頓時間越短越適合需要與用戶交互的程序,提高用戶體驗;
  • Serial Old 收集器

    • Serial收集器的老年代版本,單線程收集器;
    • jdk1.5前可與Parallel Scavenge收集器搭配使用;可作爲CMS收集器的後背預案,併發收集發生ConcurrentModeFailure時使用;
  • Parallel Old 收集器

    • Parallel Scavenge 收集器的老年代版本;多線程並行收集器;
  • CMS 收集器

    • 多線程併發收集器
    • Concurrent Mark Sweep 獲取最短回收停頓時間爲目標的收集器;
    • 使用標記-清除算法,上述收集器大多是標記-整理算法;
  • G1收集器

    • 並行與併發;
    • 分代收集;
    • 空間整合;(標記-整理 + 複製)
    • 可預測的停頓,垃圾收集上的時間不得超過N毫秒;
      • java堆的內存佈局與其他收集器有很大區別,G1將整個java堆劃分爲多個大小相等的獨立區域(region)
      • Remembered Set 限定堆GC根節點枚舉範圍,可以不對全堆掃描;

內存分配與回收策略

  • java的自動內存管理歸結爲: 給對象分配內存以及回收分配給對象的內存;

  • 給對象分配內存,大方向上說,就是在堆上分配(也可能經過JIT編譯後被拆散爲標量類型並間接的棧上分配),對象主要分配在新生代的Eden區,如果啓動了本地線程分配緩衝,將按線程優先在TLAB(Thread Local Allocation Buffer)上分配;少數情況也可能會直接分配在老年代中,分配的規則並不是一定是固定的,細節取決於當前使用的是哪一種垃圾收集器組合,還有jvm中與內存有關的參數設置;

  • 對象優先在Eden區中分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC;

    • 新生代GC (Minor Gc): 指發生在新生代的垃圾收集動作,因新生代java對象大多都是朝生夕死的特性,所以Minor GC非常頻繁,回收速度也快;
    • 老年代GC (Major GC/Full GC): 發生在老年代的GC,Major GC的速度一般會比Minor GC慢10倍以上;(通常 Major GC 會隨後發生Minor GC,與收集器的實現有關)
  • 大對象直接進入老年代

    • 需要大量連續內存空間的java對象,如很長的字符串以及數組;
    • jvm參數可設置大於某個閾值直接在老年代分配,避免在新生代Eden區和兩個Survivor區之間發生大量的內存複製;
  • 長期存活的對象將進入老年代

    • jvm給每個對象定義了一個對象年齡(Age)計數器;
    • 如果對象在Eden出生並經過第一次的Minor GC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並且對象年齡設爲1;對象在Survivor每熬過Minor GC,年齡就增加一歲;
    • 年齡默認閾值爲15歲,對象就會被晉升到老年代中;
  • Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於等於該年齡的對象就可以直接進入老年代;

  • 空間分配擔保

    • 在發生minor GC之前,jvm會檢查老年代最大可用的連續空間是否大於新生代所有對象總空間;
      • 如果大於,minor GC可以確保是安全地;
      • 如果不成立,檢查HandlerPromotionFailure設置是否允許擔保失敗;
        • 允許,繼續檢查老年代可用的連續空間是否大於歷次晉升到老年代對象的平均大小;如果大於,則嘗試一次minor GC,儘管這次minor GC是有風險的;
        • 如果小於或者設置不允許冒險,這時進行一次Major GC;
        • 允許擔保失敗的冒險:
          • 新生代使用複製算法,爲了內存利用率,只用其中一個Survivor空間作爲輪換備份,在minorGC後仍然存活的情況下,需要老年代進行分配擔保;但是有多少對象會活下來在實際完成內存回收之前是無法明確知道的,只好取之前每一次回收晉升到老年代對象容量的平均大小值作爲經驗值,與老年代的剩餘空間進行比較,決定是否進行fullGC讓老年代騰出更多空間; 如果出現HandlerPromotioinFailure 失敗,那就只好在失敗後重新發起一次FullGc,雖然擔保失敗繞的圈子是最大的,大部分還是會打開開關,避免fullGc過於頻繁;
        • jdk 6後,規則變爲只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行minorGC,否則進行FullGC;

類文件結構

語言無關性: Jvm + 字節碼存儲格式;

語言無關性

class 類文件結構

class文件格式

  • 概述

    • 任何一個class文件都對應着唯一一個類或接口的定義信息; class文件是一組以8位字節爲基礎單位的二進制流;

    • 根據jvm規範,class文件格式採用一種類似於C語言結構體的僞結構來存儲數據,各個項目嚴格按照順序緊湊地排列在Class文件之中,中間沒有添加任何分隔符,只有兩種數據類型: 無符號數;

      • 無符號數: 屬於基本的數據類型,以u1,u2,u4,u8來代表1個字節,2個字節,4個字節,8個字節的無符號數,可用來描述數字,索引引用,數量值,或者按照UTF-8編碼構成字符串值;
      • 表: 多個無符號數或者其他表作爲數據項構成的複合數據類型,所有表都習慣性的以_info結尾;用於描述有層次關係的複合結構的數據,整個class文件本質上就是一張表;
  • 魔數 Magic Number 身份識別

    • 每個Class文件的頭4個字節稱爲魔數,作用是確定這個文件是否爲一個能被虛擬機接受的class文件; class文件魔數爲: oxCAFEBABY (流弊大氣!)
    • 第5,6字節代表次版本號 minor version;
    • 第7,8字節代表主版本號 major version;
    • 主版本號之後的是常量池入口;
  • 常量池 constant_pool_count | constant_pool

    • Class文件中的資源倉庫,每一項常量都是一個表,

    • 由於常量池中常量數量不固定,在常量池的入口需要放置一項u2類型的數據,表示有多少常量,索引從1開始;

    • 常量池主要存放兩大類:

      • 字面量 Literal
        • 文本字符串,聲明爲final的常量值;
      • 符號引用 Symbolic References
        • 類和接口的全限定名 Fully Qualified Name;
        • 字段的名稱和描述符 Descriptor;
        • 方法的名稱和描述符;
    • 常量池中每一個常量(表)開始的第一位是一個u1類型的標誌位,代表當前這個常量屬於哪種常量類型;

      • 如,Constant_Utf8_info 代表Utf-8編碼的字符串,CONSTANT_Integer_info 代表整形字面量,CONSTANT_Methodref_info 代表類中方法的符號引用;CONSTANT_Class_info 類或接口的符號引用;
    • javap 輸出常量表 (-verbose)

      • 自動生成常量,會用於後面的字段表(field_info),方法表(method_info),屬性表(attribute_info)引用到,用來描述一些不方便使用"固定字段"進行表述的內容;
        • 因java的類是無窮無盡的,無法通過簡單的無符號字節來描述一個方法用到了什麼類,描述方法的這些信息時,需要引用常量表中的符號引用進行表述;

常量池

  • 訪問標誌 access_flags

    • 常量池結束後,後面兩個字節代表訪問標誌(access_flags),用於識別一些類或者接口層次的訪問信息;
    • 如: 這個class是類還是接口;是否定義爲public類型;是否定義爲Abstract類型;如果是類,是否被聲明爲final等;
    • access_flags 一共有16個標誌位可以使用,當前只定義了其中8個,沒有使用到的標誌位一律爲0;

訪問標誌

  • 類索引,父類索引,接口索引集合; this_class,super_class,interface_class,interfaces

    • this_class,super_class 都是u2類型的數據,interfaces是一組u2類型的數據的集合,class文件中由這三項確定這個累的繼承關係;
    • 類索引用於確定這個類的全限定名,父索引用於確定這個類的父類的全限定名(因此,java並不支持多繼承),除java.lang.Object外,所有的java 的父類索引都不爲0;各自指向一個類型爲CONSTANT_Class_info的類描述符常量,在通過CONSTANT_Class_info類型的常量中的索引值找到定義在CONSTANT_Utf8_info類型的常量中的全限定名字符串;
    • 接口索引集合用來描述這個類實現了哪些接口,都是按照順序排列在訪問標誌之後;
  • 字段表集合 field_info

    • 字段表包含u2類型的access_flags,u2類型name_index,u2類型descriptor_index,u2類型的attributes_count, 表類型的attribute_info;
      • access_flags 用於獲取字段訪問標誌,表示字段的修飾符或者是什麼類型;
      • name_index , descriptor_index 用於對常量池的引用,分別代表字段的簡單名稱以及字段和方法的描述符;
    • 用於描述接口或類中聲明的變量,字段(field)包括類級變量和實例級變量,不包括在方法內部聲明的局部變量;
    • 全限定名 : org/xxx/xxx/TestClass;;
    • 簡單名稱 : 指沒有類型和參數修飾的方法或者字段名稱;
    • 常量池記錄着描述符,方法和字段的描述符作用: 描述字段的數據類型,方法的參數列表(數量,類型以及順序)和返回值;
      • 基本數據類型和代表無返回值的void類型都用一個大寫字符來表示,而對象則用字符L加對象的全限定名來表示;
      • 對於數組類型的描述符,每一維度使用一個前置的[字符描述
        • 如二維字符串數組 -> [[Ljava/lang/String;一維Int數組 -> [I;
      • 用描述符來描述方法時,先參數列表,後返回值的順序描述;參數列表按照參數的嚴格順序放在一組小括號中;
        • 如tostring 方法 -> ()Ljava/lang/String;
        • int indexOf(char[]source,int sourceOffset,int sourceCount,char[] target,int targetOffset,int targetCount,int fromIndex) -> ([CII[CIII)I
    • 字段表中之後跟隨一個屬性表集合用於存儲一些額外的信息,字段都可以在屬性表中描述零至多項的額外信息; 可以描述字段的默認值;
    • 字段表集合不會列出從超類或者父接口中繼承的字段,但可能列出原java代碼中不存在的字段,如在內部類中爲了保持對外部類的引用,自動添加指向外部類實例的字段(聯想反射內部類時,第一參數默認是外部實例)
    • 在字節碼中,如果字段的描述符不一致,字段重名就是合法的;

字段訪問標誌

描述符

  • 方法表集合 method_info
    • 方法表集合 包括u2類型 access_flags,u2 name_index,u2 descriptor_index, u2 attributes_count, attribute_info 表類型 attributes;
    • 方法中的代碼經過編譯器編譯成字節碼指令後,存放在方法屬性表集合中一個名爲Code的屬性裏,屬性表作爲class文件格式中最具擴展性的一種數據項目;
    • 父類方法在子類中沒有被Override,方法表集合中就不會出現來自父類的方法信息;但有可能出現編譯器自動添加的方法,如 類構造器 <clinit>,和實例構造器<init>方法;
    • 在字節碼中,方法如果返回值不同也是可以重載的;

方法訪問標誌

  • 屬性表集合 attribute_info
    • 每個屬性的名稱需要從常量池中引用一個CONSTANT_Utf8_info類型的常量來表示;屬性表集合的結構完全是自定義的,只需要通過一個u4的長度屬性去說明屬性值所佔用的位數;
    • Code屬性 [重要] java代碼編譯成的字節碼指令
      • java程序方法中的代碼經過javac編譯器處理後,變成字節碼指令存儲在Code屬性內;但接口和抽象類中的方法不存在code屬性;
      • max_stack 代表操作數棧(Operand Stacks)深度的最大值,在方法執行的任意時刻,操作數棧都不會超過這個深度,jvm 運行的時候需要根據這個值來分配棧幀(Stack Frame)中的操作棧深度;
      • max_locals 代表局部變量表所需的存儲空間;單位是Slot,Slot是jvm爲局部變量分配內存所使用的最小單位;其中byte,char,float,int,short,boolean,returnAddres等長度不超過32位的數據類型佔用一個Slot,double,long64位的數據類型需要兩個Slot存放;局部變量所佔的Slot可以被重用;
      • codecode_length 用來存儲java源程序編譯後生成的字節碼指令; 分別代表存儲字節碼指令的一系列字節流和字節碼長度; code_length 因jvm限制一個方法不允許超過65536條字節碼指令,理論上u4類型的長度值,實際上只有u2的長度;
        • 實例方法的局部變量表中至少會存在一個指向當前對象實例的局部變量,局部變量表中也會預留出第一個Slot爲來存放對象實例的引用;
      • exception_info 異常表
    • Exceptions 屬性 方法拋出的異常
      • 列舉出方法中可能拋出的受查異常(Checked Exceptions),也就是方法描述時在throws 關鍵字後面列舉的異常;
    • LineNumberTable 屬性 Java源碼的行號與字節碼行號(字節碼的偏移量)之間的對應關係;
    • LocalVariableTable 屬性 棧幀中局部變量表中的變量與java源碼中定義的變量之間的關係;
    • SourceFile 屬性 記錄生成這個Class文件的源碼文件名稱;
    • ConstantValue 屬性 通知jvm 自動爲靜態變量賦值;只有static修飾的變量可以使用這項屬性;
      • int x = 123; 實例變量的賦值是在實例構造器<init>方法中進行的;
      • static int x = 123; 對於類變量有兩種方式可以選擇;sun javac選擇是: 如果同時使用final和static修飾一個變量且此變量的數據類型是基本類型或者java.lang.String,就使用ConstantValue屬性來初始化; 否則使用<clinit>方法中進行初始化;
        • 使用類構造器<clinit>方法
        • 使用ConstantValue屬性,字面量;
    • InnerClasses 屬性 記錄內部類和宿主類之間的關聯
      • 如果一個類定義了內部類,編譯器會爲它及它的內部類生成InnerClasses;
    • Deprecated 及 Synthetic屬性 屬於標誌類型的布爾屬性
      • Deprecated : 用於表示某個類,字段或者方法,定爲不再推薦使用;
      • Synthetic : 代表字段或者方法並不是由java源碼直接產生的,而是由編譯器自行添加的;
    • StackMapTable 屬性 jvm類加載的字節碼驗證階段被新類型檢查驗證器(Type Checker)使用,代替以前比較消耗性能的基於數據流分析的類型推導驗證器;
    • Signature 屬性 任何類,接口,初始化方法或成員的泛型簽名如果包含了類型變量(Type Variables)或參數化類型(Parameterized Types),則Signature屬性會爲他記錄泛型簽名信息;
      • java 使用的泛型是擦除法實現的僞泛型,在字節碼(Code屬性)中,泛型信息編譯(類型變量,參數化類型)之後都統統被擦除掉;
      • 好處是實現簡單,節省內存; 壞處是無法將泛型類型和用戶定義的普通類型同等對待,運行期間做反射時無法獲得到泛型信息;Signature就是彌補此缺陷;
    • BootstrapMethods 屬性 用於保存invokedynamic 指令引用的引導方法限定符;

Code屬性表的結構

異常表運作

指令碼指令簡介

  • jvm 的指令由一個字節長度的,代表着某種特定操作含義的數字(稱爲操作碼Opcode)以及跟隨其後的零至多個代表此操作所需參數(稱爲操作數Operands)構成;

  • 字節碼與數據類型

    • 大多數的指令都包含了其操作所對應的數據類型信息;
      • iload指令用於從局部變量表中加載int型的數據到操作數棧中;fload指令加載的則是float類型,l-long,s-short,b-byte,c-char,f-float,d-double,a-reference;
  • 加載和存儲指令

    • 加載和存儲指令用於將棧幀中的局部變量表和操作數棧之間來回傳輸;
      • 將一個局部變量加載到操作棧: Tload,Tload_<n> T表示i,l,f,d,a; n->slot
      • 將一個數值從操作數棧存儲到局部變量表: Tstore,Tstore_<n> T表示i,l,f,d,a;
      • 將一個常量加載到操作數棧 : bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_m1,iconst_<i>,lconst_<l>,fconst_<f>,dconst_<d>
      • 擴充局部變量表的訪問索引的指令: wide;
  • 運算指令

    • 用於對兩個操作數棧上的值進行某種特定運算,並把結果重新存入到操作棧頂;
      • 加法: Tadd
      • 減法: Tsub
      • 乘法: Tmul
      • 除法: Tdiv
      • 取餘: Trem
      • 取反: Tneg
      • 位移: Tshl,Tshr,Tushr
      • 按位或: Tor
      • 按位與: Tand
      • 按位異或: Txor
      • 局部變量自增: Tinc
      • 比較:Tcmpg
  • 類型轉換指令

    • 可以將兩種不同的數值類型進行相互轉換,jvm支持以下數值類型的寬化類型轉換(小範圍向大範圍類型的安全轉換)
      • int類型到long,float或者double類型;
      • long類型到float,double類型;
      • float類型到double類型;
    • 處理窄化類型轉換,顯示試用轉換指令完成 T2T
      • i2b,i2c,i2s…
    • 將浮點型轉換爲整數類型(int,long)
      • 如果浮點數是NaN,轉換結果爲int或long類型的0;
      • 如果浮點型不是無窮大,試用IEEE 754向0舍入模式取整,獲得整數v,如果v在目標類型T的表示範圍內,則爲V;
      • 否則根據v的符號,轉換爲T所能表示的最大或者最小正數;
    • 數值類型的窄化類型不會導致jvm拋出運行時異常
  • 對象創建和訪問指令

    • jvm對類實例和數組的創建操作使用不同的字節碼指令
      • 創建類實例的指令 new
      • 創建數組的指令: newarray,anewarray,multianewarray
      • 訪問類字段(static)和實例字段的指令: getfield,putfield,getstatic,putstatic;
      • 將一個數組元素加載到操作數棧的指令: Taload;
      • 將一個操作數棧的值存儲到數組 Tastore
      • 取數組長度指令 arraylength
      • 檢查類實例類型指令 instanceof,checkcast;
  • 操作數棧管理指令

    • 直接操作操作數棧指令
      • 將操作數棧的棧頂一個或兩個元素出棧: pop,pop2;
      • 複製棧頂一個或兩個數值並將複製值或雙份的複製值重新壓入棧頂: dup,dup2,dup_x1,dup2_x1,dup2_x2;
      • 將棧最頂端的兩個數值互換 : swap;
  • 控制轉移指令

    • 讓jvm從指定的位置指令而不是控制轉移指令的下一條指令繼續執行程序;
      • 條件分支: ifeq,iflt,ifle,ifne,ifgt,ifnull,ifnonnull,if_icmpeq,if_icmpne,if_icmplt,if_icmpgt,if_cmple,if_icmpge,if_acmpeq,if_acmpne;
      • 複合條件分支: tableswitch,lookupswitch
      • 無條件分支: goto,goto_w,jsr,jsr_w,ret;
    • boolean,byte,char,short 都是使用int類型的指令完成,對於long,float,double先執行對應類型的指令,再返回整型值到操作數棧,再執行int指令;各種類型的比較都會轉化爲int類型的比較操作;
  • 方法調用和返回指令

    • 方法調用
      • invokevirtual 調用對象的實例方法,根據對象的實際類型進行分派(虛方法分派);
      • invokeinterface 調用接口的方法;
      • invokespecial 調用一些需要特殊處理的實例方法,包括實例初始化方法,私有方法和父類方法;
      • invokestatic 調用類方法;
      • invokedynamic 用於在運行時動態解析出調用點限定符所引用的方法;
    • 返回指令 Treturn
  • 異常處理指令

    • 顯示拋出異常的操作 (throw) 由athrow指令實現,在jvm中,處理異常由異常表完成;
  • 同步指令

    • 管程(Monitor) 支持同步;
    • 方法級的同步實現在方法調用和返回操作中,jvm可從方法常量池方法表結構中的同步訪問標誌得知一個方法是否是同步方法;
      • 方法調用時,執行線程先成功持有管程,然後才能執行方法,方法完成時釋放管程;
    • 同步一段指令集序列通常是由java語言中的synchronized塊表示;jvm指令集中有 monitorenter和monitorexit支持synchronized關鍵字的語義;

同步字節碼指令


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章