初探:Java虛擬機那點破事

前言

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

不能跨平臺的緣由是怎樣形成的?

2.1 機器語言和彙編

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

使用0和1組成的機器指令來編程,太過於繁瑣,單單只是記住0和1組成的指令就使人頭大。徹底能夠用一種簡易的方式代替記憶,例如作加法運算,而這個 加操做在機器碼中多是一個 010010 固定的指令,徹底能夠用 add 這個單詞來代替記憶,簡化了編程過程,這就是彙編語言。彙編語言的特色是用符號代替了機器指令代碼,並且符號與指令代碼一一對應,基本保留了機器語言的靈活性。而再將add指令轉爲010010機器碼的程序即是彙編語言編譯器。面試

2.2 硬件關係

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

內存對於 CPU 來僅僅是一個存放指令和數據的地方,並不能在內存中完成計算功能。例如要計算 a = b + c,必須將 a、b、c 都讀取到 CPU 內部才能進行加法運算,寄存器是存儲 CPU 執行所需數據的區域,是 CPU 不可或缺的一部分,全部程序都只能經過操做寄存器,達到控制 CPU 目的,完成計算任務。編程

2.2 芯片架構

arm、X86兩種芯片架構普遍應用在 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架構的手機。ubuntu

Android能夠經過adb命令來查看cpu信息一、adb shell 二、cat /proc/cpuinfo
2.3 C語言爲何不能誇平臺?

一般認爲 C 語言是編譯型語言。在編譯階段,編譯器直接將源碼編譯爲 對應CPU架構和操做系統上的可執行文件。以下圖所示 c 語言代碼編譯爲的彙編代碼:數組

Windows 部分彙編指令:緩存

ubuntu 部分彙編指令:數據結構

雖然讀不太懂彙編指令,比較了一下差別仍是不小的。C 語言更多的是偏向底層開發,只要編譯器足夠強大,支持對應平臺的編譯,或者對應平臺提供有C 編譯器(C 語言的編譯器也是衆多語言中最多的)。程序就能在對應平臺執行,也許 C 語言歷來就沒有想過要跨平臺。代碼與平臺有關性,是不能跨平臺的緣由。架構

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

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

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

圖中紅色框內的即是字節碼指令,運行時經過逐條解釋執行,這也是之前 Java 被指性能底下的詬點。的確,解釋執行的性能確實是和 C 編譯目標代碼比不了,可是在 JDK1.2 時就支持 JIT 及時編譯器。程序運行期間,分析熱點(常常調用)函數,編譯爲本地代碼緩存起來,之後直接執行本地代碼。雖然性能仍是和編譯型的語言有必定的差別,但 Java 憑藉其語言特性以及各類成熟的 Web 解決方案,這點性能差顯得不那麼重要,徹底可以接受。JIT 編譯代碼以下:image

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

4.JVM內存結構

內存做爲程序運行中的臨時存儲介質,本質上不進行任何的區域劃分,爲了可以合理有效的使用回收內存,纔將內存劃分出更多的區域。平時聽得較多的就是堆棧內存,堆棧是一種數據結構,也是一種概念模型。不一樣的語言有本身的實現方式,一般在 Oop編程中,棧存放函數執行時所需的局部變量,函數執行完即釋放,堆內存存儲對象。操做系統內存佈局

Windows 上棧內存由系統回收,堆內存由程序員自行回收。由於棧上內存不可控,JVM 只能在操做系統的堆內存上開闢本身的空間。JVM運行時內存結構

JVM堆

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

標量替換

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

棧上分配

JDK1.8默認開啓逃逸分析,肯定對象不會再被外部引用,經過標量替換將對象分解在棧中分配,棧中的對象隨着棧幀的出棧而銷燬,大大的減小了堆內存的佔用和GC的壓力。

開啓逃逸分析(1.8默認開啓)

關閉逃逸分析:

能夠看到,關閉逃逸分析總共使用堆內存 22M ,開啓逃逸分析只使用了 5M 左右。節約了很多堆內存空間,減小了 GC 壓力。開啓逃逸-XX:+DoEscapeAnalysis -XX:+PrintGC
關閉逃逸-XX:-DoEscapeAnalysis -XX:+PrintGC
同步消除若是逃逸分析確認對象的做用範圍不會超過當前線程,則消除對變量的同步措施。

JVM棧

JVM棧 是方法執行所需的數據結構,每一個線程都擁有一個JVM棧,隨着線程的建立而建立,隨着線程的銷燬而銷燬。JVM棧 以棧幀的單元,存放局部變量、操做數棧、動態連接、方法返回信息。具體能夠參考方法區/元數據區方法區中存放已被虛擬機加載的類信息,而且每一個類只會存在一份,做爲使用該類的入口。咱們所編寫的代碼類,通過javac編譯器,編譯存儲爲 class 文件,在使用該類時(建立類的實例,調用了類靜態方法類等),若是該類還未加載,會先將該 class 字節流從磁盤或者其餘途徑方式,加載存儲到方法區當中,而且建立該類的 class對象 供之後訪問使用。image

運行時常量池運行時常量池做爲方法區的一部分,爲每個類都維護一個常量池,存放着編譯時已知的字面量和各類符號引用。可參考粗談Java虛擬機2_Class文件分析PC寄存器每一個JVM線程都有本身的PC(程序計數器)寄存器。在任什麼時候候,每一個JVM線程都在執行單個方法的代碼,若是執行的不是native方法,則pc寄存器包含當前正在執行的Java字節碼指令的地址。若是當前執行的native方法,則PC寄存器的值undefined。本地方法棧

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

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

Android開發資料+面試架構資料 免費分享 點擊連接 便可領取

《Android架構師必備學習資源免費領取(架構視頻+面試專題文檔+學習筆記)》

相關文章
相關標籤/搜索