溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

JVM的基礎(chǔ)知識總結(jié)

發(fā)布時間:2021-10-25 16:50:35 來源:億速云 閱讀:199 作者:iii 欄目:編程語言

本篇內(nèi)容主要講解“JVM的基礎(chǔ)知識總結(jié)”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學(xué)習(xí)“JVM的基礎(chǔ)知識總結(jié)”吧!

1. JDK、JRE、JVM的關(guān)系

1.1 JDK

JDK(Java Development Kit) 是用于開發(fā) Java 應(yīng)用程序的軟件開發(fā)工具集合,包括 了 Java 運行時的環(huán)境(JRE)、解釋器(Java)、編譯器(javac)、Java 歸檔 (jar)、文檔生成器(Javadoc)等工具。簡單的說我們要開發(fā)Java程序,就需要安裝某個版本的JDK工具包。

1.2 JRE

JRE(Java Runtime Enviroment )提供 Java 應(yīng)用程序執(zhí)行時所需的環(huán)境,由 Java 虛擬機(JVM)、核心類、支持文件等組成。簡單的說,我們要是想在某個機器上運 行Java程序,可以安裝JDK,也可以只安裝JRE,后者體積比較小。

1.3 JVM

Java Virtual Machine(Java 虛擬機)有三層含義,分別是:

JVM規(guī)范要求

滿足 JVM 規(guī)范要求的一種具體實現(xiàn)(一種計算機程序)

一個 JVM 運行實例,在命令提示符下編寫 Java 命令以運行 Java 類時,都會創(chuàng)建一 個 JVM 實例,我們下面如果只記到JVM則指的是這個含義;如果我們帶上了某種JVM 的名稱,比如說是Zing JVM,則表示上面第二種含義

1.4 JDK 與 JRE、JVM 之間的關(guān)系

就范圍來說,JDK > JRE > JVM:

  • JDK = JRE + 開發(fā)工具

  • JRE = JVM + 類庫

JVM的基礎(chǔ)知識總結(jié)

JVM的基礎(chǔ)知識總結(jié)

Java程序的開發(fā)運行過程為:

我們利用 JDK (調(diào)用 Java API)開發(fā)Java程序,編譯成字節(jié)碼或者打包程序 然后可以用 JRE 則啟動一個JVM實例,加載、驗證、執(zhí)行 Java 字節(jié)碼以及依賴庫, 運行Java程序。

而JVM 將程序和依賴庫的Java字節(jié)碼解析并變成本地代碼執(zhí)行,產(chǎn)生結(jié)果 。

1.5 如果不知道自動安裝/別人安裝的JDK在哪個目錄怎么辦?

最簡單/最麻煩的查詢方式是詢問相關(guān)人員。

查找的方式很多,比如,可以使用 which , whereis , ls ‐l 跟蹤軟連接, 或者 find 命令全局查找(可能需要sudo權(quán)限), 例如:

  • jps ‐v

  • whereis javac

  • ls ‐l /usr/bin/javac

  • find / ‐name javac

2. 常用性能指標(biāo)

> 沒有量化就沒有改進(jìn)

  • 分析系統(tǒng)性能問題: 比如是不是達(dá)到了我們預(yù)期性能指標(biāo),判斷資源層面有沒有問題,JVM層面有沒有問題,系統(tǒng)的關(guān)鍵處理流程有沒有問題,業(yè)務(wù)流程是否需要優(yōu)化

  • 通過工具收集系統(tǒng)的狀態(tài),日志,包括打點做內(nèi)部的指標(biāo)收集,監(jiān)控并得出關(guān)鍵性能指標(biāo)數(shù)據(jù),也包括進(jìn)行壓測,得到一些相關(guān)的壓測數(shù)據(jù)和性能內(nèi)部分析數(shù)據(jù)

  • 根據(jù)分析結(jié)果和性能指標(biāo),進(jìn)行資源配置調(diào)整,并持續(xù)進(jìn)行監(jiān)控和分析,以優(yōu)化性能,直到滿足系統(tǒng)要求,達(dá)到系統(tǒng)的最佳性能狀態(tài)

