深入理解JVM虛擬機-周志明【第三版】


          概述

一、走進虛擬機

二、自動內存管理

三、垃圾收集器與內存回收策略

四、虛擬機性能監控、故障處理工具

五、調優案例分析與實戰

六、類文件結構

七、虛擬機類加載機制

概述

Java 技術系: Kotlin 、Clojure 、JRuby、Groovy 均是運行在 Java 虛擬機上的程序語言

我們通常把Java 程序設計語言、Java虛擬機、Java 類庫 三部分統稱為 JDK

Java 前生叫做Oak , 1995 年改名Java,並發布第一個正式環境 JDK1.0

2004  jdk5 發布,將版本的命名分格改變,語法有較大的改變

2014 jdk8 發布 支持 Lambda 表達式、移除 HotSpot 的永久代

一、走進虛擬機

最原始的虛擬機 sun  Classic /Exact VM

Classic 虛擬機特點:無法執行即使編譯,通過外掛的方式可以即時編譯就會完全托管編譯

Exact Vm : 精准的內存管理

HotSpot VM : 繼承前兩款虛擬機的優點,用於獨特的熱點代碼探測技術

Mobile/Embedded Vm :用於嵌入式

BEA  JRockit: 號稱速度最快,后被收購

IBM J9 VM

軟硬合璧 BEA Liquid VM / Azul VM :與特定的硬件平台綁定

等等

二、自動內存管理

 參考:https://www.jianshu.com/p/76959115d486

1.程序計數器

Java 多線程之間切換,為了線程切換后能恢復到正確的執行位置,每條線程都需要一個獨立的程序計數器,各個線程之間的計數器互不影響

2.虛擬機棧 / 棧

棧的生命周期與線程相同,為線程私有,Java 虛擬機執行方法就會同步創建棧幀 Stack Frane ,用於存儲局部變量表、操作數棧、動態連接、方法出口

局部變量表存儲了編譯期可知的虛擬機基本數據類型、對象引用(可能是對象地址、也可能是代表對象的句柄)、返回地址,這些局部變量在局部變量表中以局部變量槽Slot 來表示。

64 位的 long 與 double 會占用 2 個局部變量槽

3.本地方法棧

虛擬機棧是為虛擬機執行Java 方法服務,而本地方法棧是為虛擬機執行本地方法服務

4.堆

按照 HotSpot 虛擬機來說,會分為 新生代、老年代、永久代、Eden 空間、From Survivor 、To Survivor 等空間,其原因是因為GC 回收都基本在堆上進行的,按垃圾收集行為來區分的。而現在垃

圾收集器與過去有較大的不同,有點虛擬機gc 都不是采用分代收集  (舉個栗子)多線程下為了更好的分配對象,線程共享的堆可以划分出多個線程私有的分配緩沖區 (Thread Local Allocation

Buffer),以提升對象分配時的效率

5.方法區 Method Area

屬於線程共享,用於存儲被虛擬機加載的類型信息、常類、靜態變量、即使編譯器編譯后的代碼緩存數據(別名:非堆、永久代),在這個區也存在垃圾回收,主要針對常量池的回收與類型的卸載

6.運行時常量池 Runtime Constant Pool

運行時常量池是方法區的一部分,Class 文件中出了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池表(Constant Pool Table),用於存放編譯期生成的各種字面量與符號引用,

這部分內容在類加載后存放到方法區的運行時常量池中

7.直接內存 Direct Memory

直接內存不是虛擬機運行時數據區的一部分,但也被頻繁的使用,而且也可能導致 OutOfMemoryError 異常。在 JDK 1.4 加入 NIO( New Input/OutPut)類,引入一種基於通道 Channel 與 緩沖區

Buffer 的IO 方式,它可以使用 Native 函數庫直接直接分配堆外內存,然后通過一個存儲在 Java 堆里面的 DirectByteBuffer 對象作為這塊內存的引用進行操作。在一些場景中提高了性能,比避免

了在Java 堆與 Native堆中來回復制數據。雖然該內存不受Java 堆內存限制,但是收到機器內存的限制,也可能導致內存溢出

