1. 程式人生 > >Java JVM再理解

Java JVM再理解

JVM簡介:

Java語言一個非常重要的特點平臺無關性主要是通過使用Java虛擬機器(JVM)來實現。JVM是指運用硬體或軟體手段實現虛擬的計算機。Java中使用JVM實現跨平臺的原理如下圖: 


                                                 

垃圾回收:(Garbage Collection——GC)

Java系統提供了垃圾收集器,用於自動檢查每一快分配出去的記憶體空間,然後將無價值的記憶體快自動回收。(物件不再被應用時自動進行垃圾回收

在Java語言中,判斷記憶體空間是否符合垃圾收集器的收集標準有2個:

  1. 為物件賦予了空值NULL後再沒有呼叫過
  2. 為物件賦予了新值,即重新分配了記憶體空間

因為垃圾回收機制的存在,使得Java程式中不存在delete函式、解構函式

(注:雖說Java中有GC的存在,但是也不能隨意的new太多物件,GC也是需要時間來回收的,new太多會導致來不及GC。)


(附加)JVM再理解

(1)JVM基本結構說明

  1. 類載入子系統與方法區:類載入子系統負責從檔案系統或者網路中載入Class資訊,載入的類資訊存放於一塊稱為方法區的記憶體空間。除了類的資訊外,方法區中可能還會存放執行時常量池資訊,包括字串字面量和數字常量(這部分常量資訊是Class檔案中常量池部分的記憶體對映)。
  2. java堆: java堆在虛擬機器啟動的時候建立,它是java程式最主要的記憶體工作區域。幾乎所有的java物件例項都存放在java堆中。堆空間是所有執行緒共享的,這是一塊與java應用密切相關的記憶體空間。
  3. 直接記憶體: java的NIO庫允許java程式使用直接記憶體。直接記憶體是在java堆外的、直接向系統申請的記憶體空間。通常訪問直接記憶體的速度會優於jawa 堆。因此出於效能的考慮,讀寫頻繁的場合可能會考態使用直接記憶體。由於直接記憶體在java堆外,因此它的大小不會直接受限於Xmx指定的最大堆大小,但是系統記憶體是有限的,java堆和直接記憶體的總和依然受限於作業系統能給出的最大記憶體。
  4. 垃圾回收系統:垃圾回收系統是java虛擬機器的重要組成部分,垃圾回收器可以對方法區、java堆和直接記憶體進行回收。其中,java堆是垃圾收集器的工作重點。和C/C++不同,java中所有的物件空間釋放都是隱式的,也就是說,java中沒有類似free()或者delete()這樣的函式釋放指定的記憶體區域。對於不再使用的垃圾物件,垃圾回收系統會在後臺默默工作,默默查詢、標識並釋放垃圾物件,完成包括java堆、方法區和直接記憶體中的全自動化管理。
  5. java:毎一個java虛擬機器執行緒都有-個私有的java桟, -個執行緒的java桟線上程建立的吋候被建立, Java桟中儲存著幀資訊,java桟中儲存著區域性変量、方法鯵數,同吋和java方法的呼叫、返回密切相關。
  6. 本地方法桟:本地方法桟和java 桟非常類似,最大的不同在於java桟用於方法的呼叫,而本地方法桟則用於本地方法的呼叫,作為対java虛擬機器的重要拓展, java虛擬機器允許java直接呼叫本地方法(通常使用C編寫)
  7. PC〈Program Counter): PC暫存器也是毎個銭程私有的空間,java虛擬機器會為毎一個java銭程建立pc暫存器.在任意肘刻,-個java銭程總是在執行一個方法,這個正在被執行的方法稱為當前方法.如果當前方法不是本地方法, PC暫存器就會指向當前正在被執行的指令。如果當前方法是本地方法,那麼PC暫存器的値就是undefined
  8. 執行引擎:執行引撃是java虛擬機器的最核心元件之一,它負責執行虛擬機器的位元組碼,現代虛擬機器為了提高執行效率,會使用即時編譯將方法編譯成機器碼再執行。

Java HotSpot Client VM(-Client), 為在客戶端環境下減小啟動時間而優化

Java HotSpot Server VM(-server)為在伺服器環境下最大化程式執行速度而設計

 

