生活中的無線網(wǎng)絡(luò)實例(生活中的無線網(wǎng)絡(luò)實例圖)
2024-07-01
更新時間:2024-07-01 18:02:48作者:未知
計算機能識別的是機器指令碼,簡稱機器碼。機器碼是二進制的,計算機可以直接識別,但與人類的語言差別太大,不容易被人理解和記憶。后來,就誕生了各種高級語言,人們用高級語言編寫程序,然后通過把程序解釋或編譯成機器碼。
比如python,就是一種解釋型語言。Python程序源碼不需要編譯,可以直接從源代碼運行程序。Python解釋器將源代碼轉(zhuǎn)換為字節(jié)碼,然后把編譯好的字節(jié)碼轉(zhuǎn)發(fā)到Python虛擬機(PVM)中進行執(zhí)行。
而C語言就是典型的編譯型語言,需要先用編譯器編譯成機器碼,比如我們通常用gcc來編譯C語言程序:
$ gcc hello.c # 編譯$ ./a.out # 執(zhí)行hello world!
那Java是解釋型語言還是編譯型語言呢?
Java是兼具編譯型語言與解釋型語言的特點的。程序員寫好Java程序后,需要先用javac編譯成JVM可以使用的字節(jié)碼class文件。然后JVM加載class文件,逐條解釋執(zhí)行。在運行過程中,部分熱點代碼會被即時編譯器編譯成機器碼。
Java語言的源代碼是.java
為后綴的文件。當(dāng)然現(xiàn)在有很多其它高級語言也架構(gòu)在JVM上,比如groovy、kotlin等。源代碼是給人看的,易于閱讀、理解、維護。
源代碼經(jīng)過編譯后得到字節(jié)碼,字節(jié)碼是給JVM用的,易于理解和識別。字節(jié)碼是以.class
為后綴,其格式是JVM的一套規(guī)劃,字節(jié)碼人類對照文檔也是勉強能看懂的,只是相對Java代碼來說要難以理解一些而已。
Java與Python不同,Python不需要編譯字節(jié)碼文件(當(dāng)然,Python也提供了這種操作),編譯是一個自動的過程,一般不會在意它的存在。而Java會先編譯好字節(jié)碼文件,這樣JVM直接讀字節(jié)碼文件,可以節(jié)省加載模塊的時間,提高效率。同時字節(jié)碼的形式也增加了反向工程的難度,可以保護源代碼(當(dāng)然,也可以被反編譯)。
熟悉JVM的小伙伴都知道,它有一個“類加載過程”,可以說是老八股文了,經(jīng)常會被面試官問到。類加載過程其實就是指的JVM從讀取一個class文件到準(zhǔn)備好這個類,以及最后銷毀的整個過程。
所以class文件其實是以“類”為單位的,這跟java文件有一些不同。如果我們在一個Java文件里面聲明多個類,用Javac編譯出來會發(fā)現(xiàn)有多個class文件。比如我們聲明一個One.java文件:
public class One { public class OneInner {} private class OnePrivateInner {} public static class OneStaticInner {} private static class OneprivateStaticInner {}}class Two{}
用Javac編譯后,會出現(xiàn)6個class文件
$ ls'One$OneInner.class' 'One$OneStaticInner.class' One.class Two.class'One$OnePrivateInner.class' 'One$OneprivateStaticInner.class' One.java
前面提到,JVM會加載class文件,然后加載后的Java類會被存放于方法區(qū)(Method Area)中。從指定的類的main方法作為入口開始運行。實際運行時,虛擬機會執(zhí)行方法區(qū)內(nèi)的代碼,JVM會使用堆和棧來存儲運行時數(shù)據(jù)。
每當(dāng)進入一個方法,Java虛擬機會在當(dāng)前線程的棧中生成一個棧幀,存放局部變量以及字節(jié)碼的操作數(shù),這個棧幀的大小是提前計算好的。
退出方法時,不管是正常返回還是異常返回,Java虛擬機均會彈出當(dāng)前線程的當(dāng)前棧幀,并將之舍棄。
Java虛擬機需要將字節(jié)碼翻譯成機器碼,才能讓機器執(zhí)行。這個過程有兩種形式,一種是解釋執(zhí)行,即逐條將字節(jié)碼翻譯成機器碼并執(zhí)行;另一種是即時編譯(Just-In-Time compilation,JIT),即將一個方法中包含的所有字節(jié)碼編譯成機器碼后再執(zhí)行。
這兩種編譯方式是怎么協(xié)作的呢?
HotSpot虛擬機包含多個即時編譯器C1、C2和Graal。其中,Graal是一個實驗性質(zhì)的即時編譯器,可以通過參數(shù) -XX:+
UnlockExperimentalVMOptions -XX:+UseJVMCICompiler啟用,并且替換C2。
C1和C2各有優(yōu)劣,適用于不同的場景。在Java 7以前,只能選擇一種編譯器。C1編譯快,但生成的代碼執(zhí)行效率一般,常用于對于執(zhí)行時間較短的,或者對啟動性能有要求的程序,常用于客戶端;C2編譯慢,但生成的代碼執(zhí)行效率快,適用于對于執(zhí)行時間較長的,或者對峰值性能有要求的程序,常用于服務(wù)端。實際上,C1對應(yīng)的參數(shù)是client,C2對應(yīng)的參數(shù)是server,也跟它們的應(yīng)用場景比較匹配。
Java7引入了分層編譯的概念,綜合了C1的啟動性能優(yōu)勢和C2的峰值性能優(yōu)勢。C1和C2編譯出的機器碼是不同的。C2代碼的執(zhí)行效率要比C1代碼高出30%以上。機器碼越快,需要的編譯時間就越長。分層編譯是一種折衷的方式,既能夠滿足部分不那么熱的代碼能夠在短時間內(nèi)編譯完成,也能滿足很熱的代碼能夠擁有最好的優(yōu)化。
那怎么判定熱點代碼呢?
JVM會收集方法的運行時信息,主要包括調(diào)用次數(shù)和循環(huán)回邊的次數(shù)。當(dāng)方法的調(diào)用次數(shù)和循環(huán)回邊的次數(shù)的和,超過指定閾值時,便會觸發(fā)即時編譯。
循環(huán)回邊次數(shù)可以簡單理解為方法內(nèi)部代碼的循環(huán)次數(shù),比如方法內(nèi)部有for循環(huán)或while循環(huán)。
在分層編譯出現(xiàn)前,這個閾值是由參數(shù)-XX:CompileThreshold
指定的,使用C1時,該值為1500;使用C2時,該值為10000。
當(dāng)啟用分層編譯時,JVM使用另一套閾值系統(tǒng)。在這套系統(tǒng)中,閾值的大小是動態(tài)調(diào)整的。JVM將閾值與某個系數(shù) s 相乘。該系數(shù)與當(dāng)前待編譯的方法數(shù)目成正相關(guān),與編譯線程的數(shù)目成負相關(guān)。
默認情況下編譯線程的總數(shù)目是根據(jù)處理器數(shù)量來調(diào)整的。Java 虛擬機會將這些編譯線程按照1:2的比例分配給 C1和C2(至少各為1個)。舉個例子,對于一個四核機器來說,總的編譯線程數(shù)目為3,其中包含一個C1編譯線程和兩個C2編譯線程。
機器資源太少的時候,也可能各1個線程。
用arthas可以看到編譯線程:
可以看到,它們的ID是-1,優(yōu)先級也是-1。我們自己創(chuàng)建的線程優(yōu)先級是0~10,所以編譯線程的優(yōu)先級會更高一些。
一句話來總結(jié)Java程序是怎么在機器上運行的呢?首先Java程序員編寫Java代碼,然后Java代碼會被編譯成class文件,多個class文件會被打包成jar包或者war包。然后JVM加載class文件,然后先解釋執(zhí)行為字節(jié)碼。程序運行一段時間后,JVM會通過方法調(diào)用次數(shù)和循環(huán)持續(xù)判斷一個方法是否為熱點代碼,如果是,會使用分層編譯,通過編譯線程編譯成字節(jié)碼,在機器上運行。