2.1 計算機系統(tǒng)中,性能相關(guān)的資源主要分為這幾類:

  • CPU:CPU是系統(tǒng)最關(guān)鍵的計算資源,在單位時間內(nèi)有限,也是比較容易由于業(yè)務(wù)邏輯處理不合理而出現(xiàn)瓶頸的地方,浪費了CPU資源和過渡消耗CPU資源都不 是理想狀態(tài),我們需要監(jiān)控相關(guān)指標(biāo);

  • 內(nèi)存:內(nèi)存則對應(yīng)程序運行時直接可使用的數(shù)據(jù)快速暫存空間,也是有限的,使用過程隨著時間的不斷的申請內(nèi)存又釋放內(nèi)存,好在JVM的GC幫我們處理了這些事情,但是如果GC配置的不合理,一樣會在一定的時間后,產(chǎn)生包括OOM宕 機之類的各種問題,所以內(nèi)存指標(biāo)也需要關(guān)注;

  • IO(存儲+網(wǎng)絡(luò)):CPU在內(nèi)存中把業(yè)務(wù)邏輯計算以后,為了長期保存,就必須通過磁盤存儲介質(zhì)持久化,如果多機環(huán)境、分布式部署、對外提供網(wǎng)絡(luò)服務(wù)能 力,那么很多功能還需要直接使用網(wǎng)絡(luò),這兩塊的IO都會比CPU和內(nèi)存速度更慢,所以也是我們關(guān)注的重點。

2.2 性能優(yōu)化中常見的套路

性能優(yōu)化一般要存在瓶頸問題,而瓶頸問題都遵循80/20原則。既我們把所有的整個處理過程中比較慢的因素都列一個清單,并按照對性能的影響排序,那么前20%的瓶頸問題,至少會對性能的影響占到80%比重。換句話說,我們優(yōu)先解決了最重要的幾個問題,那么性能就能好一大半。

我們一般先排查基礎(chǔ)資源是否成為瓶頸??促Y源夠不夠,只要成本允許,加配置可能是最快速的解決方案,還可能是最劃算,最有效的解決方案。 與JVM有關(guān)的系統(tǒng)資源,主要是 CPU 和 內(nèi)存 這兩部分。 如果發(fā)生資源告警/不足, 就需要評估系統(tǒng)容量,分析原因。

一般衡量系統(tǒng)性能的維度有3個:

  • 延遲(Latency): 一般衡量的是響應(yīng)時間(Response Time),比如平均響應(yīng)時間。 但是有時候響應(yīng)時間抖動的特別厲害,也就是說有部分用戶的響應(yīng)時間特別高, 這時我們一般假設(shè)我們要保障95%的用戶在可接受的范圍內(nèi)響應(yīng),從而提供絕大多數(shù)用戶具有良好的用戶體驗,這就是延遲的95線(P95,平均100個用戶請求中95個已經(jīng)響應(yīng)的時間),同理還有99線,最大響應(yīng)時間等(95線和99線比較常用;用戶訪問量大的時候,對網(wǎng)絡(luò)有任何抖動都可能會導(dǎo)致最大響應(yīng)時間變得非常大,最大響應(yīng)時間這個指標(biāo)不可控,一般不用)。

  • 吞吐量(Throughput): 一般對于交易類的系統(tǒng)我們使用每秒處理的事務(wù)數(shù)(TPS) 來衡量吞吐能力,對于查詢搜索類的系統(tǒng)我們也可以使用每秒處理的請求數(shù) (QPS)。

  • 系統(tǒng)容量(Capacity): 也叫做設(shè)計容量,可以理解為硬件配置,成本約束。

性能指標(biāo)還可分為兩類:

  • 業(yè)務(wù)需求指標(biāo):如吞吐量(QPS、TPS)、響應(yīng)時間(RT)、并發(fā)數(shù)、業(yè)務(wù)成功率等。

  • 資源約束指標(biāo):如CPU、內(nèi)存、I/O等資源的消耗情況。

2.3性能調(diào)優(yōu)總結(jié)

JVM的基礎(chǔ)知識總結(jié)

性能調(diào)優(yōu)的第一步是制定指標(biāo),收集數(shù)據(jù),第二步是找瓶頸,然后分析解決瓶頸問題。通過這些手段,找當(dāng)前的性能極限值。壓測調(diào)優(yōu)到不能再優(yōu)化了的 TPS和QPS, 就是極限值。知道了極限值,我們就可以按業(yè)務(wù)發(fā)展測算流量和系統(tǒng)壓力,以此做容量規(guī)劃,準(zhǔn)備機器資源和預(yù)期的擴容計劃。最后在系統(tǒng)的日常運行過程中,持續(xù)觀察,逐步重做和調(diào)整以上步驟,長期改善改進(jìn)系統(tǒng)性能。

