本篇內(nèi)容主要講解“JVM的基礎(chǔ)知識(shí)總結(jié)”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“JVM的基礎(chǔ)知識(shí)總結(jié)”吧!
創(chuàng)新互聯(lián)不只是一家網(wǎng)站建設(shè)的網(wǎng)絡(luò)公司;我們對(duì)營銷、技術(shù)、服務(wù)都有自己獨(dú)特見解,公司采取“創(chuàng)意+綜合+營銷”一體化的方式為您提供更專業(yè)的服務(wù)!我們經(jīng)歷的每一步也許不一定是最完美的,但每一步都有值得深思的意義。我們珍視每一份信任,關(guān)注我們的成都網(wǎng)站建設(shè)、成都做網(wǎng)站質(zhì)量和服務(wù)品質(zhì),在得到用戶滿意的同時(shí),也能得到同行業(yè)的專業(yè)認(rèn)可,能夠?yàn)樾袠I(yè)創(chuàng)新發(fā)展助力。未來將繼續(xù)專注于技術(shù)創(chuàng)新,服務(wù)升級(jí),滿足企業(yè)一站式成都全網(wǎng)營銷推廣需求,讓再小的品牌網(wǎng)站制作也能產(chǎn)生價(jià)值!
JDK(Java Development Kit) 是用于開發(fā) Java 應(yīng)用程序的軟件開發(fā)工具集合,包括 了 Java 運(yùn)行時(shí)的環(huán)境(JRE)、解釋器(Java)、編譯器(javac)、Java 歸檔 (jar)、文檔生成器(Javadoc)等工具。簡單的說我們要開發(fā)Java程序,就需要安裝某個(gè)版本的JDK工具包。
JRE(Java Runtime Enviroment )提供 Java 應(yīng)用程序執(zhí)行時(shí)所需的環(huán)境,由 Java 虛擬機(jī)(JVM)、核心類、支持文件等組成。簡單的說,我們要是想在某個(gè)機(jī)器上運(yùn) 行Java程序,可以安裝JDK,也可以只安裝JRE,后者體積比較小。
Java Virtual Machine(Java 虛擬機(jī))有三層含義,分別是:
JVM規(guī)范要求
滿足 JVM 規(guī)范要求的一種具體實(shí)現(xiàn)(一種計(jì)算機(jī)程序)
一個(gè) JVM 運(yùn)行實(shí)例,在命令提示符下編寫 Java 命令以運(yùn)行 Java 類時(shí),都會(huì)創(chuàng)建一 個(gè) JVM 實(shí)例,我們下面如果只記到JVM則指的是這個(gè)含義;如果我們帶上了某種JVM 的名稱,比如說是Zing JVM,則表示上面第二種含義
就范圍來說,JDK > JRE > JVM:
JDK = JRE + 開發(fā)工具
JRE = JVM + 類庫
Java程序的開發(fā)運(yùn)行過程為:
我們利用 JDK (調(diào)用 Java API)開發(fā)Java程序,編譯成字節(jié)碼或者打包程序 然后可以用 JRE 則啟動(dòng)一個(gè)JVM實(shí)例,加載、驗(yàn)證、執(zhí)行 Java 字節(jié)碼以及依賴庫, 運(yùn)行Java程序。
而JVM 將程序和依賴庫的Java字節(jié)碼解析并變成本地代碼執(zhí)行,產(chǎn)生結(jié)果 。
最簡單/最麻煩的查詢方式是詢問相關(guān)人員。
查找的方式很多,比如,可以使用 which , whereis , ls ‐l 跟蹤軟連接, 或者 find 命令全局查找(可能需要sudo權(quán)限), 例如:
jps ‐v
whereis javac
ls ‐l /usr/bin/javac
find / ‐name javac
> 沒有量化就沒有改進(jìn)
分析系統(tǒng)性能問題: 比如是不是達(dá)到了我們預(yù)期性能指標(biāo),判斷資源層面有沒有問題,JVM層面有沒有問題,系統(tǒng)的關(guān)鍵處理流程有沒有問題,業(yè)務(wù)流程是否需要優(yōu)化
通過工具收集系統(tǒng)的狀態(tài),日志,包括打點(diǎn)做內(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)
CPU:CPU是系統(tǒng)最關(guān)鍵的計(jì)算資源,在單位時(shí)間內(nèi)有限,也是比較容易由于業(yè)務(wù)邏輯處理不合理而出現(xiàn)瓶頸的地方,浪費(fèi)了CPU資源和過渡消耗CPU資源都不 是理想狀態(tài),我們需要監(jiān)控相關(guān)指標(biāo);
內(nèi)存:內(nèi)存則對(duì)應(yīng)程序運(yùn)行時(shí)直接可使用的數(shù)據(jù)快速暫存空間,也是有限的,使用過程隨著時(shí)間的不斷的申請(qǐng)內(nèi)存又釋放內(nèi)存,好在JVM的GC幫我們處理了這些事情,但是如果GC配置的不合理,一樣會(huì)在一定的時(shí)間后,產(chǎn)生包括OOM宕 機(jī)之類的各種問題,所以內(nèi)存指標(biāo)也需要關(guān)注;
IO(存儲(chǔ)+網(wǎng)絡(luò)):CPU在內(nèi)存中把業(yè)務(wù)邏輯計(jì)算以后,為了長期保存,就必須通過磁盤存儲(chǔ)介質(zhì)持久化,如果多機(jī)環(huán)境、分布式部署、對(duì)外提供網(wǎng)絡(luò)服務(wù)能 力,那么很多功能還需要直接使用網(wǎng)絡(luò),這兩塊的IO都會(huì)比CPU和內(nèi)存速度更慢,所以也是我們關(guān)注的重點(diǎn)。
性能優(yōu)化一般要存在瓶頸問題,而瓶頸問題都遵循80/20原則。既我們把所有的整個(gè)處理過程中比較慢的因素都列一個(gè)清單,并按照對(duì)性能的影響排序,那么前20%的瓶頸問題,至少會(huì)對(duì)性能的影響占到80%比重。換句話說,我們優(yōu)先解決了最重要的幾個(gè)問題,那么性能就能好一大半。
我們一般先排查基礎(chǔ)資源是否成為瓶頸??促Y源夠不夠,只要成本允許,加配置可能是最快速的解決方案,還可能是最劃算,最有效的解決方案。 與JVM有關(guān)的系統(tǒng)資源,主要是 CPU 和 內(nèi)存 這兩部分。 如果發(fā)生資源告警/不足, 就需要評(píng)估系統(tǒng)容量,分析原因。
一般衡量系統(tǒng)性能的維度有3個(gè):
延遲(Latency): 一般衡量的是響應(yīng)時(shí)間(Response Time),比如平均響應(yīng)時(shí)間。 但是有時(shí)候響應(yīng)時(shí)間抖動(dòng)的特別厲害,也就是說有部分用戶的響應(yīng)時(shí)間特別高, 這時(shí)我們一般假設(shè)我們要保障95%的用戶在可接受的范圍內(nèi)響應(yīng),從而提供絕大多數(shù)用戶具有良好的用戶體驗(yàn),這就是延遲的95線(P95,平均100個(gè)用戶請(qǐng)求中95個(gè)已經(jīng)響應(yīng)的時(shí)間),同理還有99線,最大響應(yīng)時(shí)間等(95線和99線比較常用;用戶訪問量大的時(shí)候,對(duì)網(wǎng)絡(luò)有任何抖動(dòng)都可能會(huì)導(dǎo)致最大響應(yīng)時(shí)間變得非常大,最大響應(yīng)時(shí)間這個(gè)指標(biāo)不可控,一般不用)。
吞吐量(Throughput): 一般對(duì)于交易類的系統(tǒng)我們使用每秒處理的事務(wù)數(shù)(TPS) 來衡量吞吐能力,對(duì)于查詢搜索類的系統(tǒng)我們也可以使用每秒處理的請(qǐng)求數(shù) (QPS)。
系統(tǒng)容量(Capacity): 也叫做設(shè)計(jì)容量,可以理解為硬件配置,成本約束。
性能指標(biāo)還可分為兩類:
業(yè)務(wù)需求指標(biāo):如吞吐量(QPS、TPS)、響應(yīng)時(shí)間(RT)、并發(fā)數(shù)、業(yè)務(wù)成功率等。
資源約束指標(biāo):如CPU、內(nèi)存、I/O等資源的消耗情況。
性能調(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)備機(jī)器資源和預(yù)期的擴(kuò)容計(jì)劃。最后在系統(tǒng)的日常運(yùn)行過程中,持續(xù)觀察,逐步重做和調(diào)整以上步驟,長期改善改進(jìn)系統(tǒng)性能。
我們經(jīng)常說“ 脫離場景談性能都是耍流氓 ”,實(shí)際的性能分析調(diào)優(yōu)過程中,我們需要根據(jù)具體的業(yè)務(wù)場景,綜合考慮成本和性能,使用最合適的辦法去處理。系統(tǒng)的性能優(yōu)化到3000TPS如果已經(jīng)可以在成本可以承受的范圍內(nèi)滿足業(yè)務(wù)發(fā)展的需求,那么再花幾個(gè)人月優(yōu)化到3100TPS就沒有什么意義,同樣地如果花一倍成本去優(yōu)化到5000TPS 也沒有意義。
Donald Knuth曾說過“ 過早的優(yōu)化是萬惡之源 ”,我們需要考慮在恰當(dāng)?shù)臅r(shí)機(jī)去優(yōu)化系統(tǒng)。在業(yè)務(wù)發(fā)展的早期,量不大,性能沒那么重要。我們做一個(gè)新系統(tǒng),先考慮整體設(shè)計(jì)是不是OK,功能實(shí)現(xiàn)是不是OK,然后基本的功能都做得差不多的時(shí)候(當(dāng)然整體的框架是不是滿足性能基準(zhǔn),可能需要在做項(xiàng)目的準(zhǔn)備階段就通過POC(概念證明)階段驗(yàn)證。),最后再考慮性能的優(yōu)化工作。因?yàn)槿绻婚_始就考慮優(yōu)化,就可 能要想太多導(dǎo)致過度設(shè)計(jì)了。而且主體框架和功能完成之前,可能會(huì)有比較大的改動(dòng),一旦提前做了優(yōu)化,可能這些改動(dòng)導(dǎo)致原來的優(yōu)化都失效了,又要重新優(yōu)化,多做了很多無用功。
首先,我們可以把形形色色的編程從底向上劃分為最基本的三大類:機(jī)器語言、匯編 語言、高級(jí)語言。
按《計(jì)算機(jī)編程語言的發(fā)展與應(yīng)用》一文里的定義:計(jì)算機(jī)編程語言能夠?qū)崿F(xiàn)人與機(jī)器之間的交流和溝通,而計(jì)算機(jī)編程語言主要包括匯編語言、機(jī)器語言以及高級(jí)語言,具體內(nèi)容如下:
機(jī)器語言:這種語言主要是利用二進(jìn)制編碼進(jìn)行指令的發(fā)送,能夠被計(jì)算機(jī)快速地識(shí)別,其靈活性相對(duì)較高,且執(zhí)行速度較為可觀,機(jī)器語言與匯編語言之間的相似性較高,但由于具有局限性,所以在使用上存在一定的約束性。
匯編語言:該語言主要是以縮寫英文作為標(biāo)符進(jìn)行編寫的,運(yùn)用匯編語言進(jìn)行編 寫的一般都是較為簡練的小程序,其在執(zhí)行方面較為便利,但匯編語言在程序方面較為冗長,所以具有較高的出錯(cuò)率。
高級(jí)語言:所謂的高級(jí)語言,其實(shí)是由多種編程語言結(jié)合之后的總稱,其可以對(duì)多條指令進(jìn)行整合,將其變?yōu)閱螚l指令完成輸送,其在操作細(xì)節(jié)指令以及中間過 程等方面都得到了適當(dāng)?shù)暮喕?,所以,整個(gè)程序更為簡便,具有較強(qiáng)的操作性, 而這種編碼方式的簡化,使得計(jì)算機(jī)編程對(duì)于相關(guān)工作人員的專業(yè)水平要求不斷放寬。
如果按照有沒有虛擬機(jī)來劃分,高級(jí)編程語言可分為兩類:
有虛擬機(jī):Java,Lua,Ruby,部分JavaScript的實(shí)現(xiàn)等等
無虛擬機(jī):C,C++,C#,Golang,以及大部分常見的編程語言
如果按照變量是不是有確定的類型,還是類型可以隨意變化來劃分,高級(jí)編程語言可 以分為:
靜態(tài)類型:Java,C,C++等等
動(dòng)態(tài)類型:所有腳本類型的語言
如果按照是編譯執(zhí)行,還是解釋執(zhí)行,可以分為:
編譯執(zhí)行:C,C++,Golang,Rust,C#,Java,Scala,Clojure,Kotlin, Swift...等等
解釋執(zhí)行:JavaScript的部分實(shí)現(xiàn)和NodeJS,Python,Perl,Ruby...等等
此外,我們還可以按照語言特點(diǎn)分類:
面向過程:C,Basic,Pascal,F(xiàn)ortran等等
面向?qū)ο?C++,Java,Ruby,Smalltalk等等
函數(shù)式編程:LISP、Haskell、Erlang、OCaml、Clojure、F#等等
有的甚至可以劃分為純面向?qū)ο笳Z言,例如Ruby,所有的東西都是對(duì)象(Java不是所有東西都是對(duì)象,比如基本類型 int 、 long 等等,就不是對(duì)象,但是它們的包裝 類 Integer 、 Long 則是對(duì)象)。 還有既可以當(dāng)做編譯語言又可以當(dāng)做腳本語言的,例如Groovy等語言。
現(xiàn)在我們聊聊跨平臺(tái),為什么要跨平臺(tái),因?yàn)槲覀兿M帉懙拇a和程序,在源代 碼級(jí)別或者編譯后,可以運(yùn)行在多種不同的系統(tǒng)平臺(tái)上,而不需要為了各個(gè)平臺(tái)的不 同點(diǎn)而去實(shí)現(xiàn)兩套代碼。典型地,我們編寫一個(gè)web程序,自然希望可以把它部署到 Windows平臺(tái)上,也可以部署到Linux平臺(tái)上,甚至是MacOS系統(tǒng)上。 這就是跨平臺(tái)的能力,極大地節(jié)省了開發(fā)和維護(hù)成本,贏得了商業(yè)市場上的一致好評(píng)。
這樣來看,一般來說解釋型語言都是跨平臺(tái)的,同一份腳本代碼,可以由不同平臺(tái)上的解釋器解釋執(zhí)行。但是對(duì)于編譯型語言,存在兩種級(jí)別的跨平臺(tái): 源碼跨平臺(tái)和二進(jìn)制跨平臺(tái)。
1、典型的源碼跨平臺(tái)(C++):
2、典型的二進(jìn)制跨平臺(tái)(Java字節(jié)碼):
可以看到,C++里我們需要把一份源碼,在不同平臺(tái)上分別編譯,生成這個(gè)平臺(tái)相關(guān)的二進(jìn)制可執(zhí)行文件,然后才能在相應(yīng)的平臺(tái)上運(yùn)行。 這樣就需要在各個(gè)平臺(tái)都有開發(fā)工具和編譯器,而且在各個(gè)平臺(tái)所依賴的開發(fā)庫都需要是一致或兼容的。 這一點(diǎn)在過去的年代里非常痛苦,被戲稱為 “依賴地獄”。 C++的口號(hào)是“一次編寫,到處(不同平臺(tái))編譯”,但實(shí)際情況上是一編譯就報(bào)錯(cuò),變 成了 “一次編寫,到處調(diào)試,到處找依賴、改配置”。 大家可以想象,你編譯一份代 碼,發(fā)現(xiàn)缺了幾十個(gè)依賴,到處找還找不到,或者找到了又跟本地已有的版本不兼 容,這是一件怎樣令人絕望的事情。
而Java語言通過虛擬機(jī)技術(shù)率先解決了這個(gè)難題。 源碼只需要編譯一次,然后把編譯 后的class文件或jar包,部署到不同平臺(tái),就可以直接通過安裝在這些系統(tǒng)中的JVM上 面執(zhí)行。 同時(shí)可以把依賴庫(jar文件)一起復(fù)制到目標(biāo)機(jī)器,慢慢地又有了可以在各個(gè)平臺(tái)都直接使用的Maven中央庫(類似于linux里的yum或apt-get源,macos里的 homebrew,現(xiàn)代的各種編程語言一般都有了這種包依賴管理機(jī)制:python的pip, dotnet的nuget,NodeJS的npm,golang的dep,rust的cargo等等)。這樣就實(shí)現(xiàn)了 讓同一個(gè)應(yīng)用程序在不同的平臺(tái)上直接運(yùn)行的能力。
總結(jié)一下跨平臺(tái):
腳本語言直接使用不同平臺(tái)的解釋器執(zhí)行,稱之為腳本跨平臺(tái),平臺(tái)間的差異由 不同平臺(tái)上的解釋器去解決。這樣的話代碼很通用,但是需要解釋和翻譯,效率較低。
編譯型語言的代碼跨平臺(tái),同一份代碼,需要被不同平臺(tái)的編譯器編譯成相應(yīng)的二進(jìn)制文件,然后再去分發(fā)和執(zhí)行,不同平臺(tái)間的差異由編譯器去解決。編譯產(chǎn) 生的文件是直接針對(duì)平臺(tái)的可執(zhí)行指令,運(yùn)行效率很高。但是在不同平臺(tái)上編譯 復(fù)雜軟件,依賴配置可能會(huì)產(chǎn)生很多環(huán)境方面問題,導(dǎo)致開發(fā)和維護(hù)的成本較 高。
編譯型語言的二進(jìn)制跨平臺(tái),同一份代碼,先編譯成一份通用的二進(jìn)制文件,然后分發(fā)到不同平臺(tái),由虛擬機(jī)運(yùn)行時(shí)來加載和執(zhí)行,這樣就會(huì)綜合另外兩種跨平臺(tái)語言的優(yōu)勢,方便快捷地運(yùn)行于各種平臺(tái),雖然運(yùn)行效率可能比起本地編譯類 型語言要稍低一點(diǎn)。 而這些優(yōu)缺點(diǎn)也是Java虛擬機(jī)的優(yōu)缺點(diǎn)。
我們前面提到了很多次 Java運(yùn)行時(shí) 和 JVM虛擬機(jī) ,簡單的說JRE就是Java的運(yùn)行 時(shí),包括虛擬機(jī)和相關(guān)的庫等資源。 可以說運(yùn)行時(shí)提供了程序運(yùn)行的基本環(huán)境,JVM在啟動(dòng)時(shí)需要加載所有運(yùn)行時(shí)的核心庫等資源,然后再加載我們的應(yīng)用程序字節(jié)碼,才能讓應(yīng)用程序字節(jié)碼運(yùn)行在JVM這 個(gè)容器里。
但也有一些語言是沒有虛擬機(jī)的,編譯打包時(shí)就把依賴的核心庫和其他特性支持,一 起靜態(tài)打包或動(dòng)態(tài)鏈接到程序中,比如Golang和Rust,C#等。 這樣運(yùn)行時(shí)就和程序指令組合在一起,成為了一個(gè)完整的應(yīng)用程序,好處就是不需要虛擬機(jī)環(huán)境,壞處是編譯后的二進(jìn)制文件沒法直接跨平臺(tái)了。
內(nèi)存管理就是內(nèi)存的生命周期管理,包括內(nèi)存的申請(qǐng)、壓縮、回收等操作。 Java的內(nèi)存管理就是GC,JVM的GC模塊不僅管理內(nèi)存的回收,也負(fù)責(zé)內(nèi)存的分配和壓縮整理。
Java中的字節(jié)碼,英文名為 bytecode , 是Java代碼編譯后的中間代碼格式。JVM需要讀取并解析字節(jié)碼才能執(zhí)行相應(yīng)的任務(wù)。 由單字節(jié)( byte )的指令組成, 理論上最多支持 256 個(gè)操作碼(opcode)。實(shí)際上Java只使用了200左右的操作碼, 還有一些操作碼則保留給調(diào)試操作。
操作碼, 下面稱為指令 , 主要由類型前綴和操作名稱兩部分組成。
> 例如,' i ' 前綴代表 ‘ integer ’,所以,' iadd ' 很容易理解, 表示對(duì)整數(shù)執(zhí)行加法運(yùn)算。
棧操作指令,包括與局部變量交互的指令
程序流程控制指令
對(duì)象操作指令,包括方法調(diào)用指令
算數(shù)運(yùn)算以及類型轉(zhuǎn)換指令
此外還有一些執(zhí)行專門任務(wù)的指令,比如同步(synchronization)指令,以及拋出異常相關(guān)的指令等等
我們都知道 new 是Java編程語言中的一個(gè)關(guān)鍵字, 但其實(shí)在字節(jié)碼中,也有一個(gè)指令叫做 new 。 當(dāng)我們創(chuàng)建類的實(shí)例時(shí), 編譯器會(huì)生成類似下面這樣的操作碼:
``` 0: new #2 // class demo/jvm0104/HelloByteCode 3: dup 4: invokespecial #3 // Method "":()V ```
當(dāng)你同時(shí)看到 new, dup 和 invokespecial 指令在一起時(shí),那么一定是在創(chuàng)建類的實(shí)例對(duì)象! 為什么是三條指令而不是一條呢?這是因?yàn)?
new 指令只是創(chuàng)建對(duì)象,但沒有調(diào)用構(gòu)造函數(shù)。
invokespecial 指令用來調(diào)用某些特殊方法的, 當(dāng)然這里調(diào)用的是構(gòu)造函數(shù)。
dup 指令用于復(fù)制棧頂?shù)闹怠?/p>
由于構(gòu)造函數(shù)調(diào)用不會(huì)返回值,所以如果沒有dup指令, 在對(duì)象上調(diào)用方法并初始化之后,操作數(shù)棧就會(huì)是空的,在初始化之后就會(huì)出問題, 接下來的代碼就無法對(duì)其進(jìn)行處理。
在調(diào)用構(gòu)造函數(shù)的時(shí)候,其實(shí)還會(huì)執(zhí)行另一個(gè)類似的方法
有很多指令可以操作方法棧。 前面也提到過一些基本的棧操作指令: 他們將值壓入棧,或者從棧中獲取值。 除了這些基礎(chǔ)操作之外也還有一些指令可以操作棧內(nèi)存; 比如 swap 指令用來交換棧頂兩個(gè)元素的值。下面是一些示例:
最基礎(chǔ)的是 dup 和 pop 指令。
dup 指令復(fù)制棧頂元素的值。
pop 指令則從棧中刪除最頂部的值。
還有復(fù)雜一點(diǎn)的指令:比如, swap , dup_x1 和 dup2_x1 。
顧名思義, swap 指令可交換棧頂兩個(gè)元素的值,例如A和B交換位置(圖中示例 4);
dup_x1 將復(fù)制棧頂元素的值,并在插入在最上面兩個(gè)值后(圖中示例5);
dup2_x1 則復(fù)制棧頂兩個(gè)元素的值,并插入最上面三個(gè)值后(圖中示例6)。
dup , dup_x1 , dup2_x1 指令補(bǔ)充說明 :
dup 指令:官方說明是,復(fù)制棧頂?shù)闹? 并將復(fù)制的值壓入棧.
dup_x1 指令 : 官方說明是,復(fù)制棧頂?shù)闹? 并將復(fù)制的值插入到最上面2個(gè)值的下方。
dup2_x1 指令: 官方說明是,復(fù)制棧頂 1個(gè)64位/或2個(gè)32位的值, 并將復(fù)制的值按照原始順序,插入原始值下面一個(gè)32位值的下方。
Java字節(jié)碼中有許多指令可以執(zhí)行算術(shù)運(yùn)算。實(shí)際上,指令集中有很大一部分表示都是關(guān)于數(shù)學(xué)運(yùn)算的。對(duì)于所有數(shù)值類型( int , long , double , float ),都有加, 減,乘,除,取反的指令。 那么 byte 和 char , boolean 呢? JVM 是當(dāng)做 int 來處理的。另外還有部分指令用于數(shù)據(jù)類型之間的轉(zhuǎn)換。
當(dāng)我們想將 int 類型的值賦值給 long 類型的變量時(shí),就會(huì)發(fā)生類型轉(zhuǎn)換。
invokestatic ,顧名思義,這個(gè)指令用于調(diào)用某個(gè)類的靜態(tài)方法,這也是方法調(diào)用指令中最快的一個(gè)。
invokespecial , 我們已經(jīng)學(xué)過了, invokespecial 指令用來調(diào)用構(gòu)造函數(shù), 但也可以用于調(diào)用同一個(gè)類中的 private 方法, 以及可見的超類方法。
invokevirtual ,如果是具體類型的目標(biāo)對(duì)象, invokevirtual 用于調(diào)用公共,受保護(hù)和打包私有方法。
invokeinterface ,當(dāng)要調(diào)用的方法屬于某個(gè)接口時(shí),將使用invokeinterface 指令。
> 那么 invokevirtual 和 invokeinterface 有什么區(qū)別呢?這確實(shí)是個(gè)好問 題。 為什么需要 invokevirtual 和 invokeinterface 這兩種指令呢? 畢竟 所有的接口方法都是公共方法, 直接使用 invokevirtual 不就可以了嗎? 這么做是源于對(duì)方法調(diào)用的優(yōu)化。JVM必須先解析該方法,然后才能調(diào)用它
使用 invokestatic 指令,JVM就確切地知道要調(diào)用的是哪個(gè)方法:因?yàn)檎{(diào)用的是靜態(tài)方法,只能屬于一個(gè)類。
使用 invokespecial 時(shí), 查找的數(shù)量也很少, 解析也更加容易,那么運(yùn)行時(shí)就能更快地找到所需的方法。
ava虛擬機(jī)的字節(jié)碼指令集在JDK7之前一直就只有前面提到的4種指令 (invokestatic,invokespecial,invokevirtual,invokeinterface)。隨著JDK 7的發(fā) 布,字節(jié)碼指令集新增了 invokedynamic 指令。這條新增加的指令是實(shí)現(xiàn)“動(dòng)態(tài)類型 語言”(Dynamically Typed Language)支持而進(jìn)行的改進(jìn)之一,同時(shí)也是JDK 8以后 支持的lambda表達(dá)式的實(shí)現(xiàn)基礎(chǔ)。
一個(gè)類在JVM里的生命周期有7個(gè)階段,分別是加載(Loading)、驗(yàn)證 (Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)、卸載(Unloading)。 其中前五個(gè)部分(加載,驗(yàn)證,準(zhǔn)備,解析,初始化)統(tǒng)稱為類加載,下面我們就分 別來說一下這五個(gè)過程。
加載階段也可以稱為“裝載”階段。 這個(gè)階段主要的操作是: 根據(jù)明確知道的class完全限定名, 來獲取二進(jìn)制classfile格式的字節(jié)流,簡單點(diǎn)說就是 找到文件系統(tǒng)中/jar包中/或存在于任何地方的“ class文件 ”。 如果找不到二進(jìn)制表示形式,則會(huì)拋出NoClassDefFound 錯(cuò)誤。裝載階段并不會(huì)檢查 classfile 的語法和格式。類加載的整個(gè)過程主要由JVM和Java 的類加載系統(tǒng)共同完成, 當(dāng)然具體到loading 階 段則是由JVM與具體的某一個(gè)類加載器(java.lang.classLoader)協(xié)作完成的。
鏈接過程的第一個(gè)階段是校驗(yàn) ,確保class文件里的字節(jié)流信息符合當(dāng)前虛擬機(jī)的要求,不會(huì)危害虛擬機(jī)的安全。校驗(yàn)過程檢classfile 的語義,判斷常量池中的符號(hào),并執(zhí)行類型檢查, 主要目的是判斷字節(jié)碼的合法性,比如 magic number, 對(duì)版本號(hào)進(jìn)行驗(yàn)證。 這些檢查 過程中可能會(huì)拋出 VerifyError , ClassFormatError 或 UnsupportedClassVersionError 。 因?yàn)閏lassfile的驗(yàn)證屬是鏈接階段的一部分,所以這個(gè)過程中可能需要加載其他類, 在某個(gè)類的加載過程中,JVM必須加載其所有的超類和接口。 如果類層次結(jié)構(gòu)有問題(例如,該類是自己的超類或接口,死循環(huán)了),則JVM將拋出 ClassCircularityError 。 而如果實(shí)現(xiàn)的接口并不是一個(gè) interface,或者聲明的超類是一個(gè) interface,也會(huì)拋出 IncompatibleClassChangeError 。
然后進(jìn)入準(zhǔn)備階段,這個(gè)階段將會(huì)創(chuàng)建靜態(tài)字段, 并將其初始化為標(biāo)準(zhǔn)默認(rèn)值(比如 null 或者 0值 ),并分配方法表,即在方法區(qū)中分配這些變量所使用的內(nèi)存空間。 請(qǐng)注意,準(zhǔn)備階段并未執(zhí)行任何Java代碼。
例如:
public static int i = 1;
在準(zhǔn)備階段 i 的值會(huì)被初始化為0,后面在類初始化階段才會(huì)執(zhí)行賦值為1; 但是下面如果使用final作為靜態(tài)常量,某些JVM的行為就不一樣了:
public static final int i = 1;
對(duì)應(yīng)常量i,在準(zhǔn)備階段就會(huì)被賦值1,其實(shí)這樣還是比較puzzle,例如其他語言 (C#)有直接的常量關(guān)鍵字const,讓告訴編譯器在編譯階段就替換成常量,類似 于宏指令,更簡單。
然后進(jìn)入可選的解析符號(hào)引用階段。 也就是解析常量池,主要有以下四種:類或接口的解析、字段解析、類方法解析、接 口方法解析。
簡單的來說就是我們編寫的代碼中,當(dāng)一個(gè)變量引用某個(gè)對(duì)象的時(shí)候,這個(gè)引用在 .class 文件中是以符號(hào)引用來存儲(chǔ)的(相當(dāng)于做了一個(gè)索引記錄)。 在解析階段就需要將其解析并鏈接為直接引用(相當(dāng)于指向?qū)嶋H對(duì)象)。如果有了直 接引用,那引用的目標(biāo)必定在堆中存在。加載一個(gè)class時(shí), 需要加載所有的super類和super接口。
JVM規(guī)范明確規(guī)定, 必須在類的首次“主動(dòng)使用”時(shí)才能執(zhí)行類初始化。 初始化的過程包括執(zhí)行:
類構(gòu)造器方法
static靜態(tài)變量賦值語句
static靜態(tài)代碼塊
如果是一個(gè)子類進(jìn)行初始化會(huì)先對(duì)其父類進(jìn)行初始化,保證其父類在子類之前進(jìn)行初 始化。所以其實(shí)在java中初始化一個(gè)類,那么必然先初始化過 java.lang.Object 類,因?yàn)樗械膉ava類都繼承自java.lang.Object。
了解了類的加載過程,我們再看看類的初始化何時(shí)會(huì)被觸發(fā)呢?JVM 規(guī)范枚舉了下述多種觸發(fā)情況:
當(dāng)虛擬機(jī)啟動(dòng)時(shí),初始化用戶指定的主類,就是啟動(dòng)執(zhí)行的 main方法所在的類;
當(dāng)遇到用以新建目標(biāo)類實(shí)例的 new 指令時(shí),初始化 new 指令的目標(biāo)類,就是 new一個(gè)類的時(shí)候要初始化
當(dāng)遇到調(diào)用靜態(tài)方法的指令時(shí),初始化該靜態(tài)方法所在的類;
當(dāng)遇到訪問靜態(tài)字段的指令時(shí),初始化該靜態(tài)字段所在的類;
子類的初始化會(huì)觸發(fā)父類的初始化;
如果一個(gè)接口定義了 default 方法,那么直接實(shí)現(xiàn)或者間接實(shí)現(xiàn)該接口的類的初始化,會(huì)觸發(fā)該接口的初始化;
使用反射 API 對(duì)某個(gè)類進(jìn)行反射調(diào)用時(shí),初始化這個(gè)類,其實(shí)跟前面一樣,反射調(diào)用要么是已經(jīng)有實(shí)例了,要么是靜態(tài)方法,都需要初始化;
當(dāng)初次調(diào)用 MethodHandle 實(shí)例時(shí),初始化該 MethodHandle 指向的方法所在的類。
同時(shí)以下幾種情況不會(huì)執(zhí)行類初始化:
通過子類引用父類的靜態(tài)字段,只會(huì)觸發(fā)父類的初始化,而不會(huì)觸發(fā)子類的初始化。
定義對(duì)象數(shù)組,不會(huì)觸發(fā)該類的初始化。
常量在編譯期間會(huì)存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用定義常量的類,不會(huì)觸發(fā)定義常量所在的類。
通過類名獲取Class對(duì)象,不會(huì)觸發(fā)類的初始化,Hello.class不會(huì)讓Hello類初始化。
通過Class.forName加載指定類時(shí),如果指定參數(shù)initialize為false時(shí),也不會(huì)觸發(fā)類初始化,其實(shí)這個(gè)參數(shù)是告訴虛擬機(jī),是否要對(duì)類進(jìn)行初始化。 Class.forName(“jvm.Hello”)默認(rèn)會(huì)加載Hello類。
通過ClassLoader默認(rèn)的loadClass方法,也不會(huì)觸發(fā)初始化動(dòng)作(加載了,但是不初始化)。
類加載過程可以描述為“通過一個(gè)類的全限定名a.b.c.XXClass來獲取描述此類的Class 對(duì)象”,這個(gè)過程由“類加載器(ClassLoader)”來完成。這樣的好處在于,子類加載器可以復(fù)用父加載器加載的類。系統(tǒng)自帶的類加載器分為三種 :
啟動(dòng)類加載器(BootstrapClassLoader)
啟動(dòng)類加載器(bootstrap class loader): 它用來加載 Java 的核心類,是用原生 C++代碼來實(shí)現(xiàn)的,并不繼承自
java.lang.ClassLoader(負(fù)責(zé)加載JDK中 jre/lib/rt.jar里所有的class)。它可以看做是JVM自帶的,我們再代碼層面無法直接獲取到
啟動(dòng)類加載器的引用,所以不允許直接操作它, 如果打印出來就是個(gè) null 。舉例來說,java.lang.String是由啟動(dòng)類加載器加載
的,所以 String.class.getClassLoader()就會(huì)返回null。但是后面可以看到可以通過命令行 參數(shù)影響它加載什么。
擴(kuò)展類加載器(ExtClassLoader)
擴(kuò)展類加載器(extensions class loader):它負(fù)責(zé)加載JRE的擴(kuò)展目錄,lib/ext 或者由java.ext.dirs系統(tǒng)屬性指定的目錄中的JAR包的類,代碼里直接獲取它的父 類加載器為null(因?yàn)闊o法拿到啟動(dòng)類加載器)。
應(yīng)用類加載器(AppClassLoader)
應(yīng)用類加載器(app class loader):它負(fù)責(zé)在JVM啟動(dòng)時(shí)加載來自Java命令的-classpath或者-cp選項(xiàng)、java.class.path系統(tǒng)屬性指定的jar包和類路徑。在應(yīng)用程序代碼里可以通過ClassLoader的靜態(tài)方法getSystemClassLoader()來獲取應(yīng)用類加載器。如果沒有特別指定,則在沒有使用自定義類加載器情況下,用戶自定義的類都由此加載器加載。
類加載機(jī)制有三個(gè)特點(diǎn):
雙親委托:當(dāng)一個(gè)自定義類加載器需要加載一個(gè)類,比如java.lang.String,它很懶,不會(huì)一上來就直接試圖加載它,而是先委托自己的父加載器去加載,父加載 器如果發(fā)現(xiàn)自己還有父加載器,會(huì)一直往前找,這樣只要上級(jí)加載器,比如啟動(dòng)類加載器已經(jīng)加載了某個(gè)類比如java.lang.String,所有的子加載器都不需要自己加載了。如果幾個(gè)類加載器都沒有加載到指定名稱的類,那么會(huì)拋出 ClassNotFountException異常。
負(fù)責(zé)依賴:如果一個(gè)加載器在加載某個(gè)類的時(shí)候,發(fā)現(xiàn)這個(gè)類依賴于另外幾個(gè)類或接口,也會(huì)去嘗試加載這些依賴項(xiàng)。
緩存加載:為了提升加載效率,消除重復(fù)加載,一旦某個(gè)類被一個(gè)類加載器加載,那么它會(huì)緩存這個(gè)加載結(jié)果,不會(huì)重復(fù)加載。
到此,相信大家對(duì)“JVM的基礎(chǔ)知識(shí)總結(jié)”有了更深的了解,不妨來實(shí)際操作一番吧!這里是創(chuàng)新互聯(lián)網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!