只有光頭才能變強html
JVM在準備面試的時候就有看了,一直沒時間寫筆記。如今到了一家公司實習,閒的時候就寫寫,刷刷JVM博客,刷刷電子書。java
學習JVM的目的也很簡單:面試
聲明:全文默認指的是HotSpot VM算法
如今我有一個JavaBean:bootstrap
public class Java3y { // 姓名 private String name; // 年齡 private int age; //.....各類get/set方法/toString }
一個測試類:segmentfault
public class Java3yTest { public static void main(String[] args) { Java3y java3y = new Java3y(); java3y.setName("Java3y"); System.out.println(java3y); } }
咱們在初學的時候確定用過javac
來編譯.java
文件代碼,用過java
命令來執行編譯後生成的.class
文件。緩存
Java源文件:安全
在使用IDE點擊運行的時候其實就是將這兩個命令結合起來了(編譯並運行),方便咱們開發。服務器
生成class文件數據結構
解析class文件獲得結果
.java
文件是由Java源碼編譯器(上述所說的java.exe)來完成,流程圖以下所示:
Java源碼編譯由如下三個過程組成:
語法糖能夠看作是編譯器實現的一些「小把戲」,這些「小把戲」可能會使得效率「大提高」。
最值得說明的就是泛型了,這個語法糖能夠說咱們是常常會使用到的!
有了泛型這顆語法糖之後:
瞭解泛型更多的知識:
至此,咱們經過java.exe
編譯器編譯咱們的.java
源代碼文件生成出.class
文件了!
這些.class
文件很明顯是不能直接運行的,它不像C語言(編譯cpp後生成exe文件直接運行)
這些.class
文件是交由JVM來解析運行!
如今咱們例子中生成的兩個.class
文件都會直接被加載到JVM中嗎??
虛擬機規範則是嚴格規定了有且只有5種狀況必須當即對類進行「初始化」(class文件加載到JVM中):
因此說:
class文件是經過類的加載器裝載到jvm中的!
Java默認有三種類加載器:
各個加載器的工做責任:
工做過程:
其實這就是所謂的雙親委派模型。簡單來講:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上。
好處:
特別說明:
java.lang.Class
類的實例緩存起來。下次再請求加載該類的時候,類加載器會直接使用緩存的類的實例,而不會嘗試再次加載。加載器加載到jvm中,接下來其實又分了好幾個步驟:
- 1)驗證,文件格式、元數據、字節碼、符號引用驗證; - 2)準備,爲類的靜態變量分配內存,並將其初始化爲默認值; - 3)解析,把類中的符號引用轉換爲直接引用
通常咱們可能會想:JVM在加載了這些class文件之後,針對這些字節碼,逐條取出,逐條執行-->解析器解析。
但若是是這樣的話,那就太慢了!
咱們的JVM是這樣實現的:
熱點代碼解釋:1、屢次調用的方法。2、屢次執行的循環體
使用熱點探測來檢測是否爲熱點代碼,熱點探測有兩種方式:
目前HotSpot使用的是計數器的方式,它爲每一個方法準備了兩類計數器:
按咱們程序來走,咱們的Java3yTest.class
文件會被AppClassLoader加載器(由於ExtClassLoader和BootStrap加載器都不會加載它[雙親委派模型])加載到JVM中。
隨後發現了要使用Java3y這個類,咱們的Java3y.class
文件會被AppClassLoader加載器(由於ExtClassLoader和BootStrap加載器都不會加載它[雙親委派模型])加載到JVM中
詳情參考:
擴展閱讀:
在類加載檢查經過後,接下來虛擬機將爲新生對象分配內存。
首先咱們來了解一下JVM的內存模型的怎麼樣的:
簡單看了一下內存模型,簡單看看每一個區域究竟存儲的是什麼(乾的是什麼):
我來宏觀簡述一下咱們的例子中的工做流程:
java.exe
運行Java3yTest.class
,隨後被加載到JVM中,元空間存儲着類的信息(包括類的名稱、方法信息、字段信息..)。Java3y java3y = new Java3y();
就是讓JVM建立一個Java3y對象,可是這時候方法區中沒有Java3y類的信息,因此JVM立刻加載Java3y類,把Java3y類的類型信息放到方法區中(元空間)java3y.setName("Java3y");
的時候,JVM根據java3y引用找到Java3y對象,而後根據Java3y對象持有的引用定位到方法區中Java3y類的類型信息的方法表,得到setName()
函數的字節碼的地址setName()
函數建立棧幀,開始運行setName()
函數從微觀上其實還作了不少東西,正如上面所說的類加載過程(加載-->鏈接(驗證,準備,解析)-->初始化),在類加載完以後jvm爲其分配內存(分配內存中也作了很是多的事)。因爲這些步驟並非一步一步往下走,會有不少的「混沌bootstrap」的過程,因此很難描述清楚。
參考資料:
在寫這篇文章的時候,本來覺得我對String s = "aaa";
相似這些題目已是不成問題了,直到我遇到了String.intern()
這樣的方法與諸如String s1 = new String("1") + new String("2");
混合一塊兒用的時候
首先我是先閱讀了美團技術團隊的這篇文章:https://tech.meituan.com/in_depth_understanding_string_intern.html---深刻解析String#intern
嗯,而後就懵逼了。我摘抄一下他的例子:
public static void main(String[] args) { String s = new String("1"); s.intern(); String s2 = "1"; System.out.println(s == s2); String s3 = new String("1") + new String("1"); s3.intern(); String s4 = "11"; System.out.println(s3 == s4); }
打印結果是
調換一下位置後:
public static void main(String[] args) { String s = new String("1"); String s2 = "1"; s.intern(); System.out.println(s == s2); String s3 = new String("1") + new String("1"); String s4 = "11"; s3.intern(); System.out.println(s3 == s4); }
打印結果爲:
文章中有很詳細的解析,但我簡單閱讀了幾回之後仍是很懵逼。因此我知道了本身的知識點還存在漏洞,後面閱讀了一下R大以前寫過的文章:
看完了以後,就更加懵逼了。
後來,在zhihu上看到了這個回答:
結合網上資料和本身的思考,下面整理一下對常量池的理解~~
針對於jdk1.7以後:
常量池存儲的是:
- 類和接口的全限定名(Full Qualified Name) - 字段的名稱和描述符(Descriptor) - 方法的名稱和描述符
常量池(Constant Pool Table),用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放--->來源:深刻理解Java虛擬機 JVM高級特性與最佳實踐(第二版)
如今咱們的運行時常量池只是換了一個位置(本來來方法區,如今在堆中),但能夠明確的是:類加載後,常量池中的數據會在運行時常量池中存放!
HotSpot VM裏,記錄interned string的一個全局表叫作StringTable,它本質上就是個HashSet<String>。注意它只存儲對java.lang.String實例的引用,而不存儲String對象的內容
字符串常量池只存儲引用,不存儲內容!
再來看一下咱們的intern方法:
* When the intern method is invoked, if the pool already contains a * string equal to this {@code String} object as determined by * the {@link #equals(Object)} method, then the string from the pool is * returned. Otherwise, this {@code String} object is added to the * pool and a reference to this {@code String} object is returned.
原本打算寫註釋的方式來解釋的,但好像挺難說清楚的。我仍是畫圖吧...
public static void main(String[] args) { // 1.1在堆中建立"1"字符串對象 // 1.2字符串常量池引用"1"字符串對象 // 1.3s引用指向堆中"1"字符串對象 String s = new String("1"); // 2. 發現字符串常量池中已經存在"1"字符串對象,直接返回字符串常量池中對堆的引用(但沒有接收)-->s引用仍是指向着堆中的對象 s.intern(); // 3. 發現字符串常量池已經保存了該對象的引用了,直接返回字符串常量池對堆中字符串的引用 String s2 = "1"; // 4. s指向的是堆中對象的引用,s2指向的是在字符串常量池對堆中對象的引用 System.out.println(s == s2);// false System.out.println("-----------關注公衆號:Java3y-------------"); }
第一句:String s = new String("1");
第二句:s.intern();
發現字符串常量池中已經存在"1"字符串對象,直接返回字符串常量池中對堆的引用(但沒有接收)-->此時s引用仍是指向着堆中的對象
第三句:String s2 = "1";
發現字符串常量池已經保存了該對象的引用了,直接返回字符串常量池對堆中字符串的引用
很容易看到,兩條引用是不同的!因此返回false。
public static void main(String[] args) { System.out.println("-----------關注公衆號:Java3y-------------"); // 1. 在堆中首先建立了兩個「1」對象 // 1.1 +號運算符解析成stringBuilder,最後toString(),最終在堆中建立出"11"對象 // 1.2 注意:此時"11"對象並無在字符串常量池中保存引用 String s3 = new String("1") + new String("1"); // 2. 發現"11"對象並無在字符串常量池中存在,因而將"11"對象在字符串常量池中保存當前字符串的引用,並返回當前字符串的引用 s3.intern(); // 3. 發現字符串常量池已經存在引用了,直接返回(拿到的也是與s3相同指向的引用) String s4 = "11"; System.out.println(s3 == s4); // true }
第一句:String s3 = new String("1") + new String("1");
在堆中首先建立了兩個「1」對象。 +
號運算符解析成stringBuilder,最後toString(),最終在堆中建立出"11"對象。注意:此時"11"對象並無在字符串常量池中保存引用。
第二句:s3.intern();
發現"11"對象並無在字符串常量池中,因而將"11"對象在字符串常量池中保存當前字符串的引用,並返回當前字符串的引用(但沒有接收)
第三句:String s4 = "11";
發現字符串常量池已經存在引用了,直接返回(拿到的也是與s3相同指向的引用)
根據上述所說的:最後會返回true~~~
若是仍是不太清楚的同窗,能夠試着接收一下intern()
方法的返回值,再看看上述的圖,應該就能夠理解了。
下面的就由各位來作作,看是否是掌握了:
public static void main(String[] args) { String s = new String("1"); String s2 = "1"; s.intern(); System.out.println(s == s2);//false String s3 = new String("1") + new String("1"); String s4 = "11"; s3.intern(); System.out.println(s3 == s4);//false }
還有:
public static void main(String[] args) { String s1 = new String("he") + new String("llo"); String s2 = new String("h") + new String("ello"); String s3 = s1.intern(); String s4 = s2.intern(); System.out.println(s1 == s3);// true System.out.println(s1 == s4);// true }
能夠說GC垃圾回收是JVM中一個很是重要的知識點,應該很是詳細去講解的。但在我學習的途中,我已經發現了有很好的文章去講解垃圾回收的了。
因此,這裏我只簡單介紹一下垃圾回收的東西,詳細的能夠到下面的面試題中查閱和最後給出相關的資料閱 讀吧~
在C++中,咱們知道建立出的對象是須要手動去delete掉的。咱們Java程序運行在JVM中,JVM能夠幫咱們「自動」回收不須要的對象,對咱們來講是十分方便的。
雖說「自動」回收了咱們不須要的對象,但若是咱們想變強,就要變禿..不對,就要去了解一下它到底是怎麼幹的,理論的知識有哪些。
首先,JVM回收的是垃圾,垃圾就是咱們程序中已是不須要的了。垃圾收集器在對堆進行回收前,第一件事情就是要肯定這些對象之中哪些還「存活」着,哪些已經「死去」。判斷哪些對象「死去」經常使用有兩種方式:
如今已經能夠判斷哪些對象已經「死去」了,咱們如今要對這些「死去」的對象進行回收,回收也有好幾種算法:
(這些算法詳情可看下面的面試題內容)~
不管是可達性分析算法,仍是垃圾回收算法,JVM使用的都是準確式GC。JVM是使用一組稱爲OopMap的數據結構,來存儲全部的對象引用(這樣就不用遍歷整個內存去查找了,時間換空間)。 而且不會將全部的指令都生成OopMap,只會在安全點上生成OopMap,在安全區域上開始GC。
上面所講的垃圾收集算法只能算是方法論,落地實現的是垃圾收集器:
上面這些收集器大部分是能夠互相組合使用的
不少作過JavaWeb項目(ssh/ssm)這樣的同窗可能都會遇到過OutOfMemory這樣的錯誤。通常解決起來也很方便,在啓動的時候加個參數就好了。
上面也說了不少關於JVM的東西--->JVM對內存的劃分啊,JVM各類的垃圾收集器啊。
內存的分配的大小啊,使用哪一個收集器啊,這些均可以由咱們根據需求,現實狀況來指定的,這裏就不詳細說了,等真正用到的時候纔回來填坑吧~~~~
參考資料:
拿些常見的JVM面試題來作作,加深一下理解和查缺補漏:
題目來源:
根據 JVM 規範,JVM 內存共分爲虛擬機棧、堆、方法區、程序計數器、本地方法棧五個部分。
具體可能會聊聊jdk1.7之前的PermGen(永久代),替換成Metaspace(元空間)
圖片來源:http://www.javashuo.com/article/p-smjktxqf-nt.html
參考資料:
內存泄漏的緣由很簡單:
常見的內存泄漏例子:
public static void main(String[] args) { Set set = new HashSet(); for (int i = 0; i < 10; i++) { Object object = new Object(); set.add(object); // 設置爲空,這對象我再也不用了 object = null; } // 可是set集合中還維護這obj的引用,gc不會回收object對象 System.out.println(set); }
解決這個內存泄漏問題也很簡單,將set設置爲null,那就能夠避免上訴內存泄漏問題了。其餘內存泄漏得一步一步分析了。
內存泄漏參考資料:
內存溢出的緣由:
解決:
參考資料:
這裏的線程棧應該指的是虛擬機棧吧...
JVM規範讓每一個Java線程擁有本身的獨立的JVM棧,也就是Java方法的調用棧。
當方法調用的時候,會生成一個棧幀。棧幀是保存在虛擬機棧中的,棧幀存儲了方法的局部變量表、操做數棧、動態鏈接和方法返回地址等信息
線程運行過程當中,只有一個棧幀是處於活躍狀態,稱爲「當前活躍棧幀」,當前活動棧幀始終是虛擬機棧的棧頂元素。
經過jstack工具查看線程狀態
參考資料:
這題就依據full GC的觸發條件來作:
- 因此看看是否是perm gen區的值設置得過小了。
System.gc()
方法的調用- 這個通常沒人去調用吧~~~
- 是否是頻繁建立了大對象(也有可能eden區設置太小)(大對象直接分配在老年代中,致使老年代空間不足--->從而頻繁gc) - 是否是老年代的空間設置太小了(Minor GC幾個對象就大於老年代的剩餘空間了)
雙親委託模型的重要用途是爲了解決類載入過程當中的安全性問題。
java.lang.Object
的類,想借此欺騙JVM。如今他要使用自定義ClassLoader
來加載本身編寫的java.lang.Object
類。Bootstrap ClassLoader
的路徑下找到java.lang.Object
類,並載入它Java的類加載是否必定遵循雙親委託模型?
- https://zhuanlan.zhihu.com/p/28909673 - http://www.javashuo.com/article/p-mbtipyvc-gx.html - http://www.javashuo.com/article/p-mnszpkje-ct.html
參考資料:
檢驗一下是否是真懂了:
class Dervied extends Base { private String name = "Java3y"; public Dervied() { tellName(); printName(); } public void tellName() { System.out.println("Dervied tell name: " + name); } public void printName() { System.out.println("Dervied print name: " + name); } public static void main(String[] args) { new Dervied(); } } class Base { private String name = "公衆號"; public Base() { tellName(); printName(); } public void tellName() { System.out.println("Base tell name: " + name); } public void printName() { System.out.println("Base print name: " + name); } }
輸出數據:
Dervied tell name: null Dervied print name: null Dervied tell name: Java3y Dervied print name: Java3y
第一次作錯的同窗點個贊,加個關注不過度吧(hahaha
當young gen中的eden區分配滿的時候觸發MinorGC(新生代的空間不夠放的時候).
這題不是很明白意思(水平有限...若是知道這題的意思可在評論區留言呀~~)
YGC和FGC是什麼
何時執行YGC和FGC
System.gc()
,ygc時的悲觀策略, dump live的內存信息時(jmap –dump:live),都會執行full gcGC最基礎的算法有三種:
具體:
圖來源於《深刻理解Java虛擬機:JVM高級特效與最佳實現》,圖中兩個收集器之間有連線,說明它們能夠配合使用.
stackoverflow錯誤主要出現:
permgen space錯誤(針對jdk以前1.7版本):
總的來講,JVM在初級的層面上仍是偏理論多,可能要作具體的東西纔會有更深的體會。這篇主要是入個門吧~
這篇文章懶懶散散也算把JVM比較重要的知識點理了一遍了,後面打算學學,寫寫SpringCloud的東西。