Java技術原理詳解

一、Java 運行原理 
1、高級語言運行過程 
在程序真正運行在CPU上之前,必須要讓OS的kernel理解我們在編輯器或者IDE里根據每種語言的語法規則敲入的源代碼,kernel才能做出相關的調度,所以需要先將源代碼轉化成可執行的二進制文件,這個過程通常由編譯器完成。有些編譯器直接將源代碼編譯成機器碼,載入內存後CPU可以直接運行。而機器碼的格式與跟具體的CPU架構相關連,例如ARM CPU無法理解Intel CPU機器碼。因此,同樣的源代碼需要根據不同的硬件進行特定的編譯。高級語言轉換到低級語言的橋樑就是編譯器。程序員寫好源代碼,編譯器將源碼編譯成可執行的機碼,然後CPU讀取機器碼,執行程序。 
2、Java語言的執行過程 
這裏寫圖片描述 
寬泛地講,Java源代碼(.java)經過java編譯器(javac.exe)編譯之後,並沒有直接轉化爲機器碼,而是轉化成一種中間格式——字節碼(.class),字節碼再經過Java虛擬機解釋,轉化成機器碼,然後經由操作系統到達CPU運行。整個執行過程如下圖所示: 
這裏寫圖片描述 
Java的跨平臺是基於JVM虛擬機這一中間物來實現的,Java源程序經過編譯器編譯後生成虛擬機能夠理解的字節碼(ByteCode——class文件的內容),虛擬機將每一條要執行的字節碼送給解釋器,解釋器將其翻譯成特定系統上的機器碼,然後在特定的機器上運行。每一種平臺的解釋器是不同的,但是實現的虛擬機是相同的。 
這裏寫圖片描述 
3、JVM——Java Virtual Machine 
JVM是一個虛構出來的計算機,通過在實際的計算機上仿真模擬各種計算機功能來實現的。JVM有自己完善的硬件架構,如處理器、堆棧、寄存器等,還具有相應的指令系統。JVM 的主要工作是解釋自己的指令集(即字節碼)並映射到本地的 CPU 的指令集或 OS 的系統調用。 
三、 JVM的體系結構 
Class Loader:類裝載器,從入口處開始按需加載.class文件,填充這些數據到運行時數據區 
Execution Engine:執行引擎,JVM的CPU,不斷地取指令,JIT編譯翻譯執行字節碼,或者執行本地方法 
Runtime Data Areas:運行時數據區,核心區,運行的時候操作所分配的內存區,包括方法區、堆、java棧、PC寄存器、本地方法棧 
這裏寫圖片描述 
1、類加載器 
類加載器加載其實就是根據編譯後的Class文件,將Java字節碼載入JVM內存,並完成對運行數據處於的初始化工作,供執行引擎執行。 
類加載過程: 
裝載——鏈接(驗證,準備,解析)—— 初始化 
這裏寫圖片描述 
1.Loading:,找到二進制字節碼(Class文件)並加載至JVM內存中,標識一個被加載的類:類名+類所在的包名+Class Loader instance ID 
2.Linking:

  • Verifying:驗證元數據,文件格式,字節碼等,確保class文件包含的字節碼信息符合JVM的規範,以免危及JVM安全;

  • Preparing:準備分配給類所需要內存的數據結構,指示在類中定義的字段、方法和接口;

  • Resolving:對類中的所有屬性、方法進行驗證,以確保其需要調用的屬性、方法存在,以及具備應的權限;符號引用的轉換等

