> 點擊 RoadToGrowth 便可查看原文和更多的文章,歡迎star。java
1. JDK、JRE、JVM的關係
1.1 JDK
JDK(Java Development Kit) 是用於開發 Java 應用程序的軟件開發工具集合,包括 了 Java 運行時的環境(JRE)、解釋器(Java)、編譯器(javac)、Java 歸檔 (jar)、文檔生成器(Javadoc)等工具。簡單的說咱們要開發Java程序,就須要安裝某個版本的JDK工具包。python
1.2 JRE
JRE(Java Runtime Enviroment )提供 Java 應用程序執行時所需的環境,由 Java 虛擬機(JVM)、核心類、支持文件等組成。簡單的說,咱們要是想在某個機器上運 行Java程序,能夠安裝JDK,也能夠只安裝JRE,後者體積比較小。linux
1.3 JVM
Java Virtual Machine(Java 虛擬機)有三層含義,分別是:git
JVM規範要求github
知足 JVM 規範要求的一種具體實現(一種計算機程序)golang
一個 JVM 運行實例,在命令提示符下編寫 Java 命令以運行 Java 類時,都會建立一 個 JVM 實例,咱們下面若是隻記到JVM則指的是這個含義;若是咱們帶上了某種JVM 的名稱,好比說是Zing JVM,則表示上面第二種含義web
1.4 JDK 與 JRE、JVM 之間的關係
就範圍來講,JDK > JRE > JVM:macos
- JDK = JRE + 開發工具
- JRE = JVM + 類庫
Java程序的開發運行過程爲:npm
咱們利用 JDK (調用 Java API)開發Java程序,編譯成字節碼或者打包程序 而後能夠用 JRE 則啓動一個JVM實例,加載、驗證、執行 Java 字節碼以及依賴庫, 運行Java程序。編程
而JVM 將程序和依賴庫的Java字節碼解析並變成本地代碼執行,產生結果 。
1.5 若是不知道自動安裝/別人安裝的JDK在哪一個目錄怎麼辦?
最簡單/最麻煩的查詢方式是詢問相關人員。
查找的方式不少,好比,可使用 which , whereis , ls ‐l 跟蹤軟鏈接, 或者 find 命令全局查找(可能須要sudo權限), 例如:
- jps ‐v
- whereis javac
- ls ‐l /usr/bin/javac
- find / ‐name javac
2. 經常使用性能指標
> 沒有量化就沒有改進
- 分析系統性能問題: 好比是否是達到了咱們預期性能指標,判斷資源層面有沒有問題,JVM層面有沒有問題,系統的關鍵處理流程有沒有問題,業務流程是否須要優化
- 經過工具收集系統的狀態,日誌,包括打點作內部的指標收集,監控並得出關鍵性能指標數據,也包括進行壓測,獲得一些相關的壓測數據和性能內部分析數據
- 根據分析結果和性能指標,進行資源配置調整,並持續進行監控和分析,以優化性能,直到知足系統要求,達到系統的最佳性能狀態
2.1 計算機系統中,性能相關的資源主要分爲這幾類:
- CPU:CPU是系統最關鍵的計算資源,在單位時間內有限,也是比較容易因爲業務邏輯處理不合理而出現瓶頸的地方,浪費了CPU資源和過渡消耗CPU資源都不 是理想狀態,咱們須要監控相關指標;
- 內存:內存則對應程序運行時直接可以使用的數據快速暫存空間,也是有限的,使用過程隨着時間的不斷的申請內存又釋放內存,好在JVM的GC幫咱們處理了這些事情,可是若是GC配置的不合理,同樣會在必定的時間後,產生包括OOM宕 機之類的各類問題,因此內存指標也須要關注;
- IO(存儲+網絡):CPU在內存中把業務邏輯計算之後,爲了長期保存,就必須經過磁盤存儲介質持久化,若是多機環境、分佈式部署、對外提供網絡服務能 力,那麼不少功能還須要直接使用網絡,這兩塊的IO都會比CPU和內存速度更慢,因此也是咱們關注的重點。
2.2 性能優化中常見的套路
性能優化通常要存在瓶頸問題,而瓶頸問題都遵循80/20原則。既咱們把全部的整個處理過程當中比較慢的因素都列一個清單,並按照對性能的影響排序,那麼前20%的瓶頸問題,至少會對性能的影響佔到80%比重。換句話說,咱們優先解決了最重要的幾個問題,那麼性能就能好一大半。
咱們通常先排查基礎資源是否成爲瓶頸。看資源夠不夠,只要成本容許,加配置多是最快速的解決方案,還多是最划算,最有效的解決方案。 與JVM有關的系統資源,主要是 CPU 和 內存 這兩部分。 若是發生資源告警/不足, 就須要評估系統容量,分析緣由。
通常衡量系統性能的維度有3個:
- 延遲(Latency): 通常衡量的是響應時間(Response Time),好比平均響應時間。 可是有時候響應時間抖動的特別厲害,也就是說有部分用戶的響應時間特別高, 這時咱們通常假設咱們要保障95%的用戶在可接受的範圍內響應,從而提供絕大多數用戶具備良好的用戶體驗,這就是延遲的95線(P95,平均100個用戶請求中95個已經響應的時間),同理還有99線,最大響應時間等(95線和99線比較經常使用;用戶訪問量大的時候,對網絡有任何抖動均可能會致使最大響應時間變得很是大,最大響應時間這個指標不可控,通常不用)。
- 吞吐量(Throughput): 通常對於交易類的系統咱們使用每秒處理的事務數(TPS) 來衡量吞吐能力,對於查詢搜索類的系統咱們也可使用每秒處理的請求數 (QPS)。
- 系統容量(Capacity): 也叫作設計容量,能夠理解爲硬件配置,成本約束。
性能指標還可分爲兩類:
- 業務需求指標:如吞吐量(QPS、TPS)、響應時間(RT)、併發數、業務成功率等。
- 資源約束指標:如CPU、內存、I/O等資源的消耗狀況。
2.3性能調優總結
性能調優的第一步是制定指標,收集數據,第二步是找瓶頸,而後分析解決瓶頸問題。經過這些手段,找當前的性能極限值。壓測調優到不能再優化了的 TPS和QPS, 就是極限值。知道了極限值,咱們就能夠按業務發展測算流量和系統壓力,以此作容量規劃,準備機器資源和預期的擴容計劃。最後在系統的平常運行過程當中,持續觀察,逐步重作和調整以上步驟,長期改善改進系統性能。
咱們常常說「 脫離場景談性能都是耍流氓 」,實際的性能分析調優過程當中,咱們須要根據具體的業務場景,綜合考慮成本和性能,使用最合適的辦法去處理。系統的性能優化到3000TPS若是已經能夠在成本能夠承受的範圍內知足業務發展的需求,那麼再花幾我的月優化到3100TPS就沒有什麼意義,一樣地若是花一倍成本去優化到5000TPS 也沒有意義。
Donald Knuth曾說過「 過早的優化是萬惡之源 」,咱們須要考慮在恰當的時機去優化系統。在業務發展的早期,量不大,性能沒那麼重要。咱們作一個新系統,先考慮總體設計是否是OK,功能實現是否是OK,而後基本的功能都作得差很少的時候(固然總體的框架是否是知足性能基準,可能須要在作項目的準備階段就經過POC(概念證實)階段驗證。),最後再考慮性能的優化工做。由於若是一開始就考慮優化,就可 能要想太多致使過分設計了。並且主體框架和功能完成以前,可能會有比較大的改動,一旦提早作了優化,可能這些改動致使原來的優化都失效了,又要從新優化,多作了不少無用功。
3. JVM基礎知識
3.1 常見的編程語言類型
首先,咱們能夠把形形色色的編程從底向上劃分爲最基本的三大類:機器語言、彙編 語言、高級語言。
按《計算機編程語言的發展與應用》一文裏的定義:計算機編程語言可以實現人與機器之間的交流和溝通,而計算機編程語言主要包括彙編語言、機器語言以及高級語言,具體內容以下:
- 機器語言:這種語言主要是利用二進制編碼進行指令的發送,可以被計算機快速地識別,其靈活性相對較高,且執行速度較爲可觀,機器語言與彙編語言之間的類似性較高,但因爲具備侷限性,因此在使用上存在必定的約束性。
- 彙編語言:該語言主要是以縮寫英文做爲標符進行編寫的,運用匯編語言進行編 寫的通常都是較爲簡練的小程序,其在執行方面較爲便利,但彙編語言在程序方面較爲冗長,因此具備較高的出錯率。
- 高級語言:所謂的高級語言,實際上是由多種編程語言結合以後的總稱,其能夠對多條指令進行整合,將其變爲單條指令完成輸送,其在操做細節指令以及中間過 程等方面都獲得了適當的簡化,因此,整個程序更爲簡便,具備較強的操做性, 而這種編碼方式的簡化,使得計算機編程對於相關工做人員的專業水平要求不斷放寬。
3.2 高級語言分類
-
若是按照有沒有虛擬機來劃分,高級編程語言可分爲兩類:
-
有虛擬機:Java,Lua,Ruby,部分JavaScript的實現等等
-
無虛擬機:C,C++,C#,Golang,以及大部分常見的編程語言
-
若是按照變量是否是有肯定的類型,仍是類型能夠隨意變化來劃分,高級編程語言可 以分爲:
-
靜態類型:Java,C,C++等等
-
動態類型:全部腳本類型的語言
-
若是按照是編譯執行,仍是解釋執行,能夠分爲:
-
編譯執行:C,C++,Golang,Rust,C#,Java,Scala,Clojure,Kotlin, Swift...等等
-
解釋執行:JavaScript的部分實現和NodeJS,Python,Perl,Ruby...等等
-
此外,咱們還能夠按照語言特色分類:
-
面向過程:C,Basic,Pascal,Fortran等等
-
面向對象:C++,Java,Ruby,Smalltalk等等
-
函數式編程:LISP、Haskell、Erlang、OCaml、Clojure、F#等等
有的甚至能夠劃分爲純面嚮對象語言,例如Ruby,全部的東西都是對象(Java不是全部東西都是對象,好比基本類型 int 、 long 等等,就不是對象,可是它們的包裝 類 Integer 、 Long 則是對象)。 還有既能夠當作編譯語言又能夠當作腳本語言的,例如Groovy等語言。
3.3 關於跨平臺
如今咱們聊聊跨平臺,爲何要跨平臺,由於咱們但願所編寫的代碼和程序,在源代 碼級別或者編譯後,能夠運行在多種不一樣的系統平臺上,而不須要爲了各個平臺的不 同點而去實現兩套代碼。典型地,咱們編寫一個web程序,天然但願能夠把它部署到 Windows平臺上,也能夠部署到Linux平臺上,甚至是MacOS系統上。 這就是跨平臺的能力,極大地節省了開發和維護成本,贏得了商業市場上的一致好評。
這樣來看,通常來講解釋型語言都是跨平臺的,同一份腳本代碼,能夠由不一樣平臺上的解釋器解釋執行。可是對於編譯型語言,存在兩種級別的跨平臺: 源碼跨平臺和二進制跨平臺。
一、典型的源碼跨平臺(C++):
二、典型的二進制跨平臺(Java字節碼):
能夠看到,C++裏咱們須要把一份源碼,在不一樣平臺上分別編譯,生成這個平臺相關的二進制可執行文件,而後才能在相應的平臺上運行。 這樣就須要在各個平臺都有開發工具和編譯器,並且在各個平臺所依賴的開發庫都須要是一致或兼容的。 這一點在過去的年代裏很是痛苦,被戲稱爲 「依賴地獄」。 C++的口號是「一次編寫,處處(不一樣平臺)編譯」,但實際狀況上是一編譯就報錯,變 成了 「一次編寫,處處調試,處處找依賴、改配置」。 你們能夠想象,你編譯一份代 碼,發現缺了幾十個依賴,處處找還找不到,或者找到了又跟本地已有的版本不兼 容,這是一件怎樣使人絕望的事情。
而Java語言經過虛擬機技術率先解決了這個難題。 源碼只須要編譯一次,而後把編譯 後的class文件或jar包,部署到不一樣平臺,就能夠直接經過安裝在這些系統中的JVM上 面執行。 同時能夠把依賴庫(jar文件)一塊兒複製到目標機器,慢慢地又有了能夠在各個平臺都直接使用的Maven中央庫(相似於linux裏的yum或aptget源,macos裏的 homebrew,現代的各類編程語言通常都有了這種包依賴管理機制:python的pip, dotnet的nuget,NodeJS的npm,golang的dep,rust的cargo等等)。這樣就實現了 讓同一個應用程序在不一樣的平臺上直接運行的能力。
總結一下跨平臺:
- 腳本語言直接使用不一樣平臺的解釋器執行,稱之爲腳本跨平臺,平臺間的差別由 不一樣平臺上的解釋器去解決。這樣的話代碼很通用,可是須要解釋和翻譯,效率較低。
- 編譯型語言的代碼跨平臺,同一份代碼,須要被不一樣平臺的編譯器編譯成相應的二進制文件,而後再去分發和執行,不一樣平臺間的差別由編譯器去解決。編譯產 生的文件是直接針對平臺的可執行指令,運行效率很高。可是在不一樣平臺上編譯 複雜軟件,依賴配置可能會產生不少環境方面問題,致使開發和維護的成本較 高。
- 編譯型語言的二進制跨平臺,同一份代碼,先編譯成一份通用的二進制文件,而後分發到不一樣平臺,由虛擬機運行時來加載和執行,這樣就會綜合另外兩種跨平臺語言的優點,方便快捷地運行於各類平臺,雖然運行效率可能比起本地編譯類 型語言要稍低一點。 而這些優缺點也是Java虛擬機的優缺點。
3.4 關於運行時(Runtime)與虛擬機(VM)
咱們前面提到了不少次 Java運行時 和 JVM虛擬機 ,簡單的說JRE就是Java的運行 時,包括虛擬機和相關的庫等資源。 能夠說運行時提供了程序運行的基本環境,JVM在啓動時須要加載全部運行時的核心庫等資源,而後再加載咱們的應用程序字節碼,才能讓應用程序字節碼運行在JVM這 個容器裏。
但也有一些語言是沒有虛擬機的,編譯打包時就把依賴的核心庫和其餘特性支持,一 起靜態打包或動態連接到程序中,好比Golang和Rust,C#等。 這樣運行時就和程序指令組合在一塊兒,成爲了一個完整的應用程序,好處就是不須要虛擬機環境,壞處是編譯後的二進制文件無法直接跨平臺了。
3.5 關於內存管理和垃圾回收(GC)
內存管理就是內存的生命週期管理,包括內存的申請、壓縮、回收等操做。 Java的內存管理就是GC,JVM的GC模塊不只管理內存的回收,也負責內存的分配和壓縮整理。
4. Java字節碼
Java中的字節碼,英文名爲 bytecode , 是Java代碼編譯後的中間代碼格式。JVM須要讀取並解析字節碼才能執行相應的任務。 由單字節( byte )的指令組成, 理論上最多支持 256 個操做碼(opcode)。實際上Java只使用了200左右的操做碼, 還有一些操做碼則保留給調試操做。
操做碼, 下面稱爲指令 , 主要由類型前綴和操做名稱兩部分組成。
> 例如,' i ' 前綴表明 ‘ integer ’,因此,' iadd ' 很容易理解, 表示對整數執行加法運算。
4.1 根據指令的性質,主要分爲四個大類:
- 棧操做指令,包括與局部變量交互的指令
- 程序流程控制指令
- 對象操做指令,包括方法調用指令
- 算數運算以及類型轉換指令
此外還有一些執行專門任務的指令,好比同步(synchronization)指令,以及拋出異常相關的指令等等
4.2 對象初始化指令:new指令, init 以及 clinit 簡介
咱們都知道 new 是Java編程語言中的一個關鍵字, 但其實在字節碼中,也有一個指令叫作 new 。 當咱們建立類的實例時, 編譯器會生成相似下面這樣的操做碼:
``` 0: new #2 // class demo/jvm0104/HelloByteCode 3: dup 4: invokespecial #3 // Method "<init>":()V ```
當你同時看到 new, dup 和 invokespecial 指令在一塊兒時,那麼必定是在建立類的實例對象! 爲何是三條指令而不是一條呢?這是由於:
- new 指令只是建立對象,但沒有調用構造函數。
- invokespecial 指令用來調用某些特殊方法的, 固然這裏調用的是構造函數。
- dup 指令用於複製棧頂的值。
- 因爲構造函數調用不會返回值,因此若是沒有dup指令, 在對象上調用方法並初始化以後,操做數棧就會是空的,在初始化以後就會出問題, 接下來的代碼就沒法對其進行處理。
在調用構造函數的時候,其實還會執行另外一個相似的方法 <init> ,甚至在執行構造函數以前就執行了。還有一個可能執行的方法是該類的靜態初始化方法 <clinit> ,但 <clinit> 並不能被直接調用,而是由這些指令觸發的: new , getstatic , putstatic or invokestatic。
4.3 棧內存操做指令
有不少指令能夠操做方法棧。 前面也提到過一些基本的棧操做指令: 他們將值壓入棧,或者從棧中獲取值。 除了這些基礎操做以外也還有一些指令能夠操做棧內存; 好比 swap 指令用來交換棧頂兩個元素的值。下面是一些示例:
最基礎的是 dup 和 pop 指令。
- dup 指令複製棧頂元素的值。
- pop 指令則從棧中刪除最頂部的值。
還有複雜一點的指令:好比, swap , dup_x1 和 dup2_x1 。
- 顧名思義, swap 指令可交換棧頂兩個元素的值,例如A和B交換位置(圖中示例 4);
- dup_x1 將複製棧頂元素的值,並在插入在最上面兩個值後(圖中示例5);
- dup2_x1 則複製棧頂兩個元素的值,並插入最上面三個值後(圖中示例6)。
dup , dup_x1 , dup2_x1 指令補充說明 :
- dup 指令:官方說明是,複製棧頂的值, 並將複製的值壓入棧.
- dup_x1 指令 : 官方說明是,複製棧頂的值, 並將複製的值插入到最上面2個值的下方。
- dup2_x1 指令: 官方說明是,複製棧頂 1個64位/或2個32位的值, 並將複製的值按照原始順序,插入原始值下面一個32位值的下方。
5. 算術運算指令與類型轉換指令
Java字節碼中有許多指令能夠執行算術運算。實際上,指令集中有很大一部分表示都是關於數學運算的。對於全部數值類型( int , long , double , float ),都有加, 減,乘,除,取反的指令。 那麼 byte 和 char , boolean 呢? JVM 是當作 int 來處理的。另外還有部分指令用於數據類型之間的轉換。
當咱們想將 int 類型的值賦值給 long 類型的變量時,就會發生類型轉換。
6. 方法調用指令和參數傳遞
- invokestatic ,顧名思義,這個指令用於調用某個類的靜態方法,這也是方法調用指令中最快的一個。
- invokespecial , 咱們已經學過了, invokespecial 指令用來調用構造函數, 但也能夠用於調用同一個類中的 private 方法, 以及可見的超類方法。
- invokevirtual ,若是是具體類型的目標對象, invokevirtual 用於調用公共,受保護和打包私有方法。
- invokeinterface ,當要調用的方法屬於某個接口時,將使用invokeinterface 指令。
> 那麼 invokevirtual 和 invokeinterface 有什麼區別呢?這確實是個好問 題。 爲何須要 invokevirtual 和 invokeinterface 這兩種指令呢? 畢竟 全部的接口方法都是公共方法, 直接使用 invokevirtual 不就能夠了嗎? 這麼作是源於對方法調用的優化。JVM必須先解析該方法,而後才能調用它
- 使用 invokestatic 指令,JVM就確切地知道要調用的是哪一個方法:由於調用的是靜態方法,只能屬於一個類。
- 使用 invokespecial 時, 查找的數量也不多, 解析也更加容易,那麼運行時就能更快地找到所需的方法。
- ava虛擬機的字節碼指令集在JDK7以前一直就只有前面提到的4種指令 (invokestatic,invokespecial,invokevirtual,invokeinterface)。隨着JDK 7的發 布,字節碼指令集新增了 invokedynamic 指令。這條新增長的指令是實現「動態類型 語言」(Dynamically Typed Language)支持而進行的改進之一,同時也是JDK 8之後 支持的lambda表達式的實現基礎。
7. Java類加載器
7.1 類的生命週期和加載過程
一個類在JVM裏的生命週期有7個階段,分別是加載(Loading)、驗證 (Verification)、準備(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)、卸載(Unloading)。 其中前五個部分(加載,驗證,準備,解析,初始化)統稱爲類加載,下面咱們就分 別來講一下這五個過程。
7.1.1 加載
加載階段也能夠稱爲「裝載」階段。 這個階段主要的操做是: 根據明確知道的class徹底限定名, 來獲取二進制classfile格式的字節流,簡單點說就是 找到文件系統中/jar包中/或存在於任何地方的「 class文件 」。 若是找不到二進制表示形式,則會拋出NoClassDefFound 錯誤。裝載階段並不會檢查 classfile 的語法和格式。類加載的整個過程主要由JVM和Java 的類加載系統共同完成, 固然具體到loading 階 段則是由JVM與具體的某一個類加載器(java.lang.classLoader)協做完成的。
7.1.2 校驗
連接過程的第一個階段是校驗 ,確保class文件裏的字節流信息符合當前虛擬機的要求,不會危害虛擬機的安全。校驗過程檢classfile 的語義,判斷常量池中的符號,並執行類型檢查, 主要目的是判斷字節碼的合法性,好比 magic number, 對版本號進行驗證。 這些檢查 過程當中可能會拋出 VerifyError , ClassFormatError 或 UnsupportedClassVersionError 。 由於classfile的驗證屬是連接階段的一部分,因此這個過程當中可能須要加載其餘類, 在某個類的加載過程當中,JVM必須加載其全部的超類和接口。 若是類層次結構有問題(例如,該類是本身的超類或接口,死循環了),則JVM將拋出 ClassCircularityError 。 而若是實現的接口並非一個 interface,或者聲明的超類是一個 interface,也會拋出 IncompatibleClassChangeError 。
7.1.3 準備
而後進入準備階段,這個階段將會建立靜態字段, 並將其初始化爲標準默認值(好比 null 或者 0值 ),並分配方法表,即在方法區中分配這些變量所使用的內存空間。 請注意,準備階段並未執行任何Java代碼。
例如:
public static int i = 1;
在準備階段 i 的值會被初始化爲0,後面在類初始化階段纔會執行賦值爲1; 可是下面若是使用final做爲靜態常量,某些JVM的行爲就不同了:
public static final int i = 1;
對應常量i,在準備階段就會被賦值1,其實這樣仍是比較puzzle,例如其餘語言 (C#)有直接的常量關鍵字const,讓告訴編譯器在編譯階段就替換成常量,相似 於宏指令,更簡單。
7.1.4 解析
而後進入可選的解析符號引用階段。 也就是解析常量池,主要有如下四種:類或接口的解析、字段解析、類方法解析、接 口方法解析。
簡單的來講就是咱們編寫的代碼中,當一個變量引用某個對象的時候,這個引用在 .class 文件中是以符號引用來存儲的(至關於作了一個索引記錄)。 在解析階段就須要將其解析並連接爲直接引用(至關於指向實際對象)。若是有了直 接引用,那引用的目標一定在堆中存在。加載一個class時, 須要加載全部的super類和super接口。
7.1.5 初始化
JVM規範明確規定, 必須在類的首次「主動使用」時才能執行類初始化。 初始化的過程包括執行:
- 類構造器方法
- static靜態變量賦值語句
- static靜態代碼塊
若是是一個子類進行初始化會先對其父類進行初始化,保證其父類在子類以前進行初 始化。因此其實在java中初始化一個類,那麼必然先初始化過 java.lang.Object 類,由於全部的java類都繼承自java.lang.Object。
7.2 類加載時機
瞭解了類的加載過程,咱們再看看類的初始化什麼時候會被觸發呢?JVM 規範枚舉了下述多種觸發狀況:
- 當虛擬機啓動時,初始化用戶指定的主類,就是啓動執行的 main方法所在的類;
- 當遇到用以新建目標類實例的 new 指令時,初始化 new 指令的目標類,就是 new一個類的時候要初始化
- 當遇到調用靜態方法的指令時,初始化該靜態方法所在的類;
- 當遇到訪問靜態字段的指令時,初始化該靜態字段所在的類;
- 子類的初始化會觸發父類的初始化;
- 若是一個接口定義了 default 方法,那麼直接實現或者間接實現該接口的類的初始化,會觸發該接口的初始化;
- 使用反射 API 對某個類進行反射調用時,初始化這個類,其實跟前面同樣,反射調用要麼是已經有實例了,要麼是靜態方法,都須要初始化;
- 當初次調用 MethodHandle 實例時,初始化該 MethodHandle 指向的方法所在的類。
同時如下幾種狀況不會執行類初始化:
- 經過子類引用父類的靜態字段,只會觸發父類的初始化,而不會觸發子類的初始化。
- 定義對象數組,不會觸發該類的初始化。
- 常量在編譯期間會存入調用類的常量池中,本質上並無直接引用定義常量的類,不會觸發定義常量所在的類。
- 經過類名獲取Class對象,不會觸發類的初始化,Hello.class不會讓Hello類初始化。
- 經過Class.forName加載指定類時,若是指定參數initialize爲false時,也不會觸發類初始化,其實這個參數是告訴虛擬機,是否要對類進行初始化。 Class.forName(「jvm.Hello」)默認會加載Hello類。
- 經過ClassLoader默認的loadClass方法,也不會觸發初始化動做(加載了,可是不初始化)。
7.3 類加載機制
類加載過程能夠描述爲「經過一個類的全限定名a.b.c.XXClass來獲取描述此類的Class 對象」,這個過程由「類加載器(ClassLoader)」來完成。這樣的好處在於,子類加載器能夠複用父加載器加載的類。系統自帶的類加載器分爲三種 :
- 啓動類加載器(BootstrapClassLoader)
啓動類加載器(bootstrap class loader): 它用來加載 Java 的核心類,是用原生 C++代碼來實現的,並不繼承自
java.lang.ClassLoader(負責加載JDK中 jre/lib/rt.jar裏全部的class)。它能夠看作是JVM自帶的,咱們再代碼層面沒法直接獲取到
啓動類加載器的引用,因此不容許直接操做它, 若是打印出來就是個 null 。舉例來講,java.lang.String是由啓動類加載器加載
的,因此 String.class.getClassLoader()就會返回null。可是後面能夠看到能夠經過命令行 參數影響它加載什麼。
-
擴展類加載器(ExtClassLoader)
-
擴展類加載器(extensions class loader):它負責加載JRE的擴展目錄,lib/ext 或者由java.ext.dirs系統屬性指定的目錄中的JAR包的類,代碼裏直接獲取它的父 類加載器爲null(由於沒法拿到啓動類加載器)。
-
應用類加載器(AppClassLoader)
-
應用類加載器(app class loader):它負責在JVM啓動時加載來自Java命令的classpath或者cp選項、java.class.path系統屬性指定的jar包和類路徑。在應用程序代碼裏能夠經過ClassLoader的靜態方法getSystemClassLoader()來獲取應用類加載器。若是沒有特別指定,則在沒有使用自定義類加載器狀況下,用戶自定義的類都由此加載器加載。
類加載機制有三個特色:
- 雙親委託:當一個自定義類加載器須要加載一個類,好比java.lang.String,它很懶,不會一上來就直接試圖加載它,而是先委託本身的父加載器去加載,父加載 器若是發現本身還有父加載器,會一直往前找,這樣只要上級加載器,好比啓動類加載器已經加載了某個類好比java.lang.String,全部的子加載器都不須要本身加載了。若是幾個類加載器都沒有加載到指定名稱的類,那麼會拋出 ClassNotFountException異常。
- 負責依賴:若是一個加載器在加載某個類的時候,發現這個類依賴於另外幾個類或接口,也會去嘗試加載這些依賴項。
- 緩存加載:爲了提高加載效率,消除重複加載,一旦某個類被一個類加載器加載,那麼它會緩存這個加載結果,不會重複加載。
</clinit></clinit></init></init>