文章目錄
- 記憶體結構概述
- 1、程式計數器
- 2、Java虛擬機堆疊
- 3、本地方法堆疊
- 4、方法區
- 5、運行時常量池
- 6、Java堆
- 類加載器與類的加載程序
- 1、類加載器
- 2、類的加載程序
- 加載
- 鏈接
- 初始化
- 類加載器分類
記憶體結構概述
Java檔案的執行程序大致為:Java檔案通過編譯器(javac)編譯為.class檔案,然后類加載子系統將class檔案加載進運行時資料區,再通過執行引擎將位元組碼檔案編譯/決議為機器指令,
Java虛擬機在執行Java程式的程序中會把它所管理的記憶體劃分為若干個不同的資料區域,這個資料區域就叫運行時資料區,運行時資料區主要包含了PC暫存器(程式計數器)、Java虛擬機堆疊、本地方法堆疊、Java堆、方法區以及運行時常量池,這其中Java堆、方法區跟Java虛擬機堆疊是學習的重點,
(簡圖)
(詳細圖)
1、程式計數器
程式計數器(Program counter Register)是記錄當前執行緒正在執行的位元組碼的地址,程式計數器是執行緒隔離的,每一個執行緒在作業的時候都有一個獨立的計數器,因為Java是可以多執行緒執行的,一個執行緒執行到一半可能因為CPU時間片輪轉切換到了另外一個執行緒,在切換回之前執行緒的時候,需要回到執行緒上次的執行位置,所以要執行緒私有,
程式計數器的特點
-
程式計數器具有執行緒隔離性
-
程式計數器占用的記憶體空間非常小,可以忽略不計
-
程式計數器是java虛擬機規范中唯一一個沒有規定任何OutofMemeryError的區域
-
程式執行的時候,程式計數器是有值的,其記錄的是程式正在執行的位元組碼的地址
-
執行native本地方法時,程式計數器的值為空,原因是native方法是java通過jni呼叫本地C/C++庫來實作,非java位元組碼實作,所以無法統計
2、Java虛擬機堆疊
Java虛擬機堆疊(Java Virtual Machine Stacks)也是執行緒私有的,它的生命周期與執行緒相同,虛擬機堆疊描述的是Java方法執行的記憶體模型:每個方法在執行的同到都會創建一個堆疊幀(Stack Frame)用于存盤區域量表、運算元堆疊、動態鏈接、方法出口等資訊,堆疊幀是Java方法運行時的基礎資料結構,每一個方法從呼叫直至執行完成的程序,就對應著一個堆疊幀在虛擬堆疊中從入堆疊到出堆疊的程序(說人話就是要執行一個方法,將該方法的堆疊幀壓入堆疊頂,方法執行完成其堆疊幀出堆疊),在JVM里面,堆疊幀的操作只有兩種:出堆疊和入堆疊,正在被執行緒執行的方法稱為當前執行緒方法,而該方法的堆疊幀就稱為當前幀,
區域變數表存放了編譯期可知的各種基本資料型別(boolean、byte、char、short、int、long、float、double)、物件參考(reference型別,它不等同于物件本身,可能是一個指向物件始地址的參考指標,也可能是指向一個代表物件的句柄或其地與此物件相關的位置)和returnAddress型別(指向了一條位元組碼指令的地址),
在Java虛擬機規范中,對這個區域定了兩種異狀況:如果執行緒請求的堆疊深度大于虛擬機所允許的深度,將拋出StackOverFlowError例外;一般的虛擬機堆疊都是可擴展的,如果擴展時無法豐請到足夠的記憶體,就會拋出OutOfMemoryError例外,可以通過-Xss設定每個執行緒的堆疊大小,
Java虛擬機堆疊的結構如下圖所示:Java虛擬機堆疊的生命周期與執行緒一致,一個方法對應一塊堆疊幀記憶體區域,堆疊幀中包含區域變數表、運算元堆疊、動態鏈接、方法出口等資訊,拿下面代碼舉例,程式執行main(),main()先壓入堆疊頂,然后main()方法中new了一個Math物件,math變數是指向堆中Math物件的參考,math變數就屬于區域變數表,創建Math物件之后,呼叫了其compute(),然后compute()壓入堆疊頂,compute方法執行完成后其堆疊幀出堆疊,然后根據程式計數器記錄程式執行的行號,繼續回到main方法執行,main方法中已經沒有其他執行指令了,則main方法退出,main方法對應的堆疊幀出堆疊,虛擬機堆疊中已經沒有其他堆疊幀,main執行緒生命周期結束,
3、本地方法堆疊
本地方法堆疊(Native Method Stack)與虛擬機堆疊非常相似,也是執行緒私有的,它們的區別不過是虛擬機堆疊執行的是Java方法(也就是位元組碼),而本地方法堆疊用到的是Native方法,與虛擬機戰一樣,本地方法堆疊區域也會出現StackOverFlowError和OutOfMemoryError例外,
4、方法區
方法區(Method Area),是各個執行緒共享的記憶體區域,它用于存盤虛擬機加載的:類資訊+普通常量+靜態常量+編譯器編譯后的代碼等等,雖然JVM規范將方法區描述為堆的一個邏輯部分,但它卻還有一個別名叫做Non一Heap(非堆),目的就是要和堆分開,這部分存盤的是運行時必須的類相關資訊,裝載進此區域的資料是不會被垃圾收集器回收的,只有關閉Jvm才會釋放這塊區域占用的記憶體,
對于Hotspot虛擬機,很多開發者習慣將方法區稱之為“永久代(Parmanent Gen)",但嚴格本質上說兩者不同,或者說使用永久代來實作方法區而己,永久代是方法區(相當于是一個介面interface)的一個實作,idkl.7的版本中,己經將原本放在永久代的字串常量池移走,Jdk1.7中方法區是用永久代實作的,到1.8中是用元空間(MetaSpace)實作的,而元空間使用的是直接記憶體,
根據Java虛擬機規范的規定,當方法區無法滿足記憶體分配需求時會拋出OutOfMemoryError例外,可以通過-XX:PermSize和 -XX:MaxPermSize來分別設定永久區最小、最大空間,
5、運行時常量池
運行時常量池(Runtime Constant Pool)是方法區的一部分,Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池,用于存放編譯期生產的各種字面量和符號參考,這部分內容在類加載后進入方法區的運行時常量池中存放,
常量池中主要存放兩大類常量:字面量(Literal)和符號參考(Symbolic References),字面量比較接近于Java語言層面的常量概念,如文本字串、宣告為final的常量值等,而符號參考則屬于編譯原理方面的概念,包括了下面三類常量:
類和介面的全限定名(Fully Qualified Name)
欄位的名稱和描述符(Descriptor)
方法的名稱和描述符
Java代碼在進行Javac編譯的時候,并不像C和C++那樣有“連接”這一步驟,而是在虛擬機加載Class檔案的時候進行動態連接,也就是說,在Class檔案中不會保存各個方法、欄位的最終記憶體布局資訊,因此這些欄位、方法的符號參考不經過運行期轉換的話無法得到真正的記憶體人口地址,也就無法直接被虛擬機使用,當虛擬機運行時,需要從常量池獲得對應的符號參考,再在類創建時或運行時決議、翻譯到具體的記憶體地址之中,
Java語言不要求常量一定只有編譯器才能產生,運行時也可能將新的常量放入池中,該特性用的比較多的就是String類的intern()方法,運行時常量池是方法區的一部分,在記憶體不夠時,也會拋出OutOfMemoryError例外,
6、Java堆
對于大多數應用來說,Java堆(Java Heap)是Java虛擬機所管理的記憶體中最大的一塊,Java堆是執行緒共享的,在虛擬機啟動時創建,此記憶體區域的唯一目的就是存放物件實體,幾乎所有的物件實體都在這里分配記憶體,這一點在Java擬機規范中的描述是:所有的物件實體以及陣列都要在堆上分配,但是著JIT編譯器的發展與逸分析技術逐漸成熟,堆疊上分配、標量替換優化技識訓導致一些微妙的變化發生,所有的物件都分配在堆上也漸漸變得不是那么"絕對"了,
Java堆是被收集管理的主要區域,因此很多時候也被稱做"GC堆"(Garbage Collected Heap),從記憶體回收角度來看,由于現在收集器基本都采用分代演算法(為什么要采用分代演算法,常用的垃圾收集演算法有哪些后面會進行介紹),所以堆中還以細分:新生代(Young/New)和老年代(Old/Tenure),新生代又可以劃分為Eden(伊甸園)空間、survivor(幸存區,其又可以分為from survivor和to survivor,也就是S0和S1)空間等,從記憶體分配的角度來看,執行緒共享的Java堆中可劃分出多個程私有的分配緩沖區(Thread Local Allocation Buffer,TLAB),不過無論如何劃分,都與存放內容無關,無論哪個區域,存盤的都仍然是物件實體,進一步劃分的是為了更好地回收記憶體,或更快地分配記憶體,
根據Java虛擬機規范的規定,Java堆可以處于物理上不連續的記憶體空間中,只邏輯上是連續的即可,.Java虛擬機中可以對堆進行擴展,可以通過-Xms 設定起始堆大小、通過-Xmx設定最大堆大小、通過-XX:NewSize設定新生代最小空間大小、通過 -XX:MaxNewSize設定新生代最大空間大小,如果在堆中沒有完成實體分配,并且地也無法再擴展時,將會拋OutOfMemoryError例外,
類加載器與類的加載程序
1、類加載器

