一、為什么要學習Java虛擬機?
這里我們使用舉例來說明為什么要學習Java虛擬機,其實這個問題就和為什么要學習數據結構和算法是一個道理,工欲善其事,必先利其器。曾經的我經常害怕處理內存溢出的問題,因為不知道他為什么會出現這個問題,當我在看了這本書以后明白了垃圾回收算法,以及JVM是如何幫助我們處理GC的,這個時候當出現這個問題的時候我就明白需要查找GC Root,或者查看GC日志,去查找這個問題的根源,這樣就能處理這些問題。還有以前的在理解重載和重構的時候只是在表面去理解,當我看完這本書明白,原來在方法調用時候這些東西就生成處理,另外還有一個new到底經歷那些事情等等一序列問題,如果你還在就糾結一些問題為什么是這么處理的時候那你就去看Java虛擬機吧,或許會有不一樣的感悟,以上就是為什么要學習Java虛擬機的原因,可能有部分解釋的不是很全面,我想在感悟方面在仔細說一下這個問題。
二、感悟?
其實也算不上什么感悟,只是對一些問題認識更加深刻而已,這里面我們來談一下GC,要探討這個問題我們需要從4個方面入手:
1.JVM是如何分配內存的?
2.如何才能保證正確的回收?
3.JVM什么情況下觸發GC以及GC的方式?
4.如何監控和優化GC?
首先從JVM內存分布開始:下圖是JVM內存分布圖
1.線程計數器,是一塊較小的內存空間,用來指定當前線程執行字節碼的行數,每個線程計數器都是私有的,因為每個線程都需要記錄執行的行數;這里解釋一下為什么每個線程都需要一個線程計數器,JVM的多線程是通過線程輪流切換分配執行時間來實現的,在任何時刻,每個處理器都只會執行一個線程中的指令,當線程進行切換的時,為了線程能恢復當正確的位置,所以每個線程必須有個獨立的線程計數器,這樣才能保證線程之間不互相影響。
這里注意下,如果線程執行是一個Java方法的時候,計數器記錄的是虛擬機字節碼指令的地址;當執行的是Native的方法的時候,計數器指令為空;該內存區域是Java虛擬機唯一沒有規定任何OutOfMemoryError的區域。
2.Java虛擬棧,這個也是一個線程私有的,生命周期與線程是同步的,每個方法在執行的同時,都會創建一個棧幀,用於存儲局部變量表,操作數棧,動態鏈接,方法出入口等信息,每個方法的調用到執行完成的過程就是一個棧幀入棧到出棧的過程;
這里解釋一下局部變量表,局部變量表存儲方法相關的局部變量,包括基本數據,對象引用和返回地址等。在局部變量表中,只有long和double類型會占用2個局部變量空間(Slot,對於32位機器,一個Slot就是32個bit),其它都是1個Slot。需要注意的是,局部變量表是在編譯時就已經確定好的,方法運行所需要分配的空間在棧幀中是完全確定的,在方法的生命周期內都不會改變。這部分東西我還想等下一篇博客的時候我想仔細說一下字節碼的執行過程;
虛擬機棧規定了2種異常情況,一種是線程請求棧的深度大於虛擬機棧所允許的深度,這時候將會拋出StackOverflowError異常,如果當Java虛擬機允許動態擴展虛擬機棧的時候,當擴展的時候沒辦法分配到內存的時候就會報OutOfMemoryError異常;
3.本地方法棧,與虛擬機棧執行的基本相同,唯一的區別就是虛擬機棧是執行Java方法的,本地方法棧是執行native方法的;
4.Java堆,堆區是Java虛擬機所管理的內存中最大的一塊,Java堆是被所有線程共享的內存區域,主要存儲對象的實例。
當堆中沒有內存完成實例分配,並且堆無法擴展的時候,將會拋出OutOfMemoryError異常;當前虛擬機都是可以擴展的;
5.方法區,這個也是線程共享的內存區域,存儲被虛擬機加載的類信息、常量、靜態變量、即時編譯的代碼數據等;
方法區在物理上也是不需要連續的,可以選擇固定大小或者擴展的大小,還可以選擇不實現垃圾收集,方法區的垃圾回收是比較少的,這就是方法區為什么被稱為永久區的原因,但是方法區也是可以執行回收的,該區域主要是針對常量池和類型的卸載;在方法區也規定當方法區無法滿足內存分布的時候,將會拋出OutOfMemoryError異常;
運行時常量是方法區的一部分,常量池主要用於存放編譯生成的各種字面量和符合引用,由於常量池屬於方法區的一部分,所以當常量池沒有內存空間的時候就拋出OutOfMemoryError異常;
6.直接內存,不是虛擬機運行時的一部分,可以直接訪問堆外的內存;所以當內存空間無法動態擴展的時候就會出現OutOfMemoryError異常;
以上基本是JVM內存分布的內容,簡單的理解水滿則溢出就是這個道理,系統的整個空間是一個大的容器,分不同的部分或者桶去分擔整個容量,當那個桶不夠的時候自然會溢出。明白內存區域的分布我們看下對象是如何分配在內存空間里面的?
Java對象這里指的是引用類型的對象,這里用Student stu=new Student()為例子訪問,Student stu作為引用對象,存在與Java虛擬機棧上,new Student()保存在Java堆中,堆中記錄Student類型的信息包括方法,接口,對象類型等地址,這些類型的執行的數據存儲在方法區中;
這里需要說明一下對象訪問的方式,主要包括2種句柄訪問和直接指針訪問:
1. 句柄訪問主要是Java堆中划分一塊句柄池,虛擬機棧中存放句柄池中的地址,句柄池中包括對象的實例數據和對象類型的數據的地址,基本分布如下圖:
2.直接指針訪問,就是虛擬機棧直接指向Java堆中的對象類型指針和對象的實例數據,然后對象類型指針在指向方法區中對象類型的實例數據,分布如下圖:
HotSpot就是第二種訪問方式,優點在於訪問速度快,省去一次指針開銷時間,JVM內存分布基本介紹到這里,接下來說下如何保證正確回收?
回收是已經沒有用的對象,那怎么判斷一個對象沒用引用?這里需要簡單介紹2種方法:引用計數法和可達性分析算法;
這里簡單說一下引用計數法:對象中添加一個引用計數器,每當有一個地方引用計數器就增加1,引用失效就減少1,計數器為0就不可用;缺點就在於無法處理對象直接相互引用的問題,因為相互引用以后無法使計數器為0,所以無法回收;
可達性分析算法,也就是我們常說的GC Root,,當一個對象沒有與任何引用鏈相連的時候,就可以對該對象進行回收,下面是Java中GC Root對象使用的幾個地方:
以上對象簡單就是分為可用和不可用這2種,現在Java對引用概念進行擴充:
明白這些我們基本明白JVM如何正確回收,接下來就是JVM什么情況下觸發GC以及GC觸發的方式?
第一個問題比較容易回答當然是當內存空間不足的時候就需要觸發GC,GC回收的時候采用的是分代收集的算法,主要分為年輕代和老年代,接下來我們簡單介紹一下這2種方式:
年輕代:當一個對象被創建的時候,內存分配首先分配在年輕代,大部分對象創建以后都不再使用,對象很快變得不可達,就是對象無用,由於垃圾是被年輕代清理掉的,所以被叫做Minor GC或者Young GC。
老年代:對象如果在年輕代存活了足夠長的時間而沒有被清理掉(即在幾次Young GC后存活了下來),則會被復制到年老代,年老代的空間一般比年輕代大,能存放更多的對象,在年老代上發生的GC次數也比年輕代少。當年老代內存不足時,將執行Major GC,也叫 Full GC。
明白這2塊主要存放什么東西以后接下來我們看下GC的整體結構,看一個對象如何被Kill掉的流程:
1.當一個對象被創建的時候(new)首先會在年輕代的Eden區被創建,直到當GC的時候,根據可達性算法,看一個對象是否消亡,沒有消亡的對象會被放入年輕帶的Survivor區,消亡的直接被Minor GC Kill掉;
2.進入到Survivor區的對象也不是安全的,當下一次Minor GC來的時候還是會檢查Enden和Survivor存放對象區域中對象是否存活,存活放入另外一塊Survivor區域;
3.當2個Survivor區切換幾次以后,會直接進入老年代,當然進入到老年代也不是安全的,當老年代內存空間不足的時候,會觸發Major GC,已經消亡的依然還是被Kill掉;
推薦一個這個寫的很逗可以看下:http://blog.csdn.net/sd4015700/article/details/50109939
接下來我們還需要說一下GC的算法:標記--清除,復制,標記--整理這3種算法;
了解算法和GC內存分布以后我們接下來介紹垃圾回收器,這部分內容我不計划用文字去介紹,在第三個欄目我會將我對《深入理解Java虛擬機》這本書的思維導圖,內容還不是很完善我正在整理中,但是有GC這部分內容包括各種參數配置,大家可以下載下來具體了解一下;
最后我們談一下監控和優化,當年具備以上知識以后這些都將不是問題,所以工欲善其事必先利其器,這就是我要說的,剩下就是對工具操作,這些我認為不需要介紹也是可以的,當然我也推薦一個博客:http://blog.csdn.net/renfufei/article/details/56678064
三、結束語
思維導圖工具:XMind
百度雲盤地址:鏈接:https://pan.baidu.com/s/1ge3eE2Z 密碼:hox4 這部分我還沒有完善全,正在努力中,應該大概需要1周左右的時間;如果沒有百度雲盤可以加入我QQ群:438836709,可以一起溝通學習Java的經驗,群里面我也分享很多PDF的書籍,反正歡迎大家加入吧!當然我也會把這個文件完善以后上傳到QQ群里面!!