JVM是Java Virtual Machine(Java 虛擬機)的縮寫,JVM是一種用於計算設備的規範,它是一個虛構出來的計算機,是經過在實際的計算機上仿真模擬各類計算機功能來實現的。java
Java語言的一個很是重要的特色就是平臺無關性。而使用Java虛擬機是實現這一特色的關鍵。通常的高級語言若是要在不一樣的平臺上運行,至少須要編譯成不一樣的目標代碼。而引入Java語言虛擬機後,Java語言在不一樣平臺上運行時不須要從新編譯。Java語言使用Java虛擬機屏蔽了與具體平臺相關的信息,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就能夠在多種平臺上不加修改地運行。Java虛擬機在執行字節碼時,把字節碼解釋成具體平臺上的機器指令執行。這就是Java的可以「一次編譯,處處運行」的緣由。算法
JVM整體上是由類裝載子系統(ClassLoader)、運行時數據區、執行引擎、垃圾收集這四個部分組成。其中咱們最爲關注的運行時數據區,也就是JVM的內存部分則是由方法區(Method Area)、JAVA堆(Java Heap)、虛擬機棧(JVM Stack)、程序計數器、本地方法棧(Native Method Stack)這幾部分組成。數組
Class Loader類加載器負責加載.class文件,class文件在文件開頭有特定的文件標示,而且ClassLoader負責class文件的加載等,至於它是否能夠運行,則由Execution Engine決定。緩存
棧管運行,堆管存儲。JVM調優主要是優化Java堆和方法區。多線程
方法區是各線程共享的內存區域,它用於存儲已被JVM加載的類信息、常量、靜態變量、運行時常量池等數據。併發
Java堆是各線程共享的內存區域,在JVM啓動時建立,這塊區域是JVM中最大的, 用於存儲應用的對象和數組,也是GC主要的回收區,一個 JVM 實例只存在一個堆內存,堆內存的大小是能夠調節的。類加載器讀取了類文件後,須要把類、方法、常變量放到堆內存中,以方便執行器執行,堆內存分爲三部分:新生代、老年代、永久代。函數
說明:優化
Jdk1.6及以前:常量池分配在永久代 。spa
Jdk1.7:有,但已經逐步「去永久代」 。線程
Jdk1.8及以後:無永久代,改用元空間代替(java.lang.OutOfMemoryError: PermGen space,這種錯誤將不會出如今JDK1.8中)。
Java棧是線程私有的,是在線程建立時建立,它的生命期是跟隨線程的生命期,線程結束棧內存也就釋放,對於棧來講不存在垃圾回收問題,只要線程一結束該棧就Over,生命週期和線程一致。基本類型的變量和對象的引用變量都是在函數的棧內存中分配。
每一個方法執行的時候都會建立一個棧幀,棧幀中主要存儲3類數據:
局部變量表:輸入參數和輸出參數以及方法內的變量;
棧操做:記錄出棧和入棧的操做;
棧幀數據:包括類文件、方法等等。
棧中的數據都是以棧幀的格式存在,棧幀是一個內存區塊,是一個數據集,是一個有關方法和運行期數據的數據集。每個方法被調用直至執行完成的過程,就對應着一個棧幀在棧中從入棧到出棧的過程。
本地方法棧和JVM棧發揮的做用很是類似,也是線程私有的,區別是JVM棧爲JVM執行Java方法(也就是字節碼)服務,而本地方法棧爲JVM使用到的Native方法服務。它的具體作法是在本地方法棧中登記native方法,在執行引擎執行時加載Native Liberies.有的虛擬機(好比Sun Hotpot)直接把二者合二爲一。
程序計數器是一塊很是小的內存空間,幾乎能夠忽略不計,每一個線程都有一個程序計算器,是線程私有的,能夠看做是當前線程所執行的字節碼的行號指示器,指向方法區中的方法字節碼(下一個將要執行的指令代碼),由執行引擎讀取下一條指令。
運行時常量池是方法區的一部分,用於存放編譯器生成的各類字面量和符號引用,這部份內容將在類加載後存放到方法區的運行時常量池中。相較於Class文件常量池,運行時常量池更具動態性,在運行期間也能夠將新的變量放入常量池中,而不是必定要在編譯時肯定的常量才能放入。最主要的運用即是String類的intern()方法。
執行引擎執行包在裝載類的方法中的指令,也就是方法。執行引擎以指令爲單位讀取Java字節碼。它就像一個CPU同樣,一條一條地執行機器指令。每一個字節碼指令都由一個1字節的操做碼和附加的操做數組成。執行引擎取得一個操做碼,而後根據操做數來執行任務,完成後就繼續執行下一條操做碼。
不過Java字節碼是用一種人類能夠讀懂的語言編寫的,而不是用機器能夠直接執行的語言。所以,執行引擎必須把字節碼轉換成能夠直接被JVM執行的語言。字節碼能夠經過如下兩種方式轉換成合適的語言:
解釋器: 一條一條地讀取,解釋並執行字節碼執行,因此它能夠很快地解釋字節碼,可是執行起來會比較慢。這是解釋執行語言的一個缺點。
即時編譯器:用來彌補解釋器的缺點,執行引擎首先按照解釋執行的方式來執行,而後在合適的時候,即時編譯器把整段字節碼編譯成本地代碼。而後,執行引擎就沒有必要再去解釋執行方法了,它能夠直接經過本地代碼去執行。執行本地代碼比一條一條進行解釋執行的速度快不少,編譯後的代碼能夠執行的很快,由於本地代碼是保存在緩存裏的。
垃圾收集即垃圾回收,簡單的說垃圾回收就是回收內存中再也不使用的對象。所謂使用中的對象(已引用對象),指的是程序中有指針指向的對象;而未使用中的對象(未引用對象),則沒有被任何指針給指向,所以佔用的內存也能夠被回收掉。
垃圾回收的基本步驟分兩步:
查找內存中再也不使用的對象(GC判斷策略)
釋放這些對象佔用的內存(GC收集算法)
引用計數算法是給對象添加一個引用計數器,每當有一個引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器都爲0的對象就是不可能再被使用的對象。缺點:很難解決對象之間相互循環引用的問題。
根搜索算法的基本思路就是經過一系列名爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(也就是說從GC Roots到這個對象不可達)時,則證實此對象是不可用的。
在Java語言裏,可做爲GC Roots的對象包括如下幾種:
虛擬機棧(棧幀中的本地變量表)中引用的對象;
方法區中類靜態屬性引用的對象;
方法區中常量應用的對象;
本地方法棧中JNI(Native方法)引用的對象。
注:在根搜索算法中不可達的對象,也並不是是「非死不可」的,由於要真正宣告一個對象死亡,至少要經歷兩次標記過程:第一次是標記沒有與GC Roots相鏈接的引用鏈;第二次是GC對在F-Queue執行隊列中的對象進行的小規模標記(對象須要覆蓋finalize()方法且沒被調用過)。
標記-清楚算法採用從根集合(GC Roots)進行掃描,首先標記出全部須要回收的對象(根搜索算法),標記完成後統一回收掉全部被標記的對象。
該算法有兩個問題:
效率問題:標記和清除過程的效率都不高;
空間問題:標記清除後會產生大量不連續的內存碎片, 空間碎片太多可能會致使在運行過程當中須要分配較大對象時沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集。
複製算法是將可用內存按容量劃分爲大小相等的兩塊, 每次只用其中一塊, 當這一塊的內存用完, 就將還存活的對象複製到另一塊上面, 而後把已使用過的內存空間一次清理掉。
標記整理算法的標記過程與標記清除算法相同, 但後續步驟再也不對可回收對象直接清理, 而是讓全部存活的對象都向一端移動,而後清理掉端邊界之外的內存。
分代收集算法是目前大部分JVM的垃圾收集器採用的算法。它的核心思想是根據對象存活的生命週期將內存劃分爲若干個不一樣的區域。通常狀況下將堆區劃分爲老年代(Tenured Generation)和新生代(Young Generation),在堆區以外還有一個代就是永久代(Permanet Generation)。老年代的特色是每次垃圾收集時只有少許對象須要被回收,而新生代的特色是每次垃圾回收時都有大量的對象須要被回收,那麼就能夠根據不一樣代的特色採起最適合的收集算法。
新生代(Young Generation)的回收算法(以複製算法爲主)
全部新生成的對象首先都是放在年輕代的。年輕代的目標就是儘量快速的收集掉那些生命週期短的對象。
新生代內存按照8:1:1的比例分爲一個eden區和兩個survivor(survivor0,survivor1)區。一個Eden區,兩個 Survivor區(通常而言)。大部分對象在Eden區中生成。回收時先將eden區存活對象複製到一個survivor0區,而後清空eden區,當這個survivor0區也存放滿了時,則將eden區和survivor0區存活對象複製到另外一個survivor1區,而後清空eden和這個survivor0區,此時survivor0區是空的,而後將survivor0區和survivor1區交換,即保持survivor1區爲空, 如此往復。
當survivor1區不足以存放 eden和survivor0的存活對象時,就將存活對象直接存放到老年代。如果老年代也滿了就會觸發一次Full GC(Major GC),也就是新生代、老年代都進行回收。
新生代發生的GC也叫作Minor GC,MinorGC發生頻率比較高(不必定等Eden區滿了才觸發)。
老年代(Tenured Generation)的回收算法(以標記-清除、標記-整理爲主)
在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到老年代中。所以,能夠認爲老年代中存放的都是一些生命週期較長的對象。
內存比新生代也大不少(大概比例是1:2),當老年代內存滿時觸發Major GC即Full GC,Full GC發生頻率比較低,老年代對象存活時間比較長,存活率標記高。
永久代(Permanet Generation)的回收算法
用於存放靜態文件,如Java類、方法等。永久代對垃圾回收沒有顯著影響,可是有些應用可能動態生成或者調用一些class,例如Hibernate 等,在這種時候須要設置一個比較大的永久代空間來存放這些運行過程當中新增的類。永久代也稱方法區。方法區主要回收的內容有:廢棄常量和無用的類。對於廢棄常量也可經過根搜索算法來判斷,可是對於無用的類則須要同時知足下面3個條件:
該類全部的實例都已經被回收,也就是Java堆中不存在該類的任何實例;
加載該類的ClassLoader已經被回收;
該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。
新生代單線程收集器,標記和清理都是單線程,優勢是簡單高效。是client級別默認的GC方式,能夠經過-XX:+UseSerialGC來強制指定。
老年代單線程收集器,Serial收集器的老年代版本。
新生代多線程收集器,其實就是Serial收集器的多線程版本,在多核CPU環境下有着比Serial更好的表現。
新生代並行的多線程收集器,追求高吞吐量,高效利用CPU。吞吐量通常爲99%, 吞吐量= 用戶線程時間/(用戶線程時間+GC線程時間)。適合後臺應用等對交互相應要求不高的場景。是server級別默認採用的GC方式,可用-XX:+UseParallelGC來強制指定,用-XX:ParallelGCThreads=4來指定線程數。
老年代並行的多線程收集器,Parallel Scavenge收集器的老年代版本,並行收集器,吞吐量優先。
CMS收集器是一種以獲取最短回收停頓時間爲目標的收集器,CMS收集器是基於「標記--清除」(Mark-Sweep)算法實現的,整個過程分爲四個步驟:
初始標記: 標記GC Roots能直接關聯到的對象,速度很快;
併發標記: 進行GC Roots Tracing的過程;
從新標記: 修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但比並發標記時間短;
併發清除: 整個過程當中耗時最長的併發標記和併發清除過程收集器線程均可以與用戶線程一塊兒工做,因此,從整體上來講,CMS收集器的內存回收過程是與用戶線程一塊兒併發執行的。
優勢:併發收集、低停頓
缺點:對CPU資源很是敏感、沒法處理浮動垃圾、產生大量空間碎片。
G1是一款面向服務端應用的垃圾收集器,是基於「標記-整理」算法實現的,與其餘GC收集器相比,G1具有以下特色:
並行與併發
分代收集
空間整合
可預測性的停頓
G1運做步驟:
初始標記(stop the world事件,CPU停頓只處理垃圾)
併發標記(與用戶線程併發執行)
最終標記(stop the world事件,CPU停頓處理垃圾)
篩選回收(stop the world事件,根據用戶指望的GC停頓時間回收)
做者:郭曉利
來源:宜信技術學院