轉 Java筆記:Java內存模型

Java筆記:Java內存模型

2014.04.09 | Commentshtml

1. 基本概念

《深刻理解Java內存模型》詳細講解了java的內存模型,這裏對其中的一些基本概念作個簡單的筆記。如下內容摘自 《深刻理解Java內存模型》讀書總結java

併發

定義:即,併發(同時)發生。在操做系統中,是指一個時間段中有幾個程序都處於已啓動運行到運行完畢之間,且這幾個程序都是在同一個處理機上運行,但任一個時刻點上只有一個程序在處理機上運行。web

併發須要處理兩個關鍵問題:線程之間如何通訊線程之間如何同步算法

  • 通訊:是指線程之間如何交換信息。在命令式編程中,線程之間的通訊機制有兩種:共享內存消息傳遞
  • 同步:是指程序用於控制不一樣線程之間操做發生相對順序的機制。在Java中,能夠經過volatilesynchronized等方式實現同步。

主內存和本地內存

主內存:即 main memory。在java中,實例域、靜態域和數組元素是線程之間共享的數據,它們存儲在主內存中。編程

本地內存:即 local memory。 局部變量,方法定義參數 和 異常處理器參數是不會在線程之間共享的,它們存儲在線程的本地內存中。canvas

重排序

定義:重排序是指「編譯器和處理器」爲了提升性能,而在程序執行時會對程序進行的重排序。數組

說明:重排序分爲「編譯器」和「處理器」兩個方面,而「處理器」重排序又包括「指令級重排序」和「內存的重排序」。瀏覽器

關於重排序,咱們須要理解它的思想: 爲了提升程序的併發度,從而提升性能!可是對於多線程程序,重排序可能會致使程序執行的結果不是咱們須要的結果!所以,就須要咱們經過volatile、synchronize、鎖等方式實現同步。緩存

內存屏障

定義:包括LoadLoad, LoadStore, StoreLoad, StoreStore共4種內存屏障。內存屏障是與相應的內存重排序相對應的。安全

做用:經過內存屏障能夠禁止特定類型處理器的重排序,從而讓程序按咱們預想的流程去執行。

happens-before

定義:JDK5(JSR-133)提供的概念,用於描述多線程操做之間的內存可見性。若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必須存在 happens-before 關係。

做用:描述多線程操做之間的內存可見性。

數據依賴性

定義:若是兩個操做訪問同一個變量,且這兩個操做中有一個爲寫操做,此時這兩個操做之間就存在數據依賴性

做用:編譯器和處理器不會對「存在數據依賴關係的兩個操做」執行重排序。

as-if-serial

定義:無論怎麼重排序,程序的執行結果不能被改變。

順序一致性內存模型

定義:它是理想化的內存模型。有如下規則:

  • 一個線程中的全部操做必須按照程序的順序來執行。
  • 全部線程都只能看到一個單一的操做執行順序。在順序一致性內存模型中,每一個操做都必須原子執行且馬上對全部線程可見。

Java內存模型

定義:Java Memory Mode,它是Java線程之間通訊的控制機制。

說明:JMM 對 Java 程序做出保證,若是程序是正確同步的,程序的執行將具備順序一致性。即,程序的執行結果與該程序在順序一致性內存模型中的執行結果相同。

可見性

可見性通常用於指不一樣線程之間的數據是否可見。

在 java 中, 實例域、靜態域和數組元素這些數據是線程之間共享的數據,它們存儲在主內存中;主內存中的全部數據對該內存中的線程都是可見的。而局部變量,方法定義參數和異常處理器參數這些數據是不會在線程之間共享的,它們存儲在線程的本地內存中;它們對其它線程是不可見的。

此外,對於主內存中的數據,在本地內存中會對應的建立該數據的副本(至關於緩衝);這些副本對於其它線程也是不可見的。

原子性

是指一個操做是按原子的方式執行的。要麼該操做不被執行;要麼以原子方式執行,即執行過程當中不會被其它線程中斷。

2. JVM內存模型

雖然平時咱們用的大可能是 Sun JDK 提供的 JVM,可是 JVM 自己是一個 規範,因此能夠有多種實現,除了 Hotspot 外,還有諸如 Oracle 的 JRockit、IBM 的 J9也都是很是有名的 JVM。

Java 虛擬機在執行 Java 程序的過程當中會把它所管理的內存劃分爲若干個不一樣的數據區域,這些區域都有各自的用途,以及建立和銷燬的時間。有的區域隨着虛擬機進程的啓動就存在了, 有的區域則是依賴用戶線程。根據《Java虛擬機規範(第二版)》,Java 虛擬機所管理的內存包含以下圖的幾個區域。