對象的創建

  當Java 虛擬機遇到一條字節碼 new 指令時,首先檢查這個指令的參數是否能夠在常量池中定位到一個類的符號引用,並且檢查這個符號引用所代表的類是否已經被加載、解析、初始化過。

如果沒有,則必須執行相應的類加載過程。

  在類加載檢查通過后,接下來虛擬機將為新對象分配內存,對象所需內存大小在類加載完成后便可完全確定,為對象分配空間的任務實際上等同於把一塊確定大小的內存從Java 堆中划分出出來。

假設Java 堆是絕對規整的,所有使用過的對象放在一邊,未使用的對象放在另一邊,中間方着一個指針作為分界線的指示器,分配對象就是把指針往空閑方向挪動一塊與對象大小相等的距離,這種

分配方式稱為指針碰撞 (Bump the Pointer).但如果Java堆的內存並不是規整的,已近使用的內存與未使用的內存交錯放在一起,虛擬機必須維護一個列表,記錄那些內存是空閑的,在分配的時候

從列表中找到一塊足夠大的空間分配給實例,並更新列表上的記錄,這種分配方式稱為空間列表(Free List)。選擇哪種分配方式是由Java 堆是否規整決定的,Java 堆是否規整則由垃圾收集器是否

帶有空間壓縮整理(Compact)的能力決定的。而使用 Serial 、ParNew 等帶有壓縮整理過程的收集器時,系統采用的分配算法是指針碰撞,簡單高效。而使用 CMS 這種基於清除算法的收集器時,

理論上只能采用較為復雜的空閑列表來分配內存。

  還需要考慮另一個問題是對象在虛擬機中創建是非常頻繁的行為,即使是修改一個指針指向的位置,在並發的情況下可能出現正在給對象A分配內存,指針還沒來得及修改,對象B又同事使用了原

來的指針分配內存。解決這個問題有兩種方案: 一 堆分配內存空間進行同步處理--實際上虛擬機采用 CAS 配上失敗重試的方式保證更新操作的原子性;另外一種是把內存分配的動作划分在不同的空間

中進行,即為每個線程在 Java 堆中預先分配一小塊內存,稱為本地線程分配緩沖(Thread Local Allocation Buffer TLAB),只有本地緩沖區用完了,分配新的緩存區時才需要同步鎖定。虛擬機是否使用

TLAB 可以通過 -XX: +/-UseTLAB 設定

  內存分配完,虛擬機必須將分配的內存空間(不包括對象頭)都初始化為零值,如果使用TLAB ,則這一項提前到 TLAB 分配時進行。

  Java 虛擬機還要對對象頭進行必要的設計,如對象是哪個類的實例、如何才能找到元數據信息、對象的哈希碼(實際上對象的哈希碼會延后到真正調用 Object::hashCode() 方法時才計算),對象

的GC 分代年齡等信息,是否啟動偏向鎖等。這些信息存在對象頭中,Object Header.

  上面的工作完成后,從虛擬機的角度,一個新的對象已經產生了。單從Java 的角度,對象的創建才剛開始--構造函數。 Class 文件 <init>方法還沒執行,所有的字段為默認零值,對象需要按照意圖

構造好。

 對象的內存布局

  在HotSpot 虛擬機里,對象在堆內存中存儲的布局可以划分為三部分:對象頭(Header)、實例數據(Instance Data)、和對齊填充(Padding)

  HotSpot 虛擬機對象的對象頭部分存儲兩類信息,第一類是用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡鎖狀態標志、線程持有鎖、偏向鎖ID、偏向時間戳等。這部分數據

在長度32位和64位的虛擬機(未開啟壓縮指針)中分別是 32 個比特 、64個比特。官方統稱位 Mark Word 。對象需要存儲的運行時數據很多,其實已經超過32、64位所能記錄的最大長度,但對象頭里的

信息是與對象自身定義的數據無關的額外成本,考慮到虛擬機的空間效率,MarkWord 的32位比特存儲空間中: 25 個用於存儲對象哈希碼、4個用於存儲對象分代年齡、2個用與存儲鎖標志位、1個比特固

