JVM学习笔记(一):Java内存区域
由于Java程序是交由JVM執(zhí)行的,所以我們在談Java內(nèi)存區(qū)域劃分的時候事實上是指JVM內(nèi)存區(qū)域劃分。在討論JVM內(nèi)存區(qū)域劃分之前,先來看一下Java程序具體執(zhí)行的過程:
首先Java源代碼文件(.java后綴)會被Java編譯器編譯為字節(jié)碼文件(.class后綴),然后由JVM中的類加載器加載各個類的字節(jié)碼文件,加載完畢之后,交由JVM執(zhí)行引擎執(zhí)行。在整個程序執(zhí)行過程中,JVM會用一段空間來存儲程序執(zhí)行期間需要用到的數(shù)據(jù)和相關(guān)信息,這段空間一般被稱作為Runtime Data Area(運行時數(shù)據(jù)區(qū)),也就是我們常說的JVM內(nèi)存。因此,在Java中我們常常說到的內(nèi)存管理就是針對這段空間進(jìn)行管理(如何分配和回收內(nèi)存空間)。
一、運行時數(shù)據(jù)區(qū)域
1. 程序計數(shù)器
在匯編語言中,程序計數(shù)器是指CPU中的寄存器,它保存的是程序當(dāng)前執(zhí)行的指令的地址(也可以說保存下一條指令的所在存儲單元的地址),當(dāng)CPU需要執(zhí)行指令時,需要從程序計數(shù)器中得到當(dāng)前需要執(zhí)行的指令所在存儲單元的地址,然后根據(jù)得到的地址獲取到指令,在得到指令之后,程序計數(shù)器便自動加1或者根據(jù)轉(zhuǎn)移指針得到下一條指令的地址,如此循環(huán),直至執(zhí)行完所有的指令。
雖然JVM中的程序計數(shù)器并不像匯編語言中的程序計數(shù)器一樣是物理概念上的CPU寄存器,但是JVM中的程序計數(shù)器的功能跟匯編語言中的程序計數(shù)器的功能在邏輯上是等同的,也就是說是用來指示 執(zhí)行哪條指令的。
jvm中,程序計數(shù)器是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。在虛擬機(jī)的概念模型里,字節(jié)碼解釋器的工作就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令。
每個線程都有一個獨立的程序計數(shù)器
如果線程執(zhí)行的是非Native方法(Java方法),則程序計數(shù)器中保存的是當(dāng)前需要執(zhí)行的指令的地址;如果線程執(zhí)行的是Native方法,則程序計數(shù)器中的值是undefined。
由于程序計數(shù)器中存儲的數(shù)據(jù)所占空間的大小不會隨程序的執(zhí)行而發(fā)生改變,因此,程序計數(shù)器不會發(fā)生內(nèi)存溢出現(xiàn)象(OutOfMemory)。
2. 虛擬機(jī)棧(又稱Java棧)
與程序計數(shù)器一樣,Java虛擬機(jī)棧也是線程私有的,即每個線程都會有一個自己的Java棧(因為每個線程正在執(zhí)行的方法可能不同),Java虛擬機(jī)棧的生命周期與線程相同。虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀,棧幀用于存儲局部變量表、操作數(shù)棧、指向當(dāng)前方法所屬的類的運行時常量池的引用、方法返回地址和一些額外的附加信息。
當(dāng)線程執(zhí)行一個方法時,就會隨之創(chuàng)建一個對應(yīng)的棧幀,并將建立的棧幀壓棧。當(dāng)方法執(zhí)行完畢之后,便會將棧幀出棧。因此可知,線程當(dāng)前執(zhí)行的方法所對應(yīng)的棧幀必定位于Java棧的頂部。講到這里,我們就應(yīng)該會明白為什么在使用遞歸方法的時候容易導(dǎo)致棧內(nèi)存溢出的現(xiàn)象了。
即每一個方法從調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機(jī)棧中入棧到出棧的過程。
經(jīng)常有人把Java內(nèi)存區(qū)分為堆內(nèi)存(Heap)和棧內(nèi)存(Stack),這種分法比較粗糙,Java內(nèi)存區(qū)域的劃分實際上遠(yuǎn)比這復(fù)雜。這里所指的“棧”就是虛擬機(jī)棧,或者說是虛擬機(jī)棧中的局部變量表部分。
下圖表示了一個虛擬機(jī)棧的模型:
?(1). 局部變量表,顧名思義,就是用來存儲方法中的局部變量(包括在方法中聲明的非靜態(tài)變量以及函數(shù)形參)。對于基本數(shù)據(jù)類型的變量,則直接存儲它的值,對于引用類型的變量,則存的是指向?qū)ο蟮囊谩>植孔兞勘淼拇笮≡诰幾g期就可以確定其大小了,因此在程序執(zhí)行期間局部變量表的大小是不會改變的。
八個基本數(shù)據(jù)類型:byte、short、int、long、float、double、char、boolean
?(2). 操作數(shù)棧,在數(shù)據(jù)結(jié)構(gòu)中,棧最典型的一個應(yīng)用就是用來對表達(dá)式求值。想想一個線程執(zhí)行方法的過程中,實際上就是不斷執(zhí)行語句的過程,而歸根到底就是進(jìn)行計算的過程。因此可以這么說,程序中的所有計算過程都是在借助于操作數(shù)棧來完成的。
當(dāng)一個方法剛剛開始執(zhí)行的時候,這個方法的操作數(shù)棧是空的,在方法的執(zhí)行過程中,會有各種字節(jié)碼指令向操作數(shù)棧中寫入和提取內(nèi)容,也就是入棧出棧操作。例如,在做算術(shù)運算的時候是通過操作數(shù)棧來進(jìn)行的,又或者在調(diào)用其他方法的時候是通過操作數(shù)棧來進(jìn)行參數(shù)傳遞的。
舉個例子,整數(shù)加法的字節(jié)碼指令iadd在運行的時候要求操作數(shù)棧中最接近棧頂?shù)膬蓚€元素已經(jīng)存入了兩個int型的數(shù)值,當(dāng)執(zhí)行這個指令時,會將這兩個int值出棧并相加,然后將相加的結(jié)果入棧。
?(3). 指向運行時常量池的引用,因為在方法執(zhí)行的過程中有可能需要用到類中的常量,所以必須要有一個引用指向運行時常量。
?(4). 方法返回地址,當(dāng)一個方法執(zhí)行完畢之后,要返回之前調(diào)用它的地方,因此在棧幀中必須保存一個方法返回地址。
3. 本地方法棧
本地方法棧與Java棧的作用和原理非常相似。區(qū)別只不過是Java棧是為執(zhí)行Java方法服務(wù)的,而本地方法棧則是為執(zhí)行本地方法(Native Method)服務(wù)的。在JVM規(guī)范中,并沒有對本地方法棧中方法使用的語言、使用方式以及數(shù)據(jù)結(jié)構(gòu)作強(qiáng)制規(guī)定,虛擬機(jī)可以自由實現(xiàn)它。在HotSopt虛擬機(jī)中直接就把本地方法棧和Java棧合二為一。
與虛擬機(jī)棧一樣,本地方法棧區(qū)域也會拋出StackOverflowError和OutOfMemoryError異常。
關(guān)于Native方法:
在某些情況下,若Java必須要調(diào)用其它語言的代碼,如c或C++,就需要使用Native。
Native方法稱為本地方法。在Java中以關(guān)鍵字“Native”聲明的程序,不提供函數(shù)體。其實現(xiàn)使用C/C++語言在另外的文件中編寫,編寫的規(guī)則遵循Java本地接口的規(guī)范(簡稱JNI)。簡而言就是Java中聲明的可調(diào)用使用C/C++實現(xiàn)的方法。
即Native方法就是不由Java實現(xiàn)的方法,一般這些方法都是很底層,跟平臺結(jié)合緊密,或者使用Java實現(xiàn)性能很差。
4. Java堆
對大多數(shù)應(yīng)用來說,Java堆是Java虛擬機(jī)所管理的內(nèi)存最大的一塊。Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動時創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內(nèi)存。
Java堆用來存儲對象本身的以及數(shù)組(當(dāng)然,數(shù)組引用是存放在Java棧中的)。
Java堆是垃圾收集器管理的主要區(qū)域,因此很多時候也被稱為“GC堆”。
根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定,Java堆可以處于物理上不連續(xù)的內(nèi)存空間,只要邏輯上是連續(xù)的即可,就像我們的磁盤空間一樣。在實現(xiàn)時,即可以實現(xiàn)成固定大小,也可以是可擴(kuò)展的,不過當(dāng)前主流的虛擬機(jī)都是按照可擴(kuò)展來實現(xiàn)的。
如果在堆中沒有內(nèi)存完成實例分配,并且堆也無法再擴(kuò)展時,將會拋出OutOfMemoryError異常。
5. 方法區(qū)
方法區(qū)與Java堆一樣,是各個線程共享的內(nèi)存區(qū)域。在方法區(qū)中,存儲了每個類的信息(包括類的名稱、方法信息、字段信息)、靜態(tài)變量、常量、即時編譯器編譯后的代碼等。
在Class文件中除了類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用來存儲編譯期間生成的字面量和符號引用。
在方法區(qū)中有一個非常重要的部分就是運行時常量池,它是每一個類或接口的常量池的運行時表示形式,在類和接口被加載到JVM后,對應(yīng)的運行時常量池就被創(chuàng)建出來。例:存放final修飾的常量
在JVM規(guī)范中,沒有強(qiáng)制要求方法區(qū)必須實現(xiàn)垃圾回收。很多人習(xí)慣將方法區(qū)稱為“永久代”,是因為HotSpot虛擬機(jī)以永久代來實現(xiàn)方法區(qū),從而JVM的垃圾收集器可以像管理堆區(qū)一樣管理這部分區(qū)域,從而不需要專門為這部分設(shè)計垃圾回收機(jī)制。不過自從JDK7之后,Hotspot虛擬機(jī)便將字符串常量池從永久代移除了。
例:看下面這段程序,然后畫出內(nèi)存分析圖
運行過程分析:
1、首先運行程序,Demo1_car.java就會變?yōu)镈emo1_car.class,將Demo1_car.class加入方法區(qū),檢查是否字節(jié)碼文件常量池中是否有常量值,如果有,那么就加入運行時常量池
2、遇到main方法,創(chuàng)建一個棧幀,入虛擬機(jī)棧,然后開始運行main方法中的程序
3、Car c1 = new Car(); 第一次遇到Car這個類,所以將Car.java編譯為Car.class文件,然后加入方法區(qū),跟第一步一樣。然后new Car()。就在堆中創(chuàng)建一塊區(qū)域,用于存放創(chuàng)建出來的實例對象,地址為0X001.其中有兩個屬性值 color和num。默認(rèn)值是null 和 0
4、然后通過c1這個引用變量去設(shè)置color和num的值,
5、調(diào)用run方法,然后會創(chuàng)建一個棧幀,用來裝run方法中的局部變量的,入虛擬機(jī)棧,run方法中就打印了一句話,結(jié)束之后,該棧幀出虛擬機(jī)棧。又只剩下main方法這個棧幀了
6、接著又創(chuàng)建了一個Car對象,所以又在堆中開辟了一塊內(nèi)存,之后就是跟之前的步驟一樣了。
參考:
JVM的內(nèi)存區(qū)域劃分?
java虛擬機(jī)內(nèi)存區(qū)域的劃分以及作用詳解
二、HotSpot虛擬機(jī)對象
(一)、對象的創(chuàng)建
在語言層面上,創(chuàng)建對象通常僅僅是一個new關(guān)鍵字而已,在虛擬機(jī)中,對象的創(chuàng)建過程如圖:
1. 類加載檢查 根據(jù)new指令的參數(shù)在常量池中定位對應(yīng)類的符號引用
首先檢查這個指令的參數(shù)是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執(zhí)行相應(yīng)的類的加載過程。
2. 為新生對象分配內(nèi)存(分配內(nèi)存空間的方法:指針碰撞、空閑列表。并發(fā)情況下保證線程安全:CAS、TLAB)
對象所需內(nèi)存的大小在類加載完成后便完全確定,為對象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來。
?(1).根據(jù)Java堆中是否規(guī)整劃分為兩種內(nèi)存的分配方式:(Java堆是否規(guī)整由所采用的垃圾收集器是否帶有壓縮整理功能決定)
? ? a. 指針碰撞(Bump the pointer): Java堆中的內(nèi)存是絕對規(guī)整的,所有用過的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個指針作為分界點的指示器,分配內(nèi)存也就是把指針向空閑空間那邊移動一段與內(nèi)存大小相等的距離。
? ? b. 空閑列表(Free List): Java堆中的內(nèi)存不是規(guī)整的,已使用的內(nèi)存和空閑的內(nèi)存相互交錯,就沒有辦法簡單的進(jìn)行指針碰撞了。虛擬機(jī)必須維護(hù)一張列表,記錄哪些內(nèi)存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄。
?(2).分配內(nèi)存時解決并發(fā)問題的兩種方案:(可能出現(xiàn)正在給對象A分配內(nèi)存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內(nèi)存的情況)
? ?a. 分配內(nèi)存空間的動作進(jìn)行同步處理---實際上虛擬機(jī)采用CAS配上失敗重試的方式保證更新操作的原子性;
? ?b. 把內(nèi)存分配的動作按照線程劃分為在不同的空間之中進(jìn)行,即每個線程在Java堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖(TLAB)。哪個線程要分配內(nèi)存,就在哪個線程的TLAB上分配。只有TLAB用完并分配新的TLAB時,才需要同步鎖定。
3. 將分配的內(nèi)存空間都初始化為零值
虛擬機(jī)將分配到的內(nèi)存空間都初始化為零值(不包括對象頭),如果使用了TLAB,這一工作過程也可以提前至TLAB分配時進(jìn)行。這一操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數(shù)據(jù)類型所對應(yīng)的零值。
4. 對對象進(jìn)行必要的設(shè)置,如設(shè)置對象頭
虛擬機(jī)對對象進(jìn)行必要的設(shè)置,例如這個對象是哪個類的實例、如何才能找到類的元數(shù)據(jù)信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭之中。
5. 執(zhí)行<init>方法,把對象進(jìn)行初始化
在上面的工作都完成之后,從虛擬機(jī)的角度看,一個新的對象已經(jīng)產(chǎn)生了。但是從Java程序的角度看,對象的創(chuàng)建才剛剛開始,<init>方法還沒有執(zhí)行,所有的字段都還是零。所以,一般來說,執(zhí)行new指令之后會接著執(zhí)行<init>方法,把對象按照程序員的意愿進(jìn)行初始化,這樣一個真正可用的對象才算產(chǎn)生出來。
(二)、對象的內(nèi)存布局
對象在內(nèi)存中存儲的布局可以分為3塊區(qū)域:對象頭、實例數(shù)據(jù)、對齊填充
對象頭:
第一部分存儲對象自身的運行時數(shù)據(jù),如哈希碼、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時間戳等
第二部分存儲類型指針,即對象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個指針來確定這個對象是哪個類的實例。如果對象是一個java數(shù)組,那么對象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù)。
實例數(shù)據(jù):
實例數(shù)據(jù)部分是對象真正存儲的有效信息,也是程序代碼中所定義的各種類型的字段內(nèi)容。無論是從父類中繼承下來的,還是在子類中定義的,都需要記錄下來。
對齊填充:
對齊填充并不是必然存在的,也沒有特定的含義,僅僅起著占位符的作用。由于HotSpot虛擬機(jī)的自動內(nèi)存管理系統(tǒng)要求對象的起始地址必須是8字節(jié)的整數(shù)倍,也就是對象的大小必須是8字節(jié)的整數(shù)倍。而對象頭部分正好是8字節(jié)的倍數(shù)(1倍或者2倍),因此,當(dāng)對象實例數(shù)據(jù)部分沒有對齊的時候,就需要通過對齊填充來補全。
(三)、對象的訪問定位
對象的訪問方式取決于虛擬機(jī)實現(xiàn),目前主流的訪問方式有使用句柄和直接指針兩種。
1. 使用句柄
如果使用句柄的話,那么Java堆中將會劃分出一塊內(nèi)存來作為句柄池,引用中存儲的就是對象的句柄地址,而句柄中包含了對象實例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息。
2. 直接指針
如果使用直接指針訪問,那么Java堆對象的布局中就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息,而引用中存儲的直接就是對象地址。
使用句柄訪問的優(yōu)勢在于引用中存儲的是穩(wěn)定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數(shù)據(jù)指針,而引用本身不需要修改。
使用直接指針訪問的優(yōu)勢在于速度更快,節(jié)省了一次指針定位的時間開銷。由于對象的訪問在Java中非常頻繁,因此這類開銷積少成多后也是非常可觀的執(zhí)行成本。
關(guān)于對象類型數(shù)據(jù),因為的存儲在方法區(qū)中,因此我的理解就是被虛擬機(jī)加載的類信息
主要來自:
《深入理解java虛擬機(jī) JVM高級特性與最佳實踐》
轉(zhuǎn)載于:https://www.cnblogs.com/zeroingToOne/p/9100460.html
總結(jié)
以上是生活随笔為你收集整理的JVM学习笔记(一):Java内存区域的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 淘宝信用卡支付怎么分期?分期这些条件要注
- 下一篇: Java异常详解及如何处理