自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Java 進階之字節(jié)碼剖析

開發(fā) 后端
從今天起我打算整一個 Java 系列的進階基礎(chǔ)文章,萬丈高樓平地起,打好基礎(chǔ)我們才能走得更好,舉個例子,之前我在武哥的 Kafka 文章中看到這樣的一句話「除此之外,頁緩存(pageCache)還有一個巨大的優(yōu)勢。

[[439668]]

前言

你好,我是坤哥。

從今天起我打算整一個 Java 系列的進階基礎(chǔ)文章,萬丈高樓平地起,打好基礎(chǔ)我們才能走得更好,舉個例子,之前我在武哥的 Kafka 文章中看到這樣的一句話「除此之外,頁緩存(pageCache)還有一個巨大的優(yōu)勢。用過 Java 的人都知道:如果不用頁緩存,而是用 JVM 進程中的緩存,對象的內(nèi)存開銷非常大(通常是真實數(shù)據(jù)大小的幾倍甚至更多)」,如果你不了解 Java 對象的表示,看到這樣的話會一臉懵逼:對象的開銷到底有多巨大,反過來看,如果你掌握了 Java 中的對象布局,GC,NIO 等原理,理解這些框架的原理及其設(shè)計思路就不是什么難事。

另一個讓我下決心寫這個系列的原因是經(jīng)常有一些讀者問一些學(xué)習(xí)路線的事,之前我寫過一些大綱,但沒有從點的層面展開,所以這次準備從點的思路來將各個知識點細細道來,然后再整理成 pdf,這樣之后如果有人再問起,直接把這個 pdf 扔給他們就完事了 ^_^

每個系列都會以圖文并茂的方式來講解,做到深入淺出,舉個例子,上面我們說了對象的開銷很大,到底有多大呢,我會用圖解的方式來帶你一步步分析,看完后相信你會明白為什么 int[128][2] ,int[256] 這兩個數(shù)組看起來一樣,但實際上前者比后者多了 246% 的額外開銷,再比如我們都知道 Eden 區(qū)或 tenured(老年代區(qū))滿了會觸發(fā) yong gc 或 old gc,不過導(dǎo)致 gc 停頓時間過長的原因其實有挺多的,如果你看完我總結(jié)的這些通用的思路,相信你就能根據(jù)這套理論來快速地排查問題了,這個系列干貨很多,相信對提升大家的 Java 內(nèi)功有不少幫助,記得得文末點贊支持一下哦 ^_^

Java 系列大綱如下:

本篇我們先來學(xué)習(xí)下字節(jié)碼 ,畢竟這是 Java 能跨平臺的根本原因,而且通過了解字節(jié)碼也可以徹底揭開 JVM 運行程序的秘密,整體會用問答的形式來講解。

能否簡單介紹一下 Java 的特性

Java 是一門面向?qū)ο螅o態(tài)類型的語言,具有跨平臺的特點,與 C,C++ 這些需要手動管理內(nèi)存,編譯型的語言不同,它是解釋型的,具有跨平臺和自動垃圾回收的特點,那么它的跨平臺到底是怎么實現(xiàn)的呢?

我們知道計算機只能識別二進制代碼表示的機器語言,所以不管用的什么高級語言,最終都得翻譯成機器語言才能被 CPU 識別并執(zhí)行,對于 C++這些編譯型語言來說是直接一步到位轉(zhuǎn)為相應(yīng)平臺的可執(zhí)行文件(即機器語言指令),而對 Java 來說,則首先由編譯器將源文件編譯成字節(jié)碼,再在運行時由虛擬機(JVM)解釋成機器指令來執(zhí)行,我們可以看下下圖:

也就是說 Java 的跨平臺其實是通過先生成字節(jié)碼,再由針對各個平臺實現(xiàn)的 JVM 來解釋執(zhí)行實現(xiàn)的,JVM 屏蔽了 OS 的差異,我們知道 Java 工程都是以 Jar 包分發(fā)(一堆 class 文件的集合體)部署的,這就意味著 jar 包可以在各個平臺上運行(由相應(yīng)平臺的 JVM 解釋執(zhí)行即可),這就是 Java 能實現(xiàn)跨平臺的原因所在。

