JVM內存管理機制


java內存區域與內存溢出異常

  • 程序計數器
    1. 程序計數器是一塊較小的內存空間,它可以看做是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型里,字節碼解釋器工作時就是通過改變這個計數器的值來選擇下一條需要執行的字節碼指令。
    2. 由於java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的。在任何時刻,一個處理器都只會執行一條線程中的指令。為了線程切換后能回到正確的執行位置,每條線程都需要有一個獨立的線程計數器,獨立存儲,我們稱這類內存區域為“線程私有”的內存。
    3. 如果線程正在執行的是一個java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址。如果正在執行的是Native方法,這個計數器值則為空。
  • java虛擬機棧
    1. 和程序計數器一樣,java虛擬機棧也是線程私有的,他的生命周期與線程相同。
    2. 它描述的是java方法執行的內存模型,每個方法在執行的的同時都會創建一個棧幀,用於存儲局部變量表(含有基本數據類型和引用數據類型),操作數幀,每一個方法從調用至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出站的過程。
    3. 局部變量表所需的內存空間在編譯期間完成分配,在方法運行期間不會改變局部變量表的大小。
    4. 在java虛擬機規范中,對這個區域規定了兩種異常狀況。如果線程請求的棧深度大於虛擬機所允許的深度,則拋出StackOverflowError異常。如果虛擬機棧可以動態擴展,擴展時無法申請到足夠的內存,則拋出OutOfMemoryError異常。
  • 本地方法棧
    與虛擬機棧所發揮的作用是相似的,區別是虛擬機棧執行的java方法(字節碼)服務,而本地方法棧執行的為虛擬機使用到的Native方法服務。

    1. java堆是java虛擬機所管理的內存中最大的一塊,是被所有線程共享的一塊內存區域。在虛擬機啟動時創建,此區域的唯一目的就是存放對象實例和數組。
    2. java堆是垃圾收集器管理的主要區域。因此,很多年時候稱之為GC堆,
    3. 根據java虛擬機規范的規定:java堆可以處於物理上不連續的內存空間,只要邏輯上是連續的即可。如果在堆中沒有內存完成實例分配。並且堆也無法再擴展,則會拋出OutOfMemoryError異常。
  • 方法區
    1. 也是各個線程共享的內存區域,用於存儲已被虛擬機加載的類信息,常量。靜態變量,即編譯器編譯后的代碼數據,
    2. 運行時常量池是方法區的一部分,具有動態性,java語言並不要求一定只有編譯期才能產生,運行期間也可能將新的常量放入池中。
  • 直接內存
    1. 它並不是虛擬機運行時數據區的一部分,也不是java虛擬機規范中的內存區域,但是卻被頻繁的使用
    2. JDK1.4中新加入NIO類,引入一種基於通道與緩沖區的I/O方式,它可以使用Native函數庫直接分配堆外內存,然后通過一個存儲在java堆中的DirectByteBuffer對象作為這個內存的引用進行操作。
    3. 既然是內存,雖然不受java堆大小的限制,肯定受本機總內存大小以及處理器尋址空間的限制。服務器管理員在配置虛擬機參數時,會根據實際內存設置-Xmx等參數信息,經常忽略直接內存,使得各個內存區域綜合大於物理內存限制,從而導致動態擴展時出現OutOfMemberError異常。

HotSpot虛擬機對象探索

  • 對象的創建
    1. 當虛擬機遇到一條new指令時,首先去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被加載,解析,初始化。如果沒有,那必須先執行相應的類加載過程。
    2. 為新生對象分配內存,所需大小在類加載完成后便可以完全確定,為對象分配空間的任務等同於把一塊確定大小的內存從java堆中划分出來。(指針碰撞:java堆中內存是絕對規整的,使用過的在一邊,空閑的在另一邊,中間放一個指針作為分界點的指示器,分配內存就是移動分界點。空閑列表:java堆中的內存並不是規整的,使用的和空閑的相互交錯,虛擬機就必須維護一個列表,記錄那些內存塊是可以使用的。)選擇哪種分配方式由所采用的垃圾收集器是否帶有壓縮整理功能決定。(帶Compact過程的收集器使用指針碰撞,基於Mark-Sweep算法的手機器時,通常采用空閑列表)
    3. 還有一個需要考慮的問題,對象的創建在虛擬機中是非常頻繁的行為,雖然僅僅是修改一個指針所只想的位置,在並發情況下也並不是線程安全的。解決方案一:對分配內存空間的動作進行同步處理(實際上虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性)。解決方案二:把內存分配的動作按照線程划分在不同的空間之中進行。
    4. 執行init方法

對象的內存布局

  • 對象在內存中存儲的布局可以分為3塊區域:對象頭,實例數據和對齊填充。
    1. HotSpot虛擬機的對象頭包括兩部分信息,第一部分用於存儲對象自身的運行時數據,如哈希碼等。另一部分是指針類型,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
    2. 實例數據部分是對象真正存儲的有效數據,也是代碼中所定義的各種類型的字段內容,無論是父類繼承下來的,還是在子類中定義的。
    3. 對齊填充並不是必然存在的,也沒有特別的含義,它僅僅起着占位符的作用。由於HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍。