(2)JVM堆結構圖及分代

Java虛擬機器: JVM記憶體分代策略

Java虛擬機器根據物件存活的週期不同,把堆記憶體劃分為幾塊,一般分為新生代、老年代和永久代(對HotSpot虛擬機器而言),這就是JMM的記憶體分代策略。

為什麼要分代?

 堆記憶體是虛擬機器管理的記憶體中最大的一塊,也是垃圾回收最頻繁的一塊區域,我們程式所有的物件例項都存放在堆記憶體中.給堆記憶體分代是為了提高物件記憶體分配和垃圾回收的效率。試想一下,如果堆記憶體沒有區域劃分,所有的新建立的物件和生命週期很長的物件放在一起,隨著程式的執行,堆記憶體需要頻繁進行垃圾收集而每次回收都要遍歷所有的物件,遍歷這些物件所花費的時間代價是巨大的,會嚴重影響我們的GC效率,這簡直太可怕了。有了記憶體分代,情況就不同了,新建立的物件會在新生代中分配記憶體,經過多次回收仍然存活下來的物件存放在老年代中,靜態屬性、類資訊等存放在永久代中,新生代中的物件存活時間短,只需要在新生代區域中頻繁進行GC,老年代中物件生命週期長,記憶體回收的頻率相對較低,不需要頻繁進行回收,永久代中回收效果太差,一般不進行垃圾回收,還可以根據不同年代的特點採用合適的垃圾收集演算法。分代收集大大提升了收集效率,這些都是記憶體分代帶來的好處。

 記憶體分代劃分

  Java虛擬機器將堆記憶體劃分為新生代、老年代和永久代,永久代是HotSpot虛擬機器特有的概念它採用永久代的方式來實現方法區,其他的虛擬機器實現沒有這一概念而且HotSpot也有取消永久代的趨勢,在JDK 1.7中HotSpot已經開始了“去永久化”,把原本放在永久代的字串常量池移出。永久代主要存放常量、類資訊、靜態變數等資料,與垃圾回收關係不大,新生代和老年代是垃圾回收的主要區域。記憶體分代示意圖如下:

新生代(Young Generation)

  新生成的物件優先存放在新生代中,新生代物件朝生夕死,存活率很低,在新生代中,常規應用進行一次垃圾收集一般可以回收70%~ 95%的空間,回收效率很高。

  HolSpot將新生代劃分為三塊,一塊較大的 Eden空間和兩塊較小的Survivor空間,預設比例為8: 1: 1。劃分的目的是因為HotSpot採用複製演算法來回收新生代,設定這個比例是為了充分利用記憶體空間,減少浪費。新生成的物件在Eden區分配(大物件除外,大物件直接進入老年代),當Eden區沒有足夠的空間進行分配時,虛擬機器將發起一-次 MinorGC。GC開始時,物件只會存在於Eden區和From Survvor區,To Survivor區是空的(作為保留區域)。GC進行時,Eden區中所有存活的物件都會被複制到To Survvor區,而在FromSurvvor區中,仍存活的物件會根據它們的年齡值決定去向,年齡值達到年齡閥值(預設為15,新生代中的物件每熬過一輪垃圾回收,年齡值就加1,GC分代年齡存諸在物件的header中)的物件會被移到老年代中,沒有達到閥值的物件會被複制到To Survivor區。接看清空Eden區和From Surwvor區,新生代中存活的物件都在To Survivor區。接著, From Survvor區和To Survivor區會交換它們的角色,也就是新的To Survivor區就是上次GC清空的FromSurvivor區,新的From Survivor區就是上次GC的To Survlvor區,總之,不管怎樣都會保證To Suvvor區在一輪GC後是空的。GC時當To Survivar區沒有足夠的空間存放上一次新生代收集下來的存活物件時,需要依賴老年代進行分配擔保,將這些物件存放在老年代中。

老年代(Old Generationn)

在新生代中經歷了多次<具體看虛擬機器配置的閥值》GC後仍然存活下來的物件會進入老年代中。老年代中的物件生命週期較長,存活率比較高,在老年代中進行GC的頻率相對而言較低,而且回收的速度也比較慢。

永久代(Permanent Generationn)

