Java、Python與PHP的虛擬機(jī)異同
Java-JVM
定義
- JDK(Java Development Kit) 是 Java 語(yǔ)言的軟件開(kāi)發(fā)工具包(SDK)。JDK 物理存在,是 programming tools、JRE 和 JVM 的一個(gè)集合
- JRE(Java Runtime Environment)Java 運(yùn)行時(shí)環(huán)境,JRE 物理存在,主要由Java API 和 JVM 組成,提供了用于執(zhí)行 java 應(yīng)用程序最低要求的環(huán)境。
- JVM(Java Virtual Machine) 是一種軟件實(shí)現(xiàn),執(zhí)行像物理機(jī)程序的機(jī)器(即電腦)。JVM 通過(guò)執(zhí)行 Java bytecode 可以使 java 代碼在不改變的情況下運(yùn)行在各種硬件之上。JVM是基于棧的。
JVM 執(zhí)行
- 加載代碼
- 驗(yàn)證代碼
- 執(zhí)行代碼
- 提供運(yùn)行環(huán)境
JVM 生命周期
- 啟動(dòng):任何一個(gè)擁有main函數(shù)的class都可以作為JVM實(shí)例運(yùn)行的起點(diǎn)
- 運(yùn)行:main函數(shù)為起點(diǎn),程序中的其他線程均有它啟動(dòng),包括daemon守護(hù)線程和non-daemon普通線程。daemon是JVM自己使用的線程比如GC線程,main方法的初始線程是non-daemon。
- 消亡:所有線程終止時(shí),JVM實(shí)例結(jié)束生命。
JVM結(jié)構(gòu)及內(nèi)存模型
名詞解釋?zhuān)?/p>
- Class Loader:類(lèi)加載器負(fù)責(zé)加載程序中的類(lèi)型(類(lèi)和接口),并賦予唯一的名字。為什么使用雙親委托模型——ClassLoader 隔離問(wèn)題。
- Execution Engine:執(zhí)行引擎。執(zhí)行引擎以指令為單位讀取 Java 字節(jié)碼。它就像一個(gè) CPU 一樣,一條一條地執(zhí)行機(jī)器指令。
- Runtime Data Areas::運(yùn)行時(shí)數(shù)據(jù)區(qū)。
- PS:想起面試的時(shí)候被問(wèn)到過(guò)這樣的問(wèn)題:你在使用java過(guò)程中是否遇到過(guò)OOM的情況?當(dāng)時(shí)一陣懵比。現(xiàn)在總結(jié)下:
- PC寄存器(PC Register)是唯一一個(gè)在 Java 虛擬機(jī)規(guī)范中沒(méi)有規(guī)定任何OutOfMemoryError情況的區(qū)域。
- JVM 棧(Java Virtual Machine Stack):如果JVM Stack可以動(dòng)態(tài)擴(kuò)展,但是在嘗試擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存去完成擴(kuò)展,或者在建立新的線程時(shí)沒(méi)有足夠的內(nèi)存去創(chuàng)建對(duì)應(yīng)的虛擬機(jī)棧時(shí)拋出。
- 本地方法棧(Native method stack):如果本地方法棧可以動(dòng)態(tài)擴(kuò)展,并且擴(kuò)展的動(dòng)作已經(jīng)嘗試過(guò),但是目前無(wú)法申請(qǐng)到足夠的內(nèi)存去完成擴(kuò)展,或者在建立新的線程時(shí)沒(méi)有足夠的內(nèi)存去創(chuàng)建對(duì)應(yīng)的本地方法棧,那Java虛擬機(jī)將會(huì)拋出一個(gè)OutOfMemoryError異常。
- 方法區(qū)(Method area):如果方法區(qū)的內(nèi)存空間不能滿(mǎn)足內(nèi)存分配請(qǐng)求,那Java虛擬機(jī)將拋出一個(gè)OutOfMemoryError異常。
- 運(yùn)行時(shí)常量池(Runtime constant pool):當(dāng)創(chuàng)建類(lèi)和接口時(shí),如果構(gòu)造運(yùn)行時(shí)常量池所需的內(nèi)存空間超過(guò)了方法區(qū)所能提供的最大內(nèi)存空間后就會(huì)拋出OutOfMemoryError
- 堆(Heap):如果實(shí)際所需的堆超過(guò)了自動(dòng)內(nèi)存管理系統(tǒng)能提供的最大容量時(shí)拋出。
- 總結(jié)一下就是無(wú)法申請(qǐng)到足夠的內(nèi)存以及超出最大容量?jī)煞矫嬖?/li>
垃圾回收
-
新生代
新生代由 Eden 與 Survivor Space(S0,S1)構(gòu)成,大小通過(guò)-Xmn參數(shù)指定,Eden 與 Survivor Space 的內(nèi)存大小比例默認(rèn)為8:1,可以通過(guò)-XX:SurvivorRatio 參數(shù)指定,比如新生代為10M 時(shí),Eden分配8M,S0和S1各分配1M。
Eden:希臘語(yǔ),意思為伊甸園,在圣經(jīng)中,伊甸園含有樂(lè)園的意思,根據(jù)《舊約·創(chuàng)世紀(jì)》記載,上帝耶和華照自己的形像造了第一個(gè)男人亞當(dāng),再用亞當(dāng)?shù)囊粋€(gè)肋骨創(chuàng)造了一個(gè)女人夏娃,并安置他們住在了伊甸園。
大多數(shù)情況下,對(duì)象在Eden中分配,當(dāng)Eden沒(méi)有足夠空間時(shí),會(huì)觸發(fā)一次Minor GC,虛擬機(jī)提供了-XX:+PrintGCDetails參數(shù),告訴虛擬機(jī)在發(fā)生垃圾回收時(shí)打印內(nèi)存回收日志。
Survivor:意思為幸存者,是新生代和老年代的緩沖區(qū)域。
當(dāng)新生代發(fā)生GC(Minor GC)時(shí),會(huì)將存活的對(duì)象移動(dòng)到S0內(nèi)存區(qū)域,并清空Eden區(qū)域,當(dāng)再次發(fā)生Minor GC時(shí),將Eden和S0中存活的對(duì)象移動(dòng)到S1內(nèi)存區(qū)域。存活對(duì)象會(huì)反復(fù)在S0和S1之間移動(dòng),當(dāng)對(duì)象從Eden移動(dòng)到Survivor或者在Survivor之間移動(dòng)時(shí),對(duì)象的GC年齡自動(dòng)累加,當(dāng)GC年齡超過(guò)默認(rèn)閾值15時(shí),會(huì)將該對(duì)象移動(dòng)到老年代,可以通過(guò)參數(shù)-XX:MaxTenuringThreshold 對(duì)GC年齡的閾值進(jìn)行設(shè)置。
-
老年代
老年代的空間大小即-Xmx 與-Xmn 兩個(gè)參數(shù)之差,用于存放經(jīng)過(guò)幾次Minor GC之后依舊存活的對(duì)象。當(dāng)老年代的空間不足時(shí),會(huì)觸發(fā)Major GC/Full GC,速度一般比Minor GC慢10倍以上。
-
永久代
在JDK8之前的HotSpot實(shí)現(xiàn)中,類(lèi)的元數(shù)據(jù)如方法數(shù)據(jù)、方法信息(字節(jié)碼,棧和變量大小)、運(yùn)行時(shí)常量池、已確定的符號(hào)引用和虛方法表等被保存在永久代中,32位默認(rèn)永久代的大小為64M,64位默認(rèn)為85M,可以通過(guò)參數(shù)-XX:MaxPermSize進(jìn)行設(shè)置,一旦類(lèi)的元數(shù)據(jù)超過(guò)了永久代大小,就會(huì)拋出OOM異常。
虛擬機(jī)團(tuán)隊(duì)在JDK8的HotSpot中,把永久代從Java堆中移除了,并把類(lèi)的元數(shù)據(jù)直接保存在本地內(nèi)存區(qū)域(堆外內(nèi)存),稱(chēng)之為元空間。
這樣做有什么好處?
有經(jīng)驗(yàn)的同學(xué)會(huì)發(fā)現(xiàn),對(duì)永久代的調(diào)優(yōu)過(guò)程非常困難,永久代的大小很難確定,其中涉及到太多因素,如類(lèi)的總數(shù)、常量池大小和方法數(shù)量等,而且永久代的數(shù)據(jù)可能會(huì)隨著每一次Full GC而發(fā)生移動(dòng)。而在JDK8中,類(lèi)的元數(shù)據(jù)保存在本地內(nèi)存中,元空間的最大可分配空間就是系統(tǒng)可用內(nèi)存空間,可以避免永久代的內(nèi)存溢出問(wèn)題,不過(guò)需要監(jiān)控內(nèi)存的消耗情況,一旦發(fā)生內(nèi)存泄漏,會(huì)占用大量的本地內(nèi)存。
判斷垃圾回收
引用計(jì)數(shù)法:在對(duì)象上添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)對(duì)象引用它時(shí),計(jì)數(shù)器加1,當(dāng)使用完該對(duì)象時(shí),計(jì)數(shù)器減1,計(jì)數(shù)器值為0的對(duì)象表示不可能再被使用。引用計(jì)數(shù)法實(shí)現(xiàn)簡(jiǎn)單,判定高效,但不能解決對(duì)象之間相互引用的問(wèn)題。
-
可達(dá)性分析法:通過(guò)一系列稱(chēng)為 “GC Roots” 的對(duì)象作為起點(diǎn),從這些節(jié)點(diǎn)開(kāi)始向下搜索,搜索路徑稱(chēng)為 “引用鏈”,以下對(duì)象可作為GC Roots:
- 本地變量表中引用的對(duì)象
- 方法區(qū)中靜態(tài)變量引用的對(duì)象
- 方法區(qū)中常量引用的對(duì)象
- Native方法引用的對(duì)象
當(dāng)一個(gè)對(duì)象到 GC Roots 沒(méi)有任何引用鏈時(shí),意味著該對(duì)象可以被回收。
垃圾回收算法
- 標(biāo)記-清除算法
對(duì)待回收的對(duì)象進(jìn)行標(biāo)記。
算法缺點(diǎn):效率問(wèn)題,標(biāo)記和清除過(guò)程效率都很低;空間問(wèn)題,收集之后會(huì)產(chǎn)生大量的內(nèi)存碎片,不利于大對(duì)象的分配。 - 復(fù)制算法
復(fù)制算法將可用內(nèi)存劃分成大小相等的兩塊A和B,每次只使用其中一塊,當(dāng)A的內(nèi)存用完了,就把存活的對(duì)象復(fù)制到B,并清空A的內(nèi)存,不僅提高了標(biāo)記的效率,因?yàn)橹恍枰獦?biāo)記存活的對(duì)象,同時(shí)也避免了內(nèi)存碎片的問(wèn)題,代價(jià)是可用內(nèi)存縮小為原來(lái)的一半。 - 標(biāo)記-整理算法
在老年代中,對(duì)象存活率較高,復(fù)制算法的效率很低。在標(biāo)記-整理算法中,標(biāo)記出所有存活的對(duì)象,并移動(dòng)到一端,然后直接清理邊界以外的內(nèi)存。
參考文章
Python
PVM
PVM是Python的運(yùn)行引擎。他通常表現(xiàn)為python系統(tǒng)的一部分。并且他是實(shí)際運(yùn)行腳本的組件。
編譯器:將源碼編譯成運(yùn)行在虛擬機(jī)上執(zhí)行的opcode(pyc文件),pyc文件是在python虛擬機(jī)上執(zhí)行的一種跨平臺(tái)字節(jié)碼。
運(yùn)行時(shí):虛擬機(jī)解釋器把opcode(pyc文件)解釋成具體機(jī)器的機(jī)器碼,執(zhí)行。
JVM與PVM
- Java代碼從源程序到執(zhí)行,要經(jīng)過(guò)的過(guò)程是:編譯器(javac)把源代碼轉(zhuǎn)化為字節(jié)碼,然后解釋器(Java.exe)把字節(jié)碼轉(zhuǎn)換為計(jì)算機(jī)理解的機(jī)器碼來(lái)執(zhí)行。其中編譯器和解釋器都是Java虛擬機(jī)(JVM)的一部分,由于針對(duì)不同的硬件與OS,Java解釋器有所不同,因此可以實(shí)現(xiàn)“一次編譯、到處執(zhí)行”。所以JVM是Java跨平臺(tái)特性的關(guān)鍵所在。
- 對(duì)于Python,其源代碼到執(zhí)行也要經(jīng)過(guò)如下過(guò)程:源代碼--->字節(jié)碼--->機(jī)器碼。與Java不同的是,Python使用的虛擬機(jī)是基于其他語(yǔ)言實(shí)現(xiàn)的,比如我們一般使用的Python實(shí)際為Cpython,也就是其虛擬機(jī)由C實(shí)現(xiàn),這個(gè)虛擬機(jī)負(fù)責(zé)把Python源碼編譯為字節(jié)碼,再解釋執(zhí)行。另外,還有Jypython、Ironpython等。
PHP-Zend&HHVM
- Zend引擎默認(rèn)做法,是先編譯為opcode,然后再逐條執(zhí)行,通常每條指令對(duì)應(yīng)的是C語(yǔ)言級(jí)別的函數(shù)。如果我們產(chǎn)生大量重復(fù)的opcode(純PHP寫(xiě)的代碼和函數(shù)),對(duì)應(yīng)的則是Zend多次逐條執(zhí)行這些C代碼。
- HHVM生成和執(zhí)行PHP的中間字節(jié)碼(HHVM生成自己格式的中間字節(jié)碼),執(zhí)行時(shí)通過(guò)JIT(Just In Time,即時(shí)編譯是種軟件優(yōu)化技術(shù),指在運(yùn)行時(shí)才會(huì)去編譯字節(jié)碼為機(jī)器碼)轉(zhuǎn)為機(jī)器碼執(zhí)行。JIT將大量重復(fù)執(zhí)行的字節(jié)碼在運(yùn)行的時(shí)候編譯為機(jī)器碼,達(dá)到提高執(zhí)行效率的目的。通常,觸發(fā)JIT的條件是代碼或者函數(shù)被多次重復(fù)調(diào)用。
寫(xiě)在最后
時(shí)間匆忙,囫圇吞棗,努力完善。
后端開(kāi)發(fā)離不開(kāi)Java,python和php,深入學(xué)習(xí)原理,比較異同,最佳使用。
2017.06.23