學(xué)習(xí)Java的同學(xué)注意了!!!
學(xué)習(xí)進(jìn)程中遇到甚么問題或想獲得學(xué)習(xí)資源的話,歡迎加入Java學(xué)習(xí)交換群,群號碼:183993990 我們1起學(xué)Java!
在C里面我們想履行1段自己編寫的機(jī)器指令的方法大概以下:
typedef void (*FUNC)( int ); char *
str = "your
code" ; FUNC
f = (FUNC)str; (*f)(0); |
也就是說,我們完全可以做1個工具,從1個文件中讀入指令,然后將這些指令運(yùn)行起來。上面代碼中“編好的機(jī)器指令”固然指的是能在CPU上運(yùn)行的,如果這里我還實(shí)現(xiàn)了1個翻譯機(jī)器:從自己定義的格式指令翻譯到CPU指令,那末就能夠履行根據(jù)自定義格式的代碼了。那末上面這段代碼是否是相當(dāng)于最簡單的1個虛擬機(jī)了?下面來看JVM的整體結(jié)構(gòu):
ClassLoader的作用是裝載能被JVM辨認(rèn)的指令(固然不只是從磁盤文件或內(nèi)存去裝載),那末我們先了解1下該格式:
魔數(shù)和版本就不說了(滿大街的文件格式都是這個東西),接著的便是常量池,其中不過是兩種東西:
而我們知道,在JVM里面Class都是根據(jù)全限定名去找的,那末方法的描寫固然也應(yīng)當(dāng)如此,那末就得到這些常量之間的關(guān)系以下:
在接下來的“訪問權(quán)限”中表明了該Class是public還是private等,而this&super&interface則表面了“本類”、“繼承自哪一個類”、“實(shí)現(xiàn)了哪些接口”,實(shí)際上這里只是保存了代表這些信息的CONSTANT_Class_info的下標(biāo)(u2)。
感覺這里的NameIndex和DescriptorIndex加起來和NameAndType有點(diǎn)像,那末為何不直接用1個NameAndType的索引值表示?MethodInfo和FieldInfo之間最大的不同點(diǎn)就是Attributes。比如FieldInfo的屬性表中寄存的是變量的初始值,而MethodInfo的屬性表中寄存的則是字節(jié)碼。那末我們來順次看這些Attributes,首先是Code:
有幾個成心思的地方:
Exceptions則非常簡單:
LineNumberTable保存了字節(jié)碼和源碼之間的關(guān)系,結(jié)構(gòu)以下:
LocalVariableTable描寫了棧幀中局部變量表的變量和源代碼中定義的變量之間的關(guān)系,結(jié)構(gòu)以下:
SourceFile指明了生成該Class文件的Java源碼文件名(比如在1個Java文件中申明了很多類的時候會生成很多Class文件),結(jié)構(gòu)以下:
Deprecated和Synthetic屬性只存在“有”和“沒有”的區(qū)分:
這也就是為何Code屬性后面會有Attribute的緣由?
類加載的時機(jī)就很簡單了:在用到的時候就加載(空話!)。下來看1下類加載的進(jìn)程:
履行上面這段進(jìn)程的是:ClassLoader,這個東西還是非常重要的,在JVM中是通過ClassLoader和類本身共同去判斷兩個Class是不是相同。換句話說就是:不同的ClassLoader加載同1個Class文件,那末JVM認(rèn)為他們生成的類是不同的。有些時候不會從Class文件中加載流(比如Java Applet是從網(wǎng)絡(luò)中加載),那末這個ClassLoader和普通的實(shí)現(xiàn)邏輯固然是不1樣的,通過不同的ClassLoader就能夠解決這個問題。
但是允許使用不同的ClassLoader又引發(fā)了新的問題:如果我也聲明了1個java.lang.Integer,但是里面的代碼非常危險(xiǎn),怎樣辦?這里就引出了雙親委派模式:
除頂層的啟動類加載器外,其余的類加載器都應(yīng)當(dāng)有父類加載器(通過組合實(shí)現(xiàn)),它在接到加載類的要求時優(yōu)先委派給父類加載器去完成。
這樣的話,在加載java.lang.Integer的時候會優(yōu)先使用系統(tǒng)的類加載器,這樣就不會加載用戶自己寫的。在Java程序員看到有3種系統(tǒng)提供的類加載器:
這樣默許的類會是有Application ClassLoader去加載類,然后如果發(fā)現(xiàn)要使用新的類型的時候則會遞歸地使用Application ClassLoader去加載(在前面的加載進(jìn)程中提到)。這樣,只有在自己的程序中能使用自己編寫的ClassLoader去加載類,并且這個被加載的類是不能被他人使用的。
雙親委派模式不是1個強(qiáng)迫性的束縛,而是Java設(shè)計(jì)者推薦給開發(fā)者的類加載實(shí)現(xiàn)方式。雙親委派模式出現(xiàn)過的3次“破壞”:
加載完完成后,接下來就要看程序是怎樣運(yùn)行的。棧幀是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和履行,幀的意思就是1個單位,在調(diào)用其他方法的時候會向棧中壓入棧幀,結(jié)構(gòu)以下:
在Class文件編譯完成以后,在運(yùn)行的時候需要多少個局部變量就已肯定(在前面Class文件中也已看到過了),那末這里需要注意這個特性可能會引發(fā)GC(具體如何引發(fā)就不在這里細(xì)說了)。在棧中,總是底層的棧去調(diào)用高層的棧(并且1定的相鄰的),那末他們在參數(shù)傳遞(返回結(jié)果)的時常常是通過將其壓入操作數(shù)棧,有些虛擬機(jī)為了提高這部份的效力使得相鄰棧幀“糾纏”在1起:
那末我們接下來要去看是方法是如何履行的,第1個問題就是履行哪一個方法?在“面向進(jìn)程”的編程中似乎不存在在個問題,但是在Java OR C++中這都是比較蛋疼的1個問題。緣由就是平時不會這么用,但是你必須去弄明白= =。JVM肯定目標(biāo)方法的時候有兩種方法:
其實(shí)“靜態(tài)”和“動態(tài)”給人的感覺還是比較模糊的,“靜態(tài)分派”給人的感覺是根據(jù)參數(shù)的類型向上查找方法,“動態(tài)分派”給人的感覺則是根據(jù)實(shí)例的真實(shí)類型向上查找。虛擬機(jī)優(yōu)化動態(tài)分派的效力1般是為類在方法區(qū)中建立1個虛方法表:
虛方法表中寄存各個方法實(shí)際入口地址,如果某個方法在子類中沒有被重寫,那末子類的虛方法表里面的地址入口和父類相同方法的地址入口是1致的,都指向父類的實(shí)現(xiàn)入口。如果子類重寫了這個方法,子類方法表中的地址將會被替換為指向子類實(shí)現(xiàn)版本的入口地址。其實(shí)往簡單里說,就是1個預(yù)處理。
具體單個方法的履行非常簡單,寫1個簡單的程序然后使用javap -c,再結(jié)合每條指令的含義就可以大概知道程序時怎樣履行和返回的了(大體上就是基于棧),這里就不深入和細(xì)說了。
1般情況下,從Java文件到運(yùn)行起來,總的會經(jīng)歷兩個階段:Java到Class文件和履行Class文件。第1個階段其實(shí)就是編譯了,在這個進(jìn)程中比較成心思的是“語法糖”(其他的比如詞法分析和語法分析就不說了,此處省略1萬字~!~)。所謂Java的語法糖是:for遍歷的簡寫、自動裝箱、泛型等(其實(shí)有無感覺String+String也是語法糖,在實(shí)際中會變成StringBuffer的append)。其中比較成心思的是泛型:
Java中的泛型和C++中的泛型原理是上不1樣的:對C++來講List<A>和List<B>就是兩個東西,而在Java中List<A>和List<B>都是List<Object>,由于在Java中Object是所有對象的父對象,那末Object o可以指向所有的對象,那末就能夠用List<Object>來保存所有的對象集了(感覺實(shí)現(xiàn)的有點(diǎn)廢)。
這里觸及到1個問題就是對象刪除,比以下面代碼:
static void func(List<Integer> a){
return;
}
在使用javap查看生成的Class的時候會發(fā)現(xiàn):
static void func(java.util.List);
Signature: (Ljava/util/List;)V
Code:
0: return
其中根本沒有任何Integer的痕跡,但是如果加上返回值,也就是:
static Integer func(List<Integer> a){
return null;
}
此時再查看的時候就會變成:
static java.lang.Integer func(java.util.List);
Signature: (Ljava/util/List;)Ljava/lang/Integer;
Code:
0: aconst_null
1: areturn
通過泛型實(shí)現(xiàn)的原理可以理解很多在實(shí)際中會遇到的問題,比如使用List的時候稀里糊涂的類型強(qiáng)迫轉(zhuǎn)換毛病。
接下來開始討論第2個部份,也就是Class文件的實(shí)際的履行。在C++中常會提到的兩個概念是:Debug和Release,而在Java中常提到的兩個概念是Server和Client(雖然他們劃分的根據(jù)完全不1樣),Client和Server兩種模式對應(yīng)兩種編譯器:
在監(jiān)測器發(fā)現(xiàn)有熱門代碼(被調(diào)用了很屢次的方法或是履行很屢次的循環(huán)體)的情況下,將會想即時編譯器提交1個該該方法的代碼編譯要求。當(dāng)這個方法再次被調(diào)用時,會先檢查改方法是不是存在被JIT編譯過的版本,如果存在則優(yōu)先使用編譯后的本地代碼。在默許的情況下,編譯本地代碼的進(jìn)程和舊的代碼(也就是解釋履行字節(jié)碼)是并行的,可使用-XX:-BackgroundCompilation來制止后臺編譯,也就是說履行線程會登島編譯完成后再去履行生成的本地代碼。
在具體編譯優(yōu)化的時候有1個比較好玩的東西,逃逸分析(所謂逃逸是指能被從方法外援用),對不會逃逸的對象可以進(jìn)行優(yōu)化:
關(guān)于Java和C++效力的問題,感覺討論起來就沒有甚么意義了:語言到最后肯定是要生成機(jī)器指令的,在語言的機(jī)制上面各有千秋,致使不同的語言之間生成機(jī)器指令的進(jìn)程可能不同,但是這個生成的進(jìn)程跟我們這些碼農(nóng)沒有半毛錢關(guān)系(更準(zhǔn)確的說我們生成的進(jìn)程我們毛都不知道),所以在弄清楚之前就不要爭到底哪一個效力高(乃至是哪一個更好)。
程序的并發(fā)主要是斟酌不同的線程操作同1塊內(nèi)存時候可能產(chǎn)生的1些問題(至于文件鎖之類的東西,咳咳),首先就先了解線程和內(nèi)存的關(guān)系:
這里的主內(nèi)存就像是內(nèi)存條,工作內(nèi)存就像是寄存器+Cache。Java內(nèi)存模型定義了8中操作,他們的履行以下:
Java虛擬機(jī)中最輕量級的同步機(jī)制:volatile,它的性質(zhì)以下:
從Java內(nèi)存模型操作的角度來看volatile的實(shí)現(xiàn)還是挺簡單的:在use之前必須load,在assign以后必須store,這樣就保證了每次用都是從主內(nèi)存中讀取,每次賦值以后都會同步到主內(nèi)存(貌似說的是空話)。線程的同步主要是從3個方面斟酌:
如果任什么時候候都斟酌同步那代碼寫起來就累死了。下面是Java內(nèi)存模型的天然先行產(chǎn)生關(guān)系:
其實(shí)上面的這8條規(guī)則還是很成心思的,如果其中的某1條不成立會產(chǎn)生甚么?說到底Java線程還是用戶級的線程,那末它究竟是個甚么東西(在學(xué)C的時候也糾結(jié)過這個問題- -)。實(shí)現(xiàn)線程主要有幾種方式:
這里就不深入的去看了(雖然這里的介紹根沒說1樣),想一想看都知道不同虛擬機(jī)在不同的操作系統(tǒng)上面的實(shí)現(xiàn)方式極可能是不1樣,如果想深入看還是pthread比較成心思1點(diǎn)。關(guān)于線程的其他要注意的地方(比如狀態(tài)轉(zhuǎn)移甚么的)就不在這里討論了。
線程安全:當(dāng)多個線程訪問1個對象時,如果不用斟酌這些線程在運(yùn)行時環(huán)境下的調(diào)度和交替履行,也不需要進(jìn)行額外的同步,或在調(diào)用方進(jìn)行任何其他的調(diào)和操作,調(diào)用這個對象的行動都可以取得正確的結(jié)果,那這個對象是線程安全的。
Java中線程同享的變量可以分為以下5種:
鎖的話有以下幾種實(shí)現(xiàn)方式:
如果線程之間的切換非常頻繁的話自旋鎖是1個不錯的選擇,這樣就不需要線程切換時候的系統(tǒng)調(diào)用的開消了。如果1個任務(wù)能夠很快的完成的話,將全部進(jìn)程都鎖住也許是個不錯的選擇(而不是給每一個子進(jìn)程上鎖)。其他的鎖優(yōu)化包括“輕量級鎖”和“偏心鎖”。
學(xué)習(xí)Java的同學(xué)注意了!!!
學(xué)習(xí)進(jìn)程中遇到甚么問題或想獲得學(xué)習(xí)資源的話,歡迎加入Java學(xué)習(xí)交換群,群號碼:183993990 我們1起學(xué)Java!