JVM-GC調優-字節碼篇-01

筆記來源:JVM

注意:實在想學習可以看一下,讓自己更加了解JVM,看起來可能會枯燥。

JVM-概述

1、你的問題
1.1你被JVM傷害過嗎?
你是否也遇到過這些問題?

運行著的線上系統突然卡死,系統無法訪問,甚至直接OOM!
想解決線上JVM GC問題,但卻無從下手。
新項目上線,對各種JVM參數設置一臉茫然,直接默認吧,然后就JJ了
每次面試之前都要重新背一遍JVM的一些原理概念性的東西,然而面試官卻經常問你在實際項目中如何調優JVM參數,如何解決GC、OOM等問題,一臉懵逼。
1.2架構師每天都在思考什么?
應該如何讓我的系統更快?
如何避免系統出現瓶頸?
2、Java語言及Java生態圈
在這里插入圖片描述
世界上沒有最好的編程語言,只有最適用于具體應用場景的編程語言。
2.1Oracle JDK與Open JDK什么關系?
在這里插入圖片描述

官方的說明:
https://www.oracle.com/cn/java/technologies/javase-downloads.html
Oracle Customers and ISVs targeting Oracle LTS releases: Oracle JDK is Oracle’s supported Java SE version for customers and for developing, testing, prototyping or demonstrating your Java applications.
End users and developers looking for free JDK versions: Oracle OpenJDK offers the same features and performance as Oracle JDK under the GPL license .

Oracle JDK下載路徑:
https://www.oracle.com/java/technologies/javase-jdk15-downloads.html
Open JDK下載路徑:
https://cn.azul.com/downloads/zulu-community/?version=java-11-lts&os=windows&architecture=x86-64-bit

Oracle與OpenJDK之間的主要區別

  1. Oracle JDK版本將每三年發布一次LTS版本,而OpenJDK版本每三個月發布一次。
  2. Oracle JDK將更多地關注穩定性,它重視更多的企業級用戶,而OpenJDK經常發布以支持其他性能,這可能會導致不穩定。
  3. Oracle JDK支持長期發布的更改,而Open JDK僅支持計劃和完成下一個發行版。
  4. Oracle JDK根據二進制代碼許可協議獲得許可,而OpenJDK根據GPL v2許可獲得許可。 使用Oracle平臺時會產生一些許可影響。如Oracle 宣布的那樣,在沒有商業許可的情況下,在2019年1月之后發布的Oracle Java SE 8的公開更新將無法用于商業,商業或生產用途。但是,OpenJDK是完全開源的,可以自由使用。
  5. Oracle JDK的構建過程基于OpenJDK,因此OpenJDK與Oracle JDK之間沒有技術差異。
  6. 頂級公司正在使用Oracle JDK,例如Android Studio,Minecraft和IntelliJ IDEA開發工具,其中Open JDK不太受歡迎。
  7. Oracle JDK具有Flight Recorder,Java Mission Control和Application Class-Data Sharing功能,Open JDK具有Font Renderer功能,這是OpenJDK與Oracle JDK之間的顯著差異。
  8. Oracle JDK具有良好的GC選項和更好的渲染器,而OpenJDK具有更少的GC選項,并且由于其包含自己的渲染器的分布,因此具有較慢的圖形渲染器選項。
  9. 在響應性和JVM性能方面,Oracle JDK與OpenJDK相比提供了更好的性能。
  10. 與OpenJDK相比,Oracle JDK的開源社區較少,OpenJDK社區用戶的表現優于Oracle JDK發布的功能,以提高性能。
  11. 如果使用Oracle JDK會產生許可影響,而OpenJDK沒有這樣的問題,并且可以以任何方式使用,以滿足完全開源和免費使用。
  12. Oracle JDK在運行JDK時不會產生任何問題,而OpenJDK在為某些用戶運行JDK時會產生一些問題。
  13. 根據使用方的使用和許可協議,現有應用程序可以從Oracle JDK遷移到Open JDK,反之亦然。
  14. Oracle JDK將從其10.0.X版本將收費,用戶必須付費或必須依賴OpenJDK才能使用其免費版本。
  15. Oracle JDK不會為即將發布的版本提供長期支持,用戶每次都必須通過更新到最新版本獲得支持來獲取最新版本。
  16. Oracle JDK以前的1.0版以前的版本是由Sun開發的,后來被Oracle收購并為其他版本維護,而OpenJDK最初只基于Java SDK或JDK版本7。
  17. Oracle JDK發布時大多數功能都是開源的,其中一些功能免于開源,并且根據Sun的許可授權,而OpenJDK發布了所有功能,如開源和免費。
  18. Oracle JDK完全由Oracle公司開發,而Open JDK項目由IBM,Apple,SAP AG,Redhat等頂級公司加入和合作。
    2.2JDK與JVM是什么關系?
    在這里插入圖片描述
    擴展:
    JDK(Java Development Kit,Java開發工具包) ,是整個JAVA的核心,包括了Java運行環境JRE(Java Runtime Envirnment),一堆Java工具(javac/java/jdb等)和Java基礎的類庫),包含JVM標準實現及Java核心類庫。

JRE(Java Runtime Environment Java運行環境) ,是 JDK 的子集,也就是包括 JRE 所有內容,以及開發應用程序所需的編譯器和調試器等工具。

JVM(Java Virtual Machine,Java虛擬機), 是JRE的一部分。它是整個Java實現跨平臺的最核心的部分,負責解釋執行字節碼文件,是可運行Java字節碼文件的虛擬計算機。
區別和聯系:

DK=Java開發工具+JRE

JRE=JVM+Java類庫

JDK 用于開發,JRE 用于運行Java程序 ;如果只是運行Java程序,可以只安裝JRE,無序安裝 JDK。

JDk包含JRE,JDK 和 JRE 中都包含 JVM。

JVM 是 Java 編程語言的核心并且具有平臺獨立性。
2.2.1如何理解Java是跨平臺的語言?
在這里插入圖片描述
“write once, run anywhere.”

當Java源代碼成功編譯成字節碼后,如果想在不同的平臺上面運行,則無須再次編譯
這個優勢不再那么吸引人了。Python、PHP、Perl、Ruby、Lisp等有強大的解釋器。跨平臺似乎已經快成為一門語言必選的特性。

2.2.2如何理解JVM跨語言的平臺?
在這里插入圖片描述
Java虛擬機根本不關心運行在其內部的程序到底是使用何種編程語言編寫的,它只關心“字節碼”文件。

Java不是最強大的語言,但是JVM是最強大的虛擬機。

2.3Java不存在內存溢出?內存泄漏?
在這里插入圖片描述
java = (c++)–;

垃圾收集機制為我們打理了很多繁瑣的工作,大大提高了開發的效率,但是,垃圾收集也不是萬能的,懂得JVM內部的內存結構、工作機制,是設計高擴展性應用和診斷運行時問題的基礎,也是Java工程師進階的必備能力。
2.4Java發展的幾個重大事件?
2000年,JDK 1.3發布,Java HotSpot Virtual Machine正式發布,成為Java的默認虛擬機。
2002年,JDK 1.4發布,古老的Classic虛擬機退出歷史舞臺。
2003年年底,Java平臺的Scala正式發布,同年Groovy也加入了 Java陣營。
2006年,JDK 6發布。同年,Java開源并建立了 OpenJDK。順理成章,Hotspot虛擬機也成為了 OpenJDK中的默認虛擬機。
2007年,Java平臺迎來了新伙伴Clojure。
2008 年,Oracle 收購了 BEA,得到了 JRockit 虛擬機。
2009年,Twitter宣布把后臺大部分程序從Ruby遷移到Scala,這是Java平臺的又一次大規模應用。
2010年,Oracle收購了Sun,獲得Java商標和最具價值的HotSpot虛擬機。此時,Oracle擁有市場占用率最高的兩款虛擬機HotSpot和JRockit,并計劃在未來對它們進行整合:HotRockit. JCP組織管理:Java語言
2011年,JDK7發布。在JDK 1.7u4中,正式啟用了新的垃圾回收器G1。
2017年,JDK9發布。將G1設置為默認GC,替代CMS (被標記為Deprecated)
同年,IBM的J9開源,形成了現在的Open J9社區
2018年,Android的Java侵權案判決,Google賠償Oracle計88億美元
同年,JDK11發布,LTS版本的JDK,發布革命性的ZGC,調整JDK授權許可
2019年,JDK12發布,加入RedHat領導開發的Shenandoah GC
2.4.1說說你認識的JVM?
Sun Classic VM -->解釋型
Exact VM --> Solaris
SUN公司的 HotSpot VM
BEA 的 JRockit --> 不包含解釋器,服務器端,JMC
IBM 的 J9

KVM和CDC/CLDC Hotspot
Azul VM
Liquid VM
Apache Harmony
Microsoft JVM
TaobaoJVM
Graal VM --> 2018年,“Run Programs Faster Anywhere”
在這里插入圖片描述

Dalvik VM

其他JVM:
Java Card VM、Squawk VM、JavaInJava、Maxine VM、Jikes RVM、IKVM.NET、Jam VM、Cacao VM、Sable VM、Kaffe、Jelatine JVM、Nano VM、MRP、Moxie JVM
2.4.2JVM的生命周期?
虛擬機的啟動
Java虛擬機的啟動是通過引導類加載器(bootstrap class loader)創建一個初始類(initial class)來完成的,這個類是由虛擬機的具體實現指定的。

虛擬機的退出有如下的幾種情況:
某線程調用Runtime類或System類的exit方法,或 Runtime類的halt方法,并且Java安全管理器也允許這次exit或halt操作。
程序正常執行結束
程序在執行過程中遇到了異常或錯誤而異常終止
由于操作系統出現錯誤而導致Java虛擬機進程終止
2.4.3重點說下HotSpot?
SUN的JDK版本從1.3.1開始運用HotSpot虛擬機, 2006年底開源,主要使用C++實現,JNI接口部分用C實現。
HotSpot是較新的Java虛擬機,使用JIT(Just in Time)編譯器,可以大大提高Java運行的性能。
Java原先是把源代碼編譯為字節碼在虛擬機執行,這樣執行速度較慢。而HotSpot將常用的部分代碼編譯為本地(原生,native)代碼,這樣顯著提高了性能。
HotSpot JVM 參數可以分為規則參數(standard options)和非規則參數(non-standard options)。 規則參數相對穩定,在JDK未來的版本里不會有太大的改動。 非規則參數則有因升級JDK而改動的可能。
2.5JVM的架構與知識脈絡圖
2.5.1能否畫出JVM架構圖?
在這里插入圖片描述

在這里插入圖片描述
這個架構可以分成三層看:
最上層:javac編譯器將編譯好的字節碼class文件,通過java 類裝載器執行機制,把對象或class文件存放在 jvm劃分內存區域。
中間層:稱為Runtime Data Area,主要是在Java代碼運行時用于存放數據的,從左至右為方法區(永久代、元數據區)、堆(共享,GC回收對象區域)、棧、程序計數器、寄存器、本地方法棧(私有)。
最下層:解釋器、JIT(just in time)編譯器和 GC(Garbage Collection,垃圾回收器)