永久代儲存類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料,對這一區域而言,Java虛擬機器規範指出可以不進行垃圾收集,一般而言不會進行垃圾回收。

(3)JVM垃圾回收演算法及收集器

1)垃圾回收常見演算法

引用計數( Reference Counting):

比較古老的回收演算法。原理是此物件有一個引用則增加一個計數,刪除一個引用則減少一個計數。垃圾回收時,只收集計數為0的物件。此演算法最致命的是無法處理迴圈引用的

複製( Copying):

此演算法把記憶體空間劃為兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的物件複製到另外一一個區域中。次演算法每次只處理正在使用中的物件,因此複製成本比較小,同時複製過去以後還能進行相應的記憶體整理,不會出現“碎片”問題。當然,此演算法的缺點也是很明顯的,就是需要兩倍記憶體空間。如下圖:

 

 

 

標記-清除(Mark-Sweep):

此演算法執行分兩個階段,第一階段從應用根節點開始標記所有被引用的物件,第二階段遍歷整個堆,把未標記的物件清楚。此演算法需要暫停整個應用,同時會產生記憶體碎片。如下圖:

 

標記-整理( Mark-Compact ) :

此演算法結合了“標記-清除”和“複製”兩個演算法的優點。也是分兩階段,第-階段從根節點開始標記所有被引用物件,第二階段遍歷整個堆,清除末標記物件並且把存活物件“壓縮”到堆的其中一塊,按順序排放。此演算法避免了“標記-清除” 的碎片問題,同時也避免了“複製”演算法的空問問題。如下圖:

(4)JVM中垃圾收集器

ScavengeGC (次收集)和Full GC的區別(全收集)

新生代GC (Scavenge GC) : Scavenge GC指發生在新生代的GC,因為新生代的Java物件大多都是朝生夕死;所以Scavenge GC非常頻繁,一般回收速度也比較快。當Eden空間不足以為物件分配記憶體時,會觸發Scavenge GC。一般情況下 ,當新物件生成,並且在Eden申請空間失敗時,就會觸發Scavenge GC,對Eden區域進行GC,清除非存活物件,並且把尚且存活的物件移動到Survivor區。然後整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。因為大部分物件都是從Eden區開始的,同時Eden區不會分配的很大,所以Eden區的GC會頻繁進行。因而,一般在這裡需要使用速度快、效率高的演算法,使Eden去能儘快空閒出

老年代GC (Full GC/MajorGC) : Full GC指發生在老年代的GC,出現了FullGC一般會伴隨著至少一次的Minor GC(老年代的物件大部分是Minor GC過程中從新生代進入老年代),比如:分配擔保失敗。Full GC的速度-般會比MinorGC慢10倍以上。當老年代記憶體不足或者顯式呼叫System.gc( )方法時,會觸發Full GC。

次收集   當年輕代堆空間緊張時會被觸發

相對於全收集而言,收集問隔較短

全收集    當老年代或者持久代堆空間滿了,會觸發全收集操作

可以使用System.gc( )方法來顯式的啟動全收集

  全收集-般根據堆大小的不同,需要的時間不盡相同,但一般會比較長。不過,如果全收集時間超過3到5秒鐘,那就太長了.

 

(5)分代垃圾回收器

年輕代

序列收集器(Serial):

並行收集器(ParNew):

 

Parallel Scavenge收集器:

  與ParNew類似Parallel Scavenge也是使用複製演算法,也是並行多執行緒收集器.但與其他收集器關注儘可能縮短垃圾收集時間不同,Parallel Scavenge更關注系統吞吐量系統吞吐量=執行使用者程式碼時間執行使用者程式碼時間+垃圾收集時間)停頓時間越短就越適用於使用者互動的程式良好的響應速度能提升使用者的體驗:而高吞吐量則適用於後臺運算而不需要太多互動的任務可以最高效率地利用CPU時間,儘快地完成程式的運算任務. ParallelScavenge提供瞭如下引數設定系統吞吐量:

 

老年代

Serial Old收集器:

Paraller Old收集器:

CMS收集器:

分割槽收集—G1收集器

 

分代垃圾回收總覽圖

 

(謝謝閱讀,歡迎評論)