對象的訪問定位

  • 目前主流的訪問方式有使用句柄和直接指針兩種
    1. 如果使用句柄訪問,那么java堆中會划分出一塊內存來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。優點: reference中存儲的是穩定的句柄地址,在對象被移動(垃圾收集器移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而refererce本身並不需要修改
    2. 如果使用直接指針訪問,那么java堆對象的布局中就必須考慮如何放置訪問類型數據的相關信息,而reference中存儲的直接就是對象地址。優點:速度更快,它節省了一次指針定位的時間開銷。

java堆溢出

java堆用來存儲對象實例,只要不斷地創建對象,並且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那么在對象數量到達最大堆的容納限制后就會產生內存溢出異常。

方法區和運行時常量池溢出

由於運行時常量池是方法區的一部分,因此這兩個區域的溢出測試就放在一起進行,String.intern()是一個native方法,他的作用是:如果字符串常量池中已經包含了一個等於此String對象的字符串,則返回代表池中這個字符串的String對象;否則,將此String對象包含的字符串添加到chang.iang池中,並且返回此String對象的引用。

垃圾收集器與內存分配策略

java內存運行時區域的各個部分,其中程序計數器,虛擬機棧,本地方法棧3個區域隨線程而生,隨線程而滅。棧中的棧幀隨着方法的進入和退出而有條不紊的執行者着入棧和出站操作,每一個棧幀中分配多少內存基本上是在類結構確定下來時就已知得,因此這幾個區域的內存分配和回收都具備確定性,因為方法結束或者線程結束時,內存自然就跟隨着回收了,而java堆和方法區則不一樣,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序處於運行期間時才能知道創建那些對象,這部分內存的分配和回收都是動態的。

計數算法

給對象中添加一個引用計數器,每當有一個地方引用他時,計數器值就加1;當飲用失效時,計數器值就減一;任何時刻計數器為0的對象就是不可能再被使用的。
優點:實現簡單,效率高,
缺點:不能解決對象之間相互循環引用的問題
應用案例:微軟公司的COM(Component Object Model)技術,ActionScript 3的FlashPlayer.

可達性分析算法

  • 在主流的商用程序語言(java,c#等)的主流實現中,都是通過可達性分析來判斷對象是否存活的。
  • 基本思路:通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,則證明對象是不可用的。
  • 在java語言中,可作為GC Roots的對象包括下面幾種:
    1. 虛擬機棧中引用的對象。
    2. 方法區中類靜態屬性引用的對象
    3. 方法區中常量引用的對象
    4. 本地方法棧中JNI引用的對象。

引用

  • JDK1.2以前的定義:如果reference類型的數據中存儲的數值代表的是另外一塊內存的起始地址,就稱這塊內存代表着有個引用(太過狹隘)
  • JDK1.2之后,java對引用的的概念進行了擴充。
    1. 強引用:一般的new對象等,垃圾收集器永遠不會回收掉被引用的對象
    2. 軟引用:描述一些還有用但並非必需的對象。在系統將要發生內存溢出異常之前,將會把這些對象列進回收范圍之中進行二次回收。在JDK1.2之后,提供了SoftReference類來實現軟引用。
    3. 弱引用:被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象,WeakReference類來實現弱引用。
    4. 虛引用:一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的就是能夠在這個對象唄垃圾收集器回收時收到一個系統通知 PhantomReference類來實現虛引用。

回收方法區

java虛擬機規范中說過可以不要求虛擬機在方法區實現垃圾收集,而且在方法區中進行垃圾收集的“性價比”一般比較低

垃圾收集算法

標記-清除算法

  • 優點:簡單
  • 缺點:
    1. 效率問題:標記和清除兩個過程的效率都不高
    2. 空間問題:標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾手機動作。

復制算法(為了解決效率問題)

它將可用內存按容量划分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象復制到另一塊上面,然后再把已使用過的內存空間一次清理掉。這樣使得每次都對整個半區進行內存回收。內存分配時也就不用考慮內存碎片等復雜情況。只要移動堆頂指針,按順序分配內存即可。實現簡單,運行效率高。
缺點:將內存縮小為原來的一般,代價太高。並且在對象存活率較高時,就要進行較多的復制操作,效率將會變低。

標記-整理算法

標記過程仍然與“標記-清除”算法一樣,但是后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉邊界以外的內存。

分代收集算法

該算法只是根據對象存活周期的不同將內存分為幾塊,一般把java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而老年代中因為對象存活率搞。沒有額外空間對他進行分配擔保。就必須使用“標記-清理”或者“標記-整理”算法來進行回收

垃圾收集器

java虛擬機規范中對垃圾收集器如何實現並沒有任何規定,因此不同廠商,不同版本的虛擬機所提供的垃圾收集器都可能會有很大的差別。(基於JDK1.7 Update 14之后的HotSpot虛擬機)

圖中展示了7種作用於不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配使用。虛擬機所處的位置,則表示它是屬於新生代收集器還是屬於老年代收集器。

Serial 收集器(新生代收集器)

這是一個單線程收集器,但是它的“單線程”的意義並不僅僅說明它只會使用一個cpu或一條收集線程去完成垃圾收集工作,更重要的是在他進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。
(Serial-Parallel-Concurrent Mark Sweep-Garbage First)雖然它存在這些缺點,但是實際上到現在為止,它依然是虛擬機運行在Client模式下的默認新生代收集器。因為它簡單和高效(相對其他單線程的收集器)

ParNew 收集器

ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集之外,其余行為包括Serial收集器可用的所有控制參數,收集算法,Stop The World,對象分配規則,回收策略等都與Serial收集器完全一樣。但是它是許多運行在Server模式下的虛擬機中首選的其中一個與性能無關的但是很重要的一個原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作。(ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果)

CMS收集器(老年代收集器)

在JDK1.5時期,HotSpot推出了一款在強交互應用中幾乎可以認為有划時代意義的垃圾收集器—CMS收集器(Concurrent Mark Sweep),這款收集器是HotSpot虛擬機中第一款真正意義上的並發收集器。,它第一次實現了讓垃圾收集線程與用戶線程同時工作。但是卻不能與JDK1.4.0中已經存在的新生代收集器Parallel Scavenge配合工作。只能使用上面兩種中的一個。

Parallel Scavenge 收集器(新生代收集器)

它也是使用復制算法的收集器,又是並行的多線程收集器,它的特點在於它的關注點與其他收集器不同,CMS等收集器的關注點是盡可能地縮短垃圾收集時用戶線程的停頓時間。而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量。所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值。即吞吐量=運行用戶代碼時間/(運行用戶代碼的時間+垃圾收集時間)
它主要使用兩個參數來用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間的參數(大於0的毫秒數)和直接設置吞吐量大小的參數(大於0且小於100的整數)。

Serial Old收集器(老年代)

Serial Old 是Serial收集器的老年代版本,它同樣是單線程收集器,使用“標記-整理”算法這個收集器的主要意義也是在於給Client模式下的虛擬機使用。

Parallel Old收集器(老年代)

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。

CMS收集器

  • CMS收集器是一種以獲取最短回收停頓時間為目標的收集器,適應於互聯網站或者B/S系統的服務器端上,響應快,停頓時間短,給用戶好的體驗。
  • CMS收集器是基於“標記-清除”算法實現的。它的運作過程相對於前面幾種收集器來說更復雜一些,整個過程分為4個步驟,包括:初始標記,並發標記,重復標記,並發清除。,其中,初始標記,重新標記這兩個步驟仍然需要“Stop The World”.初始標記僅僅只是標記一下GC Roots 能直接關聯到的對象,速度很快,並發標記階段就是進行GC Roots Tracing的過程,而重新標記階段則是為了修正並發標記期間因用戶程序繼續運作而導致標記產生變化的那一部分對象的標記記錄。這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。
  • 缺點:CMS收集器對CPU資源非常敏感。在並發階段,它雖然不會導致用戶線程停頓,但是會因為占用一部分線程而導致應用程序變慢,總吞吐量會降低。CMS默認啟動的回收線程數是(CPU數量+3)/4
  • CMS收集器無法處理浮動垃圾,可能出現“concurrent Mode Failure”失敗而導致另一次Full GC的產生。由於CMS並發清理階段用戶線程還在運行着,伴隨程序運行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記程序之后,CMS無法在當次收集中處理掉它們,只好停留下一次GC時再處理掉。這一部分垃圾就稱為“浮動垃圾”
  • CMS是一款基於“標記-清除”算法實現的收集器,在收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大的麻煩,往往會出現老年代還有很大的空間剩余,但是無法找到足夠大的連續空間來分配當前對象,不得不提前觸發一次Full GC。

G1收集器

G1(Garbage-First)收集器是當今 收集器技術發展的最前沿成果之一,G1是一款面向服務端應用的垃圾收集器。目標是替換掉CMS收集器。

  • 特點
    1. 並行與並發
    2. 分代收集
    3. 空間整合
    4. 可預測的停頓
  • 在G1之前的其他收集器進行收集的范圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時java堆的內存布局就與其他收集器有很大差別,他將整個java堆划分為多個大小相等的獨立區域(Region),雖然還保留着新生代和老年代的概念,但是他們已經不再是物理隔離,他們都是一部分Region(不需要連續)的集合。

內存分配

  • 對象的分配
    多數情況下,對象在新生代Eden區中分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC 。
  • 大對象直接進入老年代
    所謂大對象,需要大量連續內存空間的java對象,最典型的大對象就是那種很長的字符串及數組。
    虛擬機提供了-XX:PretenureSizeThreshold參數,令大於這個設置值得對象直接在老年代分配,這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的內存復制。
  • 長期存活的對象將進入老年代。

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com