我們經(jīng)常說“ 脫離場景談性能都是耍流氓 ”,實際的性能分析調(diào)優(yōu)過程中,我們需要根據(jù)具體的業(yè)務(wù)場景,綜合考慮成本和性能,使用最合適的辦法去處理。系統(tǒng)的性能優(yōu)化到3000TPS如果已經(jīng)可以在成本可以承受的范圍內(nèi)滿足業(yè)務(wù)發(fā)展的需求,那么再花幾個人月優(yōu)化到3100TPS就沒有什么意義,同樣地如果花一倍成本去優(yōu)化到5000TPS 也沒有意義。

Donald Knuth曾說過“ 過早的優(yōu)化是萬惡之源 ”,我們需要考慮在恰當(dāng)?shù)臅r機去優(yōu)化系統(tǒng)。在業(yè)務(wù)發(fā)展的早期,量不大,性能沒那么重要。我們做一個新系統(tǒng),先考慮整體設(shè)計是不是OK,功能實現(xiàn)是不是OK,然后基本的功能都做得差不多的時候(當(dāng)然整體的框架是不是滿足性能基準(zhǔn),可能需要在做項目的準(zhǔn)備階段就通過POC(概念證明)階段驗證。),最后再考慮性能的優(yōu)化工作。因為如果一開始就考慮優(yōu)化,就可 能要想太多導(dǎo)致過度設(shè)計了。而且主體框架和功能完成之前,可能會有比較大的改動,一旦提前做了優(yōu)化,可能這些改動導(dǎo)致原來的優(yōu)化都失效了,又要重新優(yōu)化,多做了很多無用功。

3. JVM基礎(chǔ)知識

3.1 常見的編程語言類型

首先,我們可以把形形色色的編程從底向上劃分為最基本的三大類:機器語言、匯編 語言、高級語言。

JVM的基礎(chǔ)知識總結(jié)

按《計算機編程語言的發(fā)展與應(yīng)用》一文里的定義:計算機編程語言能夠?qū)崿F(xiàn)人與機器之間的交流和溝通,而計算機編程語言主要包括匯編語言、機器語言以及高級語言,具體內(nèi)容如下:

  • 機器語言:這種語言主要是利用二進(jìn)制編碼進(jìn)行指令的發(fā)送,能夠被計算機快速地識別,其靈活性相對較高,且執(zhí)行速度較為可觀,機器語言與匯編語言之間的相似性較高,但由于具有局限性,所以在使用上存在一定的約束性。

  • 匯編語言:該語言主要是以縮寫英文作為標(biāo)符進(jìn)行編寫的,運用匯編語言進(jìn)行編 寫的一般都是較為簡練的小程序,其在執(zhí)行方面較為便利,但匯編語言在程序方面較為冗長,所以具有較高的出錯率。

  • 高級語言:所謂的高級語言,其實是由多種編程語言結(jié)合之后的總稱,其可以對多條指令進(jìn)行整合,將其變?yōu)閱螚l指令完成輸送,其在操作細(xì)節(jié)指令以及中間過 程等方面都得到了適當(dāng)?shù)暮喕?,整個程序更為簡便,具有較強的操作性, 而這種編碼方式的簡化,使得計算機編程對于相關(guān)工作人員的專業(yè)水平要求不斷放寬。

3.2 高級語言分類

  • 如果按照有沒有虛擬機來劃分,高級編程語言可分為兩類:

  • 有虛擬機:Java,Lua,Ruby,部分JavaScript的實現(xiàn)等等

  • 無虛擬機:C,C++,C#,Golang,以及大部分常見的編程語言

  • 如果按照變量是不是有確定的類型,還是類型可以隨意變化來劃分,高級編程語言可 以分為:

  • 靜態(tài)類型:Java,C,C++等等

  • 動態(tài)類型:所有腳本類型的語言

  • 如果按照是編譯執(zhí)行,還是解釋執(zhí)行,可以分為:

  • 編譯執(zhí)行:C,C++,Golang,Rust,C#,Java,Scala,Clojure,Kotlin, Swift...等等

  • 解釋執(zhí)行:JavaScript的部分實現(xiàn)和NodeJS,Python,Perl,Ruby...等等

  • 此外,我們還可以按照語言特點分類:

  • 面向過程:C,Basic,Pascal,F(xiàn)ortran等等

  • 面向?qū)ο?C++,Java,Ruby,Smalltalk等等

  • 函數(shù)式編程:LISP、Haskell、Erlang、OCaml、Clojure、F#等等

