jvm如何執行以及記憶體如何分配
這裡是修真院後端小課堂,每篇分享文從
【背景介紹】【知識剖析】【常見問題】【解決方案】【編碼實戰】【擴充套件思考】【更多討論】【參考文獻】
八個方面深度解析後端知識/技能,本篇分享的是:
【jvm如何執行以及記憶體如何分配】
大家好,我是IT修真院西安分院第4期的JAVA學員,一枚正直純潔善良的JAVA程式設計師。今天給大家分享一下,修真院官網Java任務1,深度思考中的知識點jvm如何執行以及記憶體如何分配
一、.背景介紹
作為一名Java使用者,掌握JVM的體系結構也是必須的。說起Java,人們首先想到的是Java程式語言.然而事實上,Java是一種技術,它由四方面組成:Java程式語言、Java類檔案格式、Java虛擬機器和Java應用程式介面(Java API)。它們的關係如下圖所示:
二、知識剖析
2.1 JAVA虛擬機器工作原理和流程
Java平臺由Java虛擬機器和Java應用程式介面搭建,Java語言則是進入這個平臺的通道, 用Java語言編寫並編譯的程式可以執行在這個平
在JVM 的上方是Java的基本類庫和擴充套件類庫以及它們的API,利用Java API編寫的應用程式(application) 和小程式(Java applet).可以在任何Java平臺上執行而無需考慮底層平臺, 就是因為有Java虛擬機器(JVM)實現了程式與作業系統的分離,從而實現了Java 的平臺無關性。 JVM在它的生存週期中有一個明確的任務,那就是執行Java程式,因此當Java程式啟動的時候,就產生JVM的一個例項;當程式執行結束的時候,該例項也跟著消失了。
下面我們從JVM的體系結構和它的執行過程這兩個方面來對它進行比較深入的研究。
2.2Java虛擬機器的體系結構
①類裝載子系統:裝載具有適合名稱的類或介面②執行引擎:負責執行包含在已裝載的類或介面中的指令 </p>
每個JVM都包含:
方法區、Java堆、Java棧、本地方法棧、指令計數器及其他隱含暫存器
2.3 Java程式碼編譯和執行的整個過程
(1)Java程式碼編譯是由Java原始碼編譯器來完成,也就是Java程式碼到JVM位元組碼(.class檔案)的過程。
(2)Java位元組碼的執行是由JVM執行引擎來完成
(3) Java程式碼編譯和執行的整個過程包含了以下三個重要的機制:·Java原始碼編譯機制·類載入機制·類執行機制
1)Java原始碼編譯機制
java 原始碼編譯由以下三個過程組成:①分析和輸入到符號表②註解處理③語義分析和生成class檔案
最後生成的class檔案由以下部分組成:
①結構資訊:包括class檔案格式版本號及各部分的數量與大小的資訊
②元資料:對應於Java原始碼中宣告與常量的資訊。包含類/繼承的超類/實現的介面的宣告資訊、域與方法宣告資訊和常量池
③方法資訊:對應Java原始碼中語句和表示式對應的資訊。包含位元組碼、異常處理器表、求值棧與區域性變數區大小、求值棧的型別記錄、除錯符號資訊
2)類載入機制JVM的類載入是通過ClassLoader及其子類來完成的
為什麼研究類載入全過程
有助於連線JVM執行過程
更深入瞭解java動態性(解熱部署,動態載入),提高程式的靈活性
3)載入
將class檔案位元組碼內容載入到記憶體中,並將這些靜態資料轉換成方法區中的執行時資料結構,在堆中生成一個代表這個類的java.lang.Class物件,
作為方法區類資料的訪問入口,這個過程需要類載入器參與。
載入是類載入過程中的一個階段,這個階段會在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的入口。
注意這裡不一定非得要從一個Class檔案獲取,這裡既可以從ZIP包中讀取(比如從jar包和war包中讀取),
也可以在執行時計算生成(動態代理),也可以由其它檔案生成(比如將JSP檔案轉換成對應的Class類)。
連結
將java類的二進位制程式碼合併到JVM的執行狀態之中的過程
驗證:這一階段的主要目的是為了確保Class檔案的位元組流中包含的資訊是否符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。
準備:正式為類變數(static變數)分配記憶體並設定類變數初始值的階段,即在方法區中分配這些變數所使用的記憶體空間。注意這裡所說的初始值概念,
比如一個類變數定義為:public static int v = 8080;
在編譯階段會為v生成ConstantValue屬性,在準備階段虛擬機器會根據ConstantValue屬性將v賦值為8080。
解析:解析階段是指虛擬機器將常量池中的符號引用替換為直接引用的過程。符號引用就是class檔案中的:
CONSTANT_Class_info
CONSTANT_Field_info
CONSTANT_Method_info
等型別的常量。
下面我們解釋一下符號引用和直接引用的概念:
符號引用與虛擬機器實現的佈局無關,引用的目標並不一定要已經載入到記憶體中。各種虛擬機器實現的記憶體佈局可以各不相同,但是它們能接受的符號引用必須是一致的,因為符號引用的字面量形式明確定義在Java虛擬機器規範的Class檔案格式中。</p>
直接引用可以是指向目標的指標,相對偏移量或是一個能間接定位到目標的控制代碼。 如果有了直接引用,那引用的目標必定已經在記憶體中存在。
4)初始化
初始化階段是類載入最後一個階段,前面的類載入階段之後,除了在載入階段可以自定義類載入器以外,
其它操作都由JVM主導。到了初始階段,才開始真正執行類中定義的Java程式程式碼。
初始化階段是執行類構造器<client>方法的過程。<client>方法是由編譯器自動收集類中的類變數的賦值操作和靜態語句塊中的語句合併而成的。虛擬機器會保證<client>方法執行之前,父類的<client>方法已經執行完畢。
p.s: 如果一個類中沒有對靜態變數賦值也沒有靜態語句塊,那麼編譯器可以不為這個類生成<client>()方法。
注意以下幾種情況不會執行類初始化:
通過子類引用父類的靜態欄位,只會觸發父類的初始化,而不會觸發子類的初始化。
定義物件陣列,不會觸發該類的初始化。
常量在編譯期間會存入呼叫類的常量池中,本質上並沒有直接引用定義常量的類不會觸發定義常量所在的類。
通過類名獲取Class物件,不會觸發類的初始化。
通過Class.forName載入指定類時,如果指定引數initialize為false時,也不會觸發類初始化,
其實這個引數是告訴虛擬機器,是否要對類進行初始化。
通過ClassLoader預設的loadClass方法,也不會觸發初始化動作。
5)類載入器
虛擬機器設計團隊把載入動作放到JVM外部實現,以便讓應用程式決定如何獲取所需的類,JVM提供了3種類載入器:
啟動類載入器(Bootstrap ClassLoader):負責載入 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath引數指定路徑中的,且被虛擬機器認可(按檔名識別,如rt.jar)的類
應用程式類載入器(Application ClassLoader):負責載入使用者路徑(classpath)上的類庫。
JVM通過雙親委派模型進行類的載入,當然我們也可以通過繼承java.lang.ClassLoader實現自定義的類載入器。
當一個類載入器收到類載入任務,會先交給其父類載入器去完成,
因此最終載入任務都會傳遞到頂層的啟動類載入器,只有當父類載入器無法完成載入任務時,才會嘗試執行載入任務。採用雙親委派的一個好處是比如載入位於rt.jar包中的類java.lang.Object,不管是哪個載入器載入這個類,最終都是委託給頂層的啟動類載入器進行載入,這樣就保證了使用不同的類載入器最終得到的都是同樣一個Object物件。在有些情境中可能會出現要我們自己來實現一個類載入器的需求,這裡
我們直接看一下jdk中的ClassLoader的原始碼實
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
首先通過Class c = findLoadedClass(name);判斷一個類是否已經被載入過。
如果沒有被載入過執行if (c == null)中的程式,遵循雙親委派的模型,
首先會通過遞迴從父載入器開始找,直到父類載入器是Bootstrap ClassLoader為止。
最後根據resolve的值,判斷這個class是否需要解析。
而上面的findClass()的實現如下,直接丟擲一個異常,並且方法是protected,很明顯這是留給我們開發者自己去實現的
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);}
①Bootstrap ClassLoader負責載入$JAVA_HOME中jre/lib/rt.jar裡所有的class, 由C++實現,不是ClassLoader子類
②Extension ClassLoader負責載入java平臺中擴充套件功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包
③App ClassLoader負責記載classpath中指定的jar包及目錄中class
④Custom ClassLoader屬於應用程式根據自身需要自定義的ClassLoader,
如tomcat、jboss都會根據j2ee規範自行實現ClassLoader
載入過程中會先檢查類是否被已載入,檢查順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader逐層檢查只要某個classloader已載入就視為已載入此類,保證此類只所有ClassLoader載入一次。
而載入的順序是自頂向下,也就是由上層來逐層嘗試載入此類。
6)類執行機制
JVM是基於堆疊的虛擬機器。JVM為每個新建立的執行緒都分配一個堆疊.也就是說,對於一個Java程式來說它的執行就是通過對堆疊的操作來完成的。堆疊以幀為單位儲存執行緒的狀態。JVM對堆疊只進行兩種操作:以幀為單位的壓棧和出棧操作。
三、常見問題
1、什麼是堆記憶體,什麼是棧記憶體?有何區別?
四、解決方案
1 什麼是堆記憶體,什麼是棧記憶體?有何區別
ava堆記憶體
堆記憶體在Java執行時被使用來為物件和JRE類分配記憶體。不論什麼時候我們建立了物件,它將一直會在堆記憶體上建立。垃圾回收執行在堆記憶體上來釋放沒有任何引用的物件所佔的記憶體,任何在堆上被建立的物件都有一個全域性的訪問,並且可以在應用的任何位置被引用。
Java棧記憶體
Java的棧記憶體被用來執行緒的執行,他們包含生命週期很短的具體值的方法和在堆中使用這個方法物件的引用。棧記憶體是LIFO(後進先出)序列。當方法被呼叫的時候,堆記憶體中一個新的塊被建立,儲存了本地原始值和在方法中對其他物件的引用。這個方法結束之後,這個塊對其他方法就變成可用的了。棧記憶體與堆記憶體相比是非常小的。
堆記憶體和棧記憶體的區別
基於上邊的解釋我們可以很簡單的總結出堆和棧的區別:
1、應用程式所有的部分都使用堆記憶體,然後棧記憶體通過一個執行緒執行來使用。
2、不論物件什麼時候建立,他都會儲存在堆記憶體中,棧記憶體包含它的引用。棧記憶體只包含原始值變數好和堆中物件變數的引用。
3、儲存在堆中的物件是全域性可以被訪問的,然而棧記憶體不能被其他執行緒所訪問。
4、棧中的記憶體管理使用LIFO的方式完成,而堆記憶體的管理要更復雜了,因為它是全域性被訪問的。堆記憶體被分為,年輕一代,老一代等等,更多的細節請看,這篇文章
5、棧記憶體是生命週期很短的,然而堆記憶體的生命週期從程式的執行開始到執行結束。
6、我們可以使用-Xms和-Xmx JVM選項定義開始的大小和堆記憶體的最大值,我們可以使用-Xss定義棧的大小
7、當棧記憶體滿的時候,Java丟擲java.lang.StackOverFlowError異常而堆記憶體滿的時候丟擲java.lang.OutOfMemoryError: Java Heap Space錯誤
8、和堆記憶體比,棧記憶體要小的多,因為明確使用了記憶體分配規則(LIFO),和堆記憶體相比棧記憶體非常快。
五、編碼實戰
六、擴充套件思考
假如我們自己寫了一個java.lang.String的類,我們是否可以替換調JDK本身的類?
七、參考文獻
https://www.cnblogs.com/lishun1005/p/6019678.html
http://www.importnew.com/25295.html
八、更多討論
1、類載入器之間是如何協調工作的(秦永輝提出)
java採用了委託模型機制,這個機制簡單來講,就是“類裝載器有載入類的需求時,會先請示其Parent使用其搜尋路徑幫忙載入,如果Parent 找不到,那麼才由自己依照自己的搜尋路徑搜尋類”
2、類載入的動態性是如何體現的?
一個應用程式總是由n多個類組成,Java程式啟動時,並不是一次把所有的類全部載入後再執行,它總是先把保證程式執行的基礎類一次性載入到jvm中,其它類等到jvm用到的時候再載入,這樣的好處是節省了記憶體的開銷,因為java最早就是為嵌入式系統而設計的,記憶體寶貴,這是一種可以理解的機制,而用到時再載入這也是java動態性的一種體現。
3、為什麼要使用這種雙親委託模式呢?
因為這樣可以避免重複載入,當父親已經載入了該類的時候,就沒有必要子ClassLoader再載入一次。
考慮到安全因素,我們試想一下,如果不使用這種委託模式,那我們就可以隨時使用自定義的String來動態替代java核心api中定義型別,這樣會存在非常大的安全隱患,而雙親委託的方式,就可以避免這種情況,因為String已經在啟動時被載入,所以使用者自定義類是無法載入一個自定義的ClassLoader。
技能樹.IT修真院“我們相信人人都可以成為一個工程師,現在開始,找個師兄,帶你入門,掌控自己學習的節奏,學習的路上不再迷茫”。
這裡是技能樹.IT修真院,成千上萬的師兄在這裡找到了自己的學習路線,學習透明化,成長可見化,師兄1對1免費指導。快來與我一起學習吧~我的邀請碼:28769611,或者你可以直接點選此連結:http://www.jnshu.com/login/1/28769611
今天的分享就到這裡啦,歡迎大家點贊、轉發、留言、拍磚~
更多內容,可以加入IT交流群565734203與大家一起討論交流
這裡是技能樹·IT修真院:https://www.jnshu.com,初學者轉行到網際網路的聚集地