這也是為什么 JVM 能運行 Scala、Groovy、Kotlin 這些語言的原因,并不是 JVM 直接來執(zhí)行這些語言,而是這些語言最終都會生成符合 JVM 規(guī)范的字節(jié)碼再由 JVM 執(zhí)行,不知你是否注意到,使用字節(jié)碼也利用了計算機科學(xué)中的分層理念,通過加入字節(jié)碼這樣的中間層,有效屏蔽了與上層的交互差異。

JVM 是怎么執(zhí)行字節(jié)碼的

在此之前我們先來看下 JVM 的整體內(nèi)存結(jié)構(gòu),對其有一個宏觀的認識,然后再來看 JVM 是如何執(zhí)行字節(jié)碼的。

JVM 內(nèi)存結(jié)構(gòu)

JVM 在內(nèi)存中主要分為「?!?「堆」,「非堆」以及 JVM 自身,堆主要用來分配類實例和數(shù)組,非堆包括「方法區(qū)」、「JVM內(nèi)部處理或優(yōu)化所需的內(nèi)存(如JIT編譯后的代碼緩存)」、每個類結(jié)構(gòu)(如運行時常數(shù)池、字段和方法數(shù)據(jù))以及方法和構(gòu)造方法的代碼。

我們主要關(guān)注棧,我們知道線程是 cpu 調(diào)度的最小單位,在 JVM 中一旦創(chuàng)建一個線程,就會為其分配一個線程棧,線程會調(diào)用一個個方法,每個方法都會對應(yīng)一個個的棧幀壓到線程棧里,JVM 中的棧內(nèi)存結(jié)構(gòu)如下:

JVM 棧內(nèi)存結(jié)構(gòu)

至此我們總算接近 JVM 執(zhí)行的真相了,JVM 是以棧幀為單位執(zhí)行的,棧幀由以下四個部分組成:

  • 返回值
  • 局部變量表(Local Variables):存儲方法用到的本地變量
  • 動態(tài)鏈接:在字節(jié)碼中,所有的變量和方法都是以符號引用的形式保存在 class 文件的常量池中的,比如一個方法調(diào)用另外的方法,是通過常量池中指向方法的符號引用來表示的,動態(tài)鏈接的作用就是為了將這些符號引用轉(zhuǎn)換為調(diào)用方法的直接引用,這么說可能有人還是不理解,所以我們先執(zhí)行一下 javap -verbose Demo.class命令來查看一下字節(jié)碼中的常量池是咋樣的

注意:以上只列出了常量池中的部分符號引用

可以看到 Object 的 init 方法是由 #4.#16 表示的,而 #4 又指向了 #19,#19 表示 Object,#16 又指向了 #7.#8,#7 指向了方法名,#8 指向了 ()V(表示方法的返回值為 void,且無方法參數(shù)),字節(jié)碼加載后,會把類信息加載到元空間(Java 8 以后)中的方法區(qū)中,動態(tài)鏈接會把這些符號引用替換為調(diào)用方法的直接引用,如下圖示:

那為什么要提供動態(tài)鏈接呢,通過上面這種方式繞了好幾個彎才定位到具體的執(zhí)行方法,效率不是低了很多嗎,其實主要是為了支持 Java 的多態(tài),比如我們聲明一個 Father f = new Son()這樣的變量,但執(zhí)行 f.method() 的時候會綁定到 son 的 method(如果有的話),這就是用到了動態(tài)鏈接的技術(shù),在運行時才能定位到具體該調(diào)用哪個方法,動態(tài)鏈接也稱為后期綁定,與之相對的是靜態(tài)鏈接(也稱為前期綁定),即在編譯期和運行期對象的方法都保持不變,靜態(tài)鏈接發(fā)生在編譯期,也就是說在程序執(zhí)行前方法就已經(jīng)被綁定,java 當中的方法只有final、static、private和構(gòu)造方法是前期綁定的。而動態(tài)鏈接發(fā)生在運行時,幾乎所有的方法都是運行時綁定的。