有的甚至可以劃分為純面向?qū)ο笳Z言,例如Ruby,所有的東西都是對象(Java不是所有東西都是對象,比如基本類型 int 、 long 等等,就不是對象,但是它們的包裝 類 Integer 、 Long 則是對象)。 還有既可以當(dāng)做編譯語言又可以當(dāng)做腳本語言的,例如Groovy等語言。

3.3 關(guān)于跨平臺

現(xiàn)在我們聊聊跨平臺,為什么要跨平臺,因為我們希望所編寫的代碼和程序,在源代 碼級別或者編譯后,可以運行在多種不同的系統(tǒng)平臺上,而不需要為了各個平臺的不 同點而去實現(xiàn)兩套代碼。典型地,我們編寫一個web程序,自然希望可以把它部署到 Windows平臺上,也可以部署到Linux平臺上,甚至是MacOS系統(tǒng)上。 這就是跨平臺的能力,極大地節(jié)省了開發(fā)和維護成本,贏得了商業(yè)市場上的一致好評。

這樣來看,一般來說解釋型語言都是跨平臺的,同一份腳本代碼,可以由不同平臺上的解釋器解釋執(zhí)行。但是對于編譯型語言,存在兩種級別的跨平臺: 源碼跨平臺和二進(jìn)制跨平臺。

1、典型的源碼跨平臺(C++):

JVM的基礎(chǔ)知識總結(jié)

2、典型的二進(jìn)制跨平臺(Java字節(jié)碼):

JVM的基礎(chǔ)知識總結(jié)

可以看到,C++里我們需要把一份源碼,在不同平臺上分別編譯,生成這個平臺相關(guān)的二進(jìn)制可執(zhí)行文件,然后才能在相應(yīng)的平臺上運行。 這樣就需要在各個平臺都有開發(fā)工具和編譯器,而且在各個平臺所依賴的開發(fā)庫都需要是一致或兼容的。 這一點在過去的年代里非常痛苦,被戲稱為 “依賴地獄”。 C++的口號是“一次編寫,到處(不同平臺)編譯”,但實際情況上是一編譯就報錯,變 成了 “一次編寫,到處調(diào)試,到處找依賴、改配置”。 大家可以想象,你編譯一份代 碼,發(fā)現(xiàn)缺了幾十個依賴,到處找還找不到,或者找到了又跟本地已有的版本不兼 容,這是一件怎樣令人絕望的事情。

而Java語言通過虛擬機技術(shù)率先解決了這個難題。 源碼只需要編譯一次,然后把編譯 后的class文件或jar包,部署到不同平臺,就可以直接通過安裝在這些系統(tǒng)中的JVM上 面執(zhí)行。 同時可以把依賴庫(jar文件)一起復(fù)制到目標(biāo)機器,慢慢地又有了可以在各個平臺都直接使用的Maven中央庫(類似于linux里的yum或apt-get源,macos里的 homebrew,現(xiàn)代的各種編程語言一般都有了這種包依賴管理機制:python的pip, dotnet的nuget,NodeJS的npm,golang的dep,rust的cargo等等)。這樣就實現(xiàn)了 讓同一個應(yīng)用程序在不同的平臺上直接運行的能力。

總結(jié)一下跨平臺:

  • 腳本語言直接使用不同平臺的解釋器執(zhí)行,稱之為腳本跨平臺,平臺間的差異由 不同平臺上的解釋器去解決。這樣的話代碼很通用,但是需要解釋和翻譯,效率較低。

  • 編譯型語言的代碼跨平臺,同一份代碼,需要被不同平臺的編譯器編譯成相應(yīng)的二進(jìn)制文件,然后再去分發(fā)和執(zhí)行,不同平臺間的差異由編譯器去解決。編譯產(chǎn) 生的文件是直接針對平臺的可執(zhí)行指令,運行效率很高。但是在不同平臺上編譯 復(fù)雜軟件,依賴配置可能會產(chǎn)生很多環(huán)境方面問題,導(dǎo)致開發(fā)和維護的成本較 高。

  • 編譯型語言的二進(jìn)制跨平臺,同一份代碼,先編譯成一份通用的二進(jìn)制文件,然后分發(fā)到不同平臺,由虛擬機運行時來加載和執(zhí)行,這樣就會綜合另外兩種跨平臺語言的優(yōu)勢,方便快捷地運行于各種平臺,雖然運行效率可能比起本地編譯類 型語言要稍低一點。 而這些優(yōu)缺點也是Java虛擬機的優(yōu)缺點。

3.4 關(guān)于運行時(Runtime)與虛擬機(VM)

