更新時(shí)間:2019-09-04 13:57:57 來(lái)源:動(dòng)力節(jié)點(diǎn) 瀏覽2995次
一、Java運(yùn)行原理
1、高級(jí)語(yǔ)言運(yùn)行過(guò)程
在程序真正運(yùn)行在CPU上之前,必須要讓OS的kernel理解我們?cè)诰庉嬈骰蛘逫DE里根據(jù)每種語(yǔ)言的語(yǔ)法規(guī)則敲入的源代碼,kernel才能做出相關(guān)的調(diào)度,所以需要先將源代碼轉(zhuǎn)化成可執(zhí)行的二進(jìn)制文件,這個(gè)過(guò)程通常由編譯器完成。有些編譯器直接將源代碼編譯成機(jī)器碼,載入內(nèi)存后CPU可以直接運(yùn)行。而機(jī)器碼的格式與跟具體的CPU架構(gòu)相關(guān)連,例如ARMCPU無(wú)法理解IntelCPU機(jī)器碼。因此,同樣的源代碼需要根據(jù)不同的硬件進(jìn)行特定的編譯。高級(jí)語(yǔ)言轉(zhuǎn)換到低級(jí)語(yǔ)言的橋梁就是編譯器。程序員寫好源代碼,編譯器將源碼編譯成可執(zhí)行的機(jī)碼,然后CPU讀取機(jī)器碼,執(zhí)行程序。
2、Java語(yǔ)言的執(zhí)行過(guò)程
寬泛地講,Java源代碼(.java)經(jīng)過(guò)java編譯器(javac.exe)編譯之后,并沒(méi)有直接轉(zhuǎn)化為機(jī)器碼,而是轉(zhuǎn)化成一種中間格式——字節(jié)碼(.class),字節(jié)碼再經(jīng)過(guò)Java虛擬機(jī)解釋,轉(zhuǎn)化成機(jī)器碼,然后經(jīng)由操作系統(tǒng)到達(dá)CPU運(yùn)行。整個(gè)執(zhí)行過(guò)程如下圖所示:
Java的跨平臺(tái)是基于JVM虛擬機(jī)這一中間物來(lái)實(shí)現(xiàn)的,Java源程序經(jīng)過(guò)編譯器編譯后生成虛擬機(jī)能夠理解的字節(jié)碼(ByteCode——class文件的內(nèi)容),虛擬機(jī)將每一條要執(zhí)行的字節(jié)碼送給解釋器,解釋器將其翻譯成特定系統(tǒng)上的機(jī)器碼,然后在特定的機(jī)器上運(yùn)行。每一種平臺(tái)的解釋器是不同的,但是實(shí)現(xiàn)的虛擬機(jī)是相同的。
3、JVM——JavaVirtualMachine
JVM是一個(gè)虛構(gòu)出來(lái)的計(jì)算機(jī),通過(guò)在實(shí)際的計(jì)算機(jī)上仿真模擬各種計(jì)算機(jī)功能來(lái)實(shí)現(xiàn)的。JVM有自己完善的硬件架構(gòu),如處理器、堆棧、寄存器等,還具有相應(yīng)的指令系統(tǒng)。JVM的主要工作是解釋自己的指令集(即字節(jié)碼)并映射到本地的CPU的指令集或OS的系統(tǒng)調(diào)用。
三、JVM的體系結(jié)構(gòu)
ClassLoader:類裝載器,從入口處開(kāi)始按需加載.class文件,填充這些數(shù)據(jù)到運(yùn)行時(shí)數(shù)據(jù)區(qū)
ExecutionEngine:執(zhí)行引擎,JVM的CPU,不斷地取指令,JIT編譯翻譯執(zhí)行字節(jié)碼,或者執(zhí)行本地方法
RuntimeDataAreas:運(yùn)行時(shí)數(shù)據(jù)區(qū),核心區(qū),運(yùn)行的時(shí)候操作所分配的內(nèi)存區(qū),包括方法區(qū)、堆、java棧、PC寄存器、本地方法棧
1、類加載器
類加載器加載其實(shí)就是根據(jù)編譯后的Class文件,將Java字節(jié)碼載入JVM內(nèi)存,并完成對(duì)運(yùn)行數(shù)據(jù)處于的初始化工作,供執(zhí)行引擎執(zhí)行。
類加載過(guò)程:
裝載——鏈接(驗(yàn)證,準(zhǔn)備,解析)——初始化
1.Loading:,找到二進(jìn)制字節(jié)碼(Class文件)并加載至JVM內(nèi)存中,標(biāo)識(shí)一個(gè)被加載的類:類名+類所在的包名+ClassLoaderinstanceID
2.Linking:
Verifying:驗(yàn)證元數(shù)據(jù),文件格式,字節(jié)碼等,確保class文件包含的字節(jié)碼信息符合JVM的規(guī)范,以免危及JVM安全;
Preparing:準(zhǔn)備分配給類所需要內(nèi)存的數(shù)據(jù)結(jié)構(gòu),指示在類中定義的字段、方法和接口;
Resolving:對(duì)類中的所有屬性、方法進(jìn)行驗(yàn)證,以確保其需要調(diào)用的屬性、方法存在,以及具備應(yīng)的權(quán)限;符號(hào)引用的轉(zhuǎn)換等
3.Initialing:初始化執(zhí)行類中的靜態(tài)初始化代碼、構(gòu)造器代碼以及靜態(tài)屬性
類裝載器類型:
啟動(dòng)類裝載器:JVM實(shí)現(xiàn)的一部分;
用戶自定義類裝載器:是Java程序的一部分,必須是ClassLoader類的子類。
類裝載順序:
Jvm啟動(dòng)時(shí),由Bootstrap向User-Defined方向加載類;應(yīng)用進(jìn)行ClassLoader時(shí),由User-Defined向Bootstrap方向查找并加載類;
類加載采用父類委托制,子加載器能查詢父加載器已緩存類,委托只能從下到上,反之不行。類加載器可以加載一個(gè)類,但是它不能卸載一個(gè)類。但是類加載器可以被刪除或者被創(chuàng)建。一個(gè)類可以被不同的類加載器加載。
BootstrapClassLoader
JVM的根ClassLoader,它是用C++實(shí)現(xiàn)的,在JVM啟動(dòng)的時(shí)候創(chuàng)建,負(fù)責(zé)裝載$JAVA_HOME中jre/lib/rt.jar(SunJDK的實(shí)現(xiàn))中所有class文件,這個(gè)jar中包含了Java規(guī)范定義的所有接口以及實(shí)現(xiàn)。
ExtensionClassLoader
裝載除了基本的JavaAPI以外的擴(kuò)展類,它也負(fù)責(zé)裝載其他的安全擴(kuò)展功能。
SystemClassLoader
負(fù)責(zé)加載應(yīng)用程序類,加載啟動(dòng)參數(shù)中指定的Classpath中的jar包以及目錄,在SunJDK中ClassLoader對(duì)應(yīng)的類名為AppClassLoader。
User-DefinedClassLoader
Java開(kāi)發(fā)人員繼承ClassLoader抽象類自行實(shí)現(xiàn)的ClassLoader,基于自定義的ClassLoader可用于加載非Classpath中的jar以及目錄。
2、執(zhí)行引擎
類加載器將.class文件載入內(nèi)存之后,執(zhí)行引擎以Java字節(jié)碼指令為單元,讀取Java字節(jié)碼;而后由解釋器或者即時(shí)編譯器(JITCompiler)將字節(jié)碼轉(zhuǎn)化成平臺(tái)相關(guān)的機(jī)器碼。
JVM實(shí)現(xiàn)技術(shù):
解釋器:第一代JVM,一條一條地讀取,解釋并且執(zhí)行字節(jié)碼指令。因?yàn)樗粭l一條地解釋和執(zhí)行指令,所以它可以很快地解釋字節(jié)碼,但是執(zhí)行起來(lái)會(huì)比較慢。這是解釋執(zhí)行的語(yǔ)言的一個(gè)缺點(diǎn)。字節(jié)碼這種“語(yǔ)言”基本來(lái)說(shuō)是解釋執(zhí)行的。
即時(shí)編譯器(just-in-timecompiler):第二代JVM,狹義來(lái)說(shuō)是當(dāng)某段代碼即將第一次被執(zhí)行時(shí)進(jìn)行編譯,將class類文件解釋成二進(jìn)制文件后的結(jié)果緩存下來(lái),當(dāng)?shù)诙螆?zhí)行時(shí)直接從緩存中取,因此JIT依賴更多內(nèi)存緩存解釋的結(jié)果。JIT編譯是動(dòng)態(tài)編譯的一種特例。JIT編譯一詞后來(lái)被泛化,時(shí)常與動(dòng)態(tài)編譯等價(jià);但要注意寬泛與狹義的JIT編譯所指的區(qū)別。
自適應(yīng)編譯器(adaptivecompiler):柔和第一代和第二代JVM,也是動(dòng)態(tài)編譯的一種,但它通常執(zhí)行的時(shí)機(jī)比JIT編譯遲,先讓程序“以某種形式”先運(yùn)行起來(lái),收集一些信息之后再做動(dòng)態(tài)編譯,也就是說(shuō)在所有執(zhí)行過(guò)的代碼里只尋找一部分來(lái)編譯;而”收集信息”決定了編譯哪部分代碼,換個(gè)角度說(shuō)“收集信息”就是在程序運(yùn)行過(guò)程中監(jiān)控代碼執(zhí)行的頻率,自動(dòng)緩存利用率高的代碼,這樣的編譯可以更加優(yōu)化。這個(gè)”某種形式”可以稱為“baselineexecution“,可以由解釋器或簡(jiǎn)單的JIT編譯器承擔(dān)。
HotSpot是一個(gè)JVM的實(shí)現(xiàn),得名于它得混合模式執(zhí)行引擎(包括解釋器和自適應(yīng)編譯器),這個(gè)JVM最初由Longview/Animorphic實(shí)現(xiàn),隨著公司被Sun/JavaSoft收購(gòu)而成為Sun的JVM,并于JDK1.3.0開(kāi)始成為Sun的JavaSE的主要JVM。在Sun被Oracle收購(gòu)后,現(xiàn)在HotSpotVM是Oracle的JavaSE的主要JVM。HotSpot是較新的JVM,用來(lái)代替JIT(JustinTime),Java原先是把源代碼編譯為字節(jié)碼在虛擬機(jī)執(zhí)行,這樣執(zhí)行速度較慢;而HotSpot將最需要編譯的“熱點(diǎn)”代碼編譯為本地(原生native)代碼,如果已經(jīng)被編譯成本地代碼的字節(jié)碼不再被頻繁調(diào)用了,那么HotspotVM會(huì)把編譯過(guò)的本地代碼從cache里移除,并且重新按照解釋的方式來(lái)執(zhí)行它,這樣顯著提高了性能。HotSpotVM參數(shù)可以分為規(guī)則參數(shù)(standardoptions)和非規(guī)則參數(shù)(non-standardoptions)。HotspotVM分為ServerVM和ClientVM兩種,這兩種VM使用不同的JIT編譯器。
3、運(yùn)行時(shí)數(shù)據(jù)區(qū)
當(dāng)運(yùn)行一個(gè)JVMInstance時(shí),系統(tǒng)將分配給它一塊內(nèi)存區(qū)域(大小可設(shè)置),這一內(nèi)存區(qū)域由JVM自行管理。從這一塊內(nèi)存中分出一塊用來(lái)存儲(chǔ)一些運(yùn)行數(shù)據(jù),例如創(chuàng)建的對(duì)象,傳遞給方法的參數(shù),局部變量,返回值等等。這一塊內(nèi)存就稱為運(yùn)行數(shù)據(jù)區(qū)域。運(yùn)行數(shù)據(jù)區(qū)域可以劃分為6大塊:Java棧、程序計(jì)數(shù)寄存器(PC寄存器)、本地方法棧(NativeMethodStack)、Java堆、方法區(qū)域(包括運(yùn)行常量池——RuntimeConstantPool)。其中每個(gè)線程私有程序計(jì)數(shù)器,JVM棧,本地方法棧,方法區(qū)和堆則由JVM實(shí)例中的所有線程共享,在同一個(gè)實(shí)例中可以啟用多個(gè)線程。
程序計(jì)數(shù)器
每個(gè)線程私有,線程啟動(dòng)時(shí)創(chuàng)建,用來(lái)存放當(dāng)前正在被執(zhí)行的字節(jié)碼指令(JVM指令)的地址,如該方法為native的,則PC寄存器中不存儲(chǔ)任何信息。
JVM棧
每個(gè)線程私有,線程啟動(dòng)時(shí)創(chuàng)建。存放著一系列的棧幀(StackFrame),JVM只能進(jìn)行壓入(push)和彈出(pop)棧幀這兩種操作。每當(dāng)調(diào)用一個(gè)方法時(shí),JVM就往棧里壓入一個(gè)棧幀,方法結(jié)束返回時(shí)彈出棧幀。如果方法執(zhí)行時(shí)出現(xiàn)異常,可用printStackTrace等方法來(lái)查看棧的情況。棧的示意圖如下:
每個(gè)棧幀包含三個(gè)部分:本地變量數(shù)組,操作數(shù)棧,方法所屬類的常量池引用
LocalVariableArray:從0開(kāi)始按順序存放方法所屬對(duì)象的引用、傳遞給方法的參數(shù)、局部變量。
OperandStack:存放方法執(zhí)行時(shí)的一些中間變量,JVM在執(zhí)行方法時(shí)壓入或者彈出這些變量。其實(shí),操作數(shù)棧是方法真正工作的地方,執(zhí)行方法時(shí),局部變量數(shù)組與操作數(shù)棧根據(jù)方法定義進(jìn)行數(shù)據(jù)交換。
ReferencetoConstantPool:當(dāng)JVM執(zhí)行到需要常量池的數(shù)據(jù)時(shí),就是通過(guò)這個(gè)引用來(lái)訪問(wèn)常量池的。棧幀中的數(shù)據(jù)還要負(fù)責(zé)處理方法的返回和異常。如果通過(guò)return返回,則將該方法的棧幀從Java棧中彈出。如果方法有返回值,則將返回值壓入到調(diào)用該方法的方法的操作數(shù)棧中。另外,數(shù)據(jù)區(qū)中還保存中該方法可能的異常表的引用。
本地方法棧
當(dāng)程序通過(guò)JNI(JavaNativeInterface)調(diào)用本地方法(如C或者C++代碼)時(shí),就根據(jù)本地方法的語(yǔ)言類型建立相應(yīng)的棧,此區(qū)域用于存儲(chǔ)每個(gè)native方法調(diào)用的狀態(tài)。
堆(Heap)
堆中存放的是程序創(chuàng)建的對(duì)象實(shí)例以及數(shù)組值的區(qū)域,可以認(rèn)為Java中所有通過(guò)new創(chuàng)建的對(duì)象的內(nèi)存都在此分配。當(dāng)堆中的空間無(wú)法滿足新建對(duì)象所需的內(nèi)存開(kāi)銷,會(huì)有溢出現(xiàn)象而導(dǎo)致程序崩潰,為了避免溢出,當(dāng)對(duì)象執(zhí)行結(jié)束時(shí),其占據(jù)的內(nèi)存空間需要等待GC(GarbageCollection)進(jìn)行回收,因此這個(gè)區(qū)域?qū)VM的性能影響很大。
注意:堆是JVM中所有線程共享的,因此在其上進(jìn)行對(duì)象內(nèi)存的分配均需要進(jìn)行加鎖,導(dǎo)致了new對(duì)象的開(kāi)銷是比較大的
SunHotspotJVM為了提升對(duì)象內(nèi)存分配的效率,對(duì)于所創(chuàng)建的線程都會(huì)分配一塊獨(dú)立的空間TLAB(ThreadLocalAllocationBuffer),其大小由JVM根據(jù)運(yùn)行的情況計(jì)算而得,在TLAB上分配對(duì)象時(shí)不需要加鎖,因此JVM在給線程的對(duì)象分配內(nèi)存時(shí)會(huì)盡量的在TLAB上分配,在這種情況下JVM中分配對(duì)象內(nèi)存的性能和C基本是一樣高效的,但如果對(duì)象過(guò)大的話則仍然是直接使用堆空間分配。
TLAB僅作用于新生代的EdenSpace,因此在編寫Java程序時(shí),通常多個(gè)小的對(duì)象比大的對(duì)象分配起來(lái)更加高效。
方法區(qū)域
每個(gè)線程共享的,啟動(dòng)一個(gè)JVM實(shí)例時(shí)被創(chuàng)建,它用于存運(yùn)行放常量池、所加載的類的信息(域、方法、靜態(tài)變量、final類型的常量)。開(kāi)發(fā)人員在程序中通過(guò)Class對(duì)象中的getName、isInterface等方法獲取的數(shù)據(jù)都來(lái)源于方法區(qū)域,在一定的條件下它也會(huì)被GC,當(dāng)方法區(qū)域需要使用的內(nèi)存超過(guò)其允許的大小時(shí),會(huì)拋出OutOfMemory的錯(cuò)誤信息。不同的JVM實(shí)現(xiàn)方式在實(shí)現(xiàn)方法區(qū)域的時(shí)候會(huì)有所區(qū)別。Oracle的HotSpot稱之為永久區(qū)域(PermanentArea)或者永久代(PermanentGeneration)。
運(yùn)行常量池
其空間從方法區(qū)域中分配,用來(lái)存放類、方法、接口的常量和域的引用信息,當(dāng)一個(gè)方法或者域被引用的時(shí)候,JVM就通過(guò)運(yùn)行常量池中的引用信息來(lái)查找方法和域在內(nèi)存中的的實(shí)際地址。
四、JVM垃圾回收
GarbageCollection的基本原理:
將內(nèi)存中不再被使用的對(duì)象進(jìn)行回收,GC中用于回收的方法稱為收集器,由于GC需要消耗一些資源和時(shí)間,Java在對(duì)對(duì)象的生命周期特征進(jìn)行分析后,按照新生代、舊生代的方式來(lái)對(duì)對(duì)象進(jìn)行收集,以盡可能的縮短GC對(duì)應(yīng)用造成的暫停。
垃圾回收算法
1、按照基本回收策略分為以下4種:
ReferenceCounting:引用計(jì)數(shù),比較古老的回收算法;原理是此對(duì)象有一個(gè)引用,即增加一個(gè)計(jì)數(shù),刪除一個(gè)引用則減少一個(gè)計(jì)數(shù)。垃圾回收時(shí),引用收集計(jì)數(shù)為0的對(duì)象。此算法最致命的是無(wú)法處理循環(huán)引用的問(wèn)題。
Mark-Sweep:標(biāo)記-清除,此算法執(zhí)行分兩階段;第一階段從引用根節(jié)點(diǎn)開(kāi)始標(biāo)記所有被引用的對(duì)象,第二階段遍歷整個(gè)堆,把未標(biāo)記的對(duì)象清除。此算法需要暫停整個(gè)應(yīng)用,同時(shí),會(huì)產(chǎn)生內(nèi)存碎片。
Copying:復(fù)制,把內(nèi)存空間劃為兩個(gè)相等的區(qū)域,每次只使用其中一個(gè)區(qū)域。垃圾回收時(shí),遍歷當(dāng)前使用區(qū)域,把正在使用中的對(duì)象復(fù)制到另外一個(gè)區(qū)域中。每次只處理正在使用中的對(duì)象,因此復(fù)制成本比較小,同時(shí)復(fù)制過(guò)去以后還能進(jìn)行相應(yīng)的內(nèi)存整理,不會(huì)出現(xiàn)“碎片”問(wèn)題;但是此算法的缺點(diǎn)就是需要兩倍內(nèi)存空間。
Mark-Compact:標(biāo)記-整理,結(jié)合了Mark-Sweep和Copying兩個(gè)算法的優(yōu)點(diǎn);也分兩階段,第一階段從根節(jié)點(diǎn)開(kāi)始標(biāo)記所有被引用對(duì)象,第二階段遍歷整個(gè)堆,把清除未標(biāo)記對(duì)象并且把存活對(duì)象“壓縮”到堆的其中一塊,按順序排放。避免了Mark-Sweep算法的碎片問(wèn)題,同時(shí)也避免了Copying算法的空間問(wèn)題。
2、按分區(qū)對(duì)待的方式分為以下2種
IncrementalCollecting:增量收集,實(shí)時(shí)垃圾回收算法,即:在應(yīng)用進(jìn)行的同時(shí)進(jìn)行垃圾回收。JDK5.0中的收集器沒(méi)有使用這種算法的。
GenerationalCollecting:分代收集,基于對(duì)對(duì)象生命周期分析后得出的垃圾回收算法。把對(duì)象分為年青代、年老代、持久代,對(duì)不同生命周期的對(duì)象使用不同的算法(上述方式中的一個(gè))進(jìn)行回收。現(xiàn)在的垃圾回收器(從J2SE1.2開(kāi)始)都是使用此算法的。
3、按系統(tǒng)線程分為以下3種
串行收集:串行收集使用單線程處理所有垃圾回收工作,因?yàn)闊o(wú)需多線程交互,實(shí)現(xiàn)容易,而且效率比較高。但是,其局限性是無(wú)法使用多處理器的優(yōu)勢(shì),所以此收集適合單處理器機(jī)器。當(dāng)然,此收集器也可以用在小數(shù)據(jù)量(100M左右)情況下的多處理器機(jī)器上。
并行收集:并行收集使用多線程處理垃圾回收工作,因而速度快,效率高。而且理論上CPU數(shù)目越多,越能體現(xiàn)出并行收集器的優(yōu)勢(shì)。
并發(fā)收集:相對(duì)于串行收集和并行收集而言,前面兩個(gè)在進(jìn)行垃圾回收工作時(shí),需要暫停整個(gè)運(yùn)行環(huán)境,而只有垃圾回收程序在運(yùn)行,因此,系統(tǒng)在垃圾回收時(shí)會(huì)有明顯的暫停,而且暫停時(shí)間會(huì)因?yàn)槎言酱蠖介L(zhǎng)。
處理碎片
由于不同Java對(duì)象存活時(shí)間是不一定的,因此,在程序運(yùn)行一段時(shí)間以后,如果不進(jìn)行內(nèi)存整理,就會(huì)出現(xiàn)零散的內(nèi)存碎片。碎片最直接的問(wèn)題就是會(huì)導(dǎo)致無(wú)法分配大塊的內(nèi)存空間,以及程序運(yùn)行效率降低。所以,在上面提到的基本垃圾回收算法中,“復(fù)制”方式和“標(biāo)記-整理”方式,都可以解決碎片的問(wèn)題。
對(duì)象創(chuàng)建和對(duì)象回收
垃圾回收線程是回收內(nèi)存的,而程序運(yùn)行線程則是消耗(或分配)內(nèi)存的,一個(gè)回收內(nèi)存,一個(gè)分配內(nèi)存,從這點(diǎn)看,兩者是矛盾的。因此,在現(xiàn)有的垃圾回收方式中,要進(jìn)行垃圾回收前,一般都需要暫停整個(gè)應(yīng)用(即:暫停內(nèi)存的分配),然后進(jìn)行垃圾回收,回收完成后再繼續(xù)應(yīng)用。這種實(shí)現(xiàn)方式是最直接,而且最有效的解決二者矛盾的方式。
但是這種方式有一個(gè)很明顯的弊端,就是當(dāng)堆空間持續(xù)增大時(shí),垃圾回收的時(shí)間也將會(huì)相應(yīng)的持續(xù)增大,對(duì)應(yīng)應(yīng)用暫停的時(shí)間也會(huì)相應(yīng)的增大。一些對(duì)相應(yīng)時(shí)間要求很高的應(yīng)用,比如最大暫停時(shí)間要求是幾百毫秒,那么當(dāng)堆空間大于幾個(gè)G時(shí),就很有可能超過(guò)這個(gè)限制,在這種情況下,垃圾回收將會(huì)成為系統(tǒng)運(yùn)行的一個(gè)瓶頸。為解決這種矛盾,有了并發(fā)垃圾回收算法,使用這種算法,垃圾回收線程與程序運(yùn)行線程同時(shí)運(yùn)行。在這種方式下,解決了暫停的問(wèn)題,但是因?yàn)樾枰谛律蓪?duì)象的同時(shí)又要回收對(duì)象,算法復(fù)雜性會(huì)大大增加,系統(tǒng)的處理能力也會(huì)相應(yīng)降低,同時(shí)碎片問(wèn)題將會(huì)比較難解決。
五、JRE(JavaRuntimeEnvironment)和JDK(JavaDevelopmentKit)
JRE是指運(yùn)行Java程序所必須的環(huán)境集合,包含JVM標(biāo)準(zhǔn)實(shí)現(xiàn)及Java核心類庫(kù)。JDK是Java語(yǔ)言的軟件開(kāi)發(fā)工具包,針對(duì)Java開(kāi)發(fā)員的產(chǎn)品,是整個(gè)Java的核心,包括了Java運(yùn)行環(huán)境JRE、Java工具和Java基礎(chǔ)類庫(kù)。如果運(yùn)行Java程序,只需安裝JRE就可以了。如果編寫Java程序,需要安裝JDK。OpenJDK則是包含了開(kāi)發(fā)與運(yùn)行的開(kāi)源實(shí)現(xiàn)。最主流的JDK是Sun公司發(fā)布的JDK,除了Sun之外,還有很多公司和組織都開(kāi)發(fā)了屬于自己的JDK,例如IBM,阿里等。
根據(jù)應(yīng)用領(lǐng)域的不同,JDK可分為三種版本:
SE(StandardEdition)標(biāo)準(zhǔn)版,通常用的一個(gè)版本,從JDK5.0開(kāi)始,改名為JavaSE
EE(EnterpriseEdition)企業(yè)版,使用這種JDK開(kāi)發(fā)J2EE應(yīng)用程序,從JDK5.0開(kāi)始,改名為JavaEE
ME(MicroEdition)微型版,主要用于移動(dòng)設(shè)備、嵌入式設(shè)備上的Java應(yīng)用程序,從JDK5.0開(kāi)始,改名為JavaME
(轉(zhuǎn))Java詳解JVM工作原理和流程
作為一名Java使用者,掌握J(rèn)VM的體系結(jié)構(gòu)也是必須的。
說(shuō)起Java,人們首先想到的是Java編程語(yǔ)言,然而事實(shí)上,Java是一種技術(shù),它由四方面組成:Java編程語(yǔ)言、Java類文件格式、Java虛擬機(jī)和Java應(yīng)用程序接口(JavaAPI)。它們的關(guān)系如下圖所示:
運(yùn)行期環(huán)境代表著Java平臺(tái),開(kāi)發(fā)人員編寫Java代碼(.java文件),然后將之編譯成字節(jié)碼(.class文件),再然后字節(jié)碼被裝入內(nèi)存,一旦字節(jié)碼進(jìn)入虛擬機(jī),它就會(huì)被解釋器解釋執(zhí)行,或者是被即時(shí)代碼發(fā)生器有選擇的轉(zhuǎn)換成機(jī)器碼執(zhí)行。
Java平臺(tái)由Java虛擬機(jī)和Java應(yīng)用程序接口搭建,Java語(yǔ)言則是進(jìn)入這個(gè)平臺(tái)的通道,用Java語(yǔ)言編寫并編譯的程序可以運(yùn)行在這個(gè)平臺(tái)上。這個(gè)平臺(tái)的結(jié)構(gòu)如下圖所示:
在Java平臺(tái)的結(jié)構(gòu)中,可以看出,Java虛擬機(jī)(JVM)處在核心的位置,是程序與底層操作系統(tǒng)和硬件無(wú)關(guān)的關(guān)鍵。它的下方是移植接口,移植接口由兩部分組成:適配器和Java操作系統(tǒng),其中依賴于平臺(tái)的部分稱為適配器;JVM通過(guò)移植接口在具體的平臺(tái)和操作系統(tǒng)上實(shí)現(xiàn);在JVM的上方是Java的基本類庫(kù)和擴(kuò)展類庫(kù)以及它們的API,利用JavaAPI編寫的應(yīng)用程序(application)和小程序(Javaapplet)可以在任何Java平臺(tái)上運(yùn)行而無(wú)需考慮底層平臺(tái),就是因?yàn)橛蠮ava虛擬機(jī)(JVM)實(shí)現(xiàn)了程序與操作系統(tǒng)的分離,從而實(shí)現(xiàn)了Java的平臺(tái)無(wú)關(guān)性。
JVM在它的生存周期中有一個(gè)明確的任務(wù),那就是運(yùn)行Java程序,因此當(dāng)Java程序啟動(dòng)的時(shí)候,就產(chǎn)生JVM的一個(gè)實(shí)例;當(dāng)程序運(yùn)行結(jié)束的時(shí)候,該實(shí)例也跟著消失了。下面我們從JVM的體系結(jié)構(gòu)和它的運(yùn)行過(guò)程這兩個(gè)方面來(lái)對(duì)它進(jìn)行比較深入的研究。
1、Java虛擬機(jī)的體系結(jié)構(gòu)
·每個(gè)JVM都有兩種機(jī)制:
①類裝載子系統(tǒng):裝載具有適合名稱的類或接口
②執(zhí)行引擎:負(fù)責(zé)執(zhí)行包含在已裝載的類或接口中的指令
·每個(gè)JVM都包含:
方法區(qū)、Java堆、Java棧、本地方法棧、指令計(jì)數(shù)器及其他隱含寄存器
對(duì)于JVM的學(xué)習(xí),在我看來(lái)這么幾個(gè)部分最重要:
Java代碼編譯和執(zhí)行的整個(gè)過(guò)程
JVM內(nèi)存管理及垃圾回收機(jī)制
下面分別對(duì)這幾部分進(jìn)行說(shuō)明:
2、Java代碼編譯和執(zhí)行的整個(gè)過(guò)程
也正如前面所說(shuō),Java代碼的編譯和執(zhí)行的整個(gè)過(guò)程大概是:開(kāi)發(fā)人員編寫Java代碼(.java文件),然后將之編譯成字節(jié)碼(.class文件),再然后字節(jié)碼被裝入內(nèi)存,一旦字節(jié)碼進(jìn)入虛擬機(jī),它就會(huì)被解釋器解釋執(zhí)行,或者是被即時(shí)代碼發(fā)生器有選擇的轉(zhuǎn)換成機(jī)器碼執(zhí)行。
(1)Java代碼編譯是由Java源碼編譯器來(lái)完成,也就是Java代碼到JVM字節(jié)碼(.class文件)的過(guò)程。流程圖如下所示:
(2)Java字節(jié)碼的執(zhí)行是由JVM執(zhí)行引擎來(lái)完成,流程圖如下所示:
Java代碼編譯和執(zhí)行的整個(gè)過(guò)程包含了以下三個(gè)重要的機(jī)制:
·Java源碼編譯機(jī)制
·類加載機(jī)制
·類執(zhí)行機(jī)制
(1)Java源碼編譯機(jī)制
Java源碼編譯由以下三個(gè)過(guò)程組成:
①分析和輸入到符號(hào)表
②注解處理
③語(yǔ)義分析和生成class文件
流程圖如下所示:
最后生成的class文件由以下部分組成:
①結(jié)構(gòu)信息:包括class文件格式版本號(hào)及各部分的數(shù)量與大小的信息
②元數(shù)據(jù):對(duì)應(yīng)于Java源碼中聲明與常量的信息。包含類/繼承的超類/實(shí)現(xiàn)的接口的聲明信息、域與方法聲明信息和常量池
③方法信息:對(duì)應(yīng)Java源碼中語(yǔ)句和表達(dá)式對(duì)應(yīng)的信息。包含字節(jié)碼、異常處理器表、求值棧與局部變量區(qū)大小、求值棧的類型記錄、調(diào)試符號(hào)信息
(2)類加載機(jī)制
JVM的類加載是通過(guò)ClassLoader及其子類來(lái)完成的,類的層次關(guān)系和加載順序可以由下圖來(lái)描述:
①BootstrapClassLoader
負(fù)責(zé)加載$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++實(shí)現(xiàn),不是ClassLoader子類
②ExtensionClassLoader
負(fù)責(zé)加載java平臺(tái)中擴(kuò)展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包
③AppClassLoader
負(fù)責(zé)記載classpath中指定的jar包及目錄中class
④CustomClassLoader
屬于應(yīng)用程序根據(jù)自身需要自定義的ClassLoader,如tomcat、jboss都會(huì)根據(jù)j2ee規(guī)范自行實(shí)現(xiàn)ClassLoader
加載過(guò)程中會(huì)先檢查類是否被已加載,檢查順序是自底向上,從CustomClassLoader到BootStrapClassLoader逐層檢查,只要某個(gè)classloader已加載就視為已加載此類,保證此類只所有ClassLoader加載一次。而加載的順序是自頂向下,也就是由上層來(lái)逐層嘗試加載此類。
(3)類執(zhí)行機(jī)制
JVM是基于堆棧的虛擬機(jī)。JVM為每個(gè)新創(chuàng)建的線程都分配一個(gè)堆棧.也就是說(shuō),對(duì)于一個(gè)Java程序來(lái)說(shuō),它的運(yùn)行就是通過(guò)對(duì)堆棧的操作來(lái)完成的。堆棧以幀為單位保存線程的狀態(tài)。JVM對(duì)堆棧只進(jìn)行兩種操作:以幀為單位的壓棧和出棧操作。
JVM執(zhí)行class字節(jié)碼,線程創(chuàng)建后,都會(huì)產(chǎn)生程序計(jì)數(shù)器(PC)和棧(Stack),程序計(jì)數(shù)器存放下一條要執(zhí)行的指令在方法內(nèi)的偏移量,棧中存放一個(gè)個(gè)棧幀,每個(gè)棧幀對(duì)應(yīng)著每個(gè)方法的每次調(diào)用,而棧幀又是有局部變量區(qū)和操作數(shù)棧兩部分組成,局部變量區(qū)用于存放方法中的局部變量和參數(shù),操作數(shù)棧中用于存放方法執(zhí)行過(guò)程中產(chǎn)生的中間結(jié)果。棧的結(jié)構(gòu)如下圖所示:
3、JVM內(nèi)存管理及垃圾回收機(jī)制
JVM內(nèi)存結(jié)構(gòu)分為:方法區(qū)(method),棧內(nèi)存(stack),堆內(nèi)存(heap),本地方法棧(java中的jni調(diào)用),結(jié)構(gòu)圖如下所示:
(1)堆內(nèi)存(heap)
所有通過(guò)new創(chuàng)建的對(duì)象的內(nèi)存都在堆中分配,其大小可以通過(guò)-Xmx和-Xms來(lái)控制。
操作系統(tǒng)有一個(gè)記錄空閑內(nèi)存地址的鏈表,當(dāng)系統(tǒng)收到程序的申請(qǐng)時(shí),會(huì)遍歷該鏈表,尋找第一個(gè)空間大于所申請(qǐng)空間的堆結(jié)點(diǎn),然后將該結(jié)點(diǎn)從空閑結(jié)點(diǎn)鏈表中刪除,并將該結(jié)點(diǎn)的空間分配給程序,另外,對(duì)于大多數(shù)系統(tǒng),會(huì)在這塊內(nèi)存空間中的首地址處記錄本次分配的大小,這樣代碼中的delete語(yǔ)句才能正確的釋放本內(nèi)存空間。但由于找到的堆結(jié)點(diǎn)的大小不一定正好等于申請(qǐng)的大小,系統(tǒng)會(huì)自動(dòng)的將多余的那部分重新放入空閑鏈表中。這時(shí)由new分配的內(nèi)存,一般速度比較慢,而且容易產(chǎn)生內(nèi)存碎片,不過(guò)用起來(lái)最方便。另外,在WINDOWS下,最好的方式是用VirtualAlloc分配內(nèi)存,它不是在堆,也不是在棧,而是直接在進(jìn)程的地址空間中保留一塊內(nèi)存,雖然這種方法用起來(lái)最不方便,但是速度快,也是最靈活的。堆內(nèi)存是向高地址擴(kuò)展的數(shù)據(jù)結(jié)構(gòu),是不連續(xù)的內(nèi)存區(qū)域。由于系統(tǒng)是用鏈表來(lái)存儲(chǔ)的空閑內(nèi)存地址的,自然是不連續(xù)的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限于計(jì)算機(jī)系統(tǒng)中有效的虛擬內(nèi)存。由此可見(jiàn),堆獲得的空間比較靈活,也比較大。
(2)棧內(nèi)存(stack)
在Windows下,棧是向低地址擴(kuò)展的數(shù)據(jù)結(jié)構(gòu),是一塊連續(xù)的內(nèi)存區(qū)域。這句話的意思是棧頂?shù)牡刂泛蜅5淖畲笕萘渴窍到y(tǒng)預(yù)先規(guī)定好的,在WINDOWS下,棧的大小是固定的(是一個(gè)編譯時(shí)就確定的常數(shù)),如果申請(qǐng)的空間超過(guò)棧的剩余空間時(shí),將提示overflow。因此,能從棧獲得的空間較小。只要棧的剩余空間大于所申請(qǐng)空間,系統(tǒng)將為程序提供內(nèi)存,否則將報(bào)異常提示棧溢出。由系統(tǒng)自動(dòng)分配,速度較快。但程序員是無(wú)法控制的。
堆內(nèi)存與棧內(nèi)存需要說(shuō)明:
基礎(chǔ)數(shù)據(jù)類型直接在棧空間分配,方法的形式參數(shù),直接在棧空間分配,當(dāng)方法調(diào)用完成后從棧空間回收。引用數(shù)據(jù)類型,需要用new來(lái)創(chuàng)建,既在棧空間分配一個(gè)地址空間,又在堆空間分配對(duì)象的類變量。方法的引用參數(shù),在棧空間分配一個(gè)地址空間,并指向堆空間的對(duì)象區(qū),當(dāng)方法調(diào)用完成后從棧空間回收。局部變量new出來(lái)時(shí),在棧空間和堆空間中分配空間,當(dāng)局部變量生命周期結(jié)束后,棧空間立刻被回收,堆空間區(qū)域等待GC回收。方法調(diào)用時(shí)傳入的literal參數(shù),先在棧空間分配,在方法調(diào)用完成后從棧空間收回。字符串常量、static在DATA區(qū)域分配,this在堆空間分配。數(shù)組既在棧空間分配數(shù)組名稱,又在堆空間分配數(shù)組實(shí)際的大小。
如:
(3)本地方法棧(java中的jni調(diào)用)
用于支持native方法的執(zhí)行,存儲(chǔ)了每個(gè)native方法調(diào)用的狀態(tài)。對(duì)于本地方法接口,實(shí)現(xiàn)JVM并不要求一定要有它的支持,甚至可以完全沒(méi)有。Sun公司實(shí)現(xiàn)Java本地接口(JNI)是出于可移植性的考慮,當(dāng)然我們也可以設(shè)計(jì)出其它的本地接口來(lái)代替Sun公司的JNI。但是這些設(shè)計(jì)與實(shí)現(xiàn)是比較復(fù)雜的事情,需要確保垃圾回收器不會(huì)將那些正在被本地方法調(diào)用的對(duì)象釋放掉。
(4)方法區(qū)(method)
它保存方法代碼(編譯后的java代碼)和符號(hào)表。存放了要加載的類信息、靜態(tài)變量、final類型的常量、屬性和方法信息。JVM用持久代(PermanetGeneration)來(lái)存放方法區(qū),可通過(guò)-XX:PermSize和-XX:MaxPermSize來(lái)指定最小值和最大值。
垃圾回收機(jī)制
堆里聚集了所有由應(yīng)用程序創(chuàng)建的對(duì)象,JVM也有對(duì)應(yīng)的指令比如new,newarray,anewarray和multianewarray,然并沒(méi)有向C++的delete,free等釋放空間的指令,Java的所有釋放都由GC來(lái)做,GC除了做回收內(nèi)存之外,另外一個(gè)重要的工作就是內(nèi)存的壓縮,這個(gè)在其他的語(yǔ)言中也有類似的實(shí)現(xiàn),相比C++不僅好用,而且增加了安全性,當(dāng)然她也有弊端,比如性能這個(gè)大問(wèn)題。
4、Java虛擬機(jī)的運(yùn)行過(guò)程示例
上面對(duì)虛擬機(jī)的各個(gè)部分進(jìn)行了比較詳細(xì)的說(shuō)明,下面通過(guò)一個(gè)具體的例子來(lái)分析它的運(yùn)行過(guò)程。
虛擬機(jī)通過(guò)調(diào)用某個(gè)指定類的方法main啟動(dòng),傳遞給main一個(gè)字符串?dāng)?shù)組參數(shù),使指定的類被裝載,同時(shí)鏈接該類所使用的其它的類型,并且初始化它們。例如對(duì)于程序:
編譯后在命令行模式下鍵入:javaHelloApprunvirtualmachine
將通過(guò)調(diào)用HelloApp的方法main來(lái)啟動(dòng)java虛擬機(jī),傳遞給main一個(gè)包含三個(gè)字符串"run"、"virtual"、"machine"的數(shù)組。現(xiàn)在我們略述虛擬機(jī)在執(zhí)行HelloApp時(shí)可能采取的步驟。
開(kāi)始試圖執(zhí)行類HelloApp的main方法,發(fā)現(xiàn)該類并沒(méi)有被裝載,也就是說(shuō)虛擬機(jī)當(dāng)前不包含該類的二進(jìn)制代表,于是虛擬機(jī)使用ClassLoader試圖尋找這樣的二進(jìn)制代表。如果這個(gè)進(jìn)程失敗,則拋出一個(gè)異常。類被裝載后同時(shí)在main方法被調(diào)用之前,必須對(duì)類HelloApp與其它類型進(jìn)行鏈接然后初始化。鏈接包含三個(gè)階段:檢驗(yàn),準(zhǔn)備和解析。檢驗(yàn)檢查被裝載的主類的符號(hào)和語(yǔ)義,準(zhǔn)備則創(chuàng)建類或接口的靜態(tài)域以及把這些域初始化為標(biāo)準(zhǔn)的默認(rèn)值,解析負(fù)責(zé)檢查主類對(duì)其它類或接口的符號(hào)引用,在這一步它是可選的。類的初始化是對(duì)類中聲明的靜態(tài)初始化函數(shù)和靜態(tài)域的初始化構(gòu)造方法的執(zhí)行。一個(gè)類在初始化之前它的父類必須被初始化。整個(gè)過(guò)程如下:
以上就是動(dòng)力節(jié)點(diǎn)Java培訓(xùn)機(jī)構(gòu)小編介紹的“小白如何學(xué)習(xí)Java到架構(gòu)師”的內(nèi)容,希望能夠幫助到大家,更多精彩內(nèi)容請(qǐng)繼續(xù)關(guān)注動(dòng)力節(jié)點(diǎn)Java培訓(xùn)機(jī)構(gòu)官網(wǎng),每天會(huì)有精彩內(nèi)容分享與你。
相關(guān)閱讀
0基礎(chǔ) 0學(xué)費(fèi) 15天面授
有基礎(chǔ) 直達(dá)就業(yè)
業(yè)余時(shí)間 高薪轉(zhuǎn)行
工作1~3年,加薪神器
工作3~5年,晉升架構(gòu)
提交申請(qǐng)后,顧問(wèn)老師會(huì)電話與您溝通安排學(xué)習(xí)