Java-Memory.png

由上圖能夠看出 JVM 組成以下:

  • 運行時數據區(內存空間)
    • 方法區
    • 虛擬機棧
    • 程序計數器
    • 本地方法棧
    • 直接內存
  • 執行引擎
  • 本地庫接口

從上圖中還能夠看出,在內存空間中方法區和堆是全部Java線程共享的,稱之爲線程共享數據區,而虛擬機棧、程序計數器、本地方法棧則由每一個線程私有,稱之爲線程隔離數據區

關於本地方法:

衆所周知,Java 語言具備跨平臺的特性,這也是由 JVM 來實現的。更準確地說,是 Sun 利用 JVM 在不一樣平臺上的實現幫咱們把平臺相關性的問題給解決了,這就比如是 HTML 語言能夠在不一樣廠商的瀏覽器上呈現元素(雖然某些瀏覽器在對W3C標準的支持上還有一些問題)。同時,Java 語言支持經過 JNI(Java Native Interface)來實現本地方法的調用,可是須要注意到,若是你在 Java 程序用調用了本地方法,那麼你的程序就極可能再也不具備跨平臺性,即本地方法會破壞平臺無關性。

下面分別就線程共享數據區和線程共享數據區進行說明。

2.1 線程共享數據區

所謂線程共享數據區,是指在多線程環境下,該部分區域數據能夠被全部線程所共享,主要有方法區和堆。

方法區

方法區用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等等。方法區中對於每一個類存儲瞭如下數據:

  • 類及其父類的全限定名(java.lang.Object沒有父類)
  • 類的類型(Class or Interface)
  • 訪問修飾符(public, abstract, final)
  • 實現的接口的全限定名的列表
  • 常量池
  • 字段信息
  • 方法信息
  • 靜態變量
  • ClassLoader 引用
  • Class 引用

可見類的全部信息都存儲在方法區中。因爲方法區是全部線程共享的,因此必須保證線程安全,舉例來講:若是兩個類同時要加載一個還沒有被加載的類,那麼一個類會請求它的 ClassLoader 去加載須要的類,另外一個類只能等待而不會重複加載。

注意事項:

  • 在 HotSpot 虛擬機中,不少人都把方法區成爲永久代,默認最小值爲16MB,最大值爲64MB。其實只在 HotSpot 才存在方法區,在其餘的虛擬機沒有方法區這一個說法的。本文是採用 Hotspot,因此把方法區介紹了。

  • 若是方法區沒法知足內存分配需求時候就會拋出 OutOfMemoryError 異常。

堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例及數組內容,幾乎全部的對象實例都在這裏分配內存。堆中有指向類數據的指針,該指針指向了方法區中對應的類型信息,堆中還可能存放了指向方法表的指針。堆是全部線程共享的,因此在進行實例化對象等操做時,須要解決同步問題。此外,堆中的實例數據中還包含了對象鎖,而且針對不一樣的垃圾收集策略,可能存放了引用計數或清掃標記等數據。

在 Java 中,堆被劃分紅兩個不一樣的區域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被劃分爲三個區域:Eden、From Survivor、To Survivor。

jvm-heap.png

​從圖中能夠看出: 堆大小 = 新生代 + 老年代,其中,堆的大小能夠經過參數 -Xms-Xmx 來指定。本人使用的是 JDK1.6,如下涉及的 JVM 默認值均以該版本爲準。

  • 默認的,Young : Old = 1 : m ,該比例值 m 能夠經過參數 -XX:NewRatio 來指定,默認值爲2,即新生代 ( Young ) = 1/3 的堆空間大小,老年代 ( Old ) = 2/3 的堆空間大小。

  • 默認的,Edem : from : to = n : 1 : 1 ,該比例值 n 能夠參數 -XX:SurvivorRatio 來設定,默認值爲8 ,即 Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小。

  • JVM 每次只會使用 Eden 和其中的一塊 Survivor 區域來爲對象服務,因此不管何時,老是有一塊 Survivor 區域是空閒着的,所以,新生代實際可用的內存空間爲 9/10 ( 即90% )的新生代空間。