我們前面提到了很多次 Java運行時 和 JVM虛擬機 ,簡單的說JRE就是Java的運行 時,包括虛擬機和相關(guān)的庫等資源。 可以說運行時提供了程序運行的基本環(huán)境,JVM在啟動時需要加載所有運行時的核心庫等資源,然后再加載我們的應(yīng)用程序字節(jié)碼,才能讓應(yīng)用程序字節(jié)碼運行在JVM這 個容器里。

但也有一些語言是沒有虛擬機的,編譯打包時就把依賴的核心庫和其他特性支持,一 起靜態(tài)打包或動態(tài)鏈接到程序中,比如Golang和Rust,C#等。 這樣運行時就和程序指令組合在一起,成為了一個完整的應(yīng)用程序,好處就是不需要虛擬機環(huán)境,壞處是編譯后的二進(jìn)制文件沒法直接跨平臺了。

3.5 關(guān)于內(nèi)存管理和垃圾回收(GC)

內(nèi)存管理就是內(nèi)存的生命周期管理,包括內(nèi)存的申請、壓縮、回收等操作。 Java的內(nèi)存管理就是GC,JVM的GC模塊不僅管理內(nèi)存的回收,也負(fù)責(zé)內(nèi)存的分配和壓縮整理。

4. Java字節(jié)碼

Java中的字節(jié)碼,英文名為 bytecode , 是Java代碼編譯后的中間代碼格式。JVM需要讀取并解析字節(jié)碼才能執(zhí)行相應(yīng)的任務(wù)。 由單字節(jié)( byte )的指令組成, 理論上最多支持 256 個操作碼(opcode)。實際上Java只使用了200左右的操作碼, 還有一些操作碼則保留給調(diào)試操作。

操作碼, 下面稱為指令 , 主要由類型前綴和操作名稱兩部分組成。

> 例如,' i ' 前綴代表 ‘ integer ’,所以,' iadd ' 很容易理解, 表示對整數(shù)執(zhí)行加法運算。

4.1 根據(jù)指令的性質(zhì),主要分為四個大類:

  • 棧操作指令,包括與局部變量交互的指令

  • 程序流程控制指令

  • 對象操作指令,包括方法調(diào)用指令

  • 算數(shù)運算以及類型轉(zhuǎn)換指令

此外還有一些執(zhí)行專門任務(wù)的指令,比如同步(synchronization)指令,以及拋出異常相關(guān)的指令等等

4.2 對象初始化指令:new指令, init 以及 clinit 簡介

我們都知道 new 是Java編程語言中的一個關(guān)鍵字, 但其實在字節(jié)碼中,也有一個指令叫做 new 。 當(dāng)我們創(chuàng)建類的實例時, 編譯器會生成類似下面這樣的操作碼:

```
0: new #2 // class demo/jvm0104/HelloByteCode 
3: dup
4: invokespecial #3 // Method "<init>":()V
```

當(dāng)你同時看到 new, dup 和 invokespecial 指令在一起時,那么一定是在創(chuàng)建類的實例對象! 為什么是三條指令而不是一條呢?這是因為:

  • new 指令只是創(chuàng)建對象,但沒有調(diào)用構(gòu)造函數(shù)。

  • invokespecial 指令用來調(diào)用某些特殊方法的, 當(dāng)然這里調(diào)用的是構(gòu)造函數(shù)。

  • dup 指令用于復(fù)制棧頂?shù)闹怠?/p>

  • 由于構(gòu)造函數(shù)調(diào)用不會返回值,所以如果沒有dup指令, 在對象上調(diào)用方法并初始化之后,操作數(shù)棧就會是空的,在初始化之后就會出問題, 接下來的代碼就無法對其進(jìn)行處理。

在調(diào)用構(gòu)造函數(shù)的時候,其實還會執(zhí)行另一個類似的方法 <init> ,甚至在執(zhí)行構(gòu)造函數(shù)之前就執(zhí)行了。還有一個可能執(zhí)行的方法是該類的靜態(tài)初始化方法 <clinit> ,但 <clinit> 并不能被直接調(diào)用,而是由這些指令觸發(fā)的: new , getstatic , putstatic or invokestatic。

4.3 棧內(nèi)存操作指令

有很多指令可以操作方法棧。 前面也提到過一些基本的棧操作指令: 他們將值壓入棧,或者從棧中獲取值。 除了這些基礎(chǔ)操作之外也還有一些指令可以操作棧內(nèi)存; 比如 swap 指令用來交換棧頂兩個元素的值。下面是一些示例:

最基礎(chǔ)的是 dup 和 pop 指令。

  • dup 指令復(fù)制棧頂元素的值。

  • pop 指令則從棧中刪除最頂部的值。

