粗談Java虛擬機1_開山篇

1. 前言

?從學習Java的第一天開始,到如今工作當中,想必大家都耳聞目染了各種Java的優點。其中肯定少不了:Java有虛擬機,java是跨平臺的,一次編譯到處運行。在相當長的一段時間里對此觀點都只是一個很模糊的概念,對自己寫的代碼也有一種吃不透的感覺。猶如一只攔路的大老虎,望而生畏,止步不前。一番思量,一日不解決掉,對技術難以有更深層次的理解,只好硬著頭皮上。


2. 不能跨平臺的原因是怎樣造成的?

2.1 機器語言和匯編

?計算機只認識0和1 這句話大家都聽說過。的確,正所謂大道至簡,0和1足以撐起整個互聯網世界。在早期編程中,都是編寫一條條0和1組成的指令來開發,要自己處理每一塊數據的存儲分配和輸入輸出。可想而知,滿屏的0和1,程序容易出錯且可讀性很差。

case1.jpg

?使用 0和1 組成的機器指令來編程,太過于繁瑣,單單只是記住0和1組成的指令就令人頭大。完全可以用一種簡易的方式代替記憶,例如做加法運算,而這個的操作在機器碼中可能是一個 010010 固定的指令,完全可以用 add 這個單詞來代替記憶,簡化了編程過程,這就是匯編語言。匯編語言的特點是用符號代替了機器指令代碼,而且符號與指令代碼一一對應,基本保留了機器語言的靈活性。而再將add指令轉為010010機器碼的程序便是匯編語言編譯器。

2.2 硬件關系

?組裝過電腦的朋友都知道,組裝一臺電腦需要購買:CPU、內存條、硬盤,主板等以及各種外設。對程序而言,一開始存儲在硬盤當中,即便計算機斷電,下次重啟程序依舊存在。CPU 是一個復雜的計算機部件,它內部又包含很多小零件,如下圖所示:


118274690_1_20171206084852264.jpg

?????圖片摘自C語言中文網
?內存對于 CPU 來僅僅是一個存放指令和數據的地方,并不能在內存中完成計算功能。例如要計算 a = b + c,必須將 a、b、c 都讀取到 CPU 內部才能進行加法運算,寄存器是存儲 CPU 執行所需數據的區域,是 CPU 不可或缺的一部分,所有程序都只能通過操作寄存器,達到控制 CPU 目的,完成計算任務。

2.2 芯片架構

?armX86兩種芯片架構廣泛應用在 PC 機和移動端嵌入式設備中。前者由arm公司設計,后者由Intel、amd共同設計,雙方交叉授權使用。arm 是精簡指令集架構(RSIC),功耗較低,性能隨之也降了下來。x86 是復雜指令集架構(CISC),功耗較高,性能強。arm架構的寄存器 比 x86架構 的多不少。寄存器和指令集加架構本身的差異性,也是造成不能跨平臺的原因。近幾十年來,硬件的性能一直都在飛速發展,CPU架構 也經歷了幾次較大的改變。 x86架構從最早的 16 位到 32 位再到現在的 64 位架構。arm架構 也從 v1 發展到了如今的 v8的64位架構。一般新的架構都會向前兼容幾個版本,保證舊架構上的老代碼,能夠在新架構上運行。但這樣做,卻無法發揮出新架構硬件的性能,無疑是對資源的浪費。在開發中如果涉及到底層庫的使用,則需要考慮兼容不同架構的CPU。例如在使用百度地圖SDK時,會下載不同CPU架構的so文件,還有 X86 架構的,就是為了兼容不同CPU架構的手機。

cpu_so.png

Android可以通過adb命令來查看cpu信息1、adb shell 2、cat /proc/cpuinfo

2.3 C語言為什么不能夸平臺?

?通常認為 C 語言是編譯型語言。在編譯階段,編譯器直接將源碼編譯為 對應CPU架構和操作系統上的可執行文件。
如下圖所示 c 語言代碼編譯為的匯編代碼:

#include <stdio.h>
int main() {

    printf("Hello World");
    return 0;
}

Windows 部分匯編指令:


微信截圖_20190723174501.png

ubuntu 部分匯編指令:


微信截圖_20190724170811.png

雖然讀不太懂匯編指令,比較了一下差異還是不小的。C 語言更多的是偏向底層開發,只要編譯器足夠強大,支持對應平臺的編譯,或者對應平臺提供有C 編譯器(C 語言的編譯器也是眾多語言中最多的)。程序就能在對應平臺執行,也許 C 語言從來就沒有想過要跨平臺。

代碼與平臺有關性,是不能跨平臺的原因。

3. JVM是如何做到跨平臺的

?講了這么多不能夸平臺的原因,再來理解Java是如何做到跨平臺就容易得多了。JVM 在編譯階段,只將 .java的源碼,編譯為和平臺無關的 .class 字節碼文件。不同 CPU 架構和操作系統上都會編譯為相同的 calss 文件(最多只是 JDK 版本不同,有些許差異,jdk 都會向前兼容幾個版本)。再由不同平臺上的自行實現JVM。我們只需要搭建相應平臺的運行環境即可,便可做到任意平臺開發編譯,到處運行。