根據 Java 虛擬機規範的規定,Java 堆能夠處於物理上不連續的內存空間中,只要邏輯上是連續的便可,就像咱們的磁盤空間同樣。在實現時,既能夠實現成固定大小的,也能夠是可擴展的,不過當前主流的虛擬機都是按照可擴展來實現的。若是在堆中沒有內存完成實例分配,而且堆也沒法再擴展時,將會拋出 OutOfMemoryError 異常。

2.2 線程隔離數據區

所謂線程隔離數據區是指在多線程環境下,每一個線程所獨享的數據區域。主要有程序計數器、Java虛擬機棧、本地方法棧三個數據區。

程序計數器

程序計數器 ,計算機處理器中的寄存器,它包含當前正在執行的指令的地址(位置)。當每一個指令被獲取,程序計數器的存儲地址加一。在每一個指令被獲取以後,程序計數器指向順序中的下一個指令。當計算機重啓或復位時,程序計數器一般恢復到零。

在Java中程序計數器是一塊較小的內存空間,充當當前線程所執行的字節碼的行號指示器的角色。

在多線程環境下,當某個線程失去處理器執行權時,須要記錄該線程被切換出去時所執行的程序位置。從而方便該線程被切換回來(從新被處理器處理)時能恢復到當初的執行位置,所以每一個線程都須要有一個獨立的程序計數器。各個線程的程序計數器互不影響,而且獨立存儲。

  • 當線程正在執行一個 java 方法時,這個程序計數器記錄的時正在執行的虛擬機字節碼指令的地址。
  • 當線程執行的是 Native方法,這個計數器值爲空。
  • 此內存區域是惟一一個在 java 虛擬機規範中沒有規定任何 OutOfMemoryError 狀況的區域。

Java 虛擬機棧

與程序計數器同樣,Java 虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同。Java 虛擬機棧描述的是 Java 方法執行的內存模型,每一個方法在執行的同時都會建立一個棧幀用於存儲局部變量表操做數棧動態連接、方法出口等信息。每一個方法從調用直至執行完成的過程,對應着一個棧幀在虛擬機中入棧到進棧的過程。

在 Hot Spot 虛擬機中,可使用 -Xss 參數來設置棧的大小。棧的大小直接決定了函數調用的深度。

某個線程正在執行的方法被稱爲該線程的當前方法,當前方法使用的棧幀成爲當前幀,當前方法所屬的類成爲當前類,當前類的常量池成爲當前常量池。在線程執行一個方法時,它會跟蹤當前類和當前常量池。此外,當虛擬機遇到棧內操做指令時,它對當前幀內數據執行操做。

它分爲三部分:局部變量區操做數棧幀數據區

一、局部變量區

局部變量區是以字長爲單位的數組,在這裏,byte、short、char 類型會被轉換成 int 類型存儲,除了 long 和 double 類型佔兩個字長之外,其他類型都只佔用一個字長。特別地,boolean 類型在編譯時會被轉換成 int 或 byte 類型,boolean 數組會被當作 byte 類型數組來處理。局部變量區也會包含對象的引用,包括類引用、接口引用以及數組引用。

局部變量區包含了方法參數和局部變量,此外,實例方法隱含第一個局部變量 this,它指向調用該方法的對象引用。對於對象,局部變量區中永遠只有指向堆的引用。

注意:

局部變量表中的字可能會影響 GC 回收。若是這個字沒有被後續代碼複用,那麼它所引用的對象不會被 GC 釋放,手工對要釋放的變量賦值爲 null,是一種有效的作法。

二、操做數棧

操做數棧也是以字長爲單位的數組,可是正如其名,它只能進行入棧出棧的基本操做。在進行計算時,操做數被彈出棧,計算完畢後再入棧。

每當線程調用一個Java方法時,虛擬機都會在該線程的Java棧中壓入一個新幀。而這個新幀天然就成爲了當前幀。在執行這個方法時,它使用這個幀來存儲參數、局部變量、中間運算結果等等數據。

Java 方法能夠以兩種方式完成。一種經過 return 返回的,稱爲正常返回;一種是經過拋出異常而異常停止的。無論以哪一種方式返回,虛擬機都會將當前幀彈出Java棧而後釋放掉,這樣上一個方法的幀就成爲當前幀了。

Java 棧上的全部數據都是此線程私有的。任何線程都不能訪問另外一個線程的棧數據,所以咱們不須要考慮多線程狀況下棧數據的訪問同步問題。當一個線程調用一個方法時,方法的局部變量保存在調用線程 Java 棧的幀中。只有一個線程老是訪問哪些局部變量,即調用方法的線程。

三、幀數據區