還有復(fù)雜一點的指令:比如, swap , dup_x1 和 dup2_x1 。

  • 顧名思義, swap 指令可交換棧頂兩個元素的值,例如A和B交換位置(圖中示例 4);

  • dup_x1 將復(fù)制棧頂元素的值,并在插入在最上面兩個值后(圖中示例5);

  • dup2_x1 則復(fù)制棧頂兩個元素的值,并插入最上面三個值后(圖中示例6)。

JVM的基礎(chǔ)知識總結(jié)

dup , dup_x1 , dup2_x1 指令補充說明 :

  • dup 指令:官方說明是,復(fù)制棧頂?shù)闹? 并將復(fù)制的值壓入棧.

  • dup_x1 指令 : 官方說明是,復(fù)制棧頂?shù)闹? 并將復(fù)制的值插入到最上面2個值的下方。

  • dup2_x1 指令: 官方說明是,復(fù)制棧頂 1個64位/或2個32位的值, 并將復(fù)制的值按照原始順序,插入原始值下面一個32位值的下方。

5. 算術(shù)運算指令與類型轉(zhuǎn)換指令

Java字節(jié)碼中有許多指令可以執(zhí)行算術(shù)運算。實際上,指令集中有很大一部分表示都是關(guān)于數(shù)學(xué)運算的。對于所有數(shù)值類型( int , long , double , float ),都有加, 減,乘,除,取反的指令。 那么 byte 和 char , boolean 呢? JVM 是當(dāng)做 int 來處理的。另外還有部分指令用于數(shù)據(jù)類型之間的轉(zhuǎn)換。

JVM的基礎(chǔ)知識總結(jié)

當(dāng)我們想將 int 類型的值賦值給 long 類型的變量時,就會發(fā)生類型轉(zhuǎn)換。

JVM的基礎(chǔ)知識總結(jié)

6. 方法調(diào)用指令和參數(shù)傳遞

  • invokestatic ,顧名思義,這個指令用于調(diào)用某個類的靜態(tài)方法,這也是方法調(diào)用指令中最快的一個。

  • invokespecial , 我們已經(jīng)學(xué)過了, invokespecial 指令用來調(diào)用構(gòu)造函數(shù), 但也可以用于調(diào)用同一個類中的 private 方法, 以及可見的超類方法。

  • invokevirtual ,如果是具體類型的目標(biāo)對象, invokevirtual 用于調(diào)用公共,受保護和打包私有方法。

  • invokeinterface ,當(dāng)要調(diào)用的方法屬于某個接口時,將使用invokeinterface 指令。

> 那么 invokevirtual 和 invokeinterface 有什么區(qū)別呢?這確實是個好問 題。 為什么需要 invokevirtual 和 invokeinterface 這兩種指令呢? 畢竟 所有的接口方法都是公共方法, 直接使用 invokevirtual 不就可以了嗎? 這么做是源于對方法調(diào)用的優(yōu)化。JVM必須先解析該方法,然后才能調(diào)用它

  • 使用 invokestatic 指令,JVM就確切地知道要調(diào)用的是哪個方法:因為調(diào)用的是靜態(tài)方法,只能屬于一個類。

  • 使用 invokespecial 時, 查找的數(shù)量也很少, 解析也更加容易,那么運行時就能更快地找到所需的方法。

  • ava虛擬機的字節(jié)碼指令集在JDK7之前一直就只有前面提到的4種指令 (invokestatic,invokespecial,invokevirtual,invokeinterface)。隨著JDK 7的發(fā) 布,字節(jié)碼指令集新增了 invokedynamic 指令。這條新增加的指令是實現(xiàn)“動態(tài)類型 語言”(Dynamically Typed Language)支持而進(jìn)行的改進(jìn)之一,同時也是JDK 8以后 支持的lambda表達(dá)式的實現(xiàn)基礎(chǔ)。

7. Java類加載器

7.1 類的生命周期和加載過程

JVM的基礎(chǔ)知識總結(jié)

一個類在JVM里的生命周期有7個階段,分別是加載(Loading)、驗證 (Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)、卸載(Unloading)。 其中前五個部分(加載,驗證,準(zhǔn)備,解析,初始化)統(tǒng)稱為類加載,下面我們就分 別來說一下這五個過程。

7.1.1 加載

