Java跨平臺根本緣由,面試必問JVM內存結構白話文詳解來了

目錄java

Java與C++之間有一堵由內存動態分配和垃圾收集技術所圍成的「高牆」,牆外面的人想進去,牆裏面的人想出來。linux

什麼是跨平臺?

我以前一直在想一個問題,一直在說Java能夠跨平臺,可是C代碼能夠放到 windows 平臺執行也能夠放到 linux 平臺裏面執行,爲何不說C語言跨平臺呢?程序員

跨平臺定義

跨平臺定義:首先這是基於源碼的跨平臺。也就是說,只寫一套代碼,可是在各個平臺如 Windows,Linux,unix 都能順利運行,這就是跨平臺。web

咱們知道 Java 是運行在虛擬機裏面的,無論你的服務器是windows系統仍是linux系統,只要在各個平臺上面安裝 java虛擬機,那麼就能夠愉快的運行Java代碼,因此說Java是平臺無關的語言便可跨平臺。windows

然而 C&C++ 語言,他們是平臺有關的語言,咱們在 Windows 系統下面編寫好了代碼,運行的很快樂,可是拿到 Linux 系統運行卻不必定能成功可能報錯。例如一段打開文件的代碼實現,我隨便百度一下兩個平臺的代碼實現以下圖,分爲 windows 實現和 linux 實現,也就是說C語言平臺有關,不能跨平臺運行。服務器

C++代碼打開文件系統
C++代碼打開文件系統

C語言代碼能不能跨平臺運行呢?

固然能夠了,有兩種方法可讓C程序代碼實現跨平臺運行,數據結構

  1. 寫好兼容代碼。

只要你在代碼裏面寫好兼容代碼,同時寫一套 windows 的實現,再寫一套 linux 的實現,那麼這套代碼就能夠同時在 windows 和 linux 系統下執行。假如一個打開文件的操做在windows和linux裏面的實現不同,僞代碼以下:多線程

if (當前系統 == windows){
 open1 file ; } else if (當前系統 == linux) {  open2 file ; } else {  open3 file ; } 複製代碼

能夠想象,若是你是要實現一個大的工程有不少代碼,你得寫多少兼容代碼,並且測試的時候還須要同時放到兩個平臺去測試,這是多麼的誇張,使得程序員本來就不茂密的頭髮更加雪上添霜。架構

  1. 移植編譯器

由於支持C++語言的各個平臺的架構不一樣(好比CPU可以處理的指令集不同),因此一份C++源代碼要想在另外一個操做系統平臺上執行,就必須用該平臺相對應的C++代碼編譯器對C++源代碼從新進行編譯,生成該平臺能夠直接執行的機器代碼。jvm

它的執行過程是:預處理->編譯->彙編->連接->機器碼

能夠看到編譯器是關鍵,再拿C語言爲例,Linux下直接使用 gcc編譯器 編譯C程序,在Windows下使用對應的 mingw 編譯C程序,這樣用兩套不一樣的編譯器來在不一樣的平臺進行編譯,不一樣的編譯器都是封裝了各自平臺對C語言的處理,可是這樣也很麻煩啊,因此Java虛擬機的價值就更加突顯了。

Java跨平臺的緣由

Java虛擬機定義了一種Java內存模型(Java memory model, JMM)來屏蔽掉各類硬件和操做系統的內存訪問差別,簡單理解也就是說Java虛擬機至關因而在源碼和平臺之間抽象了一層出來,專門處理一些平臺之間訪問的兼容問題,使得源碼能夠一次編譯處處運行

然而C/C++是直接使用物理硬件和操做系統的內存模型,所以會因爲不一樣平臺的內存模型不一樣而產生差別。

這樣看來JVM內存結構是很重要啊!

JVM內存結構

JVM運行規範 能夠看到Java源代碼先是通過編譯器進行編譯,變成.class文件,由類加載器加載進內存運行。

java 編譯的字節碼解析路徑直達從JVM設計者的角度來看.class文件結構,一文弄懂.class文件的身份地位

Java在 JVM 中的運行生命週期和類加載詳細過程路徑直達 java類在JVM中的生命週期

運行以前的解析讀者能夠直接看我前面寫的幾篇,本文主要想說的是運行時數據區。

運行時數據區總覽

JVM運行時數據區
JVM運行時數據區

方法區和堆區是全部線程共享的,棧和程序計數器都是線程私有各自管各自的。

線程私有: Java虛擬機的多線程實現,是經過線程輪流切換並分配處理器執行時間的方式來實現的,再任何一個肯定的時刻,一個處理器都只會執行一條線程中的指令。所以爲了線程切換以後能恢復到正確的的執行位置,每條線程都須要一個獨立的程序計數器,各個線程之間計數器不影響獨立存儲,咱們稱這類內存區域爲「線程私有」內存。

程序計數器

程序計數器存放的是下一條字節碼指令執行的地址,存放地址的地方,所以只須要一塊較小的內存空間,它的做用是當前線程所執行的字節碼行號指示器。

JVM指令
JVM指令

放上一篇文章的圖,這是 jvm 的一些指令,最終會和計算機的相關指令相對應。

字節碼指令有序進行
字節碼指令有序進行

咱們都知道在計算機裏面CPU是從指令寄存器拿到執行指令進行工做,當指令寄存器裏面一條指令被CPU拿走執行,那麼寄存器就會把程序計數器裏面指定的下一個須要執行的字節碼指令對應的CPU指令拿進來,讓CPU進行執行,因此實現字節碼指令均可以作到有序執行,須要注意的是程序計數器存放的都是下一個字節碼指令的地址,這樣才能夠一直往下執行嘛。