幀數據區的任務主要有:

  • a.記錄指向類的常量池的指針,以便於解析。

  • b.幫助方法的正常返回,包括恢復調用該方法的棧幀,設置PC寄存器指向調用方法對應的下一條指令,把返回值壓入調用棧幀的操做數棧中。

  • c.記錄異常表,發生異常時將控制權交由對應異常的catch子句,若是沒有找到對應的catch子句,會恢復調用方法的棧幀並從新拋出異常。

局部變量區和操做數棧的大小依照具體方法在編譯時就已經肯定。調用方法時會從方法區中找到對應類的類型信息,從中獲得具體方法的局部變量區和操做數棧的大小,依此分配棧幀內存,壓入Java棧。

在 Java 虛擬機規範中,對這個區域規定了兩種異常情況

  • 若是線程請求的棧深度大於虛擬機所容許的深度,將拋出 StackOverflowError 異常;
  • 若是虛擬機棧能夠動態擴展(當前大部分的Java虛擬機均可動態擴展,只不過Java虛擬機規範中也容許固定長度的虛擬機棧),當擴展時沒法申請到足夠的內存時會拋出 OutOfMemoryError 異常。

本地方法棧

本地方法棧(Native Method Stacks)與虛擬機棧所發揮的做用是很是類似的,其區別不過是虛擬機棧爲虛擬機執行 Java方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的 Native 方法服務。虛擬機規範中對本地方法棧中的方法使用的語言、使用方式與數據結構並無強制規定,所以具體的虛擬機能夠自由實現它。甚至有的虛擬機(譬如 Sun HotSpot 虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。與虛擬機棧同樣,本地方法棧區域也會拋出 StackOverflowError 和 OutOfMemoryError 異常。

2.3 直接內存

直接內存並非虛擬機運行時數據區的一部分,也不是 Java 虛擬機規範中定義的內存區域。

JDK1.4 中出現了 NIO,其引入了一種基於通道與緩衝區的 I/O 方式,它可使用 Native 函數庫直接分配堆外內存,而後經過一個存儲在 Java 堆中得 DirectoryByteBuffer 對象做爲這塊內存的引用進行操做。這樣能夠避免 Java 堆和 Native 堆之間的來回複製數據。

當機器直接內存去除 JVM 內存以後的內存不能知足直接內存大小要求其,將會拋出 OutOfMemoryError 異常。

3. 垃圾回收過程

jvm-heap.png

JVM 採用一種分代回收 (generational collection) 的策略,用較高的頻率對年輕的對象進行掃描和回收,這種叫作 minor collection ,而對老對象的檢查回收頻率要低不少,稱爲 major collection。這樣就不須要每次 GC 都將內存中全部對象都檢查一遍。

  • 新生代被劃分爲三部分,Eden 區和兩個大小嚴格相同的 Survivor 區,其中 Survivor 區間,某一時刻只有其中一個是被使用的,另一個留作垃圾收集時複製對象用,在 Young 區間變滿的時候,minor GC 就會將存活的對象移到空閒的 Survivor 區間中,根據 JVM 的策略,在通過幾回垃圾收集後,仍然存活於 Survivor 的對象將被移動到老年代

  • 老年代主要保存生命週期長的對象,通常是一些老的對象,當一些對象在 Young 複製轉移必定的次數之後,對象就會被轉移到老年區,通常若是系統中用了 application 級別的緩存,緩存中的對象每每會被轉移到這一區間。

Minor collection 的過程就是將 eden 和在用survivor space中的活對象 copy 到空閒survivor space中。所謂 survivor,也就是大部分對象在 eden 出生後,根本活不過一次 GC。對象在新生代裏經歷了必定次數的 minor collection 後,年紀大了,就會被移到老年代中,稱爲 tenuring。

剩餘內存空間不足會觸發 GC,如 eden 空間不夠了就要進行 minor collection,老年代空間不夠要進行 major collection,永久代(Permanent Space)空間不足會引起full GC。

舉例:當一個 URL 被訪問時,內存申請過程以下:

  • A. JVM 會試圖爲相關 Java 對象在 Eden 中初始化一塊內存區域
  • B. 當 Eden 空間足夠時,內存申請結束。不然到下一步
  • C. JVM 試圖釋放在 Eden 中全部不活躍的對象,釋放後若 Eden 空間仍然不足以放入新對象,則試圖將部分 Eden 中活躍對象放入 Survivor 區
  • D. Survivor 區被用來做爲 Eden 及 Old 的中間交換區域,當 Old 區空間足夠時,Survivor 區的對象會被移到 Old 區,不然會被保留在 Survivor區
  • E. 當 Old 區空間不夠時,JVM 會在 Old 區進行徹底的垃圾收集
  • F. 徹底垃圾收集後,若 Survivor 及 Old 區仍然沒法存放從 Eden 複製過來的部分對象,致使 JVM 沒法在 Eden 區爲新對象建立內存區域,則出現 out of memory 錯誤

HotSpot jvm 都給咱們提供了下面參數來對內存進行配置:

  • 配置總內存
  • -Xms :指定了 JVM 初始啓動之後初始化內存
  • -Xmx:指定 JVM 堆得最大內存,在JVM啓動之後,會分配 -Xmx 參數指定大小的內存給 JVM,可是不必定所有使用,JVM 會根據 -Xms 參數來調節真正用於JVM的內存,-Xmx-Xms 之差就是三個 Virtual 空間的大小

  • 配置新生代
  • -Xmn: 參數設置了年輕代的大小
  • -XX:SurvivorRatio: 表示 eden 和一個 surivivor 的比例,缺省值爲8
  • -XX:NewSize 和 -XX:MaxNewSize:直接指定了年輕代的缺省大小和最大大小

  • 配置老年代
  • -XX:NewRatio: 表示年老年代和新生代內存的比例,缺省值爲2

  • 配置持久代
  • -XX:MaxPermSize:表示持久代的最大值
  • -XX:PermSize:設置最小分配空間

  • 配置虛擬機棧
  • -Xss:參數來設置棧的大小,默認值爲128 kb。棧的大小直接決定了函數調用的深度

4. 常見的垃圾收集策略

垃圾收集提供了內存管理的機制,使得應用程序不須要在關注內存如何釋放,內存用完後,垃圾收集會進行收集,這樣就減輕了由於人爲的管理內存而形成的錯誤,好比在 C++ 語言裏,出現內存泄露時很常見的。Java 語言是目前使用最多的依賴於垃圾收集器的語言,可是垃圾收集器策略從20世紀60年代就已經流行起來了,好比 Smalltalk,Eiffel 等編程語言也集成了垃圾收集器的機制。

全部的垃圾收集算法都面臨同一個問題,那就是找出應用程序不可到達的內存塊,將其釋放,這裏面得不可到達主要是指應用程序已經沒有內存塊的引用了,而在 JAVA中,某個對象對應用程序是可到達的是指:這個對象被根(根主要是指類的靜態變量,常量或者活躍在全部線程棧的對象的引用)引用或者對象被另外一個可到達的對象引用。

下面咱們介紹一下幾種常見的垃圾收集策略:

4.1 Reference Counting(引用計數)

引用計數是最簡單直接的一種方式,這種方式在每個對象中增長一個引用的計數,這個計數表明當前程序有多少個引用引用了此對象,若是此對象的引用計數變爲0,那麼此對象就能夠做爲垃圾收集器的目標對象來收集。

優勢:簡單,直接,不須要暫停整個應用

缺點:須要編譯器的配合,編譯器要生成特殊的指令來進行引用計數的操做,好比每次將對象賦值給新的引用,或者者對象的引用超出了做用域等。 不能處理循環引用的問題

4.2 跟蹤收集器

跟蹤收集器首先要暫停整個應用程序,而後開始從根對象掃描整個堆,判斷掃描的對象是否有對象引用。

若是每次掃描整個堆,那麼勢必讓 GC 的時間變長,從而影響了應用自己的執行。所以在 JVM 裏面採用了分代收集,在新生代收集的時候 minor gc 只須要掃描新生代,而不須要掃描老生代。minor gc 怎麼判斷是否有老生代的對象引用了新生代的對象,JVM 採用了卡片標記的策略,卡片標記將老生代分紅了一塊一塊的,劃分之後的每個塊就叫作一個卡片,JVM 採用卡表維護了每個塊的狀態,當 JAVA 程序運行的時候,若是發現老生代對象引用或者釋放了新生代對象的引用,那麼就 JVM 就將卡表的狀態設置爲髒狀態,這樣每次 minor gc 的時候就會只掃描被標記爲髒狀態的卡片,而不須要掃描整個堆。

上面說了 Jvm 須要判斷對象是否有引用存在,而 Java 中的引用又分爲了以下幾種,不一樣種類的引用對垃圾收集有不一樣的影響,下面咱們分開描述一下:

  • 1)Strong Reference(強引用)