加載階段也可以稱為“裝載”階段。 這個階段主要的操作是: 根據(jù)明確知道的class完全限定名, 來獲取二進(jìn)制classfile格式的字節(jié)流,簡單點說就是 找到文件系統(tǒng)中/jar包中/或存在于任何地方的“ class文件 ”。 如果找不到二進(jìn)制表示形式,則會拋出NoClassDefFound 錯誤。裝載階段并不會檢查 classfile 的語法和格式。類加載的整個過程主要由JVM和Java 的類加載系統(tǒng)共同完成, 當(dāng)然具體到loading 階 段則是由JVM與具體的某一個類加載器(java.lang.classLoader)協(xié)作完成的。

7.1.2 校驗

鏈接過程的第一個階段是校驗 ,確保class文件里的字節(jié)流信息符合當(dāng)前虛擬機的要求,不會危害虛擬機的安全。校驗過程檢classfile 的語義,判斷常量池中的符號,并執(zhí)行類型檢查, 主要目的是判斷字節(jié)碼的合法性,比如 magic number, 對版本號進(jìn)行驗證。 這些檢查 過程中可能會拋出 VerifyError , ClassFormatError 或 UnsupportedClassVersionError 。 因為classfile的驗證屬是鏈接階段的一部分,所以這個過程中可能需要加載其他類, 在某個類的加載過程中,JVM必須加載其所有的超類和接口。 如果類層次結(jié)構(gòu)有問題(例如,該類是自己的超類或接口,死循環(huán)了),則JVM將拋出 ClassCircularityError 。 而如果實現(xiàn)的接口并不是一個 interface,或者聲明的超類是一個 interface,也會拋出 IncompatibleClassChangeError 。

7.1.3 準(zhǔn)備

然后進(jìn)入準(zhǔn)備階段,這個階段將會創(chuàng)建靜態(tài)字段, 并將其初始化為標(biāo)準(zhǔn)默認(rèn)值(比如 null 或者 0值 ),并分配方法表,即在方法區(qū)中分配這些變量所使用的內(nèi)存空間。 請注意,準(zhǔn)備階段并未執(zhí)行任何Java代碼。

例如:

public static int i = 1;

在準(zhǔn)備階段 i 的值會被初始化為0,后面在類初始化階段才會執(zhí)行賦值為1; 但是下面如果使用final作為靜態(tài)常量,某些JVM的行為就不一樣了:

public static final int i = 1;

對應(yīng)常量i,在準(zhǔn)備階段就會被賦值1,其實這樣還是比較puzzle,例如其他語言 (C#)有直接的常量關(guān)鍵字const,讓告訴編譯器在編譯階段就替換成常量,類似 于宏指令,更簡單。

7.1.4 解析

然后進(jìn)入可選的解析符號引用階段。 也就是解析常量池,主要有以下四種:類或接口的解析、字段解析、類方法解析、接 口方法解析。

簡單的來說就是我們編寫的代碼中,當(dāng)一個變量引用某個對象的時候,這個引用在 .class 文件中是以符號引用來存儲的(相當(dāng)于做了一個索引記錄)。 在解析階段就需要將其解析并鏈接為直接引用(相當(dāng)于指向?qū)嶋H對象)。如果有了直 接引用,那引用的目標(biāo)必定在堆中存在。加載一個class時, 需要加載所有的super類和super接口。

7.1.5 初始化

JVM規(guī)范明確規(guī)定, 必須在類的首次“主動使用”時才能執(zhí)行類初始化。 初始化的過程包括執(zhí)行:

  • 類構(gòu)造器方法

  • static靜態(tài)變量賦值語句

  • static靜態(tài)代碼塊

如果是一個子類進(jìn)行初始化會先對其父類進(jìn)行初始化,保證其父類在子類之前進(jìn)行初 始化。所以其實在java中初始化一個類,那么必然先初始化過 java.lang.Object 類,因為所有的java類都繼承自java.lang.Object。

7.2 類加載時機

了解了類的加載過程,我們再看看類的初始化何時會被觸發(fā)呢?JVM 規(guī)范枚舉了下述多種觸發(fā)情況:

  • 當(dāng)虛擬機啟動時,初始化用戶指定的主類,就是啟動執(zhí)行的 main方法所在的類;

  • 當(dāng)遇到用以新建目標(biāo)類實例的 new 指令時,初始化 new 指令的目標(biāo)類,就是 new一個類的時候要初始化

  • 當(dāng)遇到調(diào)用靜態(tài)方法的指令時,初始化該靜態(tài)方法所在的類;

  • 當(dāng)遇到訪問靜態(tài)字段的指令時,初始化該靜態(tài)字段所在的類;

  • 子類的初始化會觸發(fā)父類的初始化;

  • 如果一個接口定義了 default 方法,那么直接實現(xiàn)或者間接實現(xiàn)該接口的類的初始化,會觸發(fā)該接口的初始化;

  • 使用反射 API 對某個類進(jìn)行反射調(diào)用時,初始化這個類,其實跟前面一樣,反射調(diào)用要么是已經(jīng)有實例了,要么是靜態(tài)方法,都需要初始化;

  • 當(dāng)初次調(diào)用 MethodHandle 實例時,初始化該 MethodHandle 指向的方法所在的類。