2.5.2JVM有哪幾塊知識脈絡?
在這里插入圖片描述

字節碼文件概述

1、字節碼文件是跨平臺的嗎?
Java 虛擬機不和包括 Java 在內的任何語言綁定,它只與“Class 文件”這種特定的二進制文件格式所關聯。
無論使用何種語言進行軟件開發,只要能將源文件編譯為正確的Class文件,那么這種語言就可以在Java虛擬機上執行。可以說,統一而強大的Class文件結構,就是Java虛擬機的基石、橋梁。
在這里插入圖片描述

想要讓一個Java程序正確地運行在JVM中,Java源碼就必須要被編譯為符合JVM規范的字節碼。

https://docs.oracle.com/javase/specs/index.html
所有的JVM全部遵守Java虛擬機規范,也就是說所有的JVM環境都是一樣的,這樣一來字節碼文件可以在各種JVM上運行。

從Java虛擬機的角度看,通過Class文件,可以讓更多的計算機語言支持Java虛擬機平臺。因此,Class文件結構不僅僅是Java虛擬機的執行入口,更是Java生態圈的基礎和核心。
1.1class文件里是什么?
字節碼文件里是什么?
源代碼經過編譯器編譯之后便會生成一個字節碼文件,字節碼是一種二進制的類文件,它的內容是JVM的指令,而不像C、C++經由編譯器直接生成機器碼。

隨著Java平臺的不斷發展,在將來,Class文件的內容也一定會做進一步的擴充,但是其基本的格式和結構不會做重大調整。

1.2能介紹下生成class文件的編譯器嗎?

  1. 從位置上理解
    在這里插入圖片描述

前端編譯器 vs 后端編譯器
半編譯半解釋型語言! javac … java …
2. 前端編譯器的種類
Java源代碼的編譯結果是字節碼,那么肯定需要有一種編譯器能夠將Java源碼編譯為字節碼,承擔這個重要責任的就是配置在path環境變量中的javac編譯器。javac是一種能夠將Java源碼編譯為字節碼的前端編譯器。

HotSpot VM并沒有強制要求前端編譯器只能使用javac來編譯字節碼,其實只要編譯結果符合JVM規范都可以被JVM所識別即可。
在Java的前端編譯器領域,除了javac之外,還有一種被大家經常用到的前端編譯器,那就是內置在Eclipse中的ECJ (Eclipse Compiler for Java)編譯器。和Javac的全量式編譯不同,ECJ是一種增量式編譯器。

在Eclipse中,當開發人員編寫完代碼后,使用“Ctrl+S”快捷鍵時,ECJ編譯器所釆取的編譯方案是把未編譯部分的源碼逐行進行編譯,而非每次都全量編譯。因此ECJ的編譯效率會比javac更加迅速和高效,當然編譯質量和javac相比大致還是一樣的。
ECJ不僅是Eclipse的默認內置前端編譯器,在Tomcat中同樣也是使用ECJ編譯器來編譯jsp文件。由于ECJ編譯器是釆用GPLv2的開源協議進行源代碼公開,所以,大家可以登錄eclipse官網下載ECJ編譯器的源碼進行二次開發。
默認情況下,IntelliJ IDEA 使用 javac 編譯器。(還可以自己設置為AspectJ編譯器 ajc)
3. 前端編譯器的任務
前端編譯器的主要任務就是負責將符合Java語法規范的Java代碼轉換為符合JVM規范的字節碼文件。
1.3javac編譯器的編譯步驟?
javac編譯步驟
javac編譯器在將Java源碼編譯為一個有效的字節碼文件過程中經歷了4個步驟,分別是詞法解析、語法解析、語義解析以及生成字節碼。
在這里插入圖片描述

在這里插入圖片描述

大部分的程序代碼轉換成物理機的目標代碼或虛擬機能執行的指令集之前,都需要經過上圖中的各個步驟。
在這里插入圖片描述
1.4目前前端編譯器的局限性?
前端編譯器并不會直接涉及編譯優化等方面的技術,而是將這些具體優化細節移交給HotSpot的JIT編譯器負責。

復習:AOT(靜態提前編譯器,Ahead Of Time Compiler)

jdk9引入了AOT編譯器(靜態提前編譯器,Ahead Of Time Compiler)

Java 9 引入了實驗性 AOT 編譯工具jaotc。它借助了 Graal 編譯器,將所輸入的 Java 類文件轉換為機器碼,并存放至生成的動態共享庫之中。

所謂 AOT 編譯,是與即時編譯相對立的一個概念。我們知道,即時編譯指的是在程序的運行過程中,將字節碼轉換為可在硬件上直接運行的機器碼,并部署至托管環境中的過程。而 AOT 編譯指的則是,在程序運行之前,便將字節碼轉換為機器碼的過程。
.java -> .class -> .so
最大好處:Java虛擬機加載已經預編譯成二進制庫,可以直接執行。不必等待即時編譯器的預熱,減少Java應用給人帶來“第一次運行慢”的不良體驗。

缺點:
破壞了java“一次編譯,到處運行”,必須為每個不同硬件、OS編譯對應的發行包。
降低了Java鏈接過程的動態性,加載的代碼在編譯期就必須全部已知。
還需要繼續優化中,最初只支持Linux x64 java base
2.1哪些類型對應有Class的對象?
(1)class:
外部類,成員(成員內部類,靜態內部類),局部內部類,匿名內部類
(2)interface:接口
(3)[]:數組
(4)enum:枚舉
(5)annotation:注解@interface
(6)primitive type:基本數據類型
(7)void

@Test
public void test(){Class c1 = Object.class;Class c2 = Comparable.class;Class c3 = String[].class;Class c4 = int[][].class;Class c5 = ElementType.class;Class c6 = Override.class;Class c7 = int.class;Class c8 = void.class;Class c9 = Class.class;int[] a = new int[10];int[] b = new int[100];Class c10 = a.getClass();Class c11 = b.getClass();// 只要元素類型與維度一樣,就是同一個ClassSystem.out.println(c10 == c11);
}

2.2字節碼指令
2.2.1什么是字節碼指令?
什么是字節碼指令(byte code)?

Java虛擬機的指令由一個字節長度的、代表著某種特定操作含義的操作碼(opcode)以及跟隨其后的零至多個代表此操作所需參數的操作數(operand)所構成。虛擬機中許多指令并不包含操作數,只有一個操作碼。
比如:
在這里插入圖片描述
2.3如何解讀class文件?
如何解讀供虛擬機解釋執行的二進制字節碼?
方式一:一個一個二進制的看。這里用到的是Notepad++,需要安裝一個HEX-Editor插件,或者使用Binary Viewer
在這里插入圖片描述

方式二:使用javap指令:jdk自帶的反解析工具
方式三:使用IDEA插件:jclasslib 或jclasslib bytecode viewer客戶端工具。(可視化更好)
在這里插入圖片描述


在這里插入圖片描述

Class文件結構細節

1、class文件結構細節概述

class文件結構概述
Class文件的結構并不是一成不變的,隨著Java虛擬機的不斷發展,總是不可避免地會對Class文件結構做出一些調整,但是其基本結構和框架是非常穩定的。
Class文件的總體結構如下:
魔數
Class文件版本
常量池
訪問標識(或標志)
類索引,父類索引,接口索引集合
字段表集合
方法表集合
屬性表集合
在這里插入圖片描述

在這里插入圖片描述

這是一張Java字節碼總的結構表,我們按照上面的順序逐一進行解讀就可以了。

這里整體給大家講解一遍,這里需要注意聽。否則大家以后也很難有動力,踏實認真的一個字節一個字節的解讀一遍。

2、class文件的魔數是什么?
Magic Number(魔數):class文件的標志
每個 Class 文件開頭的4個字節的無符號整數稱為魔數(Magic Number)

它的唯一作用是確定這個文件是否為一個能被虛擬機接受的有效合法的Class文件。即:魔數是Class文件的標識符。

魔數值固定為0xCAFEBABE。不會改變。

如果一個Class文件不以0xCAFEBABE開頭,虛擬機在進行文件校驗的時候就會直接拋出以下錯誤:
Error: A JNI error has occurred, please check your installation and try again
Exception in thread “main” java.lang.ClassFormatError: Incompatible magic value 1885430635 in class file StringTest

使用魔數而不是擴展名來進行識別主要是基于安全方面的考慮,因為文件擴展名可以隨意地改動。

3、如何確保高版本的JVM可執行低版本的class文件?
不同版本的Java編譯器編譯的Class文件對應的版本是不一樣的。目前,高版本的Java虛擬機可以執行由低版本編譯器生成的Class文件,但是低版本的Java虛擬機不能執行由高版本編譯器生成的Class文件。否則JVM會拋出java.lang.UnsupportedClassVersionError異常。 (向下兼容)
在實際應用中,由于開發環境和生產環境的不同,可能會導致該問題的發生。因此,需要我們在開發時,特別注意開發編譯的JDK版本和生產環境中的JDK版本是否一致。

class文件版本號
緊接著魔數的 4 個字節存儲的是 Class 文件的版本號。同樣也是4個字節。第5個和第6個字節所代表的含義就是編譯的副版本號minor_version,而第7個和第8個字節就是編譯的主版本號major_version。

它們共同構成了class文件的格式版本號。譬如某個 Class 文件的主版本號為 M,副版本號為 m,那么這個Class 文件的格式版本號就確定為 M.m。
版本號和Java編譯器的對應關系如下表:
在這里插入圖片描述

Java 的版本號是從45開始的,JDK 1.1之后的每個JDK大版本發布主版本號向上加1。
虛擬機JDK版本為1.k (k >= 2)時,對應的class文件格式版本號的范圍為45.0 - 44+k.0 (含兩端)。
4、常量池:class文件的基石?作用是?
常量池:存放所有常量
常量池是Class文件中內容最為豐富的區域之一。常量池對于Class文件中的字段和方法解析也有著至關重要的作用。
常量池:可以理解為Class文件之中的資源倉庫,它是Class文件結構中與其他項目關聯最多的數據類型(后面的很多數據類型都會指向此處),也是占用Class文件空間最大的數據項目之一。
常量池表項中,用于存放編譯時期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池中存放。
在這里插入圖片描述

在版本號之后,緊跟著的是常量池的數量,以及若干個常量池表項。
常量池中常量的數量是不固定的,所以在常量池的入口需要放置一項u2類型的無符號數,代表常量池容量計數值(constant_pool_count)。與Java中語言習慣不一樣的是,這個容量計數是從1而不是0開始的。

由上表可見,Class文件使用了一個前置的容量計數器(constant_pool_count)加若干個連續的數據項(constant_pool)的形式來描述常量池內容。我們把這一系列連續常量池數據稱為常量池集合。
4.1、為什么需要常量池計數器?
constant_pool_count (常量池計數器)

由于常量池的數量不固定,時長時短,所以需要放置兩個字節來表示常量池容量計數值。
常量池容量計數值(u2類型):從1開始,表示常量池中有多少項常量。即constant_pool_count=1表示常量池中有0個常量項。
Demo的值為:
在這里插入圖片描述