強引用是 JAVA 中默認採用的一種方式,咱們平時建立的引用都屬於強引用。若是一個對象沒有強引用,那麼對象就會被回收。

public void testStrongReference(){     Object referent = new Object();     Object strongReference = referent;     referent = null;     System.gc();     assertNotNull(strongReference); }
  • 2)Soft Reference(軟引用)

軟引用的對象在 GC 的時候不會被回收,只有當內存不夠用的時候纔會真正的回收,所以軟引用適合緩存的場合,這樣使得緩存中的對象能夠儘可能的再內存中待長久一點。

Public void testSoftReference(){     String  str =  "test";     SoftReference<String> softreference = new SoftReference<String>(str);     str=null;     System.gc();     assertNotNull(softreference.get()); }
  • 3)Weak Reference(弱引用)

弱引用有利於對象更快的被回收,假如一個對象沒有強引用只有弱引用,那麼在 GC 後,這個對象確定會被回收。

Public void testWeakReference(){     String  str =  "test";     WeakReference<String> weakReference = new WeakReference<String>(str);     str=null;     System.gc();     assertNull(weakReference.get()); }
  • 4)Phantom reference(幽靈引用)

幽靈引用說是引用,可是你不能經過幽靈引用來獲取對象實例,它主要目的是爲了當設置了幽靈引用的對象在被回收的時候能夠收到通知。