同時以下幾種情況不會執(zhí)行類初始化:

  • 通過子類引用父類的靜態(tài)字段,只會觸發(fā)父類的初始化,而不會觸發(fā)子類的初始化。

  • 定義對象數(shù)組,不會觸發(fā)該類的初始化。

  • 常量在編譯期間會存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用定義常量的類,不會觸發(fā)定義常量所在的類。

  • 通過類名獲取Class對象,不會觸發(fā)類的初始化,Hello.class不會讓Hello類初始化。

  • 通過Class.forName加載指定類時,如果指定參數(shù)initialize為false時,也不會觸發(fā)類初始化,其實這個參數(shù)是告訴虛擬機,是否要對類進(jìn)行初始化。 Class.forName(“jvm.Hello”)默認(rèn)會加載Hello類。

  • 通過ClassLoader默認(rèn)的loadClass方法,也不會觸發(fā)初始化動作(加載了,但是不初始化)。

7.3 類加載機制

類加載過程可以描述為“通過一個類的全限定名a.b.c.XXClass來獲取描述此類的Class 對象”,這個過程由“類加載器(ClassLoader)”來完成。這樣的好處在于,子類加載器可以復(fù)用父加載器加載的類。系統(tǒng)自帶的類加載器分為三種 :

JVM的基礎(chǔ)知識總結(jié)

  • 啟動類加載器(BootstrapClassLoader)

啟動類加載器(bootstrap class loader): 它用來加載 Java 的核心類,是用原生 C++代碼來實現(xiàn)的,并不繼承自

java.lang.ClassLoader(負(fù)責(zé)加載JDK中 jre/lib/rt.jar里所有的class)。它可以看做是JVM自帶的,我們再代碼層面無法直接獲取到

啟動類加載器的引用,所以不允許直接操作它, 如果打印出來就是個 null 。舉例來說,java.lang.String是由啟動類加載器加載

的,所以 String.class.getClassLoader()就會返回null。但是后面可以看到可以通過命令行 參數(shù)影響它加載什么。

  • 擴展類加載器(ExtClassLoader)

  • 擴展類加載器(extensions class loader):它負(fù)責(zé)加載JRE的擴展目錄,lib/ext 或者由java.ext.dirs系統(tǒng)屬性指定的目錄中的JAR包的類,代碼里直接獲取它的父 類加載器為null(因為無法拿到啟動類加載器)。

  • 應(yīng)用類加載器(AppClassLoader)

  • 應(yīng)用類加載器(app class loader):它負(fù)責(zé)在JVM啟動時加載來自Java命令的-classpath或者-cp選項、java.class.path系統(tǒng)屬性指定的jar包和類路徑。在應(yīng)用程序代碼里可以通過ClassLoader的靜態(tài)方法getSystemClassLoader()來獲取應(yīng)用類加載器。如果沒有特別指定,則在沒有使用自定義類加載器情況下,用戶自定義的類都由此加載器加載。

類加載機制有三個特點:

  • 雙親委托:當(dāng)一個自定義類加載器需要加載一個類,比如java.lang.String,它很懶,不會一上來就直接試圖加載它,而是先委托自己的父加載器去加載,父加載 器如果發(fā)現(xiàn)自己還有父加載器,會一直往前找,這樣只要上級加載器,比如啟動類加載器已經(jīng)加載了某個類比如java.lang.String,所有的子加載器都不需要自己加載了。如果幾個類加載器都沒有加載到指定名稱的類,那么會拋出 ClassNotFountException異常。

  • 負(fù)責(zé)依賴:如果一個加載器在加載某個類的時候,發(fā)現(xiàn)這個類依賴于另外幾個類或接口,也會去嘗試加載這些依賴項。

  • 緩存加載:為了提升加載效率,消除重復(fù)加載,一旦某個類被一個類加載器加載,那么它會緩存這個加載結(jié)果,不會重復(fù)加載。

到此,相信大家對“JVM的基礎(chǔ)知識總結(jié)”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

jvm
AI