粗談Java虛擬機(jī)1_開山篇

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ò)且可讀性很差昵观。

case1.jpg

?使用 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)部又包含很多小零件抵卫,如下圖所示:


118274690_1_20171206084852264.jpg

?????圖片摘自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ī)蜗巧。

cpu_so.png

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 部分匯編指令:


微信截圖_20190723174501.png

ubuntu 部分匯編指令:


微信截圖_20190724170811.png

雖然讀不太懂匯編指令,比較了一下差異還是不小的装畅。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)行。

未命名文件.png

?JVM 在真機(jī)基礎(chǔ)之上模擬了一套自己的架構(gòu)模软,有自己的指令集伟骨、內(nèi)存管理等。在使用 Eclipse 追溯源碼時(shí)撵摆,常常會(huì)遇到只有 class 文件底靠,而沒有源碼出現(xiàn)下面的頁面:
微信截圖_20181120131556.png

?圖中紅色框內(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 編譯代碼如下:
微信截圖_20181202221710.png

有些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)存布局
微信圖片_20190730142730.png

?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)
微信圖片_20190730145126.png
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ì)象 供以后訪問使用徽龟。

微信圖片_20190730152335.png

運(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ī)一書

RednaxelaFX

keycoding

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末躯肌,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子破衔,更是在濱河造成了極大的恐慌清女,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件运敢,死亡現(xiàn)場離奇詭異校仑,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)传惠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門迄沫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人卦方,你說我怎么就攤上這事羊瘩。” “怎么了盼砍?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵尘吗,是天一觀的道長。 經(jīng)常有香客問我浇坐,道長睬捶,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任近刘,我火速辦了婚禮擒贸,結(jié)果婚禮上臀晃,老公的妹妹穿的比我還像新娘。我一直安慰自己介劫,他們只是感情好徽惋,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著座韵,像睡著了一般险绘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上誉碴,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天宦棺,我揣著相機(jī)與錄音,去河邊找鬼翔烁。 笑死渺氧,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蹬屹。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼白华,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼慨默!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起弧腥,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤厦取,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后管搪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體虾攻,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年更鲁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了霎箍。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡澡为,死狀恐怖漂坏,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情媒至,我是刑警寧澤顶别,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站拒啰,受9級(jí)特大地震影響驯绎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜谋旦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一剩失、第九天 我趴在偏房一處隱蔽的房頂上張望骗随。 院中可真熱鬧,春花似錦赴叹、人聲如沸鸿染。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽涨椒。三九已至,卻和暖如春绽媒,著一層夾襖步出監(jiān)牢的瞬間蚕冬,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來泰國打工是辕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留囤热,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓获三,卻偏偏與公主長得像旁蔼,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子疙教,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 第二部分 自動(dòng)內(nèi)存管理機(jī)制 第二章 java內(nèi)存異常與內(nèi)存溢出異常 運(yùn)行數(shù)據(jù)區(qū)域 程序計(jì)數(shù)器:當(dāng)前線程所執(zhí)行的字節(jié)...
    小明oh閱讀 1,130評(píng)論 0 2
  • 《深入理解Java虛擬機(jī)》筆記_第一遍 先取看完這本書(JVM)后必須掌握的部分棺聊。 第一部分 走近 Java 從傳...
    xiaogmail閱讀 5,062評(píng)論 1 34
  • 又是一年秋招季限佩,哎呀媽呀我被虐的慘來~這不,前幾陣失蹤沒更新博客裸弦,其實(shí)是我偷偷把時(shí)間用在復(fù)習(xí)課本了(霧 堅(jiān)持在社區(qū)...
    tengshe789閱讀 1,993評(píng)論 0 8
  • 這篇文章是我之前翻閱了不少的書籍以及從網(wǎng)絡(luò)上收集的一些資料的整理祟同,因此不免有一些不準(zhǔn)確的地方,同時(shí)不同JDK版本的...
    高廣超閱讀 15,545評(píng)論 3 83
  • 我突然覺得,我常常在對(duì)孩子做一些看似非常正確但卻根本沒有意義的要求沪斟。 檢查孩子的作業(yè)广辰,發(fā)現(xiàn)錯(cuò)別字好多,各種錯(cuò)法層出...
    耘心閱讀 452評(píng)論 2 2