跟蹤收集器常見的有以下幾種:

4.2.1 Mark-Sweep Collector(標記-清除收集器)

標記清除收集器最先由Lisp的發明人於1960年提出,標記清除收集器中止全部的工做,從根掃描每一個活躍的對象,而後標記掃描過的對象,標記完成之後,清除那些沒有被標記的對象。

優勢:

  • 解決循環引用的問題
  • 不須要編譯器的配合,從而就不執行額外的指令

缺點:

  • 每一個活躍的對象都要進行掃描,收集暫停的時間比較長。

4.2.2 Copying Collector(複製收集器)

複製收集器將內存分爲兩塊同樣大小空間,某一個時刻,只有一個空間處於活躍的狀態,當活躍的空間滿的時候,GC就會將活躍的對象複製到未使用的空間中去,原來不活躍的空間就變爲了活躍的空間。

優勢:

  • 只掃描能夠到達的對象,不須要掃描全部的對象,從而減小了應用暫停的時間

缺點:

  • 須要額外的空間消耗,某一個時刻,老是有一塊內存處於未使用狀態
  • 複製對象須要必定的開銷

4.2.3 Mark-Compact Collector(標記-整理收集器)

標記整理收集器汲取了標記清除和複製收集器的優勢,它分兩個階段執行,在第一個階段,首先掃描全部活躍的對象,並標記全部活躍的對象,第二個階段首先清除未標記的對象,而後將活躍的的對象複製到堆得底部。

Mark-compact 策略極大的減小了內存碎片,而且不須要像 Copy Collector 同樣須要兩倍的空間。

5. HotSpot JVM 垃圾收集策略

GC 的執行時要耗費必定的 CPU 資源和時間的,所以在 JDK1.2 之後,JVM 引入了分代收集的策略,其中對新生代採用 」Mark-Compact」 策略,而對老生代採用了 「Mark-Sweep」 的策略。其中新生代的垃圾收集器命名爲 「minor gc」,老生代的 GC 命名爲 」Full Gc 或者Major GC」。其中用 System.gc() 強制執行的是 Full GC。

HotSpot JVM 的垃圾收集器按照併發性能夠分爲以下三種類型:

5.1 串行收集器(Serial Collector)

Serial Collector 是指任什麼時候刻都只有一個線程進行垃圾收集,這種策略有一個名字 stop the whole world,它須要中止整個應用的執行。這種類型的收集器適合於單CPU的機器。

Serial Collector 有以下兩個:

  • 1)Serial Copying Collector

此種 GC 用 -XX:UseSerialGC 選項配置,它只用於新生代對象的收集。

JDK 1.5.0 之後 -XX:MaxTenuringThreshold 用來設置對象複製的次數。當 eden 空間不夠的時候,GC 會將 eden 的活躍對象和一個名叫 From survivor 空間中尚不夠資格放入 Old 代的對象複製到另一個名字叫 To Survivor 的空間。而此參數就是用來講明到底 From survivor 中的哪些對象不夠資格,假如這個參數設置爲31,那麼也就是說只有對象複製31次之後纔算是有資格的對象。

