Java比起C++一個很大的進步就在於Java不用再手動控制指針的delete與free,統一交由JVM管理,但也正由於如此,一旦出現內存溢出異常,不瞭解JVM,那麼排查問題將會變成一項艱難的工做。java
Java虛擬機在執行Java程序的過程當中會把它所管理的內存劃分爲若干個不一樣的數據區。這些區域都有各自的用途,以及建立銷燬的時間,有的區域隨着虛擬機進程的啓動而存在,有些區域則依賴用戶線程的啓動和結束而創建和銷燬。根據《Java虛擬機規範 7》的規定(注意:咱們徹底能夠重新的JDK1.9開始瞭解,可是先講1.7是由於1.7到1.8是JDK的較大的變化,咱們先經過了解JDK1.7,而後再看一下1.8在1.7的基礎上改變了什麼。有助於咱們的理解),JVM所管理的內存會被分爲如下幾個運行時數據區域,以下圖所示:linux
![](http://static.javashuo.com/static/loading.gif)
1.1 程序計數器
有些地方會將這個地方爲寄存器,這其實並無錯誤,計算機中的寄存器是中央處理器內的組成部分。寄存器是有限存貯容量的高速存貯部件,它們可用來暫存指令、數據和地址。在中央處理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序計數器(PC)。在中央處理器的算術及邏輯部件中,寄存器有累加器(ACC)。因此這裏咱們將寄存器等於程序計數器並無錯。程序員
程序計數器(Program Counter Register)是一塊較小的內存空間,它能夠看做是當前線程所執行的字節碼的行號指示器。字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令。分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。算法
因爲Java虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)都只會執行一條線程中的指令。所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立內存,咱們稱這類內存區域爲「線程私有」的內存。編程
若是線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是Native方法,這個計數器的值爲空(Undefined)。此內存區域是惟一一個在Java虛擬機規範中沒有任何OutOfMemoryError狀況的區域。segmentfault
問題:Java多線程執行native方法時程序計數器爲空,那麼線程切換後如何找到以前執行到哪裏了?
這裏的「程序計數器」是在抽象的JVM層面上的概念——當執行Java方法時,這個抽象的「程序計數器」存的是Java字節碼的地址。實現上可能有兩種形式,一種是相對該方法字節碼開始處的偏移量,叫作bytecode index,簡稱bci;另外一種是該Java字節碼指令在內存裏的地址,叫作bytecode pointer,簡稱bcp。對native方法而言,它的方法體並非由Java字節碼構成的,天然沒法應用上述的「Java字節碼地址」的概念。因此JVM規範規定,若是當前執行的方法是native的,那麼pc寄存器的值"未定義"——是什麼值均可以。windows
上面是JVM規範所定義的抽象概念,那麼實際實現呢?Java線程老是須要以某種形式映射到OS線程上。映射模型能夠是1:1(原生線程模型)、n:1(綠色線程 / 用戶態線程模型)、m:n(混合模型)。以HotSpot VM的實現爲例,它目前在大多數平臺上都使用1:1模型,也就是每一個Java線程都直接映射到一個OS線程上執行。此時,native方法就由原平生臺直接執行,並不須要理會抽象的JVM層面上的「程序計數器」概念——原生的CPU上真正的程序計數器是怎樣就是怎樣。就像一個用C或C++寫的多線程程序,它在線程切換的時候是怎樣的,Java的native方法也就是怎樣的。數組
1.2 Java虛擬機棧
與程序計數器同樣,Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每一個方法在執行的同時都會建立一個棧幀(Stack Frame),棧幀是方法運行時的基礎數據結構,棧幀用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。緩存
初學時總會將Java內存分爲方法區、堆內存和棧內存,Java實際的內存區域劃分是比這個複雜的。而咱們這種淺見的認識只能說明大多數程序員最關注就是這三塊,這裏咱們所說的棧就是Java虛擬機棧,或者說是虛擬機棧中局部變量表部分。安全
在JVM規範中對於該區域規定了兩種異常狀況:若是線程請求的棧深度大於虛擬機所容許的深度(能夠將棧理解爲一種數組,棧深度能夠理解爲數組長度,固然棧和數組仍是很不一樣的,這裏是爲了理解),將拋出StackOverflowError異常;若是虛擬機能夠動態擴展(當前大部分的Java虛擬機均可以動態擴展,只不過Java虛擬機規範中也容許固定長度的虛擬機棧),若是擴展時沒法申請到足夠的內存空間,就會拋出OutOfMemoryError異常。
對於執行引擎來講,活動線程中,只有棧頂的棧幀是有效的,稱爲當前棧幀,這個棧幀所關聯的方法稱爲當前方法。執行引擎所運行的全部字節碼指令都只針對當前棧幀進行操做。
![](http://static.javashuo.com/static/loading.gif)
文章轉載自:http://blog.csdn.net/u013678930/article/details/51980460
1.2.1 執行引擎
執行引擎是 Java 虛擬機最核心的組成部分之一。「虛擬機」 是一個相對於 「物理機」 的概念,[JVM研發之初是爲了應對快速發展的單片機,但願經過程序模擬出將來程序運行的硬件環境,除此以外也賦予了JVM不少其它功能,如垃圾回收等],這兩種機器都有代碼執行能力,其區別是物理機的執行引擎是直接創建在處理器、硬件、指令集和操做系統層面上的,而虛擬機的執行引擎則是由本身實現的,所以能夠自行制定指令集與執行引擎的結構體系,而且可以執行哪些不被硬件直接支持的指令集格式。
在 Java虛擬機規範中制定了虛擬機字節碼執行引擎的概念模型,這個概念模型稱爲各類虛擬機執行引擎的統一外觀(Facade)。在不一樣的虛擬機實現裏面,執行引擎在執行 Java代碼的時候可能會有解釋執行(經過解釋器執行)和編譯執行(經過即時編譯器產生本地代碼執行)兩種選擇,也可能二者兼備,甚至還可能會包含幾個不一樣級別的編譯器執行引擎。但從外觀上看起來,全部的 Java虛擬機的執行引擎都是一致的:輸入的是字節碼文件,處理過程是字節碼解析的等效過程,輸出的是執行結果,下面將主要從概念模型的角度來說解虛擬機的方法調用和字節碼執行。
1.2.2 運行時棧幀結構
棧幀(Stack Frame)是用於支持虛擬機進行方法調用和方法執行的數據結構,它是虛擬機運行時數據區中的虛擬機棧(Virtual Machine Stack)的棧元素。棧幀存儲了方法的局部變量表、操做數棧、動態鏈接和方法返回地址等信息。每個方法從調用開始至執行完成的過程,都對應着一個棧幀在虛擬機棧裏面從入棧到出棧的過程。
每個棧幀都包括了局部變量表、操做數棧、動態鏈接、方法返回地址和一些額外的附加信息。在編譯程序代碼的時候,棧幀中須要多大的局部變量表,多深的操做數棧都已經徹底肯定了,而且寫入到方法表的 Code 屬性之中,所以一個棧幀須要分配多少內存,不會受到程序運行期變量數據的影響,而僅僅取決於具體的虛擬機實現。
一個線程中的方法調用鏈可能會很長,不少方法都同時處於執行狀態。對於執行引擎來講,在活動線程中,只有位於棧頂的棧幀纔是有效的,稱爲當前棧幀(Current Stack Frame),與這個棧幀相關聯的方法稱爲當前方法(Current Method)。執行引擎運行的全部字節碼指令都只針對當前棧幀進行操做,在概念模型上,典型的棧幀結構如上圖所示。
1.2.2.1 局部變量表
局部變量表(Local Variable Table) 是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。在 Java 程序編譯爲 Class 文件時,就在方法的 Code 屬性的 max_locals 數據項中肯定了該方法所須要分配的局部變量表的最大容量。以下代碼所示:
public static void test1(int a,int b){
System.out.println(a+b);
int c = 9;
System.out.println(9%4);
}
![](http://static.javashuo.com/static/loading.gif)
上面三個參數 stack意味着操做數棧的深度,locals是局部變量表最大容量,args_size是參數數量;
局部變量表的容量以變量槽(Variable Slot,下稱 Slot)爲最小單位,虛擬機規範中並無明確指明一個 Slot 應占用的內存空間大小,只是頗有導向性地說到每一個 Slot 都應該能存放一個 boolean、byte、char、short、int、float、reference (注:Java 虛擬機規範中沒有明確規定 reference 類型的長度,它的長度與實際使用 32 仍是 64 位虛擬機有關,若是是 64 位虛擬機,還與是否開啓某些對象指針壓縮的優化有關,這裏暫且只取 32 位虛擬機的 reference 長度)或 returnAddress 類型的數據,這 8 種數據類型,均可以使用 32 位或更小的物理內存來存放,但這種描述與明確指出 「每一個 Slot 佔用 32 位長度的內存空間」 是有一些差異的,它容許 Slot 的長度能夠隨着處理器、操做系統或虛擬機的不一樣而發送變化。只要保證即便在 64 位虛擬機中使用了 64 位的物理內存空間去實現一個 Slot,虛擬機仍要使用對齊和補白的手段讓 Slot在外觀上看起來與 32 位虛擬機中的一致。
既然前面提到了Java 虛擬機的數據類型,在此再簡單介紹一下它們。一個 Slot能夠存放一個32 位之內的數據類型,Java 中佔用 32位之內的數據類型有 boolean、byte、char、short、int、float、reference 和 returnAddress 8 種類型。前面 6 種不須要多加解釋,讀者能夠按照 Java 語言中對應數據類型的概念去理解它們(僅是這樣理解而已,Java語言與 Java虛擬機中的基本數據類型是存在本質差異的),而第 7 種 reference 類型表示對一個對象實例的引用,虛擬機規範既沒有說明他的長度,也沒有明確指出這種引用應有怎樣的結構。但通常來講,虛擬機實現至少都應當能經過這個引用作到兩點,一是今後引用中直接或間接地查找到對象在 Java 堆中的數據存放的起始地址索引,二是此引用中直接或間接地查找到對象所屬數據類型在方法區中的存儲的類型信息,不然沒法實現 Java 語言規範中定義的語法約束約束。第 8 種即 returnAddress 類型目前已經不多見了,它是爲字節碼指令 jsr、jsr_w 和 ret 服務的,指向了一條字節碼指令的地址,很古老的Java虛擬機曾經使用這幾條指令來實現異常處理,如今已經由異常表代替。
對於 64 位的數據類型,虛擬機會以高位對齊的方式爲其分配兩個連續的 Slot 空間。Java 語言中明確的(reference 類型則多是 32 位也多是 64 位)64 位的數據類型只有 long 和 double 兩種。值得一提的是,這裏把 long 和 double 數據類型分割存儲的作法與 「虛擬機經過索引定位的方式使用局部變量表,索引值的範圍是從 0 開始至局部變量表最大的 Slot 數量。若是訪問的是 32 位數據類型的變量,索引 n 就表明了使用第 n 個 Slot,若是是 64 位數據類型的變量,則說明會同時使用 n 和 n+1 兩個 Slot。對於兩個相鄰的共同存放一個 64 位數據的兩個 Slot,不容許採用任何方式單獨訪問其中的某一個,Java 虛擬機規範中明確要求了若是遇到進行這種操做的字節碼序列,虛擬機應該在類加載的校驗階段拋出異常。
在方法執行時,虛擬機是使用局部變量表完成參數值到參數變量列表的傳遞過程的,若是執行的是實例方法(非 static 的方法),那局部變量表中第 0 位索引的 Slot 默認是用於傳遞方法所屬對象實例的引用,在方法中能夠經過關鍵字 「this」 來訪問到這個隱含的參數。其他參數則按照參數表順序排列,佔用從 1 開始的局部變量 Slot,參數表分配完畢後,再根據方法體內部定義的變量順序和做用域分配其他的 Slot。
爲了儘量節省棧幀空間,局部變量中的 Slot 是能夠重用的,方法體中定義的變量,其做用域並不必定會覆蓋整個方法體,若是當前字節碼 PC 計數器的值已經超出了某個變量的做用域,那這個變量對應的 Slot 就能夠交給其餘變量使用。不過,這樣的設計除了節省棧幀空間之外,還會伴隨一些額外的反作用,例如,在某些狀況下,Slot 的複用會直接影響到系統的垃圾收集行爲,請看代碼清單 8-1 ~ 代碼清單 8-3 的 3 個演示。
代碼清單 8-1 局部變量表 Slot 複用對垃圾收集的影響之一
public static void main(String[] args) {
byte[] placeholder = new byte[64 * 1024 * 1024];
System.gc();
}
代碼清單 8-1 中的代碼很簡單,即向內存填充了 64 MB 的數據,而後通知虛擬機進行垃圾收集。咱們在虛擬機運行參數中加上「-verbose:gc」來看看垃圾收集的過程,發如今 System.gc() 運行後並無回收這 64 MB 的內存,下面是運行的結果:
[GC (System.gc()) 66847K->66144K(129024K), 0.0015237 secs] //系統垃圾回收
[Full GC (System.gc()) 66144K->66059K(129024K), 0.0074766 secs]//老年代回收,和咱們這裏沒什麼關係
沒有回收 placeholder 所佔的內存能說得過去,由於在執行 System.gc() 時,變量 placeholder 還處於做用域以內,虛擬機天然不敢回收 placeholder 的內存。那咱們把代碼修改一下,變成代碼清單 8-2 中的樣子。
代碼清單 8-2 局部變量表 Slot 複用對垃圾收集的影響之二
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
System.gc();
}
加入了花括號以後,placeholder 的做用域被限制在花括號以內,從代碼邏輯上講,在執行 System.gc() 的時候,placeholder 已經不可能再被訪問了,但執行一下這段程序,會發現運行結果以下,仍是有 64MB 的內存沒有被回收,這又是爲何呢?
[GC (System.gc()) 66847K->66176K(129024K), 0.0012034 secs]
[Full GC (System.gc()) 66176K->66059K(129024K), 0.0075938 secs]
在解釋爲何以前,咱們先對這段代碼進行第二次修改,在調用 System.gc() 以前加入一行 「int a = 0;」,變成代碼清單 8-3 的樣子。
代碼清單 8-3 局部變量表 Slot 複用對垃圾收集的影響之三
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
int a = 0;
System.gc();
}
這個修改看起來很莫名其妙,但運行一下程序,卻發現此次內存真的被正確回收了。
[GC (System.gc()) 66847K->66144K(129024K), 0.0011405 secs]
[Full GC (System.gc()) 66144K->523K(129024K), 0.0100704 secs]
在代碼清單 8-1 ~ 代碼清單 8-3 中,placeholder 可否被回收的根本緣由是:局部變量中的 Slot是否還存在關於 placeholder 數組對象的引用。第一次修改中,代碼雖然已經離開了 placeholder 的做用域,但在此以後,沒有任何局部變量表的讀寫操做,placeholder 本來佔用的 Slot尚未被其餘變量所複用,因此做爲 GC Roots 一部分的局部變量表仍然保持着對它的關聯。這種關聯沒有被及時打斷,在絕大部分狀況下影響都很輕微。但若是遇到一個方法,其後面的代碼有一些耗時很長的操做,而前面又定義了佔用了大量的內存、實際上已經不會再使用的變量,手動將其設置爲 null 值(用來代替那句 int a=0,把變量對應的局部變量表 Slot 清空)便不見得是一個絕對無心義的操做,這種操做能夠做爲一種在極特殊情形(對象佔用內存大、此方法的棧幀長時間不能被回收、方法調用次數達不到 JIT 即時編譯 的編譯條件)下的 「奇技」 來使用。Java 語言的一本著名書籍《Practical Java》中把 「以恰當的變量做用域來控制變量回收時間纔是最優雅的解決方法,如代碼清單 8-3 那樣的場景並很少見。更關鍵的是,從執行角度來將,使用賦 null 值的操做來優化內存回收是創建在對字節碼執行引擎概念模型的理解之上的,而概念模型與實際執行過程是外部看起來等效,內部看上去則能夠徹底不一樣。在虛擬機使用解釋器執行時,一般與概念模型還比較接近,但通過 JIT 編譯器後,纔是虛擬機執行代碼的主要方式,賦 null 值的操做在通過 JIT 編譯優化後就會被消除掉,這時候將變量設置爲 null 就是沒有意義的。字節碼被編譯爲本地代碼後,對 GC Roots 的枚舉也與解釋執行時期有巨大差異,之前面例子來看,代碼清單 8-2 在通過 JIT 編譯後,System.gc() 執行時就能夠正確回收掉內存,無須寫成代碼清單 8-3 的樣子。
關於局部變量表,還有一點可能會對實際開發產生影響,就是局部變量不像前面介紹的類變量那樣存在 「準備階段」。經過以前的講解,咱們已經知道類變量有兩次賦初始值的過程,一次在準備階段,賦予系統初始化;另一次在初始化階段,賦予程序員定義的初始值。所以,即便在初始化階段程序沒有爲類變量賦值也沒有關係,類變量仍然具備一個肯定的初始值。但局部變量就不同,若是一個局部變量定義了但沒有賦初始值是不能使用的,不要認爲 Java 中任何狀況下都存在諸如整型變量默認爲 0,布爾型變量默認爲 false 等這樣的默認值。如代碼清單 8-4 所示,這段代碼其實並不能運行,還好編譯器能在編譯期間就檢查到並提示這一點,即使編譯能經過或者手動生成字節碼的方式製造出下面代碼的效果,字節碼校驗的時候也會被虛擬機發現而致使類加載失敗。
代碼清單 8-4 未賦值的局部變量
public static void main(String[] args) {
int a; //會報錯未經初始化
System.out.println(a);
}
局部變量運行時被分配在棧中,量大,生命週期短,若是虛擬機給每一個局部變量都初始化一下,是一筆很大的開銷,但變量不初始化爲默認值就使用是不安全的。出於速度和安全性兩個方面的綜合考慮,解決方案就是虛擬機不初始化,但要求編寫者必定要在使用前給變量賦值。還有一點,若是在類加載完就給局部變量賦值,那麼對內存將是很大的開銷,而這些開銷有不少可能都是沒有意義的.
1.2.2.2 操做數棧
操做數棧(Operand Stack)也常稱爲操做棧,它是一個後入先出(Last In First Out,LIFO)棧。同局部變量表同樣,操做數棧的最大深度也在編譯的時候寫入到 Code 屬性的 max_stacks 數據項中。操做數棧的每個元素能夠是任意的Java 數據類型,包括long 和 double。32 位數據類型所佔的棧容量爲1,64位數據類型所佔的棧容量爲2。在方法執行的任什麼時候候,操做數棧的深度都不會超過在 max_stacks 數據項中設定的最大值。
當一個方法剛剛開始執行的時候,這個方法的操做數棧是空的,在方法的執行過程當中,會有各類字節碼指令往操做數棧中寫入和提取內容,也就是出棧 / 入棧操做。例如,在作算術運算的時候是經過操做數棧來進行的,又或者再調用其餘方法的時候是經過操做數棧來進行參數傳遞的。
舉個例子,整數加法的字節碼指令 iadd 在運行的時候操做數棧中最接近棧頂的兩個元素已經存入了兩個int 型的數值,當執行這個指令時,會將這兩個 int值出棧並相加,而後將相加的結果入棧。
操做數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,在編譯程序代碼的時候,編譯器要嚴格保證這一點,在類校驗階段的數據流分析中還要再次驗證這一點。再以上面的 iadd 指令爲例,這個指令用於整型數加法,它執行時,最接近棧頂的兩個元素的數據類型必須爲 int 型,不能出現一個 long 和一個 float 使用 iadd 命令相加的狀況。
另外,在概念模型中,兩個棧幀做爲虛擬機棧的元素,是徹底相互獨立的。但在大多虛擬機的實現裏都會作一些優化處理,令兩個棧幀出現一部分重疊。讓下面棧幀的部分操做數棧與上面棧幀的部分局部變量表重疊在一塊兒,這樣在進行方法調用時就能夠共用一部分數據,無須進行額外的參數複製傳遞,重疊的過程如圖 8-2 所示。
![](http://static.javashuo.com/static/loading.gif)
Java 虛擬機的解釋執行引擎稱爲 「基於棧的執行引擎」,其中所指的 「棧」 就是操做數棧。再看下面一個例子:計算的是兩個數的相加100+98
![](http://static.javashuo.com/static/loading.gif)
在這個字節碼序列裏,前兩個指令iload_0和iload_1將存儲在局部變量中索引爲0和1的整數壓入操做數棧中,其後iadd指令從操做數棧中彈出那兩個整數相加,再將結果壓入操做數棧。第四條指令istore_2則從操做數棧中彈出結果,並把它存儲到局部變量區索引爲2的位置。下圖詳細表述了這個過程當中局部變量和操做數棧的狀態變化,圖中沒有使用的局部變量區和操做數棧區域以空白表示![](http://static.javashuo.com/static/loading.gif)
1.2.2.3 動態鏈接
每一個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程當中的動態鏈接(Dynamic Linking)。咱們知道 Class 文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用做爲參數。這些符號引用一部分會在類加載階段或者第一次使用的時候就轉化爲直接引用,這種轉化成爲靜態解析。另一部分將在每一次運行期間轉化爲直接引用,這部分紅爲動態鏈接。想要了解更多動態連接能夠看一下下面的贅述;
應用程序有兩種連接方式,一種是靜態連接,一種是動態連接,這兩種連接方式各有好處。程序是靜態鏈接仍是動態鏈接是根據編譯器的鏈接參數指定的。
所謂靜態連接就是在編譯連接時直接將須要的執行代碼拷貝到調用處,優勢就是在程序發佈的時候就不須要再依賴庫,也就是再也不須要帶着庫一塊發佈,程序能夠獨立執行,可是體積可能會相對大一些。(所謂庫就是一些功能代碼通過編譯鏈接後的可執行形式。)
所謂動態連接就是在編譯的時候不直接拷貝可執行代碼,而是經過記錄一系列符號和參數,在程序運行或加載時將這些信息傳遞給操做系統,操做系統負責將須要的動態庫加載到內存中,而後程序在運行到指定的代碼時,去共享執行內存中已經加載的動態庫可執行代碼,最終達到運行時鏈接的目的。優勢是多個程序能夠共享同一段代碼,而不須要在磁盤上存儲多個拷貝,缺點是因爲是運行時加載,可能會影響程序的前期執行性能。
上面的都是一些概念性的,也是比較簡單的,可能你們都知道,可是具體的實現方式是什麼樣的那?好比兩個最主流的操做系統windows和linux是怎麼實現的。
在windows上你們都是DLL是動態連接庫,裏面是一系列可執行的代碼,開發過windows程序的人可能還知道有另一種形式的庫,就是LIB,你們可能廣泛認爲LIB就是靜態庫,至少我以前是這麼認爲的,可是在實際的開發過程當中,糾正了我這個錯誤的想法。LIB形式的文件可能會有兩種形式,這裏並不排除第三種形式。1:包括符號表和二進制可執行代碼,也就是傳統意義上理解的靜態庫,能夠被靜態鏈接。2:只有符號表,也就是隻有動態庫的符號導出信息,經過這些信息能夠在程序運行時定位到動態庫中,最終實現動態鏈接。
在linux上你們也都知道SO是動態庫,相似於windows下的DLL,實現方式也是大同小異,同時開發過linux下程序的人也都知道另一種形式的庫就是A庫,一樣道理廣泛認爲是和SO對立的,也就是靜態庫,否則沒道理存在啊,呵呵。可是事實卻不是如此,A文件的做用和windows下的LIB文件做用幾乎同樣,也可能會有兩種形式,和windows下的lib文件同樣,在此就不在贅述。
動態連接和靜態連接的對比
靜態連接
優勢:
- l 代碼裝載速度快,執行速度略比動態連接庫快;
- l 只需保證在開發者的計算機中有正確的.LIB文件,在以二進制形式發佈程序時不需考慮在用戶的計算機上.LIB文件是否存在及版本問題,可避免DLL地獄等問題。
缺點:
- l 使用靜態連接生成的可執行文件體積較大,包含相同的公共代碼,形成浪費;
動態連接
優勢:
- l 更加節省內存並減小頁面交換;
- l DLL文件與EXE文件獨立,只要輸出接口不變(即名稱、參數、返回值類型和調用約定不變),更換DLL文件不會對EXE文件形成任何影響,於是極大地提升了可維護性和可擴展性;
- l 不一樣編程語言編寫的程序只要按照函數調用約定就能夠調用同一個DLL函數;
- l 適用於大規模的軟件開發,使開發過程獨立、耦合度小,便於不一樣開發者和開發組織之間進行開發和測試。
缺點:
- l 使用動態連接庫的應用程序不是自完備的,它依賴的DLL模塊也要存在,若是使用載入時動態連接,程序啓動時發現DLL不存在,系統將終止程序並給出錯誤信息。而使用運行時動態連接,系統不會終止,但因爲DLL中的導出函數不可用,程序會加載失敗;速度比靜態連接慢。當某個模塊更新後,若是新模塊與舊的模塊不兼容,那麼那些須要該模塊才能運行的軟件,通通撕掉。這在早期Windows中很常見。
1.2.2.4 方法返回地址
當一個方法開始執行後,只有兩種方式能夠退出這個方法。第一種方式是執行引擎遇到任意一個方法返回的字節碼指令,這時候可能會有返回值傳遞給上層的方法調用者(調用當前方法的方法稱爲調用者),是否有返回值和返回值的類型將根據遇到何種方法返回指令來決定,這種退出方法的方式稱爲正常完成出口(Normal Method Invocatino Completion)。咱們寫代碼過程當中發現有些時候沒有返回值,咱們省略了,那麼此時咱們會認爲沒有return,其實這是不對的,編譯器在編譯的時候,會爲咱們保留,咱們反編譯一下看看;
public static void test1(int a,int b){
System.out.println(a+b);
int c = 9;
System.out.println(9%4);
}
![](http://static.javashuo.com/static/loading.gif)
另一種退出方式是,在方法執行過程當中遇到了異常,而且這個異常沒有在方法體內獲得處理,不管是 Java虛擬機內部產生的異常,仍是代碼中使用 athrow 字節碼指令產生的異常,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會致使方法退出,這種退出方法的方式稱爲異常完成出口(Abrupt Method Invocation Completion)。一個方法使用異常完成出口的方式退出,是不會給它的上層調用者產生任何返回值的。
不管採用何種退出方式,在方法退出以後,都須要返回到方法被調用的位置,程序才能繼續執行,方法返回時可能須要在棧幀中保存一些信息,用來幫助恢復它的上層方法的執行狀態。通常來講,方法正常退出時,調用者的 PC 計數器的值能夠做爲返回地址,棧幀中極可能會保存這個計數器值。而方法異常退出時,返回地址是要經過異常處理器表來肯定的,棧幀中通常不會保存這部分信息。
方法退出的過程實際上就等同於把當前棧幀出棧,所以退出時可能執行的操做有:恢復上層方法的局部變量表和操做數棧,把返回值(若是有的話)壓入調用者棧幀的操做數棧中,調整 PC 計數器的值以指向方法調用指令後面的一條指令等。
1.2.2.5 附加信息
虛擬機規範容許具體的虛擬機實現增長一些規範裏沒有描述的信息到棧幀之中,例如與調試相關的信息,這部分信息徹底取決於具體的虛擬機實現。在實際開發中,通常會把動態鏈接、方法返回地址與其餘附加信息所有歸爲一類,稱爲棧幀信息。
1.3 本地方法棧
本地方法棧(Native Method Stack)與虛擬機棧所發揮的做用十分類似的,它們之間的區別不過是虛擬機棧爲虛擬機執行Java方法(也是字節碼)服務,而本地方法棧則爲虛擬機使用到的Native方法服務。在虛擬機規範中對本地方法棧中使用的語言、使用方式和數據結構並無強制規定,所以具體的虛擬機能夠自由實現它。甚至有的虛擬機(如HotSpot)直接就把本地方法棧和虛擬機棧合二爲一。與虛擬機棧同樣,本地方法棧也會拋出OutOfMemoryError異常。
1.3.1 JVM怎樣使Native Method跑起來
咱們知道,當一個類第一次被使用到時,這個類的字節碼會被加載到內存,而且只會回載一次。在這個被加載的字節碼的入口維持着一個該類全部方法描述符的list,這些方法描述符包含這樣一些信息:方法代碼存於何處,它有哪些參數,方法的描述符(public之類)等等。
若是一個方法描述符內有native,這個描述符塊將有一個指向該方法的實現的指針。這些實如今一些DLL文件內,可是它們會被操做系統加載到java程序的地址空間。當一個帶有本地方法的類被加載時,其相關的DLL並未被加載,所以指向方法實現的指針並不會被設置。當本地方法被調用以前,這些DLL纔會被加載,這是經過調用java.system.loadLibrary()實現的。
最後須要提示的是,使用本地方法是有開銷的,它喪失了java的不少好處。若是別無選擇,咱們能夠選擇使用本地方法。
1.4 Java堆
對於大多數應用來講,Java堆(Java Heap)是Java虛擬機所管理內存中最大的一塊。Java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存。這一點在JVM規範中的描述是:全部對象實例以及數組都要在堆上分配,可是隨着JIT編譯器的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會致使一些微妙的變化發生,全部的對象都分配在堆上也逐漸變得不是那麼「絕對」了。
Java堆是垃圾收集器管理的主要區域,所以不少時候也被稱爲「GC堆」(Garbage Collected Head,注意這不是垃圾堆)。從內存回收的角度來看,因爲如今收集器基本都採用分代收集算法,因此Java堆中能夠細分爲:新生代和老年代;再細緻一點的有Eden空間、From Survivor空間、To Survivor空間等。從內存分配的角度來看,線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,TLAB)。不過不管如何劃分,都與存放內容無關,不管哪一個區域,存儲的都仍然是對象實例,進一步劃分的目的是爲了更好地內存回收,或者更快的分配內存。這一節,咱們僅僅瞭解一下JVM的Runtime Memory Area,而針對垃圾回收和堆的詳細構成及其餘,後面在專門挑章節介紹。
根據JVM規範的規定,Java堆能夠處於物理上不連續的內存空間中,只要邏輯上是連續的就能夠,就好像咱們的磁盤空間同樣(這其實涉及到了數據結構)。在實現時,既能夠實現成固定大小的,也能夠是可擴展的,不過當前主流的虛擬機都是按照可擴展來實現的(經過-Xmx和-Xms控制)。若是在堆中沒有內存完成實例分配,而且堆也沒法再擴展時,將會拋出OutOfMemoryError異常。
1.4.1 字符串常量池
本節參看:https://segmentfault.com/a/1190000009888357
談到堆就不得不說一下字符串常量池了,字符串常量池是咱們平常很容易搞混的同樣事物了。提起字符串常量池咱們就不禁會想到String,沒錯字符串常量池的確是和String相關的。做爲最基礎的引用數據類型,高頻率的使用,使得Java設計者爲String提供了字符串常量池以提升其性能,那麼字符串常量池的具體原理是什麼,咱們帶着如下三個問題,去理解字符串常量池:
-
字符串常量池的設計意圖是什麼?
-
字符串常量池在哪裏?
-
如何操做字符串常量池?
1.4.1.1 字符串常量池的設計思想
代碼:從字符串常量池中獲取相應的字符串
String str1 = 「hello」;
String str2 = 「hello」;
System.out.printl("str1 == str2" : str1 == str2 ) //true
1.4.1.2 字符串常量池在哪裏
在分析字符串常量池的位置時,首先簡短回顧一下堆、棧、方法區:
-
堆
-
存儲的是對象,每一個對象都包含一個與之對應的class
-
JVM只有一個堆區(heap)被全部線程共享,堆中不存放基本類型和對象引用,只存放對象自己
-
對象的由垃圾回收器負責回收,所以大小和生命週期不須要肯定
-
棧
-
每一個線程包含一個棧區,棧中只保存基礎數據類型的對象和自定義對象的引用(不是對象)
-
每一個棧中的數據(原始類型和對象引用)都是私有的
-
棧分爲3個部分:基本類型變量區、執行環境上下文、操做指令區(存放操做指令)
-
數據大小和生命週期是能夠肯定的,當沒有引用指向數據時,這個數據就會自動消失
-
方法區
JDK1.6(包括)以前字符串常量池存在於方法區,JDK1.7之後字符串常量池被轉移到了堆中。
String m = "hello,world";
String n = "hello,world";
String u = new String(m);
String v = new String("hello,world");
會分配一個11長度的char數組,並在常量池分配一個由這個char數組組成的字符串,而後由m去引用這個字符串
用n去引用常量池裏邊的字符串,因此和n引用的是同一個對象
生成一個新的字符串,但內部的字符數組引用着m內部的字符數組
一樣會生成一個新的字符串,但內部的字符數組引用常量池裏邊的字符串內部的字符數組,意思是和u是一樣的字符數組
使用圖來表示的話,狀況就大概是這樣的(使用虛線只是表示二者其實沒什麼特別的關係):
測試demo:
String m = "hello,world";
String n = "hello,world";
String u = new String(m);
String v = new String("hello,world");
System.out.println(m == n); //true
System.out.println(m == u); //false
System.out.println(m == v); //false
System.out.println(u == v); //false
結論:
m和n是同一個對象
m,u,v都是不一樣的對象
m,u,v,n但都使用了一樣的字符數組,而且用equal判斷的話也會返回true
1.5 方法區
方法區(Method Area)與Java堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然Java虛擬機規範把方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫作Non-Heap(非堆),目的應該是與Java堆區分開來。
不少時候咱們看到一些文獻和資料稱呼HotSpot的方法區爲「永久代」(Permanent Generation),本質上二者是不等價的,僅僅是由於HotSpot虛擬機的設計團隊把GC分代收集擴展至方法區,或者說使用永久代來實現方法區而已,這樣HotSpot的垃圾收集器能夠像管理Java堆同樣來管理這部份內存,可以省去專門爲方法區編寫內存管理代碼的工做。對於其它虛擬機(如BEA JRockit、IBM J9等)來講是不存在永久代的概念的。原則上,如何實現方法區屬於虛擬機的實現細節,不受虛擬機規範約束,但使用永久代來實現方法區,如今看來不是一個好的主意,由於這樣很容易形成內存泄漏問題(永久代有-XX:MaxPermSize的上限,J9和JRockit只要沒有觸碰到進程可用內存的上限,例如32位系統中的4GB,就不會出現問題),並且有極少數方法(例如String.intern())會因這個緣由致使不一樣虛擬機下有不一樣的表現。所以,對於HotSpot虛擬機,根據官方發佈的路線圖信息,如今也有放棄永久代並逐步改成採用Native Memory來實現方法區的規劃了(實際上JDK1.8已經實現了該計劃,永久代消除,以元空間Metaspace替代),在JDK1.7中,已經將本來放在方法區中字符串常量池移出到堆中。
JVM規範對於方法區的限制很是寬鬆,除了和Java堆同樣不須要連續的內存和能夠選擇固定大小或者可擴展外,還能夠選擇不實現垃圾收集。相對而言,垃圾收集行爲在這個區域是比較少出現的,但並不是數據進入了方法區就如永久代的名字同樣「永久」存在了。這區域的內存回收目標主要是針對常量池的回收和對類型的卸載,通常來講,這個區域的回收成績比較使人難以滿意,尤爲是類型的卸載,條件至關苛刻,可是這部分區域的回收確實是很必要的。在Sun公司的BUG列表中,曾出現過的若干個嚴重的BUG就是因爲低版本的HotSpot虛擬機對此區域未徹底回收而致使內存泄漏。
根據Java虛擬機規範的規定,當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError異常。
1.5.1 運行時常量池
運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放。
JVM對於Class文件每一部分(天然包括常量池)的格式都有嚴格規定,每個字節用於存儲哪一種數據都必須符合規範上的要求才會被虛擬機承認、裝載和執行,但對於運行時常量池,Java虛擬機規範沒有作任何細節的要求,不一樣的提供商實現的虛擬機能夠按照本身的需求來實現這個內存區域。不過,通常來講,除了保存Class文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中。
運行時常量池相對於Class文件常量池的另一個重要的特徵是具有動態性,Java語言並不要求常量必定只有編譯期才能產生,也就是並不是預置入Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的即是String類的intern()方法。
既然運行時常量池是方法區的一部分,天然受到方法區的限制,當常量池沒法再申請到內存時會拋出OutOfMemoryError異常。
1.6 直接內存
直接內存(Direct Memory)並非虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域。可是這部份內存也會被頻繁使用,並且也可能致使OutOfMemoryError異常出現,因此咱們放到這裏一塊兒講解。
在JDK1.4中新加入了NIO(New Input/Output)類,引入一種新的基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可使用Native函數庫直接分配堆外內存,而後經過一個存儲在Java堆中的DirectByteBuffer對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在Java堆和Native堆中來回複製數據。這也是NIO的優勢。
顯然,本機直接內存的分配不會受到Java堆大小的限制,可是,既然是內存,確定仍是會受到機總內存(包括RAM以及SWAP區或者分頁文件)大小以及處理器尋址空間的限制。服務器管理員在配置虛擬機參數時,會根據實際內存配置-Xmx等參數信息,但常常忽略直接內存,使得各個內存區域總和大於物理內存限制(包括物理的和操做系統級的限制),從而致使動態擴展時出現OutOfMemoryError異常。
1.7 元空間
上面咱們知道移除永久代的工做從JDK1.7就開始了。JDK1.7中,存儲在永久代的部分數據就已經轉移到了Java Heap或者是 Native Heap。但永久代仍存在於JDK1.7中,並沒徹底移除,譬如符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了java heap;類的靜態變量(class statics)轉移到了java heap。咱們能夠經過一段程序來比較 JDK 1.6 與 JDK 1.7及 JDK 1.8 的區別,以字符串常量爲例:
package cn.metaspace.error;
import java.util.ArrayList;
import java.util.List;
public class MethodAreaTest {
static String base = "String";
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < Integer.MAX_VALUE; i++) {
String str = base + base;
base = str;
list.add(str.intern());
}
}
}
這段程序以2的指數級不斷的生成新的字符串,這樣能夠比較快速的消耗內存。咱們經過 JDK 1.6、JDK 1.7 和 JDK 1.8 分別運行:
JDK 1.6 的運行結果:
![](http://static.javashuo.com/static/loading.gif)
JDK 1.7的運行結果:
![](http://static.javashuo.com/static/loading.gif)
JDK 1.8的運行結果:
![](http://static.javashuo.com/static/loading.gif)
取消配置命令
![](http://static.javashuo.com/static/loading.gif)
-XX:PermSize=8m -XX:MaxPermSize=8m -Xmx16m
![](http://static.javashuo.com/static/loading.gif)
從上述結果能夠看出,JDK 1.6下,會出現「PermGen Space」的內存溢出,而在 JDK 1.7和 JDK 1.8 中,會出現堆內存溢出,而且 JDK 1.8中 PermSize 和 MaxPermGen 已經無效。所以,能夠大體驗證 JDK 1.7 和 1.8 將字符串常量由永久代轉移到堆中,而且 JDK 1.8 中已經不存在永久代的結論。如今咱們看看元空間究竟是一個什麼東西?
元空間的本質和永久代相似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。所以,默認狀況下,元空間的大小僅受本地內存限制,但能夠經過如下參數來指定元空間的大小:
-XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:若是釋放了大量的空間,就適當下降該值;若是釋放了不多的空間,那麼在不超過MaxMetaspaceSize時,適當提升該值。
-XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。
除了上面兩個指定大小的選項之外,還有兩個與 GC 相關的屬性:
-XX:MinMetaspaceFreeRatio,在GC以後,最小的Metaspace剩餘空間容量的百分比,減小爲分配空間所致使的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC以後,最大的Metaspace剩餘空間容量的百分比,減小爲釋放空間所致使的垃圾收集
如今咱們在 JDK 8下從新運行一下下面代碼段,不過此次再也不指定 PermSize 和 MaxPermSize。而是指定 MetaSpaceSize 和 MaxMetaSpaceSize的大小。輸出結果以下:
package cn.metaspace.error;
import java.io.File;
import java.lang.management.ClassLoadingMXBean;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
public class OOMTest {
public static void main(String[] args) {
URL url = null;
List classLoaderList = new ArrayList();
try {
url = new File("/tmp").toURI().toURL();
URL[] urls = {url};
while (true){
ClassLoader loader = new URLClassLoader(urls);
classLoaderList.add(loader);
loader.loadClass("cn.metaspace.error.ClassA");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m
![](http://static.javashuo.com/static/loading.gif)
從輸出結果,咱們能夠看出,此次再也不出現永久代溢出,而是出現了元空間的溢出。
1.8 總結
經過上面分析,大體瞭解了 JVM 的內存劃分,也清楚了 JDK 8 中永久代向元空間的轉換。不過你們應該都有一個疑問,就是爲何要作這個轉換?因此,最後給你們總結如下幾點緣由:
1、字符串存在永久代中,容易出現性能問題和內存溢出。因此JDK1.7實現了將字符串常量池轉移到堆中的操做,利用堆的大空間和垃圾回收幫助解決這個問題。
2、類及方法的信息等比較難肯定其大小,所以對於永久代的大小指定比較困難,過小容易出現永久代溢出,太大則容易致使老年代溢出。故1.8實現了去除永久代的操做。
3、永久代會爲 GC 帶來沒必要要的複雜度,而且回收效率偏低。
四、Oracle 可能會將HotSpot 與 JRockit 合二爲一。
總之,元數據的出現大大減小了OutOfMemoryError的出現機率.