作者Java后端進階 轉(zhuǎn)載請注明出處
公眾號「Java后端進階」
一、首先我們先熟悉一下JVM
1. 什么是JVM?
JVM是Java Virtual Machine(Java虛擬機)的縮寫箱舞,JVM是一種用于計算設(shè)備的規(guī)范怜浅,它是一個虛構(gòu)出來的計算機袁波,是通過在實際的計算機上仿真模擬各種計算機功能來實現(xiàn)的人断。Java虛擬機包括一套字節(jié)碼指令集、一組寄存器踪蹬、一個棧胞此、一個垃圾回收堆和一個存儲方法域。 JVM屏蔽了與具體操作系統(tǒng)平臺相關(guān)的信息跃捣,使Java程序只需生成在Java虛擬機上運行的目標代碼(字節(jié)碼),就可以在多種平臺上不加修改地運行漱牵。JVM在執(zhí)行字節(jié)碼時,實際上最終還是把字節(jié)碼解釋成具體平臺上的機器指令執(zhí)行疚漆。
Java語言的一個非常重要的特點就是與平臺的無關(guān)性酣胀。而使用Java虛擬機是實現(xiàn)這一特點的關(guān)鍵。一般的高級語言如果要在不同的平臺上運行愿卸,至少需要編譯成不同的目標代碼灵临。而引入Java語言虛擬機后趴荸,Java語言在不同平臺上運行時不需要重新編譯。Java語言使用Java虛擬機屏蔽了與具體平臺相關(guān)的信息宦焦,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節(jié)碼)发钝,就可以在多種平臺上不加修改地運行精堕。Java虛擬機在執(zhí)行字節(jié)碼時孵淘,把字節(jié)碼解釋成具體平臺上的機器指令執(zhí)行。這就是Java的能夠“一次編譯歹篓,到處運行”的原因瘫证。
2. JRE/JDK/JVM是什么關(guān)系揉阎?
JRE(JavaRuntimeEnvironment,Java運行環(huán)境)背捌,也就是Java平臺毙籽。所有的Java 程序都要在JRE下才能運行。普通用戶只需要運行已開發(fā)好的java程序毡庆,安裝JRE即可坑赡。
JDK(Java Development Kit)是程序開發(fā)者用來來編譯、調(diào)試java程序用的開發(fā)工具包么抗。JDK的工具也是Java程序毅否,也需要JRE才能運行。為了保持JDK的獨立性和完整性蝇刀,在JDK的安裝過程中搀突,JRE也是 安裝的一部分。所以熊泵,在JDK的安裝目錄下有一個名為jre的目錄仰迁,用于存放JRE文件。
JVM(JavaVirtualMachine顽分,Java虛擬機)是JRE的一部分徐许。它是一個虛構(gòu)出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現(xiàn)的卒蘸。JVM有自己完善的硬件架構(gòu)雌隅,如處理器、堆棧缸沃、寄存器等恰起,還具有相應(yīng)的指令系統(tǒng)。Java語言最重要的特點就是跨平臺運行趾牧。使用JVM就是為了支持與操作系統(tǒng)無關(guān)检盼,實現(xiàn)跨平臺。
二翘单、Java程序的基本執(zhí)行原理:
1.執(zhí)行原理說明:首先java源代碼文件(.java后綴)會被java編譯器編譯為字節(jié)碼文件(.class文件)吨枉,然后由JVM中類加載器(Class Loader)加載各個類的.class文件,加載完成之后哄芜,交給執(zhí)行引擎執(zhí)行貌亭。在整個執(zhí)行過程中,
JVM會用一段空間來存儲程序執(zhí)行期間需要用到的數(shù)據(jù)和相關(guān)信息认臊,這段空間一般被稱為運行時數(shù)據(jù)區(qū)(Runtime Data Area),也就是我們常說的JVM內(nèi)存圃庭。因此,在java中我們常說內(nèi)存管理就是針對這段空間的管理。
2.原理圖:
二剧腻、了解了java程序的執(zhí)行原理拘央,下面我們就要針對于運行時數(shù)據(jù)區(qū)進行深入理解;
運行時數(shù)據(jù)區(qū)就是我們平常所說的JVM內(nèi)存恕酸,首先我們先了解一下JVM內(nèi)存區(qū)域的劃分:
1.內(nèi)存區(qū)域:可以劃分為線程共享和非線程共享堪滨,一般垃圾回收機制gc方法發(fā)生在線程共享的區(qū)域(大部分發(fā)生在Heap上)的原因。
1.Method Area(Non-heap)(方法區(qū))-----線程共享
2.Heap(堆)----線程共享
3.Program counter register(程序計數(shù)器)----非線程共享
4.VM Stack (虛擬機棧)? ----非線程共享
5.Nativa Method Stack (本地方法棧)----非線程共享
2.Jvm的運行區(qū)域
3.為什么分為線程共享和非線程共享的呢蕊温?袱箱??
我們明白了java程序的執(zhí)行原理义矛,我們再談jvm发笔;
JVM初始運行的時候都會分配好Method Area(方法區(qū))和Heap(堆),而JVM 每遇到一個線程凉翻,就為其分配一個Program Counter Register(程序計數(shù)器),VM Stack(虛擬機棧)和Native Method Stack?(本地方法棧)了讨,當線程終止時,三者(虛擬機棧制轰,本地方法棧和程序計數(shù)器)所占用的內(nèi)存空間也會被釋放掉前计。這也是為什么把內(nèi)存區(qū)域分為線程共享和非線程共享的原因,非線程共享的那三個區(qū)域的生命周期與所屬線程相同垃杖,而線程共享的區(qū)域與JAVA程序運行的生命周期相同男杈,所以這也是系統(tǒng)垃圾回收的場所只發(fā)生在線程共享的區(qū)域(實際上對大部分虛擬機來說知發(fā)生在Heap上)的原因。
1.虛擬機棧:
Java棧也稱作虛擬機棧(Java Vitual Machine Stack)调俘,也就是我們常常所說的棧伶棒,跟C語言的數(shù)據(jù)段中的棧類似。事實上彩库,Java棧是Java方法執(zhí)行的內(nèi)存模型肤无。為什么這么說呢?下面就來解釋一下其中的原因骇钦。
Java棧中存放的是一個個的棧幀宛渐,每個棧幀對應(yīng)一個被調(diào)用的方法,在棧幀中包括局部變量表(Local Variables)司忱、操作數(shù)棧(Operand Stack)皇忿、指向當前方法所屬的類的運行時常量池(運行時常量池的概念在方法區(qū)部分會談到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些額外的附加信息坦仍。當線程執(zhí)行一個方法時,就會隨之創(chuàng)建一個對應(yīng)的棧幀叨襟,并將建立的棧幀壓棧繁扎。當方法執(zhí)行完畢之后,便會將棧幀出棧。
因此可知梳玫,線程當前執(zhí)行的方法所對應(yīng)的棧幀必定位于Java棧的頂部爹梁。講到這里,大家就應(yīng)該會明白為什么 在 使用 遞歸方法的時候容易導(dǎo)致棧內(nèi)存溢出的現(xiàn)象了以及為什么棧區(qū)的空間不用程序員去管理了(當然在Java中提澎,程序員基本不用關(guān)系到內(nèi)存分配和釋放的事情姚垃,因為Java有自己的垃圾回收機制),這部分空間的分配和釋放都是由系統(tǒng)自動實施的盼忌。對于所有的程序設(shè)計語言來說积糯,棧這部分空間對程序員來說是不透明的。下圖表示了一個Java棧的模型:
局部變量表谦纱,顧名思義看成,想必不用解釋大家應(yīng)該明白它的作用了吧。就是用來存儲方法中的局部變量(包括在方法中聲明的非靜態(tài)變量以及函數(shù)形參)跨嘉。對于基本數(shù)據(jù)類型的變量川慌,則直接存儲它的值,對于引用類型的變量祠乃,則存的是指向?qū)ο蟮囊妹沃亍>植孔兞勘淼拇笮≡诰幾g器就可以確定其大小了,因此在程序執(zhí)行期間局部變量表的大小是不會改變的亮瓷。
操作數(shù)棧琴拧,想必學過數(shù)據(jù)結(jié)構(gòu)中的棧的朋友對表達式求值問題不會陌生,棧最典型的一個應(yīng)用就是用來對表達式求值寺庄。想想一個線程執(zhí)行方法的過程中艾蓝,實際上就是不斷執(zhí)行語句的過程,而歸根到底就是進行計算的過程斗塘。因此可以這么說赢织,程序中的所有計算過程都是在借助于操作數(shù)棧來完成的。
指向運行時常量池的引用馍盟,因為在方法執(zhí)行的過程中有可能需要用到類中的常量于置,所以必須要有一個引用指向運行時常量。
方法返回地址贞岭,當一個方法執(zhí)行完畢之后八毯,要返回之前調(diào)用它的地方,因此在棧幀中必須保存一個方法返回地址瞄桨。
由于每個線程正在執(zhí)行的方法可能不同话速,因此每個線程都會有一個自己的Java棧,互不干擾芯侥。
2.堆
在C語言中泊交,堆這部分空間是唯一一個程序員可以管理的內(nèi)存區(qū)域乳讥。程序員可以通過malloc函數(shù)和free函數(shù)在堆上申請和釋放空間。那么在Java中是怎么樣的呢廓俭?
Java中的堆是用來存儲對象本身的以及數(shù)組(當然云石,數(shù)組引用是存放在Java棧中的)。只不過和C語言中的不同研乒,在Java中汹忠,程序員基本不用去關(guān)心空間釋放的問題,Java的垃圾回收機制會自動進行處理雹熬。因此這部分空間也是Java垃圾收集器管理的主要區(qū)域宽菜。另外,堆是被所有線程共享的橄唬,在JVM中只有一個堆赋焕。
3.方法區(qū)
方法區(qū)在JVM中也是一個非常重要的區(qū)域,它與堆一樣仰楚,是被線程共享的區(qū)域隆判。在方法區(qū)中,存儲了每個類的信息(包括類的名稱僧界、方法信息侨嘀、字段信息)、靜態(tài)變量捂襟、常量以及編譯器編譯后的代碼等咬腕。
在Class文件中除了類的字段、方法葬荷、接口等描述信息外涨共,還有一項信息是常量池,用來存儲編譯期間生成的字面量和符號引用宠漩。
在方法區(qū)中有一個非常重要的部分就是運行時常量池举反,它是每一個類或接口的常量池的運行時表示形式,在類和接口被加載到JVM后扒吁,對應(yīng)的運行時常量池就被創(chuàng)建出來火鼻。當然并非Class文件常量池中的內(nèi)容才能進入運行時常量池,在運行期間也可將新的常量放入運行時常量池中雕崩,比如String的intern方法魁索。
在JVM規(guī)范中,沒有強制要求方法區(qū)必須實現(xiàn)垃圾回收盼铁。很多人習慣將方法區(qū)稱為“永久代”粗蔚,是因為HotSpot虛擬機以永久代來實現(xiàn)方法區(qū),從而JVM的垃圾收集器可以像管理堆區(qū)一樣管理這部分區(qū)域饶火,從而不需要專門為這部分設(shè)計垃圾回收機制支鸡。不過自從JDK7之后冬念,Hotspot虛擬機便將運行時常量池從永久代移除了趁窃。
4.程序計數(shù)器
程序計數(shù)器(Program Counter Register)牧挣,也有稱作為PC寄存器。想必學過匯編語言的朋友對程序計數(shù)器這個概念并不陌生醒陆,在匯編語言中瀑构,程序計數(shù)器是指CPU中的寄存器,它保存的是程序當前執(zhí)行的指令的地址(也可以說保存下一條指令的所在存儲單元的地址)刨摩,當CPU需要執(zhí)行指令時寺晌,需要從程序計數(shù)器中得到當前需要執(zhí)行的指令所在存儲單元的地址,然后根據(jù)得到的地址獲取到指令澡刹,在得到指令之后呻征,程序計數(shù)器便自動加1或者根據(jù)轉(zhuǎn)移指針得到下一條指令的地址,如此循環(huán)罢浇,直至執(zhí)行完所有的指令陆赋。
雖然JVM中的程序計數(shù)器并不像匯編語言中的程序計數(shù)器一樣是物理概念上的CPU寄存器,但是JVM中的程序計數(shù)器的功能跟匯編語言中的程序計數(shù)器的功能在邏輯上是等同的嚷闭,也就是說是用來指示 執(zhí)行哪條指令的攒岛。
由于在JVM中,多線程是通過線程輪流切換來獲得CPU執(zhí)行時間的胞锰,因此灾锯,在任一具體時刻,一個CPU的內(nèi)核只會執(zhí)行一條線程中的指令嗅榕,因此顺饮,為了能夠使得每個線程都在線程切換后能夠恢復(fù)在切換之前的程序執(zhí)行位置,每個線程都需要有自己獨立的程序計數(shù)器凌那,并且不能互相被干擾兼雄,否則就會影響到程序的正常執(zhí)行次序。因此案怯,可以這么說君旦,程序計數(shù)器是每個線程所私有的。
在JVM規(guī)范中規(guī)定嘲碱,如果線程執(zhí)行的是非native方法金砍,則程序計數(shù)器中保存的是當前需要執(zhí)行的指令的地址;如果線程執(zhí)行的是native方法麦锯,則程序計數(shù)器中的值是undefined恕稠。
由于程序計數(shù)器中存儲的數(shù)據(jù)所占空間的大小不會隨程序的執(zhí)行而發(fā)生改變,因此扶欣,對于程序計數(shù)器是不會發(fā)生內(nèi)存溢出現(xiàn)象(OutOfMemory)的鹅巍。
5.本地方法棧
本地方法棧與Java棧的作用和原理非常相似千扶。區(qū)別只不過是Java棧是為執(zhí)行Java方法服務(wù)的,而本地方法棧則是為執(zhí)行本地方法(Native Method)服務(wù)的骆捧。在JVM規(guī)范中澎羞,并沒有對本地方法棧的具體實現(xiàn)方法以及數(shù)據(jù)結(jié)構(gòu)作強制規(guī)定,虛擬機可以自由實現(xiàn)它敛苇。在HotSopt虛擬機中直接就把本地方法棧和Java棧合二為一妆绞。