這裏須要注意幾個個問題:

  • From Survivor 和 To survivor的角色是不斷的變化的,同一時間只有一塊空間處於使用狀態,這個空間就叫作 From Survivor 區,當複製一次後角色就發生了變化。
  • 若是複製的過程當中發現 To survivor 空間已經滿了,那麼就直接複製到 old generation。
  • 比較大的對象也會直接複製到Old generation,在開發中,咱們應該儘可能避免這種狀況的發生。
  • 2)Serial Mark-Compact Collector

串行的標記-整理收集器是 JDK5 update6 以前默認的老生代的垃圾收集器,此收集使得內存碎片最少化,可是它須要暫停的時間比較長

5.2 並行收集器(Parallel Collector)

Parallel Collector 主要是爲了應對多 CPU,大數據量的環境。Parallel Collector又能夠分爲如下三種:

  • 1)Parallel Copying Collector

此種 GC 用 -XX:UseParNewGC 參數配置,它主要用於新生代的收集,此 GC 能夠配合CMS一塊兒使用,適用於1.4.1之後。

  • 2)Parallel Mark-Compact Collector

此種 GC 用 -XX:UseParallelOldGC 參數配置,此 GC 主要用於老生代對象的收集。適用於1.6.0之後。

  • 3)Parallel scavenging Collector

此種 GC 用 -XX:UseParallelGC 參數配置,它是對新生代對象的垃圾收集器,可是它不能和CMS配合使用,它適合於比較大新生代的狀況,此收集器起始於 jdk 1.4.0。它比較適合於對吞吐量高於暫停時間的場合。

5.3 併發收集器 (Concurrent Collector)

Concurrent Collector 經過並行的方式進行垃圾收集,這樣就減小了垃圾收集器收集一次的時間,在 HotSpot JVM 中,咱們稱之爲 CMS GC,這種 GC 在實時性要求高於吞吐量的時候比較有用。此種 GC 能夠用參數 -XX:UseConcMarkSweepGC 配置,此 GC 主要用於老生代和 Perm 代的收集。

CMS GC有可能出現併發模型失敗:

CMS GC 在運行的時候,用戶線程也在運行,當 GC 的速度比新增對象的速度慢的時候,或者說當正在 GC 的時候,老年代的空間不能知足用戶線程內存分配的需求的時候,就會出現併發模型失敗,出現併發模型失敗的時候,JVM 會觸發一次 stop-the-world 的 Full GC 這將致使暫停時間過長。不過 CMS GC 提供了一個參數 -XX:CMSInitiatingOccupancyFraction 來指定當老年代的空間超過某個值的時候即觸發 GC,所以若是此參數設置的太高,可能會致使更多的併發模型失敗。

併發和並行收集器區別:

併發收集器是指垃圾收集器線程和應用線程能夠併發的執行,也就是清除的時候不須要 stop the world,可是並行收集器指的的是能夠多個線程並行的進行垃圾收集,並行收集器仍是要暫停應用的

6. HotSpot Jvm 垃圾收集器的配置策略

下面咱們分兩種狀況來分別描述一下不一樣狀況下的垃圾收集配置策略。

6.1 吞吐量優先

吞吐量是指 GC 的時間與運行總時間的比值,好比系統運行了100 分鐘,而 GC 佔用了一分鐘,那麼吞吐量就是 99%,吞吐量優先通常運用於對響應性要求不高的場合,好比 web 應用,由於網絡傳輸原本就有延遲的問題,GC 形成的短暫的暫停使得用戶覺得是網絡阻塞所致。

吞吐量優先能夠經過 -XX:GCTimeRatio 來指定。當經過 -XX:GCTimeRatio 不能知足系統的要求之後,咱們能夠更加細緻的來對 JVM 進行調優。

首先由於要求高吞吐量,這樣就須要一個較大的 Young generation,此時就須要引入 Parallel scavenging Collector ,能夠經過參數:-XX:UseParallelGC來配置。

java -server -Xms3072m -Xmx3072m -XX:NewSize=2560m -XX:MaxNewSize=2560 -XX:SurvivorRatio=2 -XX:+UseParallelGC

當年輕代使用了 Parallel scavenge collector 後,老生代就不能使用 CMS GC 了,在 JDK1.6 以前,此時老生代只能採用串行收集,而 JDK1.6 引入了並行版本的老生代收集器,能夠用參數 -XX:UseParallelOldGC 來配置。

1.控制並行的線程數

