1. 前言
?從學(xué)習(xí)Java的第一天開始诬烹,到如今工作當(dāng)中型檀,想必大家都耳聞目染了各種Java的優(yōu)點(diǎn)冗尤。其中肯定少不了:Java有虛擬機(jī),java是跨平臺(tái)的胀溺,一次編譯到處運(yùn)行裂七。在相當(dāng)長的一段時(shí)間里對(duì)此觀點(diǎn)都只是一個(gè)很模糊的概念,對(duì)自己寫的代碼也有一種吃不透的感覺仓坞。猶如一只攔路的大老虎背零,望而生畏,止步不前扯躺。一番思量捉兴,一日不解決掉,對(duì)技術(shù)難以有更深層次的理解录语,只好硬著頭皮上。
2. 不能跨平臺(tái)的原因是怎樣造成的禾乘?
2.1 機(jī)器語言和匯編
?計(jì)算機(jī)只認(rèn)識(shí)0和1
這句話大家都聽說過澎埠。的確,正所謂大道至簡始藕,0和1足以撐起整個(gè)互聯(lián)網(wǎng)世界蒲稳。在早期編程中氮趋,都是編寫一條條0和1組成的指令
來開發(fā),要自己處理每一塊數(shù)據(jù)的存儲(chǔ)分配和輸入輸出江耀∈P玻可想而知,滿屏的0和1祥国,程序容易出錯(cuò)且可讀性很差昵观。
?使用 0和1
組成的機(jī)器指令來編程,太過于繁瑣舌稀,單單只是記住0和1組成的指令
就令人頭大啊犬。完全可以用一種簡易的方式代替記憶,例如做加法運(yùn)算壁查,而這個(gè)加
的操作在機(jī)器碼中可能是一個(gè) 010010
固定的指令觉至,完全可以用 add
這個(gè)單詞來代替記憶,簡化了編程過程睡腿,這就是匯編語言语御。匯編語言的特點(diǎn)是用符號(hào)代替了機(jī)器指令代碼,而且符號(hào)與指令代碼一一對(duì)應(yīng)席怪,基本保留了機(jī)器語言的靈活性沃暗。而再將add指令
轉(zhuǎn)為010010機(jī)器碼
的程序便是匯編語言編譯器。
2.2 硬件關(guān)系
?組裝過電腦的朋友都知道何恶,組裝一臺(tái)電腦需要購買:CPU孽锥、內(nèi)存條、硬盤细层,主板等以及各種外設(shè)惜辑。對(duì)程序而言,一開始存儲(chǔ)在硬盤當(dāng)中疫赎,即便計(jì)算機(jī)斷電盛撑,下次重啟程序依舊存在。CPU 是一個(gè)復(fù)雜的計(jì)算機(jī)部件捧搞,它內(nèi)部又包含很多小零件抵卫,如下圖所示:
?????圖片摘自C語言中文網(wǎng)
?內(nèi)存對(duì)于 CPU 來僅僅是一個(gè)存放指令和數(shù)據(jù)的地方,并不能在內(nèi)存中完成計(jì)算功能胎撇。例如要計(jì)算 a = b + c介粘,必須將 a、b晚树、c 都讀取到 CPU 內(nèi)部才能進(jìn)行加法運(yùn)算姻采,寄存器是存儲(chǔ) CPU 執(zhí)行所需數(shù)據(jù)的區(qū)域,是 CPU 不可或缺的一部分爵憎,所有程序都只能通過操作寄存器慨亲,達(dá)到控制 CPU 目的婚瓜,完成計(jì)算任務(wù)。
2.2 芯片架構(gòu)
?arm
刑棵、X86
兩種芯片架構(gòu)廣泛應(yīng)用在 PC 機(jī)和移動(dòng)端嵌入式設(shè)備中巴刻。前者由arm公司設(shè)計(jì),后者由Intel蛉签、amd共同設(shè)計(jì)胡陪,雙方交叉授權(quán)使用。arm
是精簡指令集架構(gòu)(RSIC)正蛙,功耗較低督弓,性能隨之也降了下來。x86
是復(fù)雜指令集架構(gòu)(CISC)乒验,功耗較高愚隧,性能強(qiáng)。arm架構(gòu)
的寄存器 比 x86架構(gòu)
的多不少锻全。寄存器和指令集加架構(gòu)本身的差異性狂塘,也是造成不能跨平臺(tái)的原因。近幾十年來鳄厌,硬件的性能一直都在飛速發(fā)展荞胡,CPU架構(gòu) 也經(jīng)歷了幾次較大的改變。 x86架構(gòu)
從最早的 16 位到 32 位再到現(xiàn)在的 64 位架構(gòu)了嚎。arm架構(gòu)
也從 v1 發(fā)展到了如今的 v8的64位架構(gòu)泪漂。一般新的架構(gòu)都會(huì)向前兼容幾個(gè)版本,保證舊架構(gòu)上的老代碼歪泳,能夠在新架構(gòu)上運(yùn)行萝勤。但這樣做,卻無法發(fā)揮出新架構(gòu)硬件的性能呐伞,無疑是對(duì)資源的浪費(fèi)敌卓。在開發(fā)中如果涉及到底層庫的使用,則需要考慮兼容不同架構(gòu)的CPU伶氢。例如在使用百度地圖SDK時(shí)趟径,會(huì)下載不同CPU架構(gòu)的so文件,還有 X86 架構(gòu)的癣防,就是為了兼容不同CPU架構(gòu)的手機(jī)蜗巧。
Android可以通過adb命令來查看cpu信息1、adb shell 2劣砍、cat /proc/cpuinfo
2.3 C語言為什么不能夸平臺(tái)惧蛹?
?通常認(rèn)為 C 語言是編譯型語言。在編譯階段刑枝,編譯器直接將源碼
編譯為 對(duì)應(yīng)CPU架構(gòu)和操作系統(tǒng)上的可執(zhí)行文件
香嗓。
如下圖所示 c 語言代碼編譯為的匯編代碼:
#include <stdio.h>
int main() {
printf("Hello World");
return 0;
}
Windows 部分匯編指令:
ubuntu 部分匯編指令:
雖然讀不太懂匯編指令,比較了一下差異還是不小的装畅。C 語言更多的是偏向底層開發(fā)靠娱,只要編譯器足夠強(qiáng)大,支持對(duì)應(yīng)平臺(tái)的編譯掠兄,或者對(duì)應(yīng)平臺(tái)提供有C 編譯器
(C 語言的編譯器也是眾多語言中最多的)像云。程序就能在對(duì)應(yīng)平臺(tái)執(zhí)行,也許 C 語言從來就沒有想過要跨平臺(tái)蚂夕。
代碼與平臺(tái)有關(guān)性迅诬,是不能跨平臺(tái)的原因。
3. JVM是如何做到跨平臺(tái)的
?講了這么多不能夸平臺(tái)的原因婿牍,再來理解Java是如何做到跨平臺(tái)就容易得多了侈贷。JVM 在編譯階段,只將 .java
的源碼等脂,編譯為和平臺(tái)無關(guān)的 .class
字節(jié)碼文件俏蛮。不同 CPU 架構(gòu)和操作系統(tǒng)上都會(huì)編譯為相同的 calss 文件(最多只是 JDK 版本不同,有些許差異上遥,jdk 都會(huì)向前兼容幾個(gè)版本)搏屑。再由不同平臺(tái)上的自行實(shí)現(xiàn)JVM。我們只需要搭建相應(yīng)平臺(tái)的運(yùn)行環(huán)境即可粉楚,便可做到任意平臺(tái)開發(fā)編譯辣恋,到處運(yùn)行。
?JVM 在真機(jī)基礎(chǔ)之上模擬了一套自己的架構(gòu)模软,有自己的指令集伟骨、內(nèi)存管理等。在使用 Eclipse 追溯源碼時(shí)撵摆,常常會(huì)遇到只有 class 文件底靠,而沒有源碼出現(xiàn)下面的頁面:
?圖中紅色框內(nèi)的便是字節(jié)碼指令,運(yùn)行時(shí)通過逐條解釋執(zhí)行特铝,這也是以前 Java 被指性能底下的詬點(diǎn)暑中。的確,解釋執(zhí)行的性能確實(shí)是和 C 編譯目標(biāo)代碼比不了鲫剿,但是在
JDK1.2
時(shí)就支持 JIT
及時(shí)編譯器鳄逾。程序運(yùn)行期間,分析熱點(diǎn)(經(jīng)常調(diào)用)函數(shù)灵莲,編譯為本地代碼緩存起來雕凹,以后直接執(zhí)行本地代碼。雖然性能還是和編譯型的語言有一定的差異,但 Java 憑借其語言特性以及各種成熟的 Web 解決方案枚抵,這點(diǎn)性能差顯得不那么重要线欲,完全能夠接受。JIT 編譯代碼如下:有些JVM是采用純JIT編譯方式實(shí)現(xiàn)的汽摹,內(nèi)部沒解釋器李丰,例如JRockit、Maxine VM和Jikes RVM ---RednaxelaFX
4.JVM內(nèi)存結(jié)構(gòu)
?內(nèi)存作為程序運(yùn)行中的臨時(shí)存儲(chǔ)介質(zhì)逼泣,本質(zhì)上不進(jìn)行任何的區(qū)域劃分趴泌,為了能夠合理有效的使用回收內(nèi)存,才將內(nèi)存劃分出更多的區(qū)域拉庶。平時(shí)聽得較多的就是堆棧內(nèi)存嗜憔,堆棧是一種數(shù)據(jù)結(jié)構(gòu),也是一種概念模型氏仗。不同的語言有自己的實(shí)現(xiàn)方式吉捶,通常在 Oop
編程中,棧存放函數(shù)執(zhí)行時(shí)所需的局部變量廓鞠,函數(shù)執(zhí)行完即釋放帚稠,堆內(nèi)存存儲(chǔ)對(duì)象。
操作系統(tǒng)內(nèi)存布局
?Windows 上棧內(nèi)存由系統(tǒng)回收床佳,堆內(nèi)存由程序員自行回收滋早。因?yàn)闂I蟽?nèi)存不可控,JVM 只能在操作系統(tǒng)的堆內(nèi)存上開辟自己的空間砌们。
JVM運(yùn)行時(shí)內(nèi)存結(jié)構(gòu)
JVM堆
?所有類實(shí)例和數(shù)組都從堆中分配
杆麸,官方JVMS8規(guī)范文檔 的確是這樣描述的 The heap is the run-time data area from which memory for all class instances and arrays is allocated
。有一個(gè)很常見情況下浪感,函數(shù)執(zhí)行中產(chǎn)生的對(duì)象在堆中分配昔头,函數(shù)執(zhí)行結(jié)束,不再引用的對(duì)象影兽,已經(jīng)沒有存在的必要了揭斧。這些對(duì)象在堆中等待下一次GC,而大多對(duì)象朝生即死峻堰,生命周期極短讹开,等待GC這段時(shí)間,也是對(duì)資源的浪費(fèi)捐名。在JDK1.5
時(shí)JVM
提供支持逃逸分析技術(shù)旦万,通過分析對(duì)象作用域,實(shí)現(xiàn)了棧上分配镶蹋、標(biāo)量替換成艘、同步消除優(yōu)化等技術(shù)赏半。通過函數(shù)傳遞對(duì)象,稱之為方法逃逸淆两。將對(duì)象賦值給其他線程變量断箫,稱之為線程逃逸:
標(biāo)量替換
?不可再分解的基礎(chǔ)數(shù)據(jù)類型稱之為標(biāo)量,例如Java中的八大基礎(chǔ)類型和引用類型琼腔。反之救欧、如果某個(gè)對(duì)象還可繼續(xù)分解斯嚎,則該對(duì)象屬于聚合量,Java類就是典型的聚合量朗涩。標(biāo)量替換則是將對(duì)象的成員變量分解成原始數(shù)據(jù)類型尸诽,代替對(duì)象在棧中分配甥材。
棧上分配
?JDK1.8默認(rèn)開啟逃逸分析,確定對(duì)象不會(huì)再被外部引用性含,通過標(biāo)量替換將對(duì)象分解在棧中分配洲赵,棧中的對(duì)象隨著棧幀的出棧而銷毀,大大的減少了堆內(nèi)存的占用和GC的壓力商蕴。
public class Main {
public static void main(String[] args) throws Exception {
for(int i = 0 ; i < 1000000;i++){
Child child = new Child();
child.setAge(1);
}
System.out.println("阻塞...");
System.in.read();
}
public static class Child{
private int age;
private String name;
//省略get/set方法
}
}
開啟逃逸分析(1.8默認(rèn)開啟)
C:\Program Files\Java\jdk1.8.0_91\bin>jps -l
17456 sun.tools.jps.Jps
19680 linked.Main
7608
C:\Program Files\Java\jdk1.8.0_91\bin>jmap -histo 19680
num #instances #bytes class name
----------------------------------------------
1: 220734 5297616 linked.Main$Child
2: 437 1763680 [I
3: 3099 449536 [C
4: 2392 57408 java.lang.String
5: 488 55696 java.lang.Class
6: 97 41776 [B
7: 835 33400 java.util.TreeMap$Entry
關(guān)閉逃逸分析:
C:\Program Files\Java\jdk1.8.0_91\bin>jps -l
2436 sun.tools.jps.Jps
16536 linked.Main
7608
C:\Program Files\Java\jdk1.8.0_91\bin>jmap -histo 16536
num #instances #bytes class name
----------------------------------------------
1: 1000000 24000000 linked.Main$Child
2: 451 1873120 [I
3: 3099 449536 [C
4: 2392 57408 java.lang.String
5: 488 55696 java.lang.Class
6: 97 41776 [B
7: 835 33400 java.util.TreeMap$Entry
可以看到叠萍,關(guān)閉逃逸分析總共使用堆內(nèi)存 22M ,開啟逃逸分析只使用了 5M 左右绪商。節(jié)約了不少堆內(nèi)存空間苛谷,減少了 GC 壓力。
開啟逃逸-XX:+DoEscapeAnalysis -XX:+PrintGC
關(guān)閉逃逸-XX:-DoEscapeAnalysis -XX:+PrintGC
同步消除
如果逃逸分析確認(rèn)對(duì)象的作用范圍不會(huì)超過當(dāng)前線程格郁,則消除對(duì)變量的同步措施腹殿。
JVM棧
?JVM棧 是方法執(zhí)行所需的數(shù)據(jù)結(jié)構(gòu),每個(gè)線程都擁有一個(gè)JVM棧例书,隨著線程的創(chuàng)建而創(chuàng)建锣尉,隨著線程的銷毀而銷毀。JVM棧 以棧幀的單元决采,存放局部變量自沧、操作數(shù)棧、動(dòng)態(tài)鏈接树瞭、方法返回信息拇厢。具體可以參考
方法區(qū)/元數(shù)據(jù)區(qū)
?方法區(qū)中存放已被虛擬機(jī)加載的類信息,并且每個(gè)類只會(huì)存在一份移迫,作為使用該類的入口旺嬉。我們所編寫的代碼類,經(jīng)過javac
編譯器厨埋,編譯存儲(chǔ)為 class 文件邪媳,在使用該類時(shí)(創(chuàng)建類的實(shí)例,調(diào)用了類靜態(tài)方法類等),如果該類還未加載雨效,會(huì)先將該 class 字節(jié)流從磁盤或者其他途徑方式迅涮,加載存儲(chǔ)到方法區(qū)當(dāng)中,并且創(chuàng)建該類的 class對(duì)象 供以后訪問使用徽龟。
運(yùn)行時(shí)常量池
?運(yùn)行時(shí)常量池作為方法區(qū)的一部分叮姑,為每一個(gè)類都維護(hù)一個(gè)常量池,存放著編譯時(shí)已知的字面量和各種符號(hào)引用据悔。具體可見參考第二章
PC寄存器
?每個(gè)JVM線程都有自己的PC(程序計(jì)數(shù)器)寄存器传透。在任何時(shí)候,每個(gè)JVM線程都在執(zhí)行單個(gè)方法的代碼极颓,如果執(zhí)行的不是native方法朱盐,則pc寄存器包含當(dāng)前正在執(zhí)行的Java字節(jié)碼指令的地址。如果當(dāng)前執(zhí)行的native方法菠隆,則PC寄存器的值undefined兵琳。
本地方法棧
?支持 native
方法調(diào)用,隨著線程的創(chuàng)建來分配本地方法棧骇径。
參考:
深入理解Java虛擬機(jī)一書