定為 0 ,如下圖

        

   對象頭的另一部分是類型指針,即對象指向它的類型元數據的指針,Java虛擬機通過這個指針來確定該對象是哪個類的實例。並不一定所有的虛擬機實現都必須在對象上保留類型指針,換句話說是

查找對象元數據信息並不一定要經過對象本身。如果對象是Java 數組,那么對象頭中還必須有一塊用於記錄數組長度的數據。

  接下來實例數據部分才是對象真正存儲的有效信息,即在代碼里定義的各種類型的字段內容、無論從父類繼承下來還在字類定義的字段都必須記錄下來,這部分的存儲順序會收到虛擬機分配策略參數

和字段在Java 代碼定義順序影響。HotSpot 虛擬機默認的分配順序: long/double 、int、shorts/chars、byte/boolean

  對象的第三部分是對齊填充,這並不是必然存在的,它僅僅起着占位符的作用。由於 HotSpot 虛擬機的自動內存管理系統要去對象的起始地址必須是8字節的整數倍,對象頭的部分已經是整數倍,如果

沒有實例數據,將不需要對齊填充

對象的訪問定位

  訪問對象通過棧上的reference 數據來操作具體的對象,訪問的方式由虛擬機實現而定的,主流的訪問方式主要有使用句柄直接指針

  如果使用句柄訪問,Java堆將可能會划分出一塊內存作為句柄池,reference 中存儲的地址就是句柄地址,而句柄中包含了對象實例數據和類型數據具體的地址

       

   如果使用直接指針訪問,Java 堆中對象的內存布局就必須考慮如何放置訪問類型數據的相關信息,reference 中存儲的地址是對象地址,如果是訪問對象本身的話,就不需要多一次間接的訪問

      

 使用直接指針的方式的好處是速度快,節省了一次指針定位的時間開銷,由於訪問對象多,中間會消耗可觀的時間成本

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

3.1 對象是否已經死亡

引用計數:在對象中添加一個引用計數器,每當有一個地方引用就將計數器加一,引用失效就減一,當計數器為零表示對象不再被使用。該算法原理簡單 ,效率高,但是無法解決循環引用的,

其中微軟的 COM 技術Python語言就是使用了引用技術作為內存關聯

可達性分析Java 、C# 、Lisp 使用的都是可達性分析算法,通過一系列的GC roots 的根對象作為起點,根據引用向下搜索,搜索走過的路徑稱為引用鏈,如果某個對象與 GC roots沒人任何引用

鏈,則認為是不可達的也就是不能被使用的。可以作為GC root 的節點:

  1. 棧幀中本地變量表引用的對象
  2. 方法區靜態屬性引用的對象
  3. 方法區中常量引用的對象
  4. 本地方法棧引用的對象
  5. 虛擬機內部引用的對象,如常用的異常對象
  6. 同步鎖持有的對象

3.2 再談引用

jdk1.2 之后,Java 對引用進行擴充,分為強引用、軟引用、弱引用、虛引用

強引用 例如new Object ,任何情況下,只要強引用關系存在,就不會有垃圾回收及收集

軟引用 軟引用是早內存溢出之前,將軟引用的對象列入二次回收,如果這次回收還沒有足夠的內存,才會拋出內存溢出異常

弱引用 弱引用的對象只能生存到下一次垃圾收集器發生為止

虛引用 也叫作幽靈引用。是最弱的引用,為一個對象設置虛引用唯一的目的是該對象回收會收到一個系統通知

  在可達性分析算法中判斷對象不可達,也並非”非死不可“,需要經過2次的標記: 如果在可達性分析后發現對象沒有引用鏈,它將會被第一次標記,隨后進行一次篩選,篩選的條件是對象是否有必要執行