缺省狀況下,Parallel scavenging Collector 會開啓與 cpu 數量相同的線程進行並行的收集,可是也能夠調節並行的線程數。假如你想用4個並行的線程去收集 Young generation 的話,那麼就能夠配置 -XX:ParallelGCThreads=4,此時JVM的配置參數以下:

java -server -Xms3072m -Xmx3072m -XX:NewSize=2560m -XX:MaxNewSize=2560 -XX:SurvivorRatio=2 -XX:+UseParallelGC -XX:ParallelGCThreads=4

2.自動調節新生代

在採用了 Parallel scavenge collector 後,此 GC 會根據運行時的狀況自動調節 survivor ratio 來使得性能最優,所以 Parallel scavenge collector 應該老是開啓 -XX:+UseAdaptiveSizePolicy 參數。此時JVM的參數配置以下:

java -server -Xms3072m -Xmx3072m -XX:+UseParallelGC -XX:ParallelGCThreads=4 -XX:+UseAdaptiveSizePolicy

6.2 響應時間優先

響應時間優先是指 GC 每次運行的時間不能過久,這種狀況通常使用與對及時性要求很高的系統,好比股票系統等。

響應時間優先能夠經過參數 -XX:MaxGCPauseMillis 來配置,配置之後 JVM 將會自動調節年輕代,老生代的內存分配來知足參數設置。

在通常狀況下,JVM 的默認配置就能夠知足要求,只有默認配置不能知足系統的要求時候,纔會根據具體的狀況來對 JVM 進行性能調優。若是採用默認的配置不能知足系統的要求,那麼此時就能夠本身動手來調節。此時 Young generation 能夠採用 Parallel copying collector,而 Old generation 則能夠採用 Concurrent Collector

舉個例子來講,如下參數設置了新生代用 Parallel Copying Collector,老生代採用 CMS 收集器。

java -server -Xms512m -Xmx512m -XX:NewSize=64m -XX:MaxNewSize=64m -XX:SurvivorRatio=2 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC

此時須要注意兩個問題:

  • 1.若是沒有指定 -XX:+UseParNewGC,則採用默認的非並行版本的 copy collector
  • 2.若是在一個單 CPU 的系統上設置了 -XX:+UseParNewGC,則默認仍是採用缺省的copy collector

1.控制並行的線程數

默認狀況下,Parallel copy collector 啓動和 CPU 數量同樣的線程,也能夠經過參數 -XX:ParallelGCThreads 來指定,好比你想用 4 個線程去進行併發的複製收集,那麼能夠改變上述參數以下:

java -server -Xms512m -Xmx512m -XX:NewSize=64m -XX:MaxNewSize=64m -XX:SurvivorRatio=2 -XX:ParallelGCThreads=4 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC

2.控制併發收集的臨界值

默認狀況下,CMS GC在 old generation 空間佔用率高於 68% 的時候,就會進行垃圾收集,而若是想控制收集的臨界值,能夠經過參數:-XX:CMSInitiatingOccupancyFraction 來控制,好比改變上述的JVM配置以下:

java -server -Xms512m -Xmx512m -XX:NewSize=64m -XX:MaxNewSize=64m -XX:SurvivorRatio=2 -XX:ParallelGCThreads=4 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:CMSInitiatingOccupancyFraction=35

此外順便說一個參數:-XX:+PrintCommandLineFlags 經過此參數能夠知道在沒有顯示指定內存配置和垃圾收集算法的狀況下,JVM 採用的默認配置。

好比我在本身的機器上面經過以下命令 java -XX:+PrintCommandLineFlags -version 獲得的結果以下所示:

-XX:InitialHeapSize=1055308032 -XX:MaxHeapSize=16884928512 -XX:ParallelGCThreads=8 -XX:+PrintCommandLineFlags -XX:+UseCompressedOops -XX:+UseParallelGC java version "1.6.0_45" Java(TM) SE Runtime Environment (build 1.6.0_45-b06) Java HotSpot(TM) 64-Bit Server VM (build 20.45-b01, mixed mode) You have new mail in /var/spool/mail/root

從輸出能夠清楚的看到JVM經過本身檢測硬件配置而給出的缺省配置。

參考資料


原創文章,轉載請註明: 轉載自 JavaChen Blog,做者: JavaChen
本文連接地址: http://blog.javachen.com/2014/04/09/note-about-jvm-memery-model.html
本文基於 署名2.5中國大陸許可協議發佈,歡迎轉載、演繹或用於商業目的,可是必須保留本文署名和文章連接。 如您有任何疑問或者受權方面的協商,請郵件聯繫我。
相關文章
相關標籤/搜索