3.Initialing:初始化執行類中的靜態初始化代碼、構造器代碼以及靜態屬性 
類裝載器類型: 
啓動類裝載器:JVM實現的一部分; 
用戶自定義類裝載器:是Java程序的一部分,必須是ClassLoader類的子類。 
類裝載順序: 
Jvm啓動時,由Bootstrap向User-Defined方向加載類;應用進行Class Loader時,由User-Defined向Bootstrap方向查找並加載類; 
類加載採用父類委託制,子加載器能查詢父加載器已緩存類,委託只能從下到上,反之不行。類加載器可以加載一個類,但是它不能卸載一個類。但是類加載器可以被刪除或者被創建。一個類可以被不同的類加載器加載。 
這裏寫圖片描述 
Bootstrap ClassLoader 
JVM的根ClassLoader,它是用C++實現的,在JVM啓動的時候創建,負責裝載$JAVA_HOME中jre/lib/rt.jar(Sun JDK的實現)中所有class文件,這個jar中包含了Java規範定義的所有接口以及實現。 
Extension ClassLoader 
裝載除了基本的Java API以外的擴展類,它也負責裝載其他的安全擴展功能。 
System ClassLoader 
負責加載應用程序類,加載啓動參數中指定的Classpath中的jar包以及目錄,在Sun JDK中ClassLoader對應的類名爲AppClassLoader。 
User-Defined ClassLoader 
Java開發人員繼承ClassLoader抽象類自行實現的ClassLoader,基於自定義的ClassLoader可用於加載非Classpath中的jar以及目錄。 
2、執行引擎 
類加載器將.class文件載入內存之後,執行引擎以Java 字節碼指令爲單元,讀取Java字節碼;而後由解釋器或者即時編譯器(JIT Compiler)將字節碼轉化成平臺相關的機器碼。 
這裏寫圖片描述
JVM實現技術: 
解釋器:第一代JVM,一條一條地讀取,解釋並且執行字節碼指令。因爲它一條一條地解釋和執行指令,所以它可以很快地解釋字節碼,但是執行起來會比較慢。這是解釋執行的語言的一個缺點。字節碼這種“語言”基本來說是解釋執行的。 
這裏寫圖片描述 
即時編譯器(just-in-time compiler):第二代JVM,狹義來說是當某段代碼即將第一次被執行時進行編譯,將class類文件解釋成二進制文件後的結果緩存下來,當第二次執行時直接從緩存中取,因此JIT依賴更多內存緩存解釋的結果。JIT編譯是動態編譯的一種特例。JIT編譯一詞後來被泛化,時常與動態編譯等價;但要注意寬泛與狹義的JIT編譯所指的區別。 
這裏寫圖片描述 
自適應編譯器(adaptive compiler):柔和第一代和第二代JVM,也是動態編譯的一種,但它通常執行的時機比JIT編譯遲,先讓程序“以某種形式”先運行起來,收集一些信息之後再做動態編譯,也就是說在所有執行過的代碼裏只尋找一部分來編譯;而”收集信息”決定了編譯哪部分代碼,換個角度說“收集信息”就是在程序運行過程中監控代碼執行的頻率,自動緩存利用率高的代碼,這樣的編譯可以更加優化。這個”某種形式”可以稱爲“baseline execution“,可以由解釋器或簡單的JIT編譯器承擔。 
這裏寫圖片描述 
HotSpot是一個JVM的實現,得名於它得混合模式執行引擎(包括解釋器和自適應編譯器),這個JVM最初由Longview/Animorphic實現,隨着公司被Sun/JavaSoft收購而成爲Sun的JVM,並於JDK 1.3.0開始成爲Sun的Java SE的主要JVM。在Sun被Oracle收購後,現在HotSpot VM是Oracle的Java SE的主要JVM。HotSpot是較新的JVM,用來代替JIT(Just in Time), Java原先是把源代碼編譯爲字節碼在虛擬機執行,這樣執行速度較慢;而HotSpot將最需要編譯的“熱點”代碼編譯爲本地(原生native)代碼,如果已經被編譯成本地代碼的字節碼不再被頻繁調用了,那麼Hotspot VM會把編譯過的本地代碼從cache裏移除,並且重新按照解釋的方式來執行它,這樣顯着提高了性能。 HotSpot VM 參數可以分爲規則參數(standard options)和非規則參數(non-standard options)。Hotspot VM分爲Server VM和Client VM兩種,這兩種VM使用不同的JIT編譯器。 
3、運行時數據區 
當運行一個JVM Instance時,系統將分配給它一塊內存區域(大小可設置),這一內存區域由JVM自行管理。從這一塊內存中分出一塊用來存儲一些運行數據,例如創建的對象,傳遞給方法的參數,局部變量,返回值等等。這一塊內存就稱爲運行數據區域。運行數據區域可以劃分爲6大塊:Java棧、程序計數寄存器(PC寄存器)、本地方法棧(Native Method Stack)、Java堆、方法區域(包括運行常量池——Runtime Constant Pool)。其中每個線程私有程序計數器,JVM棧,本地方法棧,方法區和堆則由JVM實例中的所有線程共享,在同一個實例中可以啓用多個線程。 
這裏寫圖片描述 
程序計數器 
每個線程私有,線程啓動時創建,用來存放當前正在被執行的字節碼指令(JVM指令)的地址,如該方法爲native的,則PC寄存器中不存儲任何信息。 
JVM棧 
每個線程私有,線程啓動時創建。存放着一系列的棧幀(Stack Frame),JVM只能進行壓入(push)和彈出(pop)棧幀這兩種操作。每當調用一個方法時,JVM就往棧裏壓入一個棧幀,方法結束返回時彈出棧幀。如果方法執行時出現異常,可用printStackTrace等方法來查看棧的情況。棧的示意圖如下: 
這裏寫圖片描述 
每個棧幀包含三個部分:本地變量數組,操作數棧,方法所屬類的常量池引用

  • Local Variable Array:從0開始按順序存放方法所屬對象的引用、傳遞給方法的參數、局部變量。

  • Operand Stack:存放方法執行時的一些中間變量,JVM在執行方法時壓入或者彈出這些變量。其實,操作數棧是方法真正工作的地方,執行方法時,局部變量數組與操作數棧根據方法定義進行數據交換。

  • Reference to Constant Pool:當JVM執行到需要常量池的數據時,就是通過這個引用來訪問常量池的。棧幀中的數據還要負責處理方法的返回和異常。如果通過return返回,則將該方法的棧幀從Java棧中彈出。如果方法有返回值,則將返回值壓入到調用該方法的方法的操作數棧中。另外,數據區中還保存中該方法可能的異常表的引用。

