本文已經收錄自筆者開源的 JavaGuide: github.com/Snailclimb (【Java學習+面試指南】 一份涵蓋大部分Java程序員所須要掌握的核心知識)若是以爲不錯的還,不妨去點個Star,鼓勵一下!html
若是沒有特殊說明,都是針對的是 HotSpot 虛擬機。java
對於 Java 程序員來講,在虛擬機自動內存管理機制下,再也不須要像 C/C++程序開發程序員這樣爲每個 new 操做去寫對應的 delete/free 操做,不容易出現內存泄漏和內存溢出問題。正是由於 Java 程序員把內存控制權利交給 Java 虛擬機,一旦出現內存泄漏和溢出方面的問題,若是不瞭解虛擬機是怎樣使用內存的,那麼排查錯誤將會是一個很是艱鉅的任務。c++
Java 虛擬機在執行 Java 程序的過程當中會把它管理的內存劃分紅若干個不一樣的數據區域。JDK. 1.8 和以前的版本略有不一樣,下面會介紹到。git
JDK 1.8 以前:程序員
JDK 1.8 :github
線程私有的:面試
線程共享的:算法
程序計數器是一塊較小的內存空間,能夠看做是當前線程所執行的字節碼的行號指示器。字節碼解釋器工做時經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等功能都須要依賴這個計數器來完成。spring
另外,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各線程之間計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存。後端
從上面的介紹中咱們知道程序計數器主要有兩個做用:
注意:程序計數器是惟一一個不會出現 OutOfMemoryError 的內存區域,它的生命週期隨着線程的建立而建立,隨着線程的結束而死亡。
與程序計數器同樣,Java 虛擬機棧也是線程私有的,它的生命週期和線程相同,描述的是 Java 方法執行的內存模型,每次方法調用的數據都是經過棧傳遞的。
Java 內存能夠粗糙的區分爲堆內存(Heap)和棧內存 (Stack),其中棧就是如今說的虛擬機棧,或者說是虛擬機棧中局部變量表部分。 (實際上,Java 虛擬機棧是由一個個棧幀組成,而每一個棧幀中都擁有:局部變量表、操做數棧、動態連接、方法出口信息。)
局部變量表主要存放了編譯器可知的各類數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型,它不一樣於對象自己,多是一個指向對象起始地址的引用指針,也多是指向一個表明對象的句柄或其餘與此對象相關的位置)。
Java 虛擬機棧會出現兩種錯誤:StackOverFlowError 和 OutOfMemoryError。
Java 虛擬機棧也是線程私有的,每一個線程都有各自的 Java 虛擬機棧,並且隨着線程的建立而建立,隨着線程的死亡而死亡。
擴展:那麼方法/函數如何調用?
Java 棧可用類比數據結構中棧,Java 棧中保存的主要內容是棧幀,每一次函數調用都會有一個對應的棧幀被壓入 Java 棧,每個函數調用結束後,都會有一個棧幀被彈出。
Java 方法有兩種返回方式:
無論哪一種返回方式都會致使棧幀被彈出。
和虛擬機棧所發揮的做用很是類似,區別是: 虛擬機棧爲虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務。 在 HotSpot 虛擬機中和 Java 虛擬機棧合二爲一。
本地方法被執行的時候,在本地方法棧也會建立一個棧幀,用於存放該本地方法的局部變量表、操做數棧、動態連接、出口信息。
方法執行完畢後相應的棧幀也會出棧並釋放內存空間,也會出現 StackOverFlowError 和 OutOfMemoryError 兩種錯誤。
Java 虛擬機所管理的內存中最大的一塊,Java 堆是全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例以及數組都在這裏分配內存。
Java 堆是垃圾收集器管理的主要區域,所以也被稱做GC 堆(Garbage Collected Heap).從垃圾回收的角度,因爲如今收集器基本都採用分代垃圾收集算法,因此 Java 堆還能夠細分爲:新生代和老年代:再細緻一點有:Eden 空間、From Survivor、To Survivor 空間等。進一步劃分的目的是更好地回收內存,或者更快地分配內存。
在 JDK 7 版本及JDK 7 版本以前,堆內存被一般被分爲下面三部分:
JDK 8 版本以後方法區(HotSpot 的永久代)被完全移除了(JDK1.7 就已經開始了),取而代之是元空間,元空間使用的是直接內存。
上圖所示的 Eden 區、兩個 Survivor 區都屬於新生代(爲了區分,這兩個 Survivor 區域按照順序被命名爲 from 和 to),中間一層屬於老年代。
大部分狀況,對象都會首先在 Eden 區域分配,在一次新生代垃圾回收後,若是對象還存活,則會進入 s0 或者 s1,而且對象的年齡還會加 1(Eden 區->Survivor 區後對象的初始年齡變爲 1),當它的年齡增長到必定程度(默認爲 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,能夠經過參數 -XX:MaxTenuringThreshold
來設置。
修正(issue552):「Hotspot遍歷全部對象時,按照年齡從小到大對其所佔用的大小進行累積,當累積的某個年齡大小超過了survivor區的一半時,取這個年齡和MaxTenuringThreshold中更小的一個值,做爲新的晉升年齡閾值」。
動態年齡計算的代碼以下
uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) { //survivor_capacity是survivor空間的大小 size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100); size_t total = 0; uint age = 1; while (age < table_size) { total += sizes[age];//sizes數組是每一個年齡段對象大小 if (total > desired_survivor_size) break; age++; } uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; ... } 複製代碼
堆這裏最容易出現的就是 OutOfMemoryError 錯誤,而且出現這種錯誤以後的表現形式還會有幾種,好比:
OutOfMemoryError: GC Overhead Limit Exceeded
: 當JVM花太多時間執行垃圾回收而且只能回收不多的堆空間時,就會發生此錯誤。java.lang.OutOfMemoryError: Java heap space
:假如在建立新的對象時, 堆內存中的空間不足以存放新建立的對象, 就會引起java.lang.OutOfMemoryError: Java heap space
錯誤。(和本機物理內存無關,和你配置的對內存大小有關!)方法區與 Java 堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然 Java 虛擬機規範把方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫作 Non-Heap(非堆),目的應該是與 Java 堆區分開來。
方法區也被稱爲永久代。不少人都會分不清方法區和永久代的關係,爲此我也查閱了文獻。
《Java 虛擬機規範》只是規定了有方法區這麼個概念和它的做用,並無規定如何去實現它。那麼,在不一樣的 JVM 上方法區的實現確定是不一樣的了。 方法區和永久代的關係很像 Java 中接口和類的關係,類實現了接口,而永久代就是 HotSpot 虛擬機對虛擬機規範中方法區的一種實現方式。 也就是說,永久代是 HotSpot 的概念,方法區是 Java 虛擬機規範中的定義,是一種規範,而永久代是一種實現,一個是標準一個是實現,其餘的虛擬機實現並無永久代這一說法。
JDK 1.8 以前永久代還沒被完全移除的時候一般經過下面這些參數來調節方法區大小
-XX:PermSize=N //方法區 (永久代) 初始大小
-XX:MaxPermSize=N //方法區 (永久代) 最大大小,超過這個值將會拋出 OutOfMemoryError 異常:java.lang.OutOfMemoryError: PermGen
複製代碼
相對而言,垃圾收集行爲在這個區域是比較少出現的,但並不是數據進入方法區後就「永久存在」了。
JDK 1.8 的時候,方法區(HotSpot 的永久代)被完全移除了(JDK1.7 就已經開始了),取而代之是元空間,元空間使用的是直接內存。
下面是一些經常使用參數:
-XX:MetaspaceSize=N //設置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //設置 Metaspace 的最大大小
複製代碼
與永久代很大的不一樣就是,若是不指定大小的話,隨着更多類的建立,虛擬機會耗盡全部可用的系統內存。
當你元空間溢出時會獲得以下錯誤:
java.lang.OutOfMemoryError: MetaSpace
你可使用 -XX:MaxMetaspaceSize
標誌設置最大元空間大小,默認值爲 unlimited,這意味着它只受系統內存的限制。-XX:MetaspaceSize
調整標誌定義元空間的初始大小若是未指定此標誌,則 Metaspace 將根據運行時的應用程序需求動態地從新調整大小。
元空間裏面存放的是類的元數據,這樣加載多少類的元數據就不禁 MaxPermSize
控制了, 而由系統的實際可用空間來控制,這樣能加載的類就更多了。
在 JDK8,合併 HotSpot 和 JRockit 的代碼時, JRockit 歷來沒有一個叫永久代的東西, 合併以後就沒有必要額外的設置這麼一個永久代的地方了。
運行時常量池是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有常量池信息(用於存放編譯期生成的各類字面量和符號引用)
既然運行時常量池是方法區的一部分,天然受到方法區內存的限制,當常量池沒法再申請到內存時會拋出 OutOfMemoryError 錯誤。
JDK1.7 及以後版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開闢了一塊區域存放運行時常量池。
——圖片來源: blog.csdn.net/wangbiao007…直接內存並非虛擬機運行時數據區的一部分,也不是虛擬機規範中定義的內存區域,可是這部份內存也被頻繁地使用。並且也可能致使 OutOfMemoryError 錯誤出現。
JDK1.4 中新加入的 NIO(New Input/Output) 類,引入了一種基於通道(Channel) 與緩存區(Buffer) 的 I/O 方式,它能夠直接使用 Native 函數庫直接分配堆外內存,而後經過一個存儲在 Java 堆中的 DirectByteBuffer 對象做爲這塊內存的引用進行操做。這樣就能在一些場景中顯著提升性能,由於避免了在 Java 堆和 Native 堆之間來回複製數據。
本機直接內存的分配不會受到 Java 堆的限制,可是,既然是內存就會受到本機總內存大小以及處理器尋址空間的限制。
經過上面的介紹咱們大概知道了虛擬機的內存狀況,下面咱們來詳細的瞭解一下 HotSpot 虛擬機在 Java 堆中對象分配、佈局和訪問的全過程。
下圖即是 Java 對象的建立過程,我建議最好是能默寫出來,而且要掌握每一步在作什麼。
虛擬機遇到一條 new 指令時,首先將去檢查這個指令的參數是否能在常量池中定位到這個類的符號引用,而且檢查這個符號引用表明的類是否已被加載過、解析和初始化過。若是沒有,那必須先執行相應的類加載過程。
在類加載檢查經過後,接下來虛擬機將爲新生對象分配內存。對象所需的內存大小在類加載完成後即可肯定,爲對象分配空間的任務等同於把一塊肯定大小的內存從 Java 堆中劃分出來。分配方式有 「指針碰撞」 和 「空閒列表」 兩種,選擇那種分配方式由 Java 堆是否規整決定,而 Java 堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。
內存分配的兩種方式:(補充內容,須要掌握)
選擇以上兩種方式中的哪種,取決於 Java 堆內存是否規整。而 Java 堆內存是否規整,取決於 GC 收集器的算法是"標記-清除",仍是"標記-整理"(也稱做"標記-壓縮"),值得注意的是,複製算法內存也是規整的
內存分配併發問題(補充內容,須要掌握)
在建立對象的時候有一個很重要的問題,就是線程安全,由於在實際開發過程當中,建立對象是很頻繁的事情,做爲虛擬機來講,必需要保證線程是安全的,一般來說,虛擬機採用兩種方式來保證線程安全:
內存分配完成後,虛擬機須要將分配到的內存空間都初始化爲零值(不包括對象頭),這一步操做保證了對象的實例字段在 Java 代碼中能夠不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。
初始化零值完成以後,虛擬機要對對象進行必要的設置,例如這個對象是那個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的 GC 分代年齡等信息。 這些信息存放在對象頭中。 另外,根據虛擬機當前運行狀態的不一樣,如是否啓用偏向鎖等,對象頭會有不一樣的設置方式。
在上面工做都完成以後,從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象建立纔剛開始,<init>
方法尚未執行,全部的字段都還爲零。因此通常來講,執行 new 指令以後會接着執行 <init>
方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算徹底產生出來。
在 Hotspot 虛擬機中,對象在內存中的佈局能夠分爲 3 塊區域:對象頭、實例數據和對齊填充。
Hotspot 虛擬機的對象頭包括兩部分信息,第一部分用於存儲對象自身的自身運行時數據(哈希碼、GC 分代年齡、鎖狀態標誌等等),另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是那個類的實例。
實例數據部分是對象真正存儲的有效信息,也是在程序中所定義的各類類型的字段內容。
對齊填充部分不是必然存在的,也沒有什麼特別的含義,僅僅起佔位做用。 由於 Hotspot 虛擬機的自動內存管理系統要求對象起始地址必須是 8 字節的整數倍,換句話說就是對象的大小必須是 8 字節的整數倍。而對象頭部分正好是 8 字節的倍數(1 倍或 2 倍),所以,當對象實例數據部分沒有對齊時,就須要經過對齊填充來補全。
創建對象就是爲了使用對象,咱們的 Java 程序經過棧上的 reference 數據來操做堆上的具體對象。對象的訪問方式由虛擬機實現而定,目前主流的訪問方式有①使用句柄和②直接指針兩種:
這兩種對象訪問方式各有優點。使用句柄來訪問的最大好處是 reference 中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而 reference 自己不須要修改。使用直接指針訪問方式最大的好處就是速度快,它節省了一次指針定位的時間開銷。
String 對象的兩種建立方式:
String str1 = "abcd";//先檢查字符串常量池中有沒有"abcd",若是字符串常量池中沒有,則建立一個,而後 str1 指向字符串常量池中的對象,若是有,則直接將 str1 指向"abcd"";
String str2 = new String("abcd");//堆中建立一個新的對象
String str3 = new String("abcd");//堆中建立一個新的對象
System.out.println(str1==str2);//false
System.out.println(str2==str3);//false
複製代碼
這兩種不一樣的建立方法是有差異的。
記住一點:只要使用 new 方法,便須要建立新的對象。
再給你們一個圖應該更容易理解,圖片來源:www.journaldev.com/797/what-is…:
String 類型的常量池比較特殊。它的主要使用方法有兩種:
String s1 = new String("計算機");
String s2 = s1.intern();
String s3 = "計算機";
System.out.println(s2);//計算機
System.out.println(s1 == s2);//false,由於一個是堆內存中的 String 對象一個是常量池中的 String 對象,
System.out.println(s3 == s2);//true,由於兩個都是常量池中的 String 對象
複製代碼
字符串拼接:
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";//常量池中的對象
String str4 = str1 + str2; //在堆上建立的新的對象
String str5 = "string";//常量池中的對象
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
複製代碼
儘可能避免多個字符串拼接,由於這樣會從新建立對象。若是須要改變字符串的話,可使用 StringBuilder 或者 StringBuffer。
將建立 1 或 2 個字符串。若是池中已存在字符串常量「abc」,則只會在堆空間建立一個字符串常量「abc」。若是池中沒有字符串常量「abc」,那麼它將首先在池中建立,而後在堆空間中建立,所以將建立總共 2 個字符串對象。
驗證:
String s1 = new String("abc");// 堆內存的地址值
String s2 = "abc";
System.out.println(s1 == s2);// 輸出 false,由於一個是堆內存,一個是常量池的內存,故二者是不一樣的。
System.out.println(s1.equals(s2));// 輸出 true
複製代碼
結果:
false
true
複製代碼
Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 輸出 true
Integer i11 = 333;
Integer i22 = 333;
System.out.println(i11 == i22);// 輸出 false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 輸出 false
複製代碼
Integer 緩存源代碼:
/** *此方法將始終緩存-128 到 127(包括端點)範圍內的值,並能夠緩存此範圍以外的其餘值。 */
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
複製代碼
應用場景:
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);//輸出 false
複製代碼
Integer 比較更豐富的一個例子:
Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
System.out.println("i1=i2 " + (i1 == i2));
System.out.println("i1=i2+i3 " + (i1 == i2 + i3));
System.out.println("i1=i4 " + (i1 == i4));
System.out.println("i4=i5 " + (i4 == i5));
System.out.println("i4=i5+i6 " + (i4 == i5 + i6));
System.out.println("40=i5+i6 " + (40 == i5 + i6));
複製代碼
結果:
i1=i2 true
i1=i2+i3 true
i1=i4 false
i4=i5 false
i4=i5+i6 true
40=i5+i6 true
複製代碼
解釋:
語句 i4 == i5 + i6,由於+這個操做符不適用於 Integer 對象,首先 i5 和 i6 進行自動拆箱操做,進行數值相加,即 i4 == 40。而後 Integer 對象沒法與數值進行直接比較,因此 i4 自動拆箱轉爲 int 值 40,最終這條語句轉爲 40 == 40 進行數值比較。
若是你們想要實時關注我更新的文章以及分享的乾貨的話,能夠關注個人公衆號。
《Java面試突擊》: 由本文檔衍生的專爲面試而生的《Java面試突擊》V2.0 PDF 版本公衆號後臺回覆 "Java面試突擊" 便可免費領取!
Java工程師必備學習資源: 一些Java工程師經常使用學習資源公衆號後臺回覆關鍵字 「1」 便可免費無套路獲取。
做者的其餘開源項目推薦:
安利一下阿里雲雙 12 的活動,1 核 2g 只要 89 一年,薅波羊毛,感受甚爽,不過最低的優惠都是新人才能享有的,我是用我女友的帳號買的,沒有女友的,emm.....,能夠考慮一下親人的。