其值為0x0016,掐指一算,也就是22。
需要注意的是,這實際上只有21項常量。索引為范圍是1-21。為什么呢?
通常我們寫代碼時都是從0開始的,但是這里的常量池卻是從1開始,因為它把第0項常量空出來了。這是為了滿足后面某些指向常量池的索引值的數據在特定情況下需要表達“不引用任何一個常量池項目”的含義,這種情況可用索引值0來表示。

int[] arr = new int[10];
arr[0];
arr[1];ar[10 - 1];

4.2、常量池表
constant_pool [](常量池)

constant_pool是一種表結構,以 1 ~ constant_pool_count - 1為索引。表明了后面有多少個常量項。
常量池主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic References)
它包含了class文件結構及其子結構中引用的所有字符串常量、類或接口名、字段名和其他常量。常量池中的每一項都具備相同的特征。第1個字節作為類型標記,用于確定該項的格式,這個字節稱為tag byte (標記字節、標簽字節)。
在這里插入圖片描述4.2.1、字面量和符號引用
2.1 字面量和符號引用
在對這些常量解讀前,我們需要搞清楚幾個概念。
常量池主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic References)。如下表:
在這里插入圖片描述

String str = "atguigu";
final int NUM = 10;

2.1.1 全限定名
com/atguigu/test/Demo這個就是類的全限定名,僅僅是把包名的".“替換成”/",為了使連續的多個全限定名之間不產生混淆,在使用時最后一般會加入一個“;”表示全限定名結束。
2.1.2 簡單名稱
簡單名稱是指沒有類型和參數修飾的方法或者字段名稱,上面例子中的類的add()方法和num字段的簡單名稱分別是add和num。
2.1.3 描述符
描述符的作用是用來描述字段的數據類型、方法的參數列表(包括數量、類型以及順序)和返回值。根據描述符規則,基本數據類型(byte、char、double、float、int、long、short、boolean)以及代表無返回值的void類型都用一個大寫字符來表示,而對象類型則用字符L加對象的全限定名來表示,詳見下表: (數據類型:基本數據類型 、 引用數據類型)
在這里插入圖片描述

用描述符來描述方法時,按照先參數列表,后返回值的順序描述,參數列表按照參數的嚴格順序放在一組小括號“()”之內。如:
方法java.lang.String toString()的描述符為() Ljava/lang/String;,
方法int abc(int[] x, int y)的描述符為([II) I。
4.2.2談談你對符號引用、直接引用的理解?
Java代碼在進行Javac編譯的時候,并不像C和C++那樣有“連接”這一步驟,而是在虛擬機加載Class文件的時候進行動態鏈接。也就是說,在Class文件中不會保存各個方法、字段的最終內存布局信息,因此這些字段、方法的符號引用不經過運行期轉換的話無法得到真正的內存入口地址,也就無法直接被虛擬機使用。當虛擬機運行時,需要從常量池獲得對應的符號引用,再在類創建時或運行時解析、翻譯到具體的內存地址之中。

虛擬機在加載Class文件時才會進行動態鏈接,也就是說,Class文件中不會保存各個方法和字段的最終內存布局信息,因此,這些字段和方法的符號引用不經過轉換是無法直接被虛擬機使用的。當虛擬機運行時,需要從常量池中獲得對應的符號引用,再在類加載過程中的解析階段將其替換為直接引用,并翻譯到具體的內存地址中。
這里說明下符號引用和直接引用的區別與關聯:
符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標并不一定已經加載到了內存中。
直接引用:直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現的內存布局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那說明引用的目標必定已經存在于內存之中了。
4.2.3常量類型和結構
2.2 常量類型和結構
常量池中每一項常量都是一個表,JDK1.7之后共有14種不同的表結構數據。如下表格所示:
在這里插入圖片描述
在這里插入圖片描述

根據上圖每個類型的描述我們也可以知道每個類型是用來描述常量池中哪些內容(主要是字面量、符號引用)的。比如:CONSTANT_Integer_info是用來描述常量池中字面量信息的,而且只是整型字面量信息。
標志為15、16、18的常量項類型是用來支持動態語言調用的(jdk1.7時才加入的)。
細節說明:
CONSTANT_Class_info 結構用于表示類或接口
CONSTANT_Fieldref_info、CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info結構表示字段、方法和接口方法
CONSTANT_String_info結構用于表示String類型的常量對象
CONSTANT_Integer_info和CONSTANT_Float_info 表示4字節(int和float)的數值常量
CONSTANT_Long_info和CONSTANT_Double_info結構表示8字節(long和double)的數值常量
在class文件的常量池表中,所有的8字節常量均占兩個表成員(項)的空間。如果一個CONSTANT_Long_info或CONSTANT_Double_info結構的項在常量池表中的索引位n,則常量池表中下一個可用項的索引位n+2,此時常量池表中索引為n+1的項仍然有效但必須視為不可用的。
CONSTANT_NameAndType_info結構用于表示字段或方法,但是和之前的3個結構不同,CONSTANT_NameAndType_info結構沒有指明該字段或方法所屬的類或接口。
CONSTANT_Utf8_info用于表示字符常量的值
CONSTANT_MethodHandle_info結構用于表示方法句柄
CONSTANT_MethodType_info結構表示方法類型
CONSTANT_InvokeDynamic_info結構用于表示invokedynamic指令所用到的引導方法(bootstrap method)、引導方法所用到的動態調用名稱(dynamic invocation name)、參數和返回類型,并可以給引導方法傳入一系列稱為靜態參數(static argument)的常量。

解析方式:
一個字節一個字節的解析

使用javap命令解析:javap -verbose Demo.class 或 jclasslib工具會更方便。

==================================
總結1:
這14種表(或者常量項結構)的共同點是:表開始的第一位是一個u1類型的標志位(tag),代表當前這個常量項使用的是哪種表結構,即哪種常量類型。

在常量池列表中,CONSTANT_Utf8_info常量項是一種使用改進過的UTF-8編碼格式來存儲諸如文字字符串、類或者接口的全限定名、字段或者方法的簡單名稱以及描述符等常量字符串信息。

這14種常量項結構還有一個特點是,其中13個常量項占用的字節固定,只有CONSTANT_Utf8_info占用字節不固定,其大小由length決定。為什么呢?因為從常量池存放的內容可知,其存放的是字面量和符號引用,最終這些內容都會是一個字符串,這些字符串的大小是在編寫程序時才確定,比如你定義一個類,類名可以取長取短,所以在沒編譯前,大小不固定,編譯后,通過utf-8編碼,就可以知道其長度。
5、訪問標識
訪問標識(access_flag、訪問標志、訪問標記)

在常量池后,緊跟著訪問標記。該標記使用兩個字節表示,用于識別一些類或者接口層次的訪問信息,包括:這個 Class 是類還是接口;是否定義為 public 類型;是否定義為 abstract 類型;如果是類的話,是否被聲明為 final 等。各種訪問標記如下所示:
在這里插入圖片描述

類的訪問權限通常為 ACC_ 開頭的常量。
每一種類型的表示都是通過設置訪問標記的32位中的特定位來實現的。比如,若是public final的類,則該標記為ACC_PUBLIC | ACC_FINAL。
使用ACC_SUPER可以讓類更準確地定位到父類的方法super.method(),現代編譯器都會設置并且使用這個標記。

補充說明:

  1. 帶有ACC_INTERFACE標志的class文件表示的是接口而不是類,反之則表示的是類而不是接口。
    1)如果一個class文件被設置了 ACC_INTERFACE 標志,那么同時也得設置ACC_ABSTRACT 標志。同時它不能再設置 ACC_FINAL、ACC_SUPER 或 ACC_ENUM 標志。
    2)如果沒有設置ACC_INTERFACE標志,那么這個class文件可以具有上表中除 ACC_ANNOTATION外的其他所有標志。當然,ACC_FINAL和ACC_ABSTRACT這類互斥的標志除外。這兩個標志不得同時設置。

  2. ACC_SUPER標志用于確定類或接口里面的invokespecial指令使用的是哪一種執行語義。針對Java虛擬機指令集的編譯器都應當設置這個標志。對于Java SE 8及后續版本來說,無論class文件中這個標志的實際值是什么,也不管class文件的版本號是多少,Java虛擬機都認為每個class文件均設置了ACC_SUPER標志。
    1)ACC_SUPER標志是為了向后兼容由舊Java編譯器所編譯的代碼而設計的。目前的 ACC_SUPER標志在由JDK 1.0.2之前的編譯器所生成的access_flags中是沒有確定含義的,如果設置了該標志,那么Oracle的Java虛擬機實現會將其忽略。

  3. ACC_SYNTHETIC標志意味著該類或接口是由編譯器生成的,而不是由源代碼生成的。

  4. 注解類型必須設置ACC_ANNOTATION標志。如果設置了 ACC_ANNOTATION標志, 那么也必須設置ACC_INTERFACE標志。

  5. ACC_ENUM標志表明該類或其父類為枚舉類型。

  6. 表中沒有使用的access_flags標志是為未來擴充而預留的,這些預留的標志在編譯器中應該設置為0, Java虛擬機實現也應該忽略它們。
    5、類索引、父類索引、接口索引集合
    在訪問標記后,會指定該類的類別、父類類別以及實現的接口,格式如下:
    在這里插入圖片描述

這三項數據來確定這個類的繼承關系。
類索引用于確定這個類的全限定名
父類索引用于確定這個類的父類的全限定名。由于 Java語言不允許多重繼承,所以父類索引只有一個,除了java.lang.Object 之外,所有的Java類都有父類,因此除了java.lang.Object 外,所有Java類的父類索引都不為 0。
接口索引集合就用來描述這個類實現了哪些接口,這些被實現的接口將按 implements 語句(如果這個類本身是一個接口,則應當是 extends 語句)后的接口順序從左到右排列在接口索引集合中。

1.this_class(類索引)
2字節無符號整數,指向常量池的索引。它提供了類的全限定名,如com/atguigu/java1/Demo。this_class的值必須是對常量池表中某項的一個有效索引值。常量池在這個索引處的成員必須為CONSTANT_Class_info類型結構體,該結構體表示這個class文件所定義的類或接口。

2.super_class (父類索引)
2字節無符號整數,指向常量池的索引。它提供了當前類的父類的全限定名。如果我們沒有繼承任何類,其默認繼承的是java/lang/Object類。同時,由于Java不支持多繼承,所以其父類只有一個。
superclass指向的父類不能是final。

  1. interfaces
    指向常量池索引集合,它提供了一個符號引用到所有已實現的接口
    由于一個類可以實現多個接口,因此需要以數組形式保存多個接口的索引,表示接口的每個索引也是一個指向常量池的CONSTANT_Class (當然這里就必須是接口,而不是類)。

3.1 interfaces_count (接口計數器)
interfaces_count項的值表示當前類或接口的直接超接口數量。