本地方法棧 
當程序通過JNI(Java Native Interface)調用本地方法(如C或者C++代碼)時,就根據本地方法的語言類型建立相應的棧,此區域用於存儲每個native方法調用的狀態。 
堆(Heap) 
堆中存放的是程序創建的對象實例以及數組值的區域,可以認爲Java中所有通過new創建的對象的內存都在此分配。當堆中的空間無法滿足新建對象所需的內存開銷,會有溢出現象而導致程序崩潰,爲了避免溢出,當對象執行結束時,其佔據的內存空間需要等待GC(Garbage Collection)進行回收,因此這個區域對JVM的性能影響很大。 
注意: 堆是JVM中所有線程共享的,因此在其上進行對象內存的分配均需要進行加鎖,導致了new對象的開銷是比較大的 
Sun Hotspot JVM爲了提升對象內存分配的效率,對於所創建的線程都會分配一塊獨立的空間TLAB(Thread Local Allocation Buffer),其大小由JVM根據運行的情況計算而得,在TLAB上分配對象時不需要加鎖,因此JVM在給線程的對象分配內存時會盡量的在TLAB上分配,在這種情況下JVM中分配對象內存的性能和C基本是一樣高效的,但如果對象過大的話則仍然是直接使用堆空間分配。 
TLAB僅作用於新生代的Eden Space,因此在編寫Java程序時,通常多個小的對象比大的對象分配起來更加高效。 
方法區域 
每個線程共享的,啓動一個JVM實例時被創建,它用於存運行放常量池、所加載的類的信息(域、方法、靜態變量、final類型的常量)。開發人員在程序中通過Class對象中的getName、isInterface等方法獲取的數據都來源於方法區域,在一定的條件下它也會被GC,當方法區域需要使用的內存超過其允許的大小時,會拋出Out Of Memory的錯誤信息。不同的JVM實現方式在實現方法區域的時候會有所區別。Oracle的HotSpot稱之爲永久區域(Permanent Area)或者永久代(Permanent Generation)。 
運行常量池 
其空間從方法區域中分配,用來存放類、方法、接口的常量和域的引用信息,當一個方法或者域被引用的時候,JVM就通過運行常量池中的引用信息來查找方法和域在內存中的的實際地址。 
四、JVM垃圾回收 
Garbage Collection的基本原理: 
將內存中不再被使用的對象進行回收,GC中用於回收的方法稱爲收集器,由於GC需要消耗一些資源和時間,Java在對對象的生命週期特徵進行分析後,按照新生代、舊生代的方式來對對象進行收集,以儘可能的縮短GC對應用造成的暫停。 
垃圾回收算法 
1、按照基本回收策略分爲以下4種: 
Reference Counting:引用計數,比較古老的回收算法;原理是此對象有一個引用,即增加一個計數,刪除一個引用則減少一個計數。垃圾回收時,引用收集計數爲0的對象。此算法最致命的是無法處理循環引用的問題。