ps:這個地方我解釋這麼清楚是由於其餘的你們耳聞較多,很容易理解,可是這個區域至少我大學的時候這個地方就不是很理解。

注:程序計數器是惟一一個在JVM規範中沒有規定任何 OutOfMemoryError 的區域。

java堆(Heap)

new的世界
new的世界

Java堆是垃圾收集器管理的主要區域,所以不少時候也被稱做「GC堆」,幸虧國內沒翻譯成「垃圾堆」。

對於大多數應用來講,Java堆(Heap)是Java虛擬機所管理的內存中最大的一塊,主要記住三點:

  1. Java堆是被全部 線程共享的一塊內存區域,在虛擬機啓動時建立;
  2. 此內存區域的 惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存;
  3. OutOfMemoryError異常。若是在堆中沒有內存完成實例分配,而且堆也沒法再擴展時會拋出此異常。

棧區

入棧出棧
入棧出棧

每個方法從調用開始到完成的過程,就是一個棧幀在在虛擬機中入棧到出棧的過程

棧幀
棧幀

棧幀包括的內容:

  1. 局部變量表
  2. 操做數棧
  3. 動態連接

說白了就是:

  • 存放了編譯期可知的各類基本數據類型(boolean,byte,char,short,int...)
  • 存放對象引用(注意不是對象自己,是引用,即指針)
  • 存放字節碼指令地址 returnAddress 類型(即方法返回地址,方法出口)

局部變量表的內存空間在編譯期間就完成了分配,進入一個方法的時候,這個方法須要在幀裏面分配多少局部變量空間是肯定的,不會改變。

本地方法棧

這裏簡單提一下,他與Java虛擬機棧所發揮的做用是很是類似的,其區別不過是虛擬機棧爲虛擬機執行 Java 方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的 Native 方法服務。

Navtive 方法是 Java 經過 JNI 直接調用本地 C/C++ 庫,能夠認爲是 Native 方法至關於 C/C++ 暴露給 Java 的一個接口,Java 經過調用這個接口從而調用到 C/C++ 方法。當線程調用 Java 方法時,虛擬機會建立一個棧幀並壓入 Java 虛擬機棧。然而當它調用的是 native 方法時,虛擬機會保持 Java 虛擬機棧不變,也不會向 Java 虛擬機棧中壓入新的棧幀,虛擬機只是簡單地動態鏈接並直接調用指定的 native 方法。

異常:

  1. StackOverFlowError:棧溢出,線程請求的棧深度大於虛擬機所容許的深度(代碼實現能夠寫一個遞歸方法,而後不給遞歸出口,調用遞歸方法每次遞歸都會產生新的棧幀直到把棧區打滿溢出)
  2. OutOfMemoryError:虛擬機棧擴展時沒法申請到足夠內存(代碼實現能夠一直循環new對象,直到把堆區打滿內存溢出)

方法區

方法區
方法區

由圖看出此部分主要有靜態的常量(類信息不會變),和運行時常量池。

用於存儲:

  1. 已被虛擬機加載的類信息
  2. 常量
  3. 靜態變量
  4. 即時編譯器(JIT)編譯後的代碼

傳說中的永久代

不少人都把方法區稱爲永久代,本質上二者不等價。

這個說法是創建在HotPot虛擬機的,其設計團隊把GC分代收集擴展到了方法區,也便是用永久代來實現方法區以致於垃圾收集器能夠像管理堆同樣管理方法區內存,對其餘的虛擬機來講是不存在永久代的概念的。

運行時常量池

運行時常量池是方法區的一部分,class文件除了類的版本,字段,方法,接口等描述信息以外,還有一項信息是常量池,用於存放編譯期生成的各類字面量和符號引用,這些內容將在類加載後進入方法區的運行時常量池中存放。

這部份內容是具有動態性的,運行期間能夠放入新的常量,例如String類的intern()方法,以及new String(「123」)的時候,String類型先會先去常量池看123存在不,存在的話直接在堆區生成對象而且引用他,若是不存在會先去常量池建立一個「123」再去堆引用指向他。

JVM堆,棧,方法區對應結構

JVM堆,棧,方法區模型圖
JVM堆,棧,方法區模型圖

從設計者角度根據類的內容來劃分JVM內存:

方法區其實就是存放一些「死的東西」,不會動的東西,例如類的一些死的信息(類名,常量等);

棧是存放活的動起來的東西,專門對類函數方法來設置的,入棧出棧也不用垃圾回收內存什麼的(試想一下,類裏面的方法不少,並且是方法進入方法返回,都有進有出,在數據結構裏面只有棧這種結構能知足設計,棧幀固然就是根據方法裏面的局部變量什麼的來設計的);

堆區存放對象,對象變化比較大,涉及到垃圾回收所以也單獨劃分區域來存儲,便於管理和回收;

程序計數器方便調度字節碼指令,讓程序動起來,即代碼按照代碼順序執行;

本地方法棧因爲是調用的c代碼,是經過動態連接的方式而不是傳統數據結構中的棧結構,因此也抽出來進行特殊處理,劃分一個本地方法棧。

這樣看,整個類信息主要是拆開來,爲了方便管理,存儲和調度從而劃分紅了這幾個區域。

公衆號下篇內容預告: 對象的建立,內存佈局以及訪問定位

用了洪荒之力寫完這篇,有收穫的朋友點個在看或者分享鼓勵一下吧,十分感謝~

愛我你就關注我
愛我你就關注我

本文使用 mdnice 排版

相關文章
相關標籤/搜索