未命名文件.png

?JVM 在真機基礎之上模擬了一套自己的架構,有自己的指令集、內存管理等。在使用 Eclipse 追溯源碼時,常常會遇到只有 class 文件,而沒有源碼出現下面的頁面:
微信截圖_20181120131556.png

?圖中紅色框內的便是字節碼指令,運行時通過逐條解釋執行,這也是以前 Java 被指性能底下的詬點。的確,解釋執行的性能確實是和 C 編譯目標代碼比不了,但是在 JDK1.2 時就支持 JIT 及時編譯器。程序運行期間,分析熱點(經常調用)函數,編譯為本地代碼緩存起來,以后直接執行本地代碼。雖然性能還是和編譯型的語言有一定的差異,但 Java 憑借其語言特性以及各種成熟的 Web 解決方案,這點性能差顯得不那么重要,完全能夠接受。JIT 編譯代碼如下:
微信截圖_20181202221710.png

有些JVM是采用純JIT編譯方式實現的,內部沒解釋器,例如JRockit、Maxine VM和Jikes RVM ---RednaxelaFX

4.JVM內存結構

?內存作為程序運行中的臨時存儲介質,本質上不進行任何的區域劃分,為了能夠合理有效的使用回收內存,才將內存劃分出更多的區域。平時聽得較多的就是堆棧內存,堆棧是一種數據結構,也是一種概念模型。不同的語言有自己的實現方式,通常在 Oop編程中,棧存放函數執行時所需的局部變量,函數執行完即釋放,堆內存存儲對象。

操作系統內存布局
微信圖片_20190730142730.png

?Windows 上棧內存由系統回收,堆內存由程序員自行回收。因為棧上內存不可控,JVM 只能在操作系統的堆內存上開辟自己的空間。

JVM運行時內存結構
微信圖片_20190730145126.png
JVM堆

?所有類實例和數組都從堆中分配,官方JVMS8規范文檔 的確是這樣描述的 The heap is the run-time data area from which memory for all class instances and arrays is allocated 。有一個很常見情況下,函數執行中產生的對象在堆中分配,函數執行結束,不再引用的對象,已經沒有存在的必要了。這些對象在堆中等待下一次GC,而大多對象朝生即死,生命周期極短,等待GC這段時間,也是對資源的浪費。在JDK1.5JVM提供支持逃逸分析技術,通過分析對象作用域,實現了棧上分配、標量替換、同步消除優化等技術。通過函數傳遞對象,稱之為方法逃逸。將對象賦值給其他線程變量,稱之為線程逃逸:

標量替換

?不可再分解的基礎數據類型稱之為標量,例如Java中的八大基礎類型和引用類型。反之、如果某個對象還可繼續分解,則該對象屬于聚合量,Java類就是典型的聚合量。標量替換則是將對象的成員變量分解成原始數據類型,代替對象在棧中分配。

棧上分配

?JDK1.8默認開啟逃逸分析,確定對象不會再被外部引用,通過標量替換將對象分解在棧中分配,棧中的對象隨著棧幀的出棧而銷毀,大大的減少了堆內存的占用和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默認開啟)

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

關閉逃逸分析:

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
   

可以看到,關閉逃逸分析總共使用堆內存 22M ,開啟逃逸分析只使用了 5M 左右。節約了不少堆內存空間,減少了 GC 壓力。

開啟逃逸-XX:+DoEscapeAnalysis -XX:+PrintGC

關閉逃逸-XX:-DoEscapeAnalysis -XX:+PrintGC

同步消除

如果逃逸分析確認對象的作用范圍不會超過當前線程,則消除對變量的同步措施。

JVM棧

?JVM棧 是方法執行所需的數據結構,每個線程都擁有一個JVM棧,隨著線程的創建而創建,隨著線程的銷毀而銷毀。JVM棧 以棧幀的單元,存放局部變量、操作數棧、動態鏈接、方法返回信息。具體可以參考

方法區/元數據區

?方法區中存放已被虛擬機加載的類信息,并且每個類只會存在一份,作為使用該類的入口。我們所編寫的代碼類,經過javac編譯器,編譯存儲為 class 文件,在使用該類時(創建類的實例,調用了類靜態方法類等),如果該類還未加載,會先將該 class 字節流從磁盤或者其他途徑方式,加載存儲到方法區當中,并且創建該類的 class對象 供以后訪問使用。

微信圖片_20190730152335.png

運行時常量池

?運行時常量池作為方法區的一部分,為每一個類都維護一個常量池,存放著編譯時已知的字面量和各種符號引用。具體可見參考第二章

PC寄存器

?每個JVM線程都有自己的PC(程序計數器)寄存器。在任何時候,每個JVM線程都在執行單個方法的代碼,如果執行的不是native方法,則pc寄存器包含當前正在執行的Java字節碼指令的地址。如果當前執行的native方法,則PC寄存器的值undefined。

本地方法棧

?支持 native 方法調用,隨著線程的創建來分配本地方法棧。


參考:
深入理解Java虛擬機一書

RednaxelaFX

keycoding

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容