finalize 方法。沒必要執行:對象沒有覆蓋 finalize() 方法,finalize() 方法已經被虛擬機調用過。

  如果對象有必要執行finalize() 方法,將會被放置在一個 F-Queue的隊列中,由一條低優先級的線程執行他們的 finalize 方法。如果對象在被回收期重新被引用,那么第二次回收將被移除隊列。如果這時對象

還沒有逃脫,那么將被回收。

3.3 回收方法區

  方法區的回收主要是兩部分內容:廢棄的常量不能再使用的類型

如果常量池中某常量沒有任何引用,那么發送回收就會被清理。判斷一個類是否被回收,需要滿足一下條件

  1. 所有該類的實例都被回收了
  2. 加載該類的類加載器被回收
  3. 該類對應的Java.lang.Class 對象沒有任何地方被引用,無法通過反射獲取該類的方法

3.4 垃圾收集算法

當前的虛擬機的垃圾收集器,大多都遵循分代收集的理論,它是建立在2個假說之上

  1. 弱分代假說 絕大多數對象都是朝生夕滅
  2. 強分代假說  熬過多次垃圾回收的對象越難以被回收

收集器將Java 堆划分出不同的區域,然后將回收對象依據其年齡分配到不同的區域中。因此垃圾器才可以每次只回收其中的某些部分的區域,因此才有了 Minor GC 、Major GC、Full GC 回收分類的划分,

才能夠安排某些不同區域與對象死亡特征相匹配的收集算法。因而發展出收集算法有:

  • 標記-復制
  • 標記-清除
  • 標記-整理

