作JAVA也有接近2年的時間了,公司的leader說,作JAVA,三年是個坎,若是過了三年你尚未去研究JVM的話,那麼你這個程序員只能是板磚的工具了。恰逢辭職,來個JVM的解析可好?java
JVM是Java Virtual Machine(Java虛擬機)的縮寫,也就是指的JVM虛擬機,屬因而一種虛構出來的計算機,在咱們實際的電腦上來進行模擬各類計算機的功能的這麼個東西。程序員
由於有了JVM的存在,搞JAVA的再也不須要去關心何時去釋放內存,也不會像C++程序員那樣爲了一點點內存而惆悵,對就是你,JVM虛擬機幫你把這些東西都完成了,那麼咱們來講說JAVA的JVM吧!面試
咱們先來看看JVM的模型吧,以前在百度上看文檔,上面就說了幾個,方法區,堆,棧,計數器。沒了,很難受,因而看了深刻理解JVM的書,也算是有點體會。算法
在深刻理解JVM一書中提到,JVM運行時的數據區域會劃分爲幾個不一樣的區域,有方法區(Method Area),虛擬機棧(VM Stack),本地方法棧(Native Method Stack),堆(heap),程序計數器(Program Counter Register),下面就是書中的圖:多線程
我們一個一個來解釋,工具
先說程序計數器(Program Counter Register):程序計數器實際上就是用於存放下一條指令所在地址的地方,當咱們執行一條指令的時候,要先知道他存放的指令位置,而後把指令帶到寄存器上這是就是獲取指令,而後程序計數器中的存貯地址會加1,而後這樣子循環的去執行,url
並且程序計數器這個小的內存區是「線程私有的內存」。爲何會是私有的呢?,在深刻理解JVM一書中說的是虛擬機的多線程經過線程的輪流切換來切換分配處理器的執行時間的方式來實現,提及來其實很拗口的,其實也就是說一個處理器,同一個時刻,只會執行一個線程的指令,可是時間可能不均衡,可能第一分鐘在a線程,第二分鐘就去執行b線程了,可是呢,爲了保證切換回來還須要是一致的,那麼每一個線程中就會有一個獨立存在的程序計數器,獨立來存貯,爲了保證不影響。因此他是一個「線程私有的內存」。線程
程序計數器還有幾個特色:3d
1.若是線程正在執行的是Java 方法,則這個計數器記錄的是正在執行的虛擬機字節碼指令地址。對象
2.若是正在執行的是Native 方法,則這個計數器值爲空(Undefined)
3.此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域、
分別解釋一下這三句話吧,這是深刻理解java虛擬機中的原話,第一句好像已經很直白了,沒的說,來講說第二句話吧
由於這個計數器記錄的是字節碼指令地址,可是Native(本地方法);就好比說(System.currentTimeMillis())他是經過C來實現,直接經過系統就能直接調用了不須要去編譯成須要執行的字節碼指令的話,那麼就至關於不過程序計數器,它沒有記錄的話,那他的計數器的值就確定爲空了。
第三句話 咱們能夠試試編譯一小段代碼,而後反編譯出來看看
也就是其實是這個樣子的
public class Test{ public int test(){ int a = 10; //0 ...... int b = 20; //3....... int c = 30; //6...... return (a+b)*c; //11.... 13.... 14...執行加減乘除操做 } }
上面的0,2,3,5,6,8....就是指令的偏移地址bipush就是入棧指令, 在執行到test方法的時候,線程就會建立對應的程序計數器在計數器中放0,2,3,5,6,8....這些指令地址,因此計數器裏改變的不是內存的大小,它也就沒有溢出了。
下面咱們再來講一下:JAVA虛擬機棧(VM Stack)
線程私有,生命週期和線程同樣,這個虛擬機棧描述的是JAVA方法執行的內存模型,用於存局部變量,操做數棧,方法出口等信息的,上面那個bipush就是入棧指令,在這裏最須要注意的就是他存放的是什麼數據.局部變量裏面放的就是那些咱們所知道的基本的數據類型,對象引用的話那就是一個地址。
在虛擬機規範裏面還說,他的2個異常情況,
1.一個是StackOverflowError異常,棧內存溢出,這確定很容易理解,就是棧的內存不夠,你的請求線程太大。(固定長度的棧)
2.若是說在動態擴展的過程當中,申請的長度仍是不夠,那麼會拋出另一個異常OutOfMemoryError異常。
3.本地方法棧(Native Method Stack)
它和虛擬機棧很相似,區別就在於虛擬機棧執行的是JAVA方法,可是本地方法棧則是Native方法,其餘的沒啥不一樣就連拋出異常都同樣的,
4.JAVA堆(heap)
在JVM一書中也有提到,Heap是在JAVA虛擬機中內存佔用最大的一個地方,也是全部線程共享的一個內存區域,堆內存中主要就是用於存放對象實例的。
幾乎是全部的對象實例都在這裏分配內存,JAVA堆是垃圾收集器管理的主要區域,那麼如今重點來了,面試中問到最多的垃圾回收機制接下來就要仔細說說了。
內存回收,如今都是進行的分代算法,堆中也是,新生代,老年代,並且兩種垃圾回收機制是採用的不一樣的回收機制的,在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用"標記-清理"或"標記-壓縮"算法來進行回收,說回收機制先看看heap的分區(這個from和to 並非絕對的,看對象處在哪一個位置,GC的次數不同以後,那from和to會有相應轉變)
分區一目瞭然,下面研究一下算法實現吧
Minor GC:GC新生代,
Full GC:老年代GC,
由於新生代中對象的存活率比較低,因此通常採用複製算法,老年代的存活率通常比較高,通常使用」標記-清理」或者」標記-整理」算法進行回收。
看了有幾天才明白啥意思,我說說我本身的看法吧,仍是畫圖吧,
Minor GC:
咱們每次new對象的時候都會先在新生代的Enden區放着也就是最開始 是這樣子的
而後在Enden用完的時候裏面會出現待回收的
而後就來了把存活的對象複製放到Survior1(from)中,待回收的等待給他回收掉 就是這樣的
而後把Enden區清空回收掉
這樣的話 第一次GC就完成了,下面再往下走
當Enden充滿的時候就會再次GC
先是這個樣子的
而後會把 Enden和Survoir1中的內容複製到Survior中,
而後就會把Enden和Survior進行回收
而後從Enden中過去的就至關於次數少的,而從Survior1中過去的就至關於移動了2次
這樣新生代的GC就執行了2次了,當Enden再次被使用完成的時候,就會從Survior2複製到Survior1中,接下來是連圖
通過回收以後Surior1就變了,1對象是從Enden直接複製過來的,2對象是Enden-->Survior2-->Survior1 ,3對象則是從Enden-->Surivior1-->Survior2-->Survior1 複製過來的,這樣一步一步的執行下去的時候,就是新生代的GC。
既然這樣,那爲何還會存在老年代呢?其實若是GC在執行的時候有些對象一直沒有被回收,那麼他移動次數就會無限的累計,每次從Surior(from)到Surior(to)的過程當中就至關於又增長了一次移動,當他達到必定的次數的時候(默認是15),就會移動到老年代裏了,因此不存在不會被回收的對象,可是這個次數能夠設置的,
-XX:MaxTenuringThreshold
就相似這樣子
其實上邊的這只是一種狀況,還有就是若是對象太大,存不下,那就直接會進入老年代。
還有那種默認就是長期活着的也會進入老年代,
並且這種複製算法的垃圾回收機制是比較浪費內存的,每次都會有一塊內存區是閒着不幹活的,可是優勢很明顯,簡單高效
以上就是GC中垃圾回收中的新生代複製算法解析,新生代的Minor GC也算是知道了很多東西了,以上就是一些我的的看法,圖比較清晰,容易理解,有不對的地方但願可以各位同行指點一下。
精彩回顧:
面試點:Java 中 hashCode() 和 equals() 的關係
強烈推薦:
《Java 極客技術》知識星球限時優惠,如今加入只需 50 元,僅限前 1000 名,機不可失時再也不來。趁早行動吧!
https://t.zsxq.com/J6Em2nU
隆重介紹:
Java 極客技術公衆號,是由一羣熱愛 Java 開發的技術人組建成立,專一分享原創、高質量的 Java 文章。若是您以爲咱們的文章還不錯,請幫忙讚揚、在看、轉發支持,鼓勵咱們分享出更好的文章。