類加載子系統的作用
- 類加載子系統負責從檔案系統或者網路中加載Class檔案,Class檔案在檔案開頭有特定的檔案標識,
- ClassLoader只負責class檔案的加載,至于它是否可以運行,則由Execution Engine決定,
- 加載的類資訊存放于一塊稱為方法區的記憶體空間,除了累的資訊外,方法去中還會存放運行時常量池資訊,可能還包括字串字面量和數字常量(這部分常量資訊是Class檔案中常量池部分的記憶體映射),
類加載器ClassLoader角色
- class file存在于本地硬碟上,可以理解為設計師畫在紙上的模板,而最終這個模板在執行的時候是要加載到JVM當中來根據這個檔案實體化出n個一模一樣的實體,
- class file加載到JVM中,被稱為DNA元資料模板,放在方法區,
- 在.class檔案–> JVM–>最終成為元資料模板,此程序就要一個運輸工具(類裝載器Class Loader),扮演一個快遞員的角色,
2、類的加載程序


加載
- 通過一個類的全限定名獲取定義此類的二進制位元組流
- 將這個位元組流所代表的靜態存盤結構轉化為方法區的運行時資料結構
- 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口
加載.class檔案的方式
- 從本地系統中直接加載
- 通過網路獲取,典型場景:web applet
- 從zip壓縮包中讀取,成為日后jar、war格式的基礎
- 運行時計算生成,使用最多的是:動態代理技術
- 由其他檔案生成,典型場景:JSP應用
- 從專有資料庫中提取.class(比較少見)
- 從加密檔案中獲取,典型的防Class檔案被反編譯的保護措施
鏈接
驗證(Verify)
目的在于確保Class檔案的位元組流中包含資訊符合當前虛擬機要求,保證被加載類的正確性,不會危害虛擬機自身安全,
主要包括四種驗證:檔案格式驗證、元資料驗證、位元組碼驗證、符號參考驗證,
準備(Prepare)
- 為變數分配記憶體并且設定該類變數的默認初始值,
- 這里不包含用final修飾的static,因為final在編譯的時候就會分配了,準備階段會顯示初始化,
- 這里不會為實體變數分配初始化,類變數會分配在方法區中,而實體變數是會隨著物件一起分配到java堆中,
決議(Resolve)
- 將常量池內的符號參考轉換為直接參考的程序
- 決議操作往往會伴隨著JVM在執行完初始化之后再執行
- 符號參考就是一組符號來描述所參考的目標,符號參考的字面量形式明確定義在《Java虛擬機規范》的Class檔案格式中,直接參考就是直接指向目標的指標、相對偏移量或一個間接定位到目標的句柄,
- 決議動作主要針對類或介面、欄位、類方法、介面方法、方法型別等,對應常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info等,
初始化
- 初始化階段就是執行類構造器方法()的程序,
- 此方法不需要定義,是javac編譯器自動收集類中的所有類變數的賦值動作和靜態代碼塊中的陳述句合并而來,
- 構造器方法中指令按陳述句在源檔案中出現的順序執行,- - ()不同于類的構造器,(關聯:構造器是虛擬機視角下的()),
- 若該類具有父類,JVM會保證子類的()執行前,父類的()已經執行完畢,
- 虛擬機必須保證一個類的()方法在多執行緒下被同步加鎖,
類加載器分類
JVM支持兩種型別的類加載器,分別為引導類加載器(Bootstrap ClassLoader)和自定義類加載器User-Defined ClassLoader),
從概念上講,自定義類加載器一般指的是程式中有開發人員自定義的一類類加載器,但是Java虛擬機規范卻沒有這么定義,而是將所有派生于抽象類ClassLoader的類加載器都劃分為自定義類加載器,無論類加載器的型別如何劃分,在程式中我們最常見的類加載器始終只有3個,如下所示:

轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/169184.html
標籤:其他
上一篇:手撕面試題演算法<樹> (1) —— 樹的層序遍歷以及相關題解
下一篇:第十五章 泛型