3.2 interfaces
interfaces []中每個成員的值必須是對常量池表中某項的有效索引值,它的長度為 interfaces_count。 每個成員 interfaces[i]必須為 CONSTANT_Class_info結構,其中 0 <= i < interfaces_count。在 interfaces[]中,各成員所表示的接口順序和對應的源代碼中給定的接口順序(從左至右)一樣,即 interfaces[0]對應的是源代碼中最左邊的接口。
6、字段表集合
字段表集合

fields
用于描述接口或類中聲明的變量。字段(field)包括類級變量以及實例級變量,但是不包括方法內部、代碼塊內部聲明的局部變量。(local variables)
字段叫什么名字、字段被定義為什么數據類型,這些都是無法固定的,只能引用常量池中的常量來描述。
它指向常量池索引集合,它描述了每個字段的完整信息。比如字段的標識符、訪問修飾符(public、private或protected)、是類變量還是實例變量(static修飾符)、是否是常量(final修飾符)等。

注意事項:
字段表集合中不會列出從父類或者實現的接口中繼承而來的字段,但有可能列出原本Java代碼之中不存在的字段。譬如在內部類中為了保持對外部類的訪問性,會自動添加指向外部類實例的字段。
在Java語言中字段是無法重載的,兩個字段的數據類型、修飾符不管是否相同,都必須使用不一樣的名稱,但是對于字節碼來講,如果兩個字段的描述符不一致,那字段重名就是合法的。
6.1字段計數器
6.1 fields_count (字段計數器)
fields_count的值表示當前class文件fields表的成員個數。使用兩個字節來表示。
fields表中每個成員都是一個field_info結構,用于表示該類或接口所聲明的所有類字段或者實例字段,不包括方法內部聲明的變量,也不包括從父類或父接口繼承的那些字段。
6.2字段表
6.2 fields [](字段表)
fields表中的每個成員都必須是一個fields_info結構的數據項,用于表示當前類或接口中某個字段的完整描述。
一個字段的信息包括如下這些信息。這些信息中,各個修飾符都是布爾值,要么有,要么沒有。

作用域(public、private、protected修飾符)
是實例變量還是類變量(static修飾符)
可變性(final)
并發可見性(volatile修飾符,是否強制從主內存讀寫)
可否序列化(transient修飾符)
字段數據類型(基本數據類型、對象、數組)
字段名稱
字段表結構
字段表作為一個表,同樣有他自己的結構:
在這里插入圖片描述

6.2.1 字段表訪問標識
我們知道,一個字段可以被各種關鍵字去修飾,比如:作用域修飾符(public、private、protected)、static修飾符、final修飾符、volatile修飾符等等。因此,其可像類的訪問標志那樣,使用一些標志來標記字段。字段的訪問標志有如下這些:
在這里插入圖片描述

6.2.2 字段名索引
根據字段名索引的值,查詢常量池中的指定索引項即可。

6.2.3 描述符索引
描述符的作用是用來描述字段的數據類型、方法的參數列表(包括數量、類型以及順序)和返回值。根據描述符規則,基本數據類型(byte,char,double,float,int,long,short,boolean)及代表無返回值的void類型都用一個大寫字符來表示,而對象則用字符L加對象的全限定名來表示,如下所示:

在這里插入圖片描述

6.2.4 屬性表集合
一個字段還可能擁有一些屬性,用于存儲更多的額外信息。比如初始化值、一些注釋信息等。屬性個數存放在attribute_count中,屬性具體內容存放在attributes數組中。
以常量屬性為例,結構為:
ConstantValue_attribute{
u2 attribute_name_index;
u4 attribute_length;
u2 constantvalue_index;
}
說明:對于常量屬性而言,attribute_length值恒為2。
7、方法表集合
方法表集合

methods:指向常量池索引集合,它完整描述了每個方法的簽名。

在字節碼文件中,每一個method_info項都對應著一個類或者接口中的方法信息。比如方法的訪問修飾符(public、private或protected),方法的返回值類型以及方法的參數信息等。
如果這個方法不是抽象的或者不是native的,那么字節碼中會體現出來。
一方面,methods表只描述當前類或接口中聲明的方法,不包括從父類或父接口繼承的方法。另一方面,methods表有可能會出現由編譯器自動添加的方法,最典型的便是編譯器產生的方法信息(比如:類(接口)初始化方法()和實例初始化方法())。

使用注意事項:
在Java語言中,要重載(Overload)一個方法,除了要與原方法具有相同的簡單名稱之外,還要求必須擁有一個與原方法不同的特征簽名,特征簽名就是一個方法中各個參數在常量池中的字段符號引用的集合,也就是因為返回值不會包含在特征簽名之中,因此Java語言里無法僅僅依靠返回值的不同來對一個已有方法進行重載。但在Class文件格式中,特征簽名的范圍更大一些,只要描述符不是完全一致的兩個方法就可以共存。也就是說,如果兩個方法有相同的名稱和特征簽名,但返回值不同,那么也是可以合法共存于同一個class文件中。
也就是說,盡管Java語法規范并不允許在一個類或者接口中聲明多個方法簽名相同的方法,但是和Java語法規范相反,字節碼文件中卻恰恰允許存放多個方法簽名相同的方法,唯一的條件就是這些方法之間的返回值不能相同。

7.1方法計數器

7.1 methods_count (方法計數器)

methods_count的值表示當前class文件methods表的成員個數。使用兩個字節來表示。
methods 表中每個成員都是一個method_info結構。
7.2方法表
7.2 methods [](方法表)
methods表中的每個成員都必須是一個method_info結構,用于表示當前類或接口中某個方法的完整描述。如果某個method_info結構的access_flags項既沒有設置 ACC_NATIVE 標志也沒有設置ACC_ABSTRACT標志,那么該結構中也應包含實現這個方法所用的Java虛擬機指令。
method_info結構可以表示類和接口中定義的所有方法,包括實例方法、類方法、實例初始化方法和類或接口初始化方法
方法表的結構實際跟字段表是一樣的,方法表結構如下:
在這里插入圖片描述

7.2.1 方法表訪問標志
跟字段表一樣,方法表也有訪問標志,而且他們的標志有部分相同,部分則不同,方法表的具體訪問標志如下:

在這里插入圖片描述

7.2.2 方法名索引
根據方法名索引的值,查詢常量池中的指定索引項即可。

7.2.3 描述符索引
根據描述符索引的值,查詢常量池中的指定索引項即可。

7.2.4 屬性計數器
根據屬性計數器的值,判斷出方法中屬性的個數。

7.2.5 屬性表
屬性計數器后面就是屬性表了,由于只有一個屬性,所以這里也只有一個屬性表。
由于涉及到屬性表,這里簡單說下,下一節會詳細介紹。
8、屬性表集合
屬性表集合(attributes)

方法表集合之后的屬性表集合,指的是class文件所攜帶的輔助信息,比如該 class 文件的源文件的名稱。以及任何帶有RetentionPolicy.CLASS 或者RetentionPolicy.RUNTIME的注解。這類信息通常被用于Java虛擬機的驗證和運行,以及Java程序的調試,一般無須深入了解。

此外,字段表、方法表都可以有自己的屬性表。用于描述某些場景專有的信息。

屬性表集合的限制沒有那么嚴格,不再要求各個屬性表具有嚴格的順序,并且只要不與已有的屬性名重復,任何人實現的編譯器都可以向屬性表中寫入自己定義的屬性信息,但Java虛擬機運行時會忽略掉它不認識的屬性。
8.1屬性計數器
8.1 attributes_count (屬性計數器)
attributes_count的值表示當前class文件屬性表的成員個數。屬性表中每一項都是一個attribute_info結構。
8.2屬性表
8.2 attributes [](屬性表)
屬性表的每個項的值必須是attribute_info結構。屬性表的結構比較靈活,各種不同的屬性只要滿足以下結構即可。

8.2.1 屬性的通用格式
在這里插入圖片描述

即只需說明屬性的名稱以及占用位數的長度即可,屬性表具體的結構可以去自定義。

8.2.2 屬性類型
屬性表實際上可以有很多類型,上面看到的Code屬性只是其中一種,Java8里面定義了23種屬性。
下面這些是虛擬機中預定義的屬性:
在這里插入圖片描述
在這里插入圖片描述
在這里插入圖片描述

或(查看官網)
在這里插入圖片描述
8.2.1ConstantValue 屬性
① ConstantValue 屬性
ConstantValue 屬性表示一個常量字段的值。位于 field_info結構的屬性表中。
ConstantValue_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 constantvalue_index;//字段值在常量池中的索引,常量池在該索引處的項給出該屬性表示的常量值。(例如,值是long型的,在常量池中便是CONSTANT_Long)
}
8.2.2Deprecated 屬性
② Deprecated 屬性
Deprecated 屬性是在 JDK 1.1 為了支持注釋中的關鍵詞@deprecated 而引入的。
Deprecated_attribute {
u2 attribute_name_index;
u4 attribute_length;
}
8.2.3Code 屬性
③ Code 屬性
Code屬性就是存放方法體里面的代碼。但是,并非所有方法表都有Code屬性。像接口或者抽象方法,他們沒有具體的方法體,因此也就不會有Code屬性了。
Code屬性表的結構,如下圖:
在這里插入圖片描述

可以看到:Code屬性表的前兩項跟屬性表是一致的,即Code屬性表遵循屬性表的結構,后面那些則是他自定義的結構。
8.2.4 InnerClasses 屬性
④ InnerClasses 屬性
為了方便說明特別定義一個表示類或接口的 Class 格式為 C。如果 C 的常量池中包含某個CONSTANT_Class_info 成員,且這個成員所表示的類或接口不屬于任何一個包,那么 C 的ClassFile 結構的屬性表中就必須含有對應的 InnerClasses 屬性。InnerClasses 屬性是在 JDK 1.1 中為了支持內部類和內部接口而引入的,位于 ClassFile結構的屬性表。
8.2.5LineNumberTable 屬性
⑤ LineNumberTable 屬性
LineNumberTable 屬性是可選變長屬性,位于 Code結構的屬性表。
LineNumberTable屬性是用來描述Java源碼行號與字節碼行號之間的對應關系。這個屬性可以用來在調試的時候定位代碼執行的行數。
start_pc,即字節碼行號;line_number,即Java源代碼行號。
在 Code 屬性的屬性表中,LineNumberTable 屬性可以按照任意順序出現,此外,多個 LineNumberTable屬性可以共同表示一個行號在源文件中表示的內容,即 LineNumberTable 屬性不需要與源文件的行一一對應。

LineNumberTable屬性表結構:
在這里插入圖片描述
8.2.6LocalVariableTable 屬性
⑥ LocalVariableTable 屬性
LocalVariableTable 是可選變長屬性,位于 Code屬性的屬性表中。它被調試器用于確定方法在執行過程中局部變量的信息。在 Code 屬性的屬性表中,LocalVariableTable 屬性可以按照任意順序出現。 Code 屬性中的每個局部變量最多只能有一個 LocalVariableTable 屬性。
start pc + length表示這個變量在字節碼中的生命周期起始和結束的偏移位置(this生命周期從頭0到結尾)
index就是這個變量在局部變量表中的槽位(槽位可復用)
name就是變量名稱
Descriptor表示局部變量類型描述
LocalVariableTable 屬性表結構:
在這里插入圖片描述
8.2.7 Signature 屬性
⑦ Signature 屬性
Signature 屬性是可選的定長屬性,位于 ClassFile, field_info
或 method_info結構的屬性表中。在 Java 語言中,任何類、 接口、 初始化方法或成員的泛型簽名如果包含了類型變量( Type Variables) 或參數化類型( Parameterized Types),則 Signature 屬性會為它記錄泛型簽名信息。
8.2.8SourceFile屬性
⑧ SourceFile屬性
SourceFile屬性結構
在這里插入圖片描述