在分代收集中,夜班會將堆分為:新生代、老年代。 新生代每次垃圾回收都會有大量對象死去,而每次存活的對象將逐步晉升到老年代(缺點:對象之間存在跨代引用

  1. 增加第三條假說:跨代引用相對同代引用來說僅占極少數

例如:老年代中引用了新生代對象,導致新生代對象無法被回收,最后也進入老年代,增加系統的消耗

標記-清除算法

標記清除算法最早1960 Lisp 之父提出,分為標記階段、清除階段。首先標記需要回收的對象,標記完統一回收。主要缺點:執行效率不穩定(內存中如果有大量需要被回收的對象,導致標記與回收效率逐漸降低),

第二個缺點:標記清除后會產生大量不連續的內存碎片(將導致有大對象需要創建,沒有足夠大的空間而觸發GC)

標記-復制算法

簡稱復制算法,1969 年提出,將可用的內存划分為大小相等的2部分,每次只用其中的一部分,當用完就將存活的對象復制到另一半,刪除清理之前的一半。缺點:將產生大量的內存間的復制開銷,可用內存減半

(現在改算法適應於新生代,因為新生代的對象98% 不能存活過第一輪收集),hotspot 虛擬機將將分為較大的Eden 空間、2塊較小的survivor ,比例為 8:1:1 ,因次只有10%的空間是被浪費的。雖然新生代98%

的對象被回收,但是也不一定是絕對的,垃圾回收將Eden + 1塊survivor 空間存活的對象復制到另一塊 survivor ,當不足容納時,需要進行分配擔保,無法保留的對象將直接進入老年代

標記-整理算法

針對老年代的特征,1974年提出標記-整理算法,其中標記過程與標記-清除算法一樣,后續步驟不是直接刪除對象,而是將存活的對象整理到一邊,然后清理掉剩余的對象。

相對於標記清除,標記整理的特點是移動的,優缺並行。

  不一定對象停頓時間短,甚至不停頓,但從整體程序的吞吐量,移動的標記整理更划算。HotSpot 虛擬機里關注吞吐量的 Parallel Scavenge 收集器是基於標記-整理算法的。而關注低延時的CMS 收集器則

基於標記-清除算法。

3.5 HotSpot 算法細節實現

 根節點枚舉: 可達性分析算法中一系列 GC Roots 集合找引用鏈的過程,都必須暫停用戶線程。Java程序越來越大,gc root 數據也是越來越大,使用可達性分析逐個掃描會消耗很大的時間。HotSpot 虛擬機

實現直接定位對象存儲位置使用 OopMap 的數據結構。

安全點:在 OopMap 的協助下,HotSpot 可以快速的完成根節點枚舉,但是其並沒有為每一條指令都生成 OopMap ,在特定的位置記錄信息,這些位置稱為安全點。

安全區域:安全點保證程序執行時,安全區域確保在代碼中,引用關系不會發生變化,在該區域中任意地方開始垃圾回收都是安全的

3.6 經典的垃圾收集器

 

 插入:jdk1.8 查看使用默認的垃圾收集器

Java  -XX:+PrintGCDetails  -XX:+PrintCommandLineFlags
Heap
 PSYoungGen      total 37888K, used 2621K [0x00000000d6100000, 0x00000000d8b00000, 0x0000000100000000)
  eden space 32768K, 8% used [0x00000000d6100000,0x00000000d638f7d0,0x00000000d8100000)
  from space 5120K, 0% used [0x00000000d8600000,0x00000000d8600000,0x00000000d8b00000)
  to   space 5120K, 0% used [0x00000000d8100000,0x00000000d8100000,0x00000000d8600000)
 ParOldGen       total 86016K, used 0K [0x0000000082200000, 0x0000000087600000, 0x00000000d6100000)
  object space 86016K, 0% used [0x0000000082200000,0x0000000082200000,0x0000000087600000)
 Metaspace       used 3262K, capacity 4554K, committed 4864K, reserved 1056768K
  class space    used 367K, capacity 386K, committed 512K, reserved 1048576K

可以看到上述的結果,PSYoungGen 表示由 Parallel Scavenge 垃圾收集器管理新生代 , ParOldGen 表示由 Parallel Old 管理老年代

 比較廣泛使用的收集器是 CMS 與 G1

經典的收集器

Serial 收集器

最基礎的收集器,在 jdk 1.3.1 之前HotSpot虛擬機新生代收集器。為單線程收集器,單線程的含義是其進行垃圾收集時必須暫停其他所有的線程

 從Serial 收集器到 Parallel 收集器,再到 Concurrent Mark Sweep(CMS)、Grabage First(G1) 收集器,最終至 ShenandoahZGC 

ParNew 收集器

ParNew收集器本質上是 Serial 收集器的多線程並行版本

其最為新生代的收集器,可與老年代收集器 CMS 搭配使用。但隨着發展,G1 收集器可以收集全棧

Parallel Scavenge 收集器

parallel scavenge 收集器是一款新生代的收集器,基於標記-復制算法的收集器,也能夠支持並行。CMS 收集器的目標是減少垃圾收集導致用戶的停頓時間,而 Parallel Scavenge 目的是達到一個可控的吞吐量

虛擬機參數:-Xmn 新生代大小 

Serial Old 收集器

單線程的老年代收集器,使用標記-整理算法

Parallel Old 收集器

是parallel scavenge收集器的老年代版本,支持多線程並發收集,基於標記-整理算法實現。是jdk 1.6 才開始提供

CMS 收集器

Concurrent Mark Sweep 收集器是以獲取最短回收停頓時間為目標的收集器,運行過程包括

  1. 初始標記
  2. 並發標記
  3. 重新標記
  4. 並發清除

過程只有初始標記與重新標記需要停頓。耗時較長的並發標記與並發清除可以與用戶線程一起工作,所以整體停頓時間較短。、

CMS 是基於標記-清除算法實現的收集器,(容易產生碎片)

Garbage First 收集器

簡稱G1 收集器,作為垃圾收集器發展史的里程碑,是第一款面向全局的收集器。在jdk9 之中,由G1 收集器代替Parallel Scavenge 與 Parallel Old 收集器。稱為默認的收集器。

G1 收集器不再固定的收集那塊區域,而是將堆內存划分為若干相對的區域,基於Region 的堆內存布局,回收性價比最高的區域。該模式也稱為 Mixed GC 模式

G1 收集器運行示意圖

低延遲垃圾收集器

Shenandoah

shenandoah 是 RedHat 公司發展的新型收集器,后貢獻 openjdk ,目標是任何情況下都把垃圾收集時間控制在10毫秒以內,其不僅要進行並發的垃圾標記,還要並發的進行對象清理后的整理。

也是使用 Region 的堆內存布局,回收策略也是優先回收價值最大的(該回收期並沒有分代收集,不會區分新生代、老年代)

ZGC 收集器

 z Garbage collection, 是在 jdk11 中加入的特性,是Oracle 公司研發的。目標也是盡可能的在不影響吞吐量的情況下,降低堆內存的垃圾回收停頓時間

垃圾收集器參數總結

 

在大多數情況下,對象優先在 Eden 區分配,當Eden 沒有足夠空間分配時,虛擬機將發起一次 minor  gc. 

大對象直接進入老年代,長期存活的對象進入老年代

在發生 minor gc 前,虛擬機必須檢查老年代最大可用的連續空間是否大於新生代所有對象總空間。如果條件成立,那么minor gc 可以確保是安全的,否則,虛擬機查詢是否允許擔保失敗,

如果允許,則繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代的平均大小,如果大於,則進行一個 minor gc ,盡管是有風險的。如果小於,或者不允許冒險,則進行一次Full  GC

四、虛擬機性能監控、故障處理工具

五、調優案例分析與實戰

六、類文件結構

 6.1 Class 類文件結構

Class 文件是一組以8字節為基礎的二進制流,Class 文件以無符號數組成。

 當用於描述若干個數量不定的數據時,經常會用一個前置的容量計數器與若干個連續的數據項形式,Class 文件沒有任何分隔符

6.2 魔術與版本號

      每個 Class 文件的頭4個字節被稱為魔術(Magic Number),唯一的作用是確認這個文件能否被虛擬機接收的Class 文件。這里采用魔數而非擴展名來識別,主要是基於安全的考慮。

OxCAFEBABE ,固定的魔數,十六進制標識,長度為 4B 。

  緊接着魔數的4個字節存儲的是Class 文件的版本號,前2個字節存儲次版本號(Minor Version),后2個字節存儲主版本號(Major Version)。Java 的版本號是從45 開始。虛擬機規范

,高版本的 jdk 能夠向下兼容以前版本的 Class 文件,但是不能允許以后版本的 Class 文件,Class 文件校驗部分明確要求了即使文件格式未發送任何變化,虛擬機也必須拒絕執行超過其

版本號的Class 文件。

jdk    版本號

1.x       45 

1.2x    45~46

1.3x    45~47

1.4x    45~48

1.5x    45~ 49

1.6x    45~50

1.7x    45~51

1.8x    45~52

6.3 常量池

主次版本號之后是常量池入口,常量池的數量不少不定的,所以在常量池入口需要一項 2u 類型數據,代表常量池計數器。與Java語言習慣不同,常量池計數器從 1 開始。

如下述,0x0016 ,即十進制 22 ,標識常量池有21 項常量,所以范圍為 1~21 .

 常量池主要存儲:字面量 與 符號引用。字面量如文本字符串,被聲明為 final 的常量值。符號引用屬於編譯原理方面的概念,主要包括下幾類常量:

  1. 被模塊導出或者開放的包
  2. 類和接口的全限定名
  3. 字段的名稱和描述符
  4. 方法的名稱和描述符
  5. 方法句柄和方法類型
  6. 動態調用點和動態常量

當虛擬機在做類加載時,常會從常量池獲取對應的符號引用,再在類創建時會運行時解析,翻譯到具體的內存地址中。

常量池的每一項常量都是一個表,截至jdk 13 ,常量表中分別有17 種不同類型的常量

6.4 訪問標志位

在常量池結束后,緊接着2個字節代表訪問標志位(access_flag),這個標志用於識別一些類或者接口層次的訪問信息,包括:這個class 是類還是接口,是否定義為 public 類型,是否定義為 abstract 類型。

如果是類,是否被申明為final

 

 access_flags 中共有16個標志位可以使用。

6.5 類索引、父類索引、與所有集合

類所以 this_class  父類所有 super_class 都是一個 u2 類型的數據,接口索引集合是一組 u2 類型的數據的集合。

類索引確定類的全限定名,父類索引確定這個類的父類的全限定名。類所以與父類索引是兩個 u2 類型的索引值,以下是查詢類全限定名的過程

 接口索引,入口項是一個 u2 類型的接口計數器,標識索引的容量,如果類沒有實現任何接口,則計數器值為 0 

6.6 字段表集合

  字段表(field_info)用於描述接口或者類中聲明的變量。 Java 中的字段包括類變量和實例變量,單不包括在方法內部申明的變量。字段表可以修飾的包括

  1. 作用域 public private protected
  2. 類/實例 static
  3. 可變性 final
  4. 並發現 volatile
  5. 能否序列化 transient
  6. 字段數據類型

字段表結構

 字段訪問標志

訪問權限 acc_public 、acc_private、acc_protected 三個標志最多只能選其一

acc_final 、acc_volatile 不能同時選擇

接口必須包含 acc_public 、acc_static、acc_final

在acc_flag 之后是 name_index ,descriptor_index ,他們都是對常量池的引用,分別代表字段的簡單名稱以及字段和方法的描述符。

現在簡單的區別一下 描述符 、簡單名稱、全限定名

全限定名和簡單名稱,例如 org/apache/TestClass,這是這個類id全限定名,只是把 . 換成了 / ,為了使多個全限定名區別,最后一般會加入一個 ; 

簡單名稱就是指沒有類型和參數修改的方法或者字段名稱,例如inc()方法和 m 字段的簡單名稱 inc , m

描述符相對復雜一點,描述符的作用是用來描述字段的數據類型、方法的參數列表(數量、類型、順序)和返回值。描述符的規則對於基本數據類型使用大寫字母表示,而對象使用字符L j加對象的全限定名表示

 對於數組類型,將維度前加 [ ,二維 [[ 

描述符描述方法時,按照先參數列表,后返回值的順序描述,參數列表嚴格按照順序。

例如 void inc() 描述為   ()V

6.7 方法表集合

Class 文件存儲格式中對方法的描述與對字段的描述幾乎是完全一致的方式,方法表的結構依次是訪問標志位(access_flags)、名稱索引(name_index)、描述符索引(descriptor_index)、屬性表集合(attributes)

 因為volatile 關鍵字和 transient 關鍵字不能修飾方法,所以方法表的標志中沒有了 ACC_VOLATILE 和 ACC_TRANSIENNT 。方法表可取參數值如下

 方法的頂義可以通過訪問標志、名稱所以、描述符索引訪問,但是方法里的代碼存儲在哪里呢?方法里的Java 代碼經過 Javac 編譯器編譯成字節碼指令之后,存放在方法屬性表集合中名為 Code 的屬性里。

 在Java 中要重載一個方法,除了要與原方法具有相同的簡單名稱外,還要求必須擁有一個與原方法不同的特征簽名。

注:特征簽名分為字節碼層面的方法特征簽名以及Java 層面的方法特征簽名,Java 代碼的方法特征簽名只包含方法名稱、參數順序、參數類型,而字節碼層面的特征簽名還包括方法返回值以及受查異常表

所以在Java 語言里無法通過返回值來區別方法重載,但是在 Class 文件格式中,僅僅返回值不同也可以使得方法共存在一個 class 文件中

6.8 屬性表

Class 文件,字段表,方法表都可以攜帶自己的屬性表集合,以描述某些場景專有的信息,屬性表集合的限制稍微寬松,不嚴格要求順序

 對於每一個屬性,它的名稱都要從常量池中引用一個 constant_utf8_info 類型的常量來表示,屬性表結構如下

 attribute_name_index 是一項指向 constant_utf8_info 類型常量的索引,由此常量值固定為 Code, 它代表了該屬性的屬性名稱。

max_stack 代表了操作數棧 (Operand Stack) 深度的最大值。操作數棧不會超過這個深度。虛擬機運行的時候需要根據這個值來分配棧幀(Stack Frame)中的操作棧深度。

 七、虛擬機類加載機制

 


免責聲明!

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



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