對於Java程序員來講,虛擬機和多線程方面的知識是必不可少的。這裏就來聊一聊Java虛擬機的一些基礎和概念,主要內容源自《深刻理解Java虛擬機》這本書。html
首先爲何要有虛擬機呢?由於對象的建立和銷燬是一個很頻繁的操做,由程序員來維護,一方面成本有點高,增長開發成本;另外一方面,若是操做不當,發生了內存泄露,要本身去調試代碼,找出緣由。因此虛擬機的這種機制的誕生能夠說是程序員的福音,解放了生產力。但凡事有利也有弊,虛擬機的引入使得Java在性能方面跟C++比仍是有必定差距,像網絡遊戲或者數據庫這種對性能要求比較高的應用,都會選擇用C/C++來開發。同時,雖然咱們有了虛擬機,但仍是要對它的運行機制和性能調優有所瞭解,否則萬一發生java內存泄露,就會無從下手了。java
圖中的五大數據區域能夠分爲兩類:1.由全部線程共享的數據區域 2. 線程隔離的數據區程序員
第一類:方法區,堆算法
第二類:虛擬機棧,本地方法棧,程序計數器數據庫
(圖片源自博客http://blog.sina.com.cn/s/blog_ed30769e0102v233.html)網絡
能夠把它看做是當前線程所執行的字節碼的行號指示器。咱們經過改變這個計數器的值來選取下一個須要執行的字節碼指令。每一個線程都有一個獨立的程序計數器。若是線程正在執行的是一個java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是native方法,這個計數器值爲空。此內存區域是惟一一個在java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。多線程
它描述的是Java方法執行的內存模型:每一個方法被執行都會建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。併發
局部變量表存放了編譯期可知的各類基本數據類型、對象引用和returnAddress類型(指向一條字節碼指令的地址)。局部變量表所需的內存空間是在編譯期間完成分配的。函數
這個區域規定了兩種異常:佈局
1.若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError異常。
2.內存動態擴展時,若沒法申請到足夠的內存時會拋出OutOfMemoryError異常。
此方法區與Java虛擬機棧相似,區別在於它是爲虛擬機使用到的Native方法服務。有的虛擬機(如Sun HotSpot虛擬機)直接把它們合二爲一了。
與虛擬機棧同樣,它也會拋出 StackOverflowError和OutOfMemoryError異常。
全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例。
能夠經過-Xmx和-Xms的設置來控制堆內存大小。
若是堆沒法擴展時,將會拋出OutOfMemoryError異常。
它也是各個線程共享的內存區域,用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
Java虛擬機對這個區域的限制很是寬鬆。垃圾收集行爲在這個區域是比較少出現的;這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載。
當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError異常。
它是方法區的一部分; 用於存放編譯期間生成的各類字面量和符號引用,這部份內容將在類加載後存放到方法區的運行時常量池中。
運行期間也可能將新的常量放入池中,好比說使用String類的intern()方法。
沒法申請到內存時會拋出OutOfMemoryError異常。
直接內存並非虛擬機運行時數據區的一部分,也不是虛擬機規範中的內存區域,但這部份內存被頻繁地使用,也可能會致使OutOfMemoryError異常出現。
在JDK1.4中加入了NIO,引入了一種基於通道與緩衝區的I/O方式,它可使用Native函數庫直接分配堆外內存,而後經過存儲在堆裏面的DirectByteBuffer對象做爲這塊內存的引用操做。
Object obj = new Object();
虛擬機棧:會在本地變量表中存儲一個對象引用
堆:存儲了Object類型全部實例數據值
方法區:存儲對象的類信息
不一樣的虛擬機的對象訪問方式會有所不一樣,主流的訪問方式有兩種:使用句柄和直接指針。
堆會劃分出一塊內存來做爲句柄池,引用中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據和類型數據各自的具體地址信息
好處:引用中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而引用自己不須要修改。
堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,引用中直接存儲的就是對象地址。
好處:速度更快,節省了一次指針定位的時間開銷。
堆溢出:經過-XX:+HeapDumpOnOutOfMemoryError能夠在出現內存溢出時Dump出當前的內存堆轉儲快照以便過後進行分析。
虛擬機棧和本地方法棧溢出:經過-Xss設置棧容量。通常棧深度能夠到達1000-2000。棧容量過大,多線程時容易耗盡內存,由於單個線程耗內存比較多。
運行時常量池溢出:常見的PermGen issue。Java8對方法區作了調整,因此這個問題不會再出現了。
方法區溢出:每每出現於動態生成大量Class的應用中。
本機直接內存溢出:可經過-XX:MaxDirectMemorySize指定,若是不指定,則默認與堆的最大值同樣。
如何肯定對象已經死去,能夠採起回收了呢?
很是直觀的一種方法,增長一處引用時加一,減小一處引用時減一。
但它的一個主要缺陷是很難解決對象之間的循環依賴問題。
基本思路:經過一系列GC roots對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈。當一個對象到GC roots沒有任何引用鏈相連時,則證實此對象是不可達的,做爲回收對象。
GC roots對象包括下面幾種:
1.虛擬機棧(棧幀中的本地變量表)中的引用的對象
2.方法區中的類靜態屬性引用的對象
3.方法區中的常量引用的對象
4.本地方法棧中JNI的引用的對象
在JDK1.2以後,Java擴充了引用的概念,分爲四種:
1.強引用(strong reference):通常引用,有引用,不會回收對象。
2.軟引用(soft reference):系統將要發生溢出時,會把它們引用的對象列入回收範圍進行一次回收。若內存仍是不夠,才拋異常。
3.弱引用(weak reference):被引用的對象只能活到下一次垃圾收集發生以前。
4.虛引用(phantom reference):一個對象是否有虛引用,徹底不會對其生存時間構成影響,也沒法經過虛引用取得一個對象實例。設置此引用得目標就是爲了能在這個對象被回收時收到一個系統通知。
若是一個對象在根搜索後,沒有關聯得引用鏈,它將會被第一次標記而且進行一次篩選,篩選得條件是此對象是否有必要執行finalize()方法。
若是沒有覆蓋此方法或者方法已被虛擬機調用過,則視爲「沒有必要執行」。
若是須要執行finalize()方法,則會被放置到一個F-Quenue隊列,由低優先級得Finalizer線程去執行。執行此方法,但並不保證運行結束(爲了運行效率考慮)。
稍後會對F-Queue進行第二次小規模得標記,只要在finalize方法中與引用鏈上得任意一個對象產生關聯就會在這次標記時被移除「即將回收」的集合。
在堆中,尤爲是新生代中,一次垃圾回收能夠回收70%~95%的空間,而方法區的垃圾收集效率遠低於此。
方法區的垃圾回收主要內容:廢棄常量和無用的類。
類須要知足下面3個條件才能算是「無用的類」,能夠進行回收:
1.該類全部的實例已經被回收
2.加載該類的ClassLoader已經被回收
3.該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法
是否對類進行回收,由參數-Xnoclassgc進行控制
算法分爲標記和清除兩個階段:首先標記出全部須要回收的對象,在標記完成後統一回收掉全部被標記的對象。
缺點:
1.效率問題,標記和清除過程的效率都不高
2.空間問題,會產生大量不連續的內存碎片。若是發現不能分配內存給大對象時,不得再也不觸發一次垃圾回收
將可以使用的內存按容量分爲兩塊,每次只使用其中的一塊。
通過實驗發現,新生代中的對象98%是朝生夕死的,因此內存能夠分配爲一塊較大的Eden空間和兩塊較小的Survivor空間。每次使用Eden和其中一塊Survivor空間;回收內存時,將還存活的對象拷貝到另一塊Survivor空間上,而後清理掉另外兩塊的空間。
Sun HotSpot虛擬機默認Eden和Survivor的大小比例是8:1。當Survivor空間不夠時,須要依賴老年代進行分配擔保,如有擔保直接分配進老年代。
複製算法在對象存活率較高時就要執行較多的複製操做,效率將會變低。因此老年代通常不能直接選用這種算法。
標記-整理算法在標記後,讓全部存活的對象向一端移動,而後直接清理掉端邊界之外的內存。
歷史最悠久的收集器,在JDK1.3.1以前是虛擬機新生代收集惟一的選擇。單線程的收集器。
優勢:簡單高效
缺點:收集時必須暫停其餘全部的工做線程(stop the world)。
使用場景:虛擬機運行在Client模式下默認新生代收集器。
它是Serial收集器的多線程版本。默認開啓的收集線程數與CPU的數量相同。JDK1.4引入。
除了Serial收集器外,只有它能與CMS收集器配合工做。
使用場景:運行在Server模式下的虛擬機首選的新生代收集器。
目標:達到一個可控制的吞吐量。所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值。適合在後臺運算而不須要太多交互的任務。
-XX:MaxGCPauseMillis 控制最大垃圾收集停頓時間
-XX:GCTimeRatio用來設置吞吐量大小,默認99
-XX:+UserAdaptiveSizePolicy, 開啓GC自適應的調節策略。
使用場景:新生代收集器
它是Serial收集器的老年代版本,使用標記整理算法。
使用場景:Client模式下的老年代。
它是Parallel Scavenge收集器的老年代版本。JDK1.6引入。
注重吞吐率的場合,能夠考慮採用Parallel Scavenge加Parallel Old收集器。
它是一種以獲取最短回收停頓時間爲目標的收集器,併發執行。標記清除算法:初始標記,併發標記,從新標記,併發清除。其中初始標記和從新標記仍需stop the world。
缺點:
1.對CPU資源很是敏感。默認回收線程數是(CPU數量+3)/ 4。
2.沒法處理浮動垃圾,可能出現「concurrent mode failure」失敗而致使另外一次full gc的產生。要是在運行收集期間,預留的內存沒法知足程序須要,就會出現「concurrent mode failure」失敗,這時會臨時採用serial old收集器來進行老年代的垃圾收集,這樣停頓時間會變長。
3.收集結束時會產生大量碎片,容易因沒法分配大對象,而觸發full gc。能夠啓用參數-XX:+UseCMSCompactAtFullCollection來享受full gc後來一次碎片整理。另外有-XX:CMSFullGCsBeforeCompaction這個參數來設置執行了多少次不壓縮的full gc後,進行一次帶壓縮的。
目標是替代CMS收集器。能夠分代收集,不會產生碎片。有分區(region)的概念,優先回收垃圾最多的區域。經過remenbered set來避免全堆掃描。
收集可分爲四個步驟:初始標記,併發標記,最終標記,篩選回收。
當Eden區沒有足夠空間時,會觸發一次minor gc。虛擬機提供-XX:+PrintGCDetails這個參數來打印內存回收日誌以及進程退出時當前內存各區域的分配狀況。
這樣作的目的時避免Eden區裏面發生大量的內存拷貝。可經過-XX:PretenureSizeThreshold參數設置多大的對象進入老年代。
熬過15次minor gc後,到達15歲,就會被晉升到老年代。關於這個年齡,能夠經過-XX:MaxTenuringThreshold來設置。
爲了適應不一樣程序的內存狀況,若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代。
在發生minor gc時,虛擬機會檢測以前每次晉升到老年代的平均大小是否大於老年代的剩餘空間大小。
若是大於,則改成進行一次 full gc。
若是小於,先查看HandlePromotionFailure設置(JDK1.6默認開啓)是否容許擔保失敗。若是容許,進行minor gc;若是不容許,仍是要 full gc。
若是最後發生擔保失敗,仍是要從新發起一次full gc。