舉個例子來看看兩者的區(qū)別,一目了解。

  1. class Animal{ 
  2.     public void eat(){ 
  3.         System.out.println("動物進食"); 
  4.     } 
  5.  
  6. class Cat extends Animal{ 
  7.     @Override 
  8.     public void eat() { 
  9.         super.eat();//表現(xiàn)為早期綁定(靜態(tài)鏈接) 
  10.         System.out.println("貓進食"); 
  11.     } 
  12. public class AnimalTest { 
  13.     public void showAnimal(Animal animal){ 
  14.         animal.eat();//表現(xiàn)為晚期綁定(動態(tài)鏈接) 
  15.     } 
  • 操作數(shù)棧(Operand Stack):程序主要由指令和操作數(shù)組成,指令用來說明這條操作做什么,比如是做加法還是乘法,操作數(shù)就是指令要執(zhí)行的數(shù)據(jù),那么指令怎么獲取數(shù)據(jù)呢,指令集的架構(gòu)模型分為基于棧的指令集架構(gòu)和基于寄存器的指令集架構(gòu)兩種,JVM 中的指令集屬于前者,也就是說任何操作都是用棧來管理,基于棧指令可以更好地實現(xiàn)跨平臺,棧都是是在內(nèi)存中分配的,而寄存器往往和硬件掛鉤,不同的硬件架構(gòu)是不一樣的,不利于跨平臺,當然基于棧的指令集架構(gòu)缺點也很明顯,基于棧的實現(xiàn)需要更多指令才能完成(因為棧只是一個FILO結(jié)構(gòu),需要頻繁壓棧出棧),而寄存器是在CPU的高速緩存區(qū),相較而言,基于棧的速度要慢不少,這也是為了跨平臺而做出的一點性能犧牲,畢竟魚和熊掌不可兼得。

Java 字節(jié)碼技術(shù)簡介

注意線程中還有一個「PC 程序計數(shù)器」,是每個線程獨有的,記錄著當前線程所執(zhí)行的字節(jié)碼的行號指示器,也就是指向下一條指令的地址,也就是將執(zhí)行的指令代碼。由執(zhí)行引擎讀取下一條指令。我們先來看下看一下字節(jié)碼長啥樣。假設(shè)我們有以下 Java 代碼:

  1. package com.mahai; 
  2. public class Demo { 
  3.       private  int a = 1; 
  4.     public static void foo() { 
  5.         int a = 1; 
  6.         int b = 2; 
  7.         int c = (a + b) * 5; 
  8.     } 

執(zhí)行 javac Demo.java 后可以看到其字節(jié)碼如下:

字節(jié)碼是給 JVM 看的,所以我們需要將其翻譯成人能看懂的代碼,好在 JDK 提供了反解析工具 javap ,可以根據(jù)字節(jié)碼反解析出 code 區(qū)(匯編指令)、本地變量表、異常表和代碼行偏移量映射表、常量池等信息。我們執(zhí)行以下命令來看下根據(jù)字節(jié)碼反解析的文件長啥樣(更詳細的信息可以執(zhí)行 javap -verbose 命令,在本例中我們重點關(guān)注 Code 區(qū)是如何執(zhí)行的,所以使用了 javap -c 來執(zhí)行。

  1. javap -c Demo.class 

轉(zhuǎn)換成這種形式可讀性強了很多,那么aload_0,invokespecial 這些表示什么含義呢, javap 是怎么根據(jù)字節(jié)碼來解析出這些指令出來的呢!

首先我們需要明白什么是指令,指令=操作碼+操作數(shù),操作碼表示這條指令要做什么,比如加減乘除,操作數(shù)即操作碼操作的數(shù),比如 1+ 2 這條指令,操作碼其實是加法,1,2 為操作數(shù),在 Java 中每個操作碼都由一個字節(jié)表示,每個操作碼都有對應(yīng)類似 aload_0,invokespecial,iconst_1 這樣的助記符,有些操作碼本來就包含著操作數(shù),比如字節(jié)碼 0x04 對應(yīng)的助記符為 iconst_1, 表示 將 int 型 1 推送至棧頂,這些操作碼就相當于指令,而有些操作碼需要配合操作數(shù)才能形成指令,如字節(jié)碼 0x10 表示 bipush,后面需要跟著一個操作數(shù),表示 將單字節(jié)的常量值(-128~127)推送至棧頂。以下為列出的幾個字節(jié)碼與助記符示例:

字節(jié)碼 助記符 表示含義
0x04 iconst_1 將int型1推送至棧頂
0xb7 invokespecial 調(diào)用超類構(gòu)建方法, 實例初始化方法, 私有方法
0x1a iload_0 將第一個int型本地變量推送至棧頂
0x10 bipush 將單字節(jié)的常量值(-128~127)推送至棧頂

至此我們不難明白 javap 的作用了,它主要就是找到字節(jié)碼對應(yīng)的的助記符然后再展示在我們面前的,我們簡單看下上述的默認構(gòu)造方法是如何根據(jù)字節(jié)碼映射成助記符并最終呈現(xiàn)在我們面前的:

最左邊的數(shù)字是 Code 區(qū)中每個字節(jié)的偏移量,這個是保存在 PC 的程序計數(shù)中的,比如如果當前指令指向 1,下一條就指向 4。

另外大家不難發(fā)現(xiàn),在源碼中其實我們并沒有定義默認構(gòu)造函數(shù),但在字節(jié)碼中卻生成了,而且你會發(fā)現(xiàn)我們在源碼中定義了private int a = 1;但這個變量賦值的操作卻是在構(gòu)造方法中執(zhí)行的(下文會分析到),這就是理解字節(jié)碼的意義:它可以反映 JVM 執(zhí)行程序的真正邏輯,而源碼只是表象,要深入分析還得看字節(jié)碼!

接下來我們就來瞧一瞧構(gòu)造方法對應(yīng)的指令是如何執(zhí)行的,首先我們來看一下在 JVM 中指令是怎么執(zhí)行的。

  • 首先 JVM 會為每個方法分配對應(yīng)的局部變量表,可以認為它是一個數(shù)組,每個坑位(我們稱為 slot)為方法中分配的變量,如果是實例方法,這些局部變量可以是 this, 方法參數(shù),方法里分配的局部變量,這些局部變量的類型即我們熟知的 int,long 等八大基本,還有引用,返回地址,每個 slot 為 4 個字節(jié),所以像 Long , Double 這種 8 個字節(jié)的要占用 2 個 slot, 如果這個方法為實例方法,則第一個 slot 為 this 指針, 如果是靜態(tài)方法則沒有 this 指針。
  • 分配好局部變量表后,方法里如果涉及到賦值,加減乘除等操作,那么這些指令的運算就需要依賴于操作數(shù)棧了,將這些指令對應(yīng)的操作數(shù)通過壓棧,彈棧來完成指令的執(zhí)行。

比如有 int i = 69 這樣的指令,對應(yīng)的字碼節(jié)指令如下:

  1. 0:bipush 69 
  2. 2:istore_0 

其在內(nèi)存中的操作過程如下:

可以看到主要分兩步:第一步首先把 69 這個 int 值壓棧,然后再彈棧,把 69 彈出放到局部變量表 i 對應(yīng)的位置,istore_0 表示彈棧,將其從操作數(shù)棧中彈出整型數(shù)字存儲到本地變量中,0 表示本地變量在局部變量表的第 0 個 slot。

理解了上面這個操作,我們再來看一下默認構(gòu)造函數(shù)對應(yīng)的字節(jié)碼指令是如何執(zhí)行的:

首先我們需要先來理解一下上面幾個指令:

  • aload_0:從局部變量表中加載第 0 個 slot 中的對象引用到操作數(shù)棧的棧頂,這里的 0 表示第 0 個位置,也就是 this。
  • invokespecial:用來調(diào)用構(gòu)造函數(shù),但也可以用于調(diào)用同一個類中的 private 方法, 以及 可見的超類方法,在此例中表示調(diào)用父類的構(gòu)造器(因為 #1 符號引用指向?qū)?yīng)的 init 方法)。
  • iconst_1:將 int 型 1推送至棧頂。
  • putfield:它接受一個操作數(shù),這個操作數(shù)引用的是運行時常量池里的一個字段,在這里這個字段是 a。賦給這個字段的值,以及包含這個字段的對象引用,在執(zhí)行這條指令的時候,都會從操作數(shù)棧頂上 pop 出來。前面的 aload_0 指令已經(jīng)把包含這個字段的對象(this)壓到操作數(shù)棧上了,而后面的 iconst_1 又把 1 壓到棧里。最后 putfield 指令會將這兩個值從棧頂彈出。執(zhí)行完的結(jié)果就是這個對象的 a 這個字段的值更新成了 1。

接下來我們來詳細解釋以上以上助記符代表的含義:

  • 第一條命令 aload_0,表示從局部變量表中加載第 0 個 slot 中的對象引用到操作數(shù)棧的棧頂,也就是將 this 加載到棧頂,如下:

  • 第二步 invokespecial #1,表示彈棧并且執(zhí)行 #1 對應(yīng)的方法,#1 代表的含義可以從旁邊的解釋(# Method java/lang/Object."":()V)看出,即調(diào)用父類的初始化方法,這也印證了那句話:子類初始化時會從初始化父類
  • 之后的命令 aload_0,iconst_1,putfied #2 圖解如下:

可能有人有些奇怪,上述 6: putfield #2命令中的 #2 怎么就代表 Demo 的私有成員 a 了,這就涉及到字節(jié)碼中的常量池概念了,我們執(zhí)行 javap -verbose path/Demo.class 可以看到這些字面量代表的含義,#1,#2 這種數(shù)字形式的表示形式也被稱為符號引用,程序運行期會將符號引用轉(zhuǎn)換為直接引用。

由此可知 #2 代表 Demo 類的 a 屬性,如下:

從最終的葉子節(jié)點可以看出 #2 最終代表的是 Demo 類中類型為 int(I 代表 int 代表 int 類型),名稱為 a 的變量。

我們再來用動圖看一下 foo 的執(zhí)行流程,相信你現(xiàn)在能理解其含義了。

唯一需要注意的此例中的 foo 是個靜態(tài)方法,所以局部變量區(qū)是沒有 this 的。

相信你不難發(fā)現(xiàn) JVM 執(zhí)行字節(jié)碼的流程與 CPU 執(zhí)行機器碼步驟如出一轍,都經(jīng)歷了「取指令」,「譯碼」,「執(zhí)行」,「存儲計算結(jié)果」這四步,首先程序計數(shù)器指向下一條要執(zhí)行的指令,然后 JVM 獲取指令,由本地執(zhí)行引擎將字節(jié)碼操作數(shù)轉(zhuǎn)成機器碼(譯碼)執(zhí)行,執(zhí)行后將值存儲到局部變量區(qū)(存儲計算結(jié)果)中。

最后關(guān)于字節(jié)碼我推薦兩款工具:

  • 一個是 Hex Fiend,一款很好的十六進制編輯器,可以用來查看編輯字節(jié)碼
  • 一款是 Intellij Idea 的插件 jclasslib Bytecode viewer,能為你展示 javap -verbose 命令對應(yīng)的常量池,接口, Code 等數(shù)據(jù),非常的直觀,對于分析字節(jié)碼非常有幫忙,如下:

本文轉(zhuǎn)載自微信公眾號「碼?!梗梢酝ㄟ^以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系碼海公眾號。

 

責任編輯:武曉燕 來源: 碼海
相關(guān)推薦

2024-10-29 10:54:07

2011-12-01 14:56:30

Java字節(jié)碼

2010-09-25 10:20:05

JAVA字節(jié)碼

2023-07-03 08:11:48

java字節(jié)碼字段

2019-10-30 08:45:21

JS代碼NodeJS

2022-03-30 10:10:17

字節(jié)碼??臻g

2019-12-20 12:38:28

Java技術(shù)工具

2023-08-30 11:03:47

Java工具

2012-01-12 09:20:49

Java

2021-09-12 07:30:10

配置

2023-03-27 16:44:23

2018-04-04 15:05:17

虛擬機字節(jié)碼引擎

2012-03-28 10:30:33

ScalaJava

2009-06-30 16:46:45

Criteria進階查

2022-03-01 09:01:56

SwiftUI動畫進階Canvas

2010-08-27 13:07:00

CSS規(guī)則

2023-02-15 19:07:30

Java字節(jié)碼指令

2022-03-09 09:00:41

SwiftUI視圖生成器Swift

2021-05-28 23:04:23

Python利器執(zhí)行

2010-09-25 10:32:52

Java字節(jié)碼
點贊
收藏

51CTO技術(shù)棧公眾號