可以看到,其長度總是固定的8個字節。
小結:
在這里插入圖片描述

oracle官方的反解析工具:javap=======================略

字節碼指令集與解析概述

1、字節碼與數據類型
在Java虛擬機的指令集中,大多數的指令都包含了其操作所對應的數據類型信息。例如,iload指令用于從局部變量表中加載int型的數據到操作數棧中,而fload指令加載的則是float類型的數據。

對于大部分與數據類型相關的字節碼指令,它們的操作碼助記符中都有特殊的字符來表明專門為哪種數據類型服務:
i代表對int類型的數據操作
l代表long類型的數據操作
s代表short類型的數據操作
b代表byte類型的數據操作
c代表char類型的數據操作
f代表float類型的數據操作
d代表double類型的數據操作

也有一些指令的助記符中沒有明確地指明操作類型的字母,如arraylength指令,它沒有代表數據類型的特殊字符,但操作數永遠只能是一個數組類型的對象。

還有另外一些指令,如無條件跳轉指令goto則是與數據類型無關的。

大部分的指令都沒有支持整數類型byte、char和short,甚至沒有任何指令支持boolean類型。編譯器會在編譯期或運行期將byte和short類型的數據帶符號擴展(Sign-Extend)為相應的int類型數據,將boolean和char類型數據零位擴展(Zero-Extend)為相應的int類型數據。與之類似,在處理boolean、byte、short和char類型的數組時,也會轉換為使用對應的int類型的字節碼指令來處理。因此,大多數對于boolean、byte、short和char類型數據的操作,實際上都是使用相應的int類型作為運算類型。

byte b1 = 12;
short s1 = 10;
int i = b1 + s1;
2、指令分類
由于完全介紹和學習這些指令需要花費大量時間。為了讓大家能夠更快地熟悉和了解這些基本指令,這里將JVM中的字節碼指令集按用途大致分成 9 類。
加載與存儲指令
算術指令
類型轉換指令
對象的創建與訪問指令
方法調用與返回指令
操作數棧管理指令
控制轉移指令
異常處理指令
同步控制指令

(說在前面)在做值相關操作時:
一個指令,可以從局部變量表、常量池、堆中對象、方法調用、系統調用中等取得數據,這些數據(可能是值,可能是對象的引用)被壓入操作數棧。
一個指令,也可以從操作數棧中取出一到多個值(pop多次),完成賦值、加減乘除、方法傳參、系統調用等等操作。

字節碼指令

1、加載與存儲指令
加載和存儲指令
1、作用
加載和存儲指令用于將數據從棧幀的局部變量表和操作數棧之間來回傳遞。

2、常用指令
1、【局部變量壓棧指令】將一個局部變量加載到操作數棧:xload、xload_(其中x為i、l、f、d、a,n 為 0 到 3)
2、【常量入棧指令】將一個常量加載到操作數棧:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_、lconst_、fconst_、dconst_
3、【出棧裝入局部變量表指令】將一個數值從操作數棧存儲到局部變量表:xstore、xstore_(其中x為i、l、f、d、a,n 為 0 到 3)
4、擴充局部變量表的訪問索引的指令:wide。

上面所列舉的指令助記符中,有一部分是以尖括號結尾的(例如iload_)。這些指令助記符實際上代表了一組指令(例如 iload_代表了iload_0、iload_1、iload_2和iload_3這幾個指令)。這幾組指令都是某個帶有一個操作數的通用指令(例如 iload)的特殊形式,對于這若干組特殊指令來說,它們表面上沒有操作數,不需要進行取操作數的動作,但操作數都隱含在指令中。

比如:
iload_0:將局部變量表中索引為0位置上的數據壓入操作數棧中。
iload 0:將局部變量表中索引為0位置上的數據壓入操作數棧中。
iload 4:將局部變量表中索引為4位置上的數據壓入操作數棧中。

除此之外,它們的語義與原生的通用指令完全一致(例如 iload_0的語義與操作數為0時的 iload 指令語義完全一致)。在尖括號之間的字母指定了指令隱含操作數的數據類型,代表非負的整數, 代表是int類型數據, 代表long類型,代表float類型, 代表double類型。

操作byte、char、short和boolean類型數據時,經常用int類型的指令來表示。
1.1操作數棧與局部變量表
1、操作數棧(Operand Stacks)
我們知道,Java字節碼是Java虛擬機所使用的指令集。因此,它與Java虛擬機基于棧的計算模型是密不可分的。
在解釋執行過程中,每當為Java方法分配棧楨時,Java虛擬機往往需要開辟一塊額外的空間作為操作數棧,來存放計算的操作數以及返回結果。
具體來說便是:執行每一條指令之前,Java 虛擬機要求該指令的操作數已被壓入操作數棧中。在執行指令時,Java 虛擬機會將該指令所需的操作數彈出,并且將指令的結果重新壓入棧中。
在這里插入圖片描述

以加法指令 iadd 為例。假設在執行該指令前,棧頂的兩個元素分別為 int 值 1 和 int 值 2,那么 iadd 指令將彈出這兩個 int,并將求得的和 int 值 3 壓入棧中。
在這里插入圖片描述

由于 iadd 指令只消耗棧頂的兩個元素,因此,對于離棧頂距離為 2 的元素,即圖中的問號,iadd 指令并不關心它是否存在,更加不會對其進行修改。

2、局部變量表(Local Variables)
Java 方法棧楨的另外一個重要組成部分則是局部變量區,字節碼程序可以將計算的結果緩存在局部變量區之中。
實際上,Java 虛擬機將局部變量區當成一個數組,依次存放 this 指針(僅非靜態方法),所傳入的參數,以及字節碼中的局部變量。
和操作數棧一樣,long 類型以及 double 類型的值將占據兩個單元,其余類型僅占據一個單元。
在這里插入圖片描述

舉例:
public void foo(long l, float f) {
{
int i = 0;
}
{
String s = “Hello, World”;
}
}
對應的圖示:
在這里插入圖片描述

在棧幀中,與性能調優關系最為密切的部分就是局部變量表。局部變量表中的變量也是重要的垃圾回收根節點,只要被局部變量表中直接或間接引用的對象都不會被回收。

在方法執行時,虛擬機使用局部變量表完成方法的傳遞。
1.21局部變量壓棧指令
局部變量壓棧指令將給定的局部變量表中的數據壓入操作數棧。

這類指令大體可以分為:
> xload_ (x為i、l、f、d、a,n為 0 到 3)
> xload (x為i、l、f、d、a)
說明:在這里,x的取值表示數據類型。

指令xload_n表示將第n個局部變量壓入操作數棧,比如iload_1、fload_0、aload_0等指令。其中aload_n表示將一個對象引用壓棧。
指令xload通過指定參數的形式,把局部變量壓入操作數棧,當使用這個命令時,表示局部變量的數量可能超過了4個,比如指令iload、fload等。
1.32常量入棧指令
常量入棧指令的功能是將常數壓入操作數棧,根據數據類型和入棧內容的不同,又可以分為const系列、push系列和ldc指令。

指令const系列:用于對特定的常量入棧,入棧的常量隱含在指令本身里。指令有:iconst_ (i從-1到5)、lconst_ (l從0到1)、fconst_ (f從0到2)、dconst_ (d從0到1)、aconst_null。
比如,
iconst_m1將-1壓入操作數棧;
iconst_x(x為0到5)將x壓入棧:
lconst_0、lconst_1分別將長整數0和1壓入棧;
fconst_0、fconst_1、fconst_2分別將浮點數0、1、2壓入棧;
dconst_0和dconst_1分別將double型0和1壓入棧。
aconst_null將null壓入操作數棧;
從指令的命名上不難找出規律,指令助記符的第一個字符總是喜歡表示數據類型,i表示整數,l表示長整數,f表示浮點數,d表示雙精度浮點,習慣上用a表示對象引用。如果指令隱含操作的參數,會以下劃線形式給出。
int i = 3; iconst_3
int j = 6; iconst 6? bipush 6?
int k = 32768 ldc ?
指令push系列:主要包括bipush和sipush。它們的區別在于接收數據類型的不同,bipush接收8位整數作為參數,sipush接收16位整數,它們都將參數壓入棧。

指令ldc系列:如果以上指令都不能滿足需求,那么可以使用萬能的ldc指令,它可以接收一個8位的參數,該參數指向常量池中的int、float或者String的索引,將指定的內容壓入堆棧。
類似的還有ldc_w,它接收兩個8位參數,能支持的索引范圍大于ldc。
如果要壓入的元素是long或者double類型的,則使用ldc2_w指令,使用方式都是類似的。

總結如下:
在這里插入圖片描述
1.4出棧裝入局部變量表指令
出棧裝入局部變量表指令用于將操作數棧中棧頂元素彈岀后,裝入局部變量表的指定位置,用于給局部變量賦值。

這類指令主要以store的形式存在,比如xstore (x為i、l、f、d、a)、 xstore_n (x 為 i、l、f、d、a, n 為 0 至 3)。
其中,指令istore_n將從操作數棧中彈出一個整數,并把它賦值給局部變量索引n位置。
指令xstore由于沒有隱含參數信息,故需要提供一個byte類型的參數類指定目標局部變量表的位置。

說明:
一般說來,類似像store這樣的命令需要帶一個參數,用來指明將彈出的元素放在局部變量表的第幾個位置。但是,為了盡可能壓縮指令大小,使用專門的istore_1指令表示將彈出的元素放置在局部變量表第1個位置。類似的還有istore_0、istore_2、istore_3,它們分別表示從操作數棧頂彈出一個元素,存放在局部變量表第0、2、3個位置。

由于局部變量表前幾個位置總是非常常用,因此這種做法雖然增加了指令數量,但是可以大大壓縮生成的字節碼的體積。如果局部變量表很大,需要存儲的槽位大于3,那么可以使用istore指令,外加一個參數,用來表示需要存放的槽位位置。
2、算術指令
1、作用:
算術指令用于對兩個操作數棧上的值進行某種特定運算,并把結果重新壓入操作數棧。
2、分類:
大體上算術指令可以分為兩種:對整型數據進行運算的指令與對浮點類型數據進行運算的指令。

3、byte、short、char和boolean類型說明
在每一大類中,都有針對Java虛擬機具體數據類型的專用算術指令。但沒有直接支持byte、short、char和boolean類型的算術指令,對于這些數據的運算,都使用int類型的指令來處理。此外,在處理boolean、byte、short和char類型的數組時,也會轉換為使用對應的int類型的字節碼指令來處理。
在這里插入圖片描述