Mark-Sweep:標記-清除,此算法執行分兩階段;第一階段從引用根節點開始標記所有被引用的對象,第二階段遍歷整個堆,把未標記的對象清除。此算法需要暫停整個應用,同時,會產生內存碎片。 
這裏寫圖片描述 
Copying: 複製,把內存空間劃爲兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的對象複製到另外一個區域中。每次只處理正在使用中的對象,因此複製成本比較小,同時複製過去以後還能進行相應的內存整理,不會出現“碎片”問題;但是此算法的缺點就是需要兩倍內存空間。 
這裏寫圖片描述 
Mark-Compact:標記-整理,結合了Mark-Sweep和Copying兩個算法的優點;也分兩階段,第一階段從根節點開始標記所有被引用對象,第二階段遍歷整個堆,把清除未標記對象並且把存活對象“壓縮”到堆的其中一塊,按順序排放。避免了Mark-Sweep算法的碎片問題,同時也避免了Copying算法的空間問題。 
這裏寫圖片描述 
2、按分區對待的方式分爲以下2種

  • Incremental Collecting:增量收集,實時垃圾回收算法,即:在應用進行的同時進行垃圾回收。JDK5.0中的收集器沒有使用這種算法的。

  • Generational Collecting:分代收集,基於對對象生命週期分析後得出的垃圾回收算法。把對象分爲年青代、年老代、持久代,對不同生命週期的對象使用不同的算法(上述方式中的一個)進行回收。現在的垃圾回收器(從J2SE1.2開始)都是使用此算法的。

3、按系統線程分爲以下3種

  • 串行收集:串行收集使用單線程處理所有垃圾回收工作,因爲無需多線程交互,實現容易,而且效率比較高。但是,其侷限性是無法使用多處理器的優勢,所以此收集適合單處理器機器。當然,此收集器也可以用在小數據量(100M左右)情況下的多處理器機器上。

  • 並行收集:並行收集使用多線程處理垃圾回收工作,因而速度快,效率高。而且理論上CPU數目越多,越能體現出並行收集器的優勢。

  • 併發收集:相對於串行收集和並行收集而言,前面兩個在進行垃圾回收工作時,需要暫停整個運行環境,而只有垃圾回收程序在運行,因此,系統在垃圾回收時會有明顯的暫停,而且暫停時間會因爲堆越大而越長。

處理碎片 
由於不同Java對象存活時間是不一定的,因此,在程序運行一段時間以後,如果不進行內存整理,就會出現零散的內存碎片。碎片最直接的問題就是會導致無法 分配大塊的內存空間,以及程序運行效率降低。所以,在上面提到的基本垃圾回收算法中,“複製”方式和“標記-整理”方式,都可以解決碎片的問題。 
對象創建和對象回收 
垃圾回收線程是回收內存的,而程序運行線程則是消耗(或分配)內存的,一個回收內存,一個分配內存,從這點看,兩者是矛盾的。因此,在現有的垃圾回收方式 中,要進行垃圾回收前,一般都需要暫停整個應用(即:暫停內存的分配),然後進行垃圾回收,回收完成後再繼續應用。這種實現方式是最直接,而且最有效的解決二者矛盾的方式。 
但是這種方式有一個很明顯的弊端,就是當堆空間持續增大時,垃圾回收的時間也將會相應的持續增大,對應應用暫停的時間也會相應的增大。一些對相應時間要求很高的應用,比如最大暫停時間要求是幾百毫秒,那麼當堆空間大於幾個G時,就很有可能超過這個限制,在這種情況下,垃圾回收將會成爲系統運行的一個瓶頸。 爲解決這種矛盾,有了併發垃圾回收算法,使用這種算法,垃圾回收線程與程序運行線程同時運行。在這種方式下,解決了暫停的問題,但是因爲需要在新生成對象的同時又要回收對象,算法複雜性會大大增加,系統的處理能力也會相應降低,同時碎片問題將會比較難解決。

五、JRE(Java Runtime Environment)和JDK(Java Development Kit) 
JRE是指運行Java程序所必須的環境集合,包含JVM標準實現及Java核心類庫。JDK 是 Java 語言的軟件開發工具包,針對Java開發員的產品,是整個Java的核心,包括了Java運行環境JRE、Java工具和Java基礎類庫。如果運行Java程序,只需安裝JRE就可以了。如果編寫Java程序,需要安裝JDK。OpenJDK則是包含了開發與運行的開源實現。最主流的JDK是Sun公司發佈的JDK,除了Sun之外,還有很多公司和組織都開發了屬於自己的JDK,例如IBM,阿里等。 
根據應用領域的不同,JDK可分爲三種版本:

SE(Standard Edition)標準版,通常用的一個版本,從JDK 5.0開始,改名爲Java SE

EE(Enterprise Edition)企業版,使用這種JDK開發J2EE應用程序,從JDK 5.0開始,改名爲Java EE

ME(Micro Edition)微型版,主要用於移動設備、嵌入式設備上的Java應用程序,從JDK 5.0開始,改名爲Java ME

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章