4、運算時的溢出
數據運算可能會導致溢出,例如兩個很大的正整數相加,結果可能是一個負數。其實Java虛擬機規范并無明確規定過整型數據溢出的具體結果,僅規定了在處理整型數據時,只有除法指令以及求余指令中當出現除數為0時會導致虛擬機拋出異常ArithmeticException。

5、運算模式
向最接近數舍入模式:JVM要求在進行浮點數計算時,所有的運算結果都必須舍入到適當的精度,非精確結果必須舍入為可被表示的最接近的精確值,如果有兩種可表示的形式與該值一樣接近,將優先選擇最低有效位為零的;
向零舍入模式:將浮點數轉換為整數時,采用該模式,該模式將在目標數值類型中選擇一個最接近但是不大于原值的數字作為最精確的舍入結果;

6、NaN值使用
當一個操作產生溢出時,將會使用有符號的無窮大表示,如果某個操作結果沒有明確的數學定義的話,將會使用 NaN值來表示。而且所有使用NaN值作為操作數的算術操作,結果都會返回 NaN;
2.1所有算術指令
所有的算術指令包括:
加法指令:iadd、ladd、fadd、dadd
減法指令:isub、lsub、fsub、dsub
乘法指令:imul、lmul、 fmul、dmul
除法指令:idiv、ldiv、fdiv、ddiv
求余指令:irem、lrem、frem、drem //remainder:余數
取反指令:ineg、lneg、fneg、dneg //negation:取反
自增指令:iinc
位運算指令,又可分為:
位移指令:ishl、ishr、iushr、lshl、lshr、lushr
按位或指令:ior、lor
按位與指令:iand、land
按位異或指令:ixor、lxor

比較指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
舉例:

public static int bar(int i) {return ((i + 1) - 2) * 3 / 4;
}

字節碼指令對應的圖示:
在這里插入圖片描述
在這里插入圖片描述
2.2比較指令的說明
比較指令的說明
比較指令的作用是比較棧頂兩個元素的大小,并將比較結果入棧。
比較指令有:dcmpg, dcmpl、fcmpg、fcmpl、lcmp。
與前面講解的指令類似,首字符d表示double類型,f表示float,l表示long。
對于double和float類型的數字,由于NaN的存在,各有兩個版本的比較指令。以float為例,有fcmpg和fcmpl兩個指令,它們的區別在于在數字比較時,若遇到NaN值,處理結果不同。
指令dcmpl和dcmpg也是類似的,根據其命名可以推測其含義,在此不再贅述。
指令lcmp針對long型整數,由于long型整數沒有NaN值,故無需準備兩套指令。

舉例:
指令fcmpg和fcmpl都從棧中彈出兩個操作數,并將它們做比較,設棧頂的元素為v2,棧頂順位第2位的元素為v1,若v1=v2,則壓入0;若v1>v2則壓入1;若v1<v2則壓入-1。
兩個指令的不同之處在于,如果遇到NaN值,fcmpg會壓入1,而fcmpl會壓入-1。

數值類型的數據,才可以談大小! (byte\short\char\int;long\float\double)
boolean、引用數據類型不能比較大小。
3、類型轉換指令
3.1寬化類型轉換
寬化類型轉換(Widening Numeric Conversions)
1.轉換規則:
Java虛擬機直接支持以下數值的寬化類型轉換(widening numeric conversion,小范圍類型向大范圍類型的安全轉換)。也就是說,并不需要指令執行,包括:
從int類型到long、float或者double類型。對應的指令為:i2l、i2f、i2d
從long類型到float、double類型。對應的指令為:l2f、l2d
從float類型到double類型。對應的指令為:f2d

簡化為:int --> long --> float --> double

  1. 精度損失問題
    2.1 寬化類型轉換是不會因為超過目標類型最大值而丟失信息的,例如,從int轉換到 long,或者從int轉換到double,都不會丟失任何信息,轉換前后的值是精確相等的。

2.2 從int、long類型數值轉換到float,或者long類型數值轉換到double時,將可能發生精度丟失——可能丟失掉幾個最低有效位上的值,轉換后的浮點數值是根據IEEE754最接近舍入模式所得到的正確整數值。

盡管寬化類型轉換實際上是可能發生精度丟失的,但是這種轉換永遠不會導致Java虛擬機拋出運行時異常。

3.補充說明
從byte、char和short類型到int類型的寬化類型轉換實際上是不存在的。對于byte類型轉為int,虛擬機并沒有做實質性的轉化處理,只是簡單地通過操作數棧交換了兩個數據。而將byte轉為long時,使用的是i2l,可以看到在內部byte在這里已經等同于int類型處理,類似的還有short類型,這種處理方式有兩個特點:
一方面可以減少實際的數據類型,如果為short和byte都準備一套指令,那么指令的數量就會大增,而虛擬機目前的設計上,只愿意使用一個字節表示指令,因此指令總數不能超過256個,為了節省指令資源,將short和byte當做int處理也在情理之中。
另一方面,由于局部變量表中的槽位固定為32位,無論是byte或者short存入局部變量表,都會占用32位空間。從這個角度說,也沒有必要特意區分這幾種數據類型。
3.2窄化類型轉換
窄化類型轉換(Narrowing Numeric Conversion)

1.轉換規則
Java虛擬機也直接支持以下窄化類型轉換:
從int類型至byte、short或者char類型。對應的指令有:i2b、i2s、i2c
從long類型到int類型。對應的指令有:l2i
從float類型到int或者long類型。對應的指令有:f2i、f2l
從double類型到int、long或者float類型。對應的指令有:d2i、d2l、d2f

  1. 精度損失問題
    窄化類型轉換可能會導致轉換結果具備不同的正負號、不同的數量級,因此,轉換過程很可能會導致數值丟失精度。

盡管數據類型窄化轉換可能會發生上限溢出、下限溢出和精度丟失等情況,但是Java虛擬機規范中明確規定數值類型的窄化轉換指令永遠不可能導致虛擬機拋出運行時異常

  1. 補充說明
    3.1 當將一個浮點值窄化轉換為整數類型T(T限于int或long類型之一)的時候,將遵循以下轉換規則:
    如果浮點值是NaN,那轉換結果就是int或long類型的0。
    如果浮點值不是無窮大的話,浮點值使用IEEE 754的向零舍入模式取整,獲得整數值v,如果v在目標類型T(int或long)的表示范圍之內,那轉換結果就是v。否則,將根據v的符號,轉換為T所能表示的最大或者最小正數

3.2 當將一個 double 類型窄化轉換為 float 類型時,將遵循以下轉換規則:
通過向最接近數舍入模式舍入一個可以使用float類型表示的數字。最后結果根據下面這3條規則判斷:
如果轉換結果的絕對值太小而無法使用 float來表示,將返回 float類型的正負零。
如果轉換結果的絕對值太大而無法使用 float來表示,將返回 float類型的正負無窮大。
對于double 類型的 NaN值將按規定轉換為 float類型的 NaN值。
4、對象的創建與訪問指令
Java是面向對象的程序設計語言,虛擬機平臺從字節碼層面就對面向對象做了深層次的支持。有一系列指令專門用于對象操作,可進一步細分為創建指令、字段訪問指令、數組操作指令、類型檢查指令。
4.1創建指令
一、創建指令
雖然類實例和數組都是對象,但Java虛擬機對類實例和數組的創建與操作使用了不同的字節碼指令:

  1. 創建類實例的指令:
    創建類實例的指令:new
    它接收一個操作數,為指向常量池的索引,表示要創建的類型,執行完成后,將對象的引用壓入棧。
  2. 創建數組的指令:
    創建數組的指令:newarray、anewarray、multianewarray。
    newarray:創建基本類型數組
    anewarray:創建引用類型數組
    multianewarray:創建多維數組

上述創建指令可以用于創建對象或者數組,由于對象和數組在Java中的廣泛使用,這些指令的使用頻率也非常高。
4.2字段訪問指令
二、字段訪問指令
對象創建后,就可以通過對象訪問指令獲取對象實例或數組實例中的字段或者數組元素。

訪問類字段(static字段,或者稱為類變量)的指令:getstatic、putstatic
訪問類實例字段(非static字段,或者稱為實例變量)的指令:getfield、putfield

舉例:
以getstatic指令為例,它含有一個操作數,為指向常量池的Fieldref索引,它的作用就是獲取Fieldref指定的對象或者值,并將其壓入操作數棧。

public void sayHello() {System.out.println("hello");
}

對應的字節碼指令:

0 getstatic #8 <java/lang/System.out>
3 ldc #9 <hello>
5 invokevirtual #10 <java/io/PrintStream.println>
8 return

圖示:
在這里插入圖片描述

在這里插入圖片描述
在這里插入圖片描述
4.3數組操作指令
三、數組操作指令
數組操作指令主要有:xastore和xaload指令。具體為:
把一個數組元素加載到操作數棧的指令:baload、caload、saload、iaload、laload、faload、daload、aaload

將一個操作數棧的值存儲到數組元素中的指令:bastore、 castore、 sastore、iastore、 lastore、fastore、dastore、aastore
即:
在這里插入圖片描述

取數組長度的指令:arraylength
該指令彈出棧頂的數組元素,獲取數組的長度,將長度壓入棧。

  1. 說明
    指令xaload表示將數組的元素壓棧,比如saload、caload分別表示壓入short數組和char數組。指令xaload在執行時,要求操作數中棧頂元素為數組索引i,棧頂順位第2個元素為數組引用a,該指令會彈岀棧頂這兩個元素,并將a[i]重新壓入棧。
    xastore則專門針對數組操作,以iastore為例,它用于給一個int數組的給定索引賦值。在iastore執行前,操作數棧頂需要以此準備3個元素:值、索引、數組引用,iastore會彈出這3個值,并將值賦給數組中指定索引的位置。
    4.4類型檢查指令
    四、類型檢查指令

檢查類實例或數組類型的指令:instanceof、checkcast。
指令checkcast用于檢查類型強制轉換是否可以進行。如果可以進行,那么checkcast指令不會改變操作數棧,否則它會拋出ClassCastException異常。
指令instanceof用來判斷給定對象是否是某一個類的實例,它會將判斷結果壓入操作數棧。
5、方法調用與返回指令
5.1方法調用指令
1.方法調用指令:invokevirtual、invokeinterface、invokespecial、invokestatic 、invokedynamic

以下5條指令用于方法調用:
invokevirtual指令用于調用對象的實例方法,根據對象的實際類型進行分派(虛方法分派),支持多態。這也是Java語言中最常見的方法分派方式。
invokeinterface指令用于調用接口方法,它會在運行時搜索由特定對象所實現的這個接口方法,并找出適合的方法進行調用。
invokespecial指令用于調用一些需要特殊處理的實例方法,包括實例初始化方法(構造器)、私有方法和父類方法。這些方法都是靜態類型綁定的,不會在調用時進行動態派發。
invokestatic指令用于調用命名類中的類方法(static方法)。這是靜態綁定的。
invokedynamic:調用動態綁定的方法,這個是JDK 1.7后新加入的指令。用于在運行時動態解析出調用點限定符所引用的方法,并執行該方法。前面4條調用指令的分派邏輯都固化在 java 虛擬機內部,而 invokedynamic指令的分派邏輯是由用戶所設定的引導方法決定的。
舉例:

package com.atguigu.java1;import java.util.Comparator;/*** @author shkstart* @create 14:47*/
public class MethodInvokeTest {public static void main(String[] args) {Father f = new Father();Son s = new Son();System.out.println(f.getInfo());System.out.println(s.getInfo());Comparator<Integer> comparator = new Comparator<Integer>() {@Overridepublic int compare(Integer o1, Integer o2) {return 0;}};//comparator = Integer::compare;comparator.compare(12,32);}
}class Father {private String info = "atguigu";public void setInfo(String info) {this.info = info;}public String getInfo() {return info;}
}class Son extends Father {private String info = "尚硅谷";public void setInfo(String info) {this.info = info;}public String getInfo() {return info;}}

5.2方法返回指令
2.方法返回指令:
方法調用結束前,需要進行返回。方法返回指令是根據返回值的類型區分的。
包括ireturn(當返回值是 boolean、byte、char、short和int 類型時使用)、lreturn、freturn、dreturn和areturn
另外還有一條return 指令供聲明為 void的方法、實例初始化方法以及類和接口的類初始化方法使用。
在這里插入圖片描述

舉例:
通過ireturn指令,將當前函數操作數棧的頂層元素彈出,并將這個元素壓入調用者函數的操作數棧中(因為調用者非常關心函數的返回值),所有在當前函數操作數棧中的其他元素都會被丟棄。

如果當前返回的是synchronized方法,那么還會執行一個隱含的monitorexit指令,退出臨界區。

最后,會丟棄當前方法的整個幀,恢復調用者的幀,并將控制權轉交給調用者。
在這里插入圖片描述

對應的代碼:

public int methodReturn(){int i = 500;int j = 200;int k = 50;return (i + j) / k;
}

6、操作數棧管理指令
操作數棧管理指令

如同操作一個普通數據結構中的堆棧那樣,JVM提供的操作數棧管理指令,可以用于直接操作操作數棧的指令。

這類指令包括如下內容:
將一個或兩個元素從棧頂彈出,并且直接廢棄: pop,pop2;
復制棧頂一個或兩個數值并將復制值或雙份的復制值重新壓入棧頂: dup, dup2, dup_x1, dup2_x1, dup_x2, dup2_x2;
將棧最頂端的兩個Slot數值位置交換: swap。Java虛擬機沒有提供交換兩個64位數據類型(long、double)數值的指令。
指令nop,是一個非常特殊的指令,它的字節碼為0x00。和匯編語言中的nop一樣,它表示什么都不做。這條指令一般可用于調試、占位等。

這些指令屬于通用型,對棧的壓入或者彈出無需指明數據類型。

說明:
不帶_x的指令是復制棧頂數據并壓入棧頂。包括兩個指令,dup和dup2。dup的系數代表要復制的Slot個數。
dup開頭的指令用于復制1個Slot的數據。例如1個int或1個reference類型數據
dup2開頭的指令用于復制2個Slot的數據。例如1個long,或2個int,或1個int+1個float類型數據
帶_x的指令是復制棧頂數據并插入棧頂以下的某個位置。共有4個指令,dup_x1, dup2_x1, dup_x2, dup2_x2。對于帶_x的復制插入指令,只要將指令的dup和x的系數相加,結果即為需要插入的位置。因此
dup_x1插入位置:1+1=2,即棧頂2個Slot下面
dup_x2插入位置:1+2=3,即棧頂3個Slot下面
dup2_x1插入位置:2+1=3,即棧頂3個Slot下面
dup2_x2插入位置:2+2=4,即棧頂4個Slot下面
pop:將棧頂的1個Slot數值出棧。例如1個short類型數值
pop2:將棧頂的2個Slot數值出棧。例如1個double類型數值,或者2個int類型數值
7、控制轉移指令
程序流程離不開條件控制,為了支持條件跳轉,虛擬機提供了大量字節碼指令,大體上可以分為
1)比較指令、2)條件跳轉指令、3)比較條件跳轉指令、4)多條件分支跳轉指令、5)無條件跳轉指令等。
7.1條件跳轉指令
一、條件跳轉指令
條件跳轉指令通常和比較指令結合使用。在條件跳轉指令執行前,一般可以先用比較指令進行棧頂元素的準備,然后進行條件跳轉。
條件跳轉指令有: ifeq, iflt, ifle, ifne, ifgt, ifge, ifnull, ifnonnull。這些指令都接收兩個字節的操作數,用于計算跳轉的位置(16位符號整數作為當前位置的offset)。
它們的統一含義為:彈出棧頂元素,測試它是否滿足某一條件,如果滿足條件,則跳轉到給定位置。

具體說明:
在這里插入圖片描述

注意:

  1. 與前面運算規則一致:
    對于boolean、byte、char、short類型的條件分支比較操作,都是使用int類型的比較指令完成
    對于long、float、double類型的條件分支比較操作,則會先執行相應類型的比較運算指令,運算指令會返回一個整型值到操作數棧中,隨后再執行int類型的條件分支比較操作來完成整個分支跳轉

  2. 由于各類型的比較最終都會轉為 int 類型的比較操作,所以Java虛擬機提供的int類型的條件分支指令是最為豐富和強大的。
    7.2比較條件跳轉指令
    二、比較條件跳轉指令
    比較條件跳轉指令類似于比較指令和條件跳轉指令的結合體,它將比較和跳轉兩個步驟合二為一。
    這類指令有:if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。其中指令助記符加上“if_”后,以字符“i”開頭的指令針對int型整數操作(也包括short和byte類型),以字符“a”開頭的指令表示對象引用的比較。

具體說明:
在這里插入圖片描述

這些指令都接收兩個字節的操作數作為參數,用于計算跳轉的位置。同時在執行指令時,棧頂需要準備兩個元素進行比較。指令執行完成后,棧頂的這兩個元素被清空,且沒有任何數據入棧。如果預設條件成立,則執行跳轉,否則,繼續執行下一條語句。
7.3多條件分支跳轉
三、多條件分支跳轉指令
多條件分支跳轉指令是專為switch-case語句設計的,主要有tableswitch和lookupswitch。
在這里插入圖片描述

從助記符上看,兩者都是switch語句的實現,它們的區別:
tableswitch要求多個條件分支值是連續的,它內部只存放起始值和終止值,以及若干個跳轉偏移量,通過給定的操作數index, 可以立即定位到跳轉偏移量位置,因此效率比較高。
指令lookupswitch內部存放著各個離散的case-offset對,每次執行都要搜索全部的case-offset對,找到匹配的case值,并根據對應的offset計算跳轉地址,因此效率較低。

指令tableswitch的示意圖如下圖所示。由于tableswitch的case值是連續的,因此只需要記錄最低值和最高值,以及每一項對應的offset偏移量,根據給定的index值通過簡單的計算即可直接定位到offset。
在這里插入圖片描述

指令lookupswitch處理的是離散的case值,但是出于效率考慮,將case-offset對按照case 值大小排序,給定index時,需要査找與index相等的case,獲得其offset,如果找不到則跳轉到default。指令lookupswitch 如下圖所示。
在這里插入圖片描述
7.4無條件跳轉
四、無條件跳轉指令
目前主要的無條件跳轉指令為goto。指令goto接收兩個字節的操作數,共同組成一個帶符號的整數,用于指定指令的偏移量,指令執行的目的就是跳轉到偏移量給定的位置處。

如果指令偏移量太大,超過雙字節的帶符號整數的范圍,則可以使用指令goto_w,它和goto有相同的作用,但是它接收4個字節的操作數,可以表示更大的地址范圍。

指令jsr、jsr_w、ret雖然也是無條件跳轉的,但主要用于 try-finally語句,且已經被虛擬機逐漸廢棄,故不在這里介紹這兩個指令。
在這里插入圖片描述
8異常處理指令
8.1拋出異常指令
1、拋出異常指令:

(1)athrow指令
在Java程序中顯示拋出異常的操作(throw語句)都是由athrow指令來實現。
除了使用throw語句顯示拋出異常情況之外,JVM規范還規定了許多運行時異常會在其他Java虛擬機指令檢測到異常狀況時自動拋出。例如,在之前介紹的整數運算時,當除數為零時,虛擬機會在 idiv或 ldiv指令中拋出 ArithmeticException異常。

(2)注意
正常情況下,操作數棧的壓入彈出都是一條條指令完成的。唯一的例外情況是在拋異常時,Java 虛擬機會清除操作數棧上的所有內容,而后將異常實例壓入調用者操作數棧上。

##############
異常及異常的處理:
過程一:異常對象的生成過程 —> throw (手動 / 自動) —> 指令:athrow
過程二:異常的處理:抓拋模型。 try-catch-finally —> 使用異常表
8.2異常處理與異常表
1、處理異常:
在Java虛擬機中,處理異常(catch語句)不是由字節碼指令來實現的(早期使用jsr、ret指令),而是采用異常表來完成的。

2、異常表
如果一個方法定義了一個try-catch 或者try-finally的異常處理,就會創建一個異常表。它包含了每個異常處理或者finally塊的信息。異常表保存了每個異常處理信息。比如:
起始位置
結束位置
程序計數器記錄的代碼處理的偏移地址
被捕獲的異常類在常量池中的索引

當一個異常被拋出時,JVM會在當前的方法里尋找一個匹配的處理,如果沒有找到,這個方法會強制結束并彈出當前棧幀,并且異常會重新拋給上層調用的方法(在調用方法棧幀)。如果在所有棧幀彈出前仍然沒有找到合適的異常處理,這個線程將終止。如果這個異常在最后一個非守護線程里拋出,將會導致JVM自己終止,比如這個線程是個main線程。

不管什么時候拋出異常,如果異常處理最終匹配了所有異常類型,代碼就會繼續執行。在這種情況下,如果方法結束后沒有拋出異常,仍然執行finally塊,在return前,它直接跳到finally塊來完成目標
9.同步控制指令
組成
java虛擬機支持兩種同步結構:方法級的同步 和 方法內部一段指令序列的同步,這兩種同步都是使用monitor來支持的。
9.1方法級的同步
方法級的同步:是隱式的, 即無須通過字節碼指令來控制,它實現在方法調用和返回操作之中。虛擬機可以從方法常量池的方法表結構中的 ACC_SYNCHRONIZED 訪問標志得知一個方法是否聲明為同步方法;

當調用方法時,調用指令將會檢查方法的ACC_SYNCHRONIZED訪問標志是否設置。
如果設置了,執行線程將先持有同步鎖,然后執行方法。最后在方法完成(無論是正常完成還是非正常完成)時釋放同步鎖。
在方法執行期間,執行線程持有了同步鎖,其他任何線程都無法再獲得同一個鎖。
如果一個同步方法執行期間拋出了異常,并且在方法內部無法處理此異常,那這個同步方法所持有的鎖將在異常拋到同步方法之外時自動釋放。

舉例:

private int i = 0;
public synchronized void add(){i++;
}

對應的字節碼:

 0 aload_01 dup2 getfield #2 <com/atguigu/java1/SynchronizedTest.i>5 iconst_16 iadd7 putfield #2 <com/atguigu/java1/SynchronizedTest.i>
10 return

說明:
這段代碼和普通的無同步操作的代碼沒有什么不同,沒有使用monitorenter和monitorexit進行同步區控制。這是因為,對于同步方法而言,當虛擬機通過方法的訪問標示符判斷是一個同步方法時,會自動在方法調用前進行加鎖,當同步方法執行完畢后,不管方法是正常結束還是有異常拋岀,均會由虛擬機釋放這個鎖。因此,對于同步方法而言,monitorenter 和monitorexit指令是隱式存在的,并未直接出現在字節碼中。
9.2方法內指定指令序列的同步
同步一段指令集序列:通常是由java中的synchronized語句塊來表示的。jvm的指令集有 monitorenter 和 monitorexit 兩條指令來支持 synchronized關鍵字的語義。

當一個線程進入同步代碼塊時,它使用monitorenter指令請求進入。如果當前對象的監視器計數器為0,則它會被準許進入,若為1,則判斷持有當前監視器的線程是否為自己,如果是,則進入,否則進行等待,直到對象的監視器計數器為0,才會被允許進入同步塊。

當線程退岀同步塊時,需要使用monitorexit聲明退出。在Java虛擬機中,任何對象都有一個監視器與之相關聯,用來判斷對象是否被鎖定,當監視器被持有后,對象處于鎖定狀態。
指令monitorenter和monitorexit在執行時,都需要在操作數棧頂壓入對象,之后monitorenter和monitorexit的鎖定和釋放都是針對這個對象的監視器進行的。
下圖展示了監視器如何保護臨界區代碼不同時被多個線程訪問,只有當線程4離開臨界區后,線程1、2、3才有可能進入。
在這里插入圖片描述

舉例:

private int i = 0;public void subtract(){synchronized (this){i--;}
}

對應的字節碼:

  0: aload_01: dup2: astore_13: monitorenter4: aload_05: dup6: getfield      #2                  // Field i:I9: iconst_110: isub11: putfield      #2                  // Field i:I14: aload_115: monitorexit16: goto          2419: astore_220: aload_121: monitorexit22: aload_223: athrow24: returnException table:from    to  target type4    16    19   any19    22    19   any

編譯器必須確保無論方法通過何種方式完成,方法中調用過的每條monitorenter指令都必須執行其對應的monitorexit指令,而無論這個方法是正常結束還是異常結束。
為了保證在方法異常完成時monitorenter和monitorexit指令依然可以正確配對執行,編譯器會自動產生一個異常處理器,這個異常處理器聲明可處理所有的異常,它的目的就是用來執行monitorexit指令

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/214977.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/214977.shtml
英文地址,請注明出處:http://en.pswp.cn/news/214977.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Flink SQL: 高效解析 Kafka 數據并存儲為 Parquet 至 HDFS

目錄 總體流程介紹 1. 從 Kafka 讀取數據 2. 使用 UDF 進行數據解析 3. 將

HTML中如何設置音頻和視頻?

文章目錄 &#x1f50a;嵌入音頻&#x1f39e;?嵌入視頻 &#x1f50a;嵌入音頻 HTML 元素用于在文檔中嵌入音頻內容。 元素可以包含一個或多個音頻資源&#xff0c; 這些音頻資源可以使用 src 屬性或者 元素來進行描述&#xff1a;瀏覽器將會選擇最合適的一個來使用。也可以使…

Centos7云服務器上安裝cobalt_strike_4.7。附cobalt_strike_4.7安裝包

環境這里是阿里的一臺Centos7系統。 開始安裝之前首先要確保自己安裝了java11及以上環境。 安裝java11步驟&#xff1a; sudo yum update sudo yum install java-11-openjdk-devel把服務器端&#xff08;CS工具分服務器端和客戶端&#xff09;的CS安裝到服務器上后給目錄下的…

Mongoose 對象文檔模型庫

一、介紹 Mongoose是一個對象文檔模型庫&#xff0c;官網&#xff1a;http://www.mongoosejs.net/ 二、作用 方便使用代碼操作Mongodb數據庫 三、使用流程 //1. 安裝 mongoose //2. 導入 mongoose const mongoose require(mongoose); //3. 連接數據庫 mongoose.connect(m…

某省資源交易中心 (js逆向)

該文章只是用于逆向學習&#xff0c;不得以商用或者是破壞他人利益的目的進行使用。如有侵權請聯系作者。 網站鏈接&#xff1a; bse64 aHR0cHM6Ly9nZ3p5ZncuZnVqaWFuLmdvdi5jbi9idXNpbmVzcy9saXN0Lw 分析環節 進入網站 進行翻頁請求時我們會發現改請求時ajax請求。 這里&…

hive-窗口函數

1 窗口函數語法 分析函數/專用窗口函數 over(partition by 列名 order by 列名 rows between 開始位置 and 結束位置) 常用的分析函數 常用的分析函數&#xff1a;sum()、max()、min()、avg()、count() 常用的專用窗口函數 專用窗口函數&#xff1a;row_number()、rank()、dens…

【簡易版】Linux下Protobuf 實現網絡版通訊錄--C++

一、介紹 該項目的主要目的是用于熟悉protobuf的使用&#xff0c;體驗數據在網絡中序列化反序列化的形式&#xff0c;并非一個完整的項目。 該通訊錄只實現了增加聯系人的功能。服務器端接收到請求后會將聯系人的信息打印。 二、環境搭建 使用Httplib庫&#xff0c;可以快速…

jsp文件引用的css修改后刷新不生效問題

問題 在對 JavaWeb 項目修改的過程中&#xff0c;發現修改了 jsp 文件引入的 css 文件的代碼后頁面的樣式沒有更新的問題。 原因 導致這個問題的原因可能是因為瀏覽器緩存的問題。 解決方法 下面介紹兩種解決方法&#xff0c;供大家參考&#xff1a; 1、給 link 標簽的 c…

TrustZone之安全虛擬化

在Armv7-A首次引入虛擬化時,它僅在非安全狀態中添加。在Armv8.3之前,Armv8也是如此,如下圖所示: 如前所述在切換安全狀態時,EL3用于托管固件和安全監視器。安全EL0/1托管受信任的執行環境(TEE),由受信任的服務和內核組成。 在安全狀態下,沒有對多個虛擬機的需…

Java基礎——什么是main方法

main方法是Java虛擬機調用的入口&#xff0c;該方法的權限必須是public&#xff0c;Java虛擬機在執行main方法時不必創建對象&#xff0c;所以該方法是static修飾&#xff0c;接收一個String類型的數組參數&#xff0c;數組保存執行Java命令時傳遞給所運行的類的參數&#xff0…

基于微信小程序和Spring、SpringMVC、MyBatis的汽車租賃管理系統

文章目錄 項目介紹主要功能截圖:部分代碼展示設計總結項目獲取方式?? 作者主頁:超級無敵暴龍戰士塔塔開 ?? 簡介:Java領域優質創作者??、 簡歷模板、學習資料、面試題庫【關注我,都給你】 ??文末獲取源碼聯系?? 項目介紹 基于微信小程序和Spring、SpringMVC、My…

Kafka生產問題總結及性能優化實踐

1、消息丟失情況 消息發送端&#xff1a; &#xff08;1&#xff09;acks0&#xff1a; 表示producer不需要等待任何broker確認收到消息的回復&#xff0c;就可以繼續發送下一條消息。性能最高&#xff0c;但是最容易丟消息。大數據統計報表場景&#xff0c;對性能要求很高&am…

JavaCV之rtmp推流(FLV和M3U8)

JavaCV與FFmpeg FFmpeg是一款開源的多媒體處理工具集&#xff0c;它包含了一系列用于處理音頻、視頻、字幕等多媒體數據的庫和工具。 JavaCV集成了FFmpeg庫&#xff0c;使得Java開發者可以使用FFmpeg的功能&#xff0c;比如視頻解碼、編碼、格式轉換等。 除了FFmpeg&#xff0…

LeetCode力扣每日一題(Java):35、搜索插入位置

一、題目 二、解題思路 1、我的思路&#xff08;又稱&#xff1a;論API的重要性&#xff09; 讀完題目之后&#xff0c;我心想這題目怎么看著這么眼熟&#xff1f;好像我之前學過的一個API呀&#xff01; 于是我回去翻了翻我之前寫的博客&#xff1a;小白備戰藍橋杯&#xf…

通用的AGI 安全風險

傳統安全風險 平臺基礎設施安全風險 模型與數據層安全風險 應用層安全風險 平臺基礎設施安全風險 &#xff08;1&#xff09;物理攻擊&#xff1a;機房管控不到位 &#xff08;2&#xff09;網絡攻擊 &#xff08;3&#xff09;計算環境&#xff1a;自身安全漏洞&#xf…

編輯器Sublime text 常用快捷命令 列模式 替換空行

平替notepad 下載可取官網 www.sublimetext.com 據說可以無限試用&#xff0c;沒有功能限制 1、快速刪除空行 ctrl h選擇正則表達式 .*Find輸入&#xff1a; ^(\t)*$\nReplace輸入&#xff1a;點擊Replace All 2、快速選擇指定字符 用鼠標選中alt f3修改 3、列編輯模式 ct…

如何理解HTML下的網頁結構?

HTML&#xff08;Hypertext Markup Language&#xff09;是一種標記語言&#xff0c;用于描述網頁的結構和內容。以下是對網頁結構的理解以及網絡爬蟲在處理不同類型網頁時可能遇到的情況&#xff1a; 1. HTML基本結構 HTML文檔的基本結構通常包括以下幾個部分&#xff1a; …

宇視科技視頻監控 main-cgi 文件信息泄露漏洞復現

0x01 產品簡介 宇視(Uniview)高清網絡攝像機是一種高性能的網絡攝像機,它可以通過網絡進行視頻傳輸和監控。該攝像機采用先進的視頻技術,具有高清晰度、低照度、寬動態等特點,能夠提供高質量的視頻圖像。 0x02 漏洞概述 宇視(Uniview)高清網絡攝像機存在信息泄露漏洞…

ppt編輯密碼如何設置?

大家在PPT中設置了限制編輯&#xff0c;發現后面任然可以編輯文件。那么如何將PPT文件設置成禁止修改模式呢&#xff1f;今天分享幾個方法給大家。 方法一 將PPT文件直接保存或者另存為一份文件&#xff0c;在保存時&#xff0c;將文件格式選擇為PowerPoint圖片演示文稿 方法…

.NET 8 編寫 LiteDB vs SQLite 數據庫 CRUD 接口性能測試(測試篇)

WebAppDbTest 項目測試 測試工具 ltt介紹安裝使用方式1、Drill2、Hammer3、Nailgun 測試主機規格配置CRUD 性能測試對比1、ltt 工具測試1.1、AddSingle 單條數據添加1.2、AddBulk 批量數據&#xff08;1000&#xff09;條添加1.3、GetSingle 單條數據查詢1.4、GetAll 多條&…