深刻理解JVM

深刻理解JVMhtml

1   Java技術與Java虛擬機

提及Java,人們首先想到的是Java編程語言,然而事實上,Java是一種技術,它由四方面組成: Java編程語言、Java類文件格式、Java虛擬機和Java應用程序接口(Java API)。它們的關係以下圖所示:java


圖1   Java四個方面的關係程序員

運行期環境表明着Java平臺,開發人員編寫Java代碼(.java文件),而後將之編譯成字節碼(.class文件)。最後字節碼被裝入內存,一旦字節碼進入虛擬機,它就會被解釋器解釋執行,或者是被即時代碼發生器有選擇的轉換成機器碼執行。從上圖也能夠看出Java平臺由Java虛擬機和 Java應用程序接口搭建,Java語言則是進入這個平臺的通道,用Java語言編寫並編譯的程序能夠運行在這個平臺上。這個平臺的結構以下圖所示:web


在Java平臺的結構中, 能夠看出,Java虛擬機(JVM) 處在覈心的位置,是程序與底層操做系統和硬件無關的關鍵。它的下方是移植接口,移植接口由兩部分組成:適配器和Java操做系統, 其中依賴於平臺的部分稱爲適配器;JVM 經過移植接口在具體的平臺和操做系統上實現;在JVM 的上方是Java的基本類庫和擴展類庫以及它們的API, 利用Java API編寫的應用程序(application) 和小程序(Java applet) 能夠在任何Java平臺上運行而無需考慮底層平臺, 就是由於有Java虛擬機(JVM)實現了程序與操做系統的分離,從而實現了Java 的平臺無關性。算法

那麼到底什麼是Java虛擬機(JVM)呢?一般咱們談論JVM時,咱們的意思多是:編程

1. 對JVM規範的的比較抽象的說明;小程序

2. 對JVM的具體實現;數組

3. 在程序運行期間所生成的一個JVM實例。緩存

對JVM規範的的抽象說明是一些概念的集合,它們已經在書《The Java Virtual Machine Specification》(《Java虛擬機規範》)中被詳細地描述了;對JVM的具體實現要麼是軟件,要麼是軟件和硬件的組合,它已經被許多生產廠商所實現,並存在於多種平臺之上;運行Java程序的任務由JVM的運行期實例單個承擔。在本文中咱們所討論的Java虛擬機(JVM)主要針對第三種狀況而言。它能夠被當作一個想象中的機器,在實際的計算機上經過軟件模擬來實現,有本身想象中的硬件,如處理器、堆棧、寄存器等,還有本身相應的指令系統。安全

JVM在它的生存週期中有一個明確的任務,那就是運行Java程序,所以當Java程序啓動的時候,就產生JVM的一個實例;當程序運行結束的時候,該實例也跟着消失了。下面咱們從JVM的體系結構和它的運行過程這兩個方面來對它進行比較深刻的研究。

2   Java虛擬機的體系結構

剛纔已經提到,JVM能夠由不一樣的廠商來實現。因爲廠商的不一樣必然致使JVM在實現上的一些不一樣,然而JVM仍是能夠實現跨平臺的特性,這就要歸功於設計JVM時的體系結構了。

咱們知道,一個JVM實例的行爲不光是它本身的事,還涉及到它的子系統、存儲區域、數據類型和指令這些部分,它們描述了JVM的一個抽象的內部體系結構,其目的不光規定實現JVM時它內部的體系結構,更重要的是提供了一種方式,用於嚴格定義實現時的外部行爲。每一個JVM都有兩種機制,一個是裝載具備合適名稱的類(類或是接口),叫作類裝載子系統;另外的一個負責執行包含在已裝載的類或接口中的指令,叫作運行引擎。每一個JVM又包括方法區、堆、 Java棧、程序計數器和本地方法棧這五個部分,這幾個部分和類裝載機制與運行引擎機制一塊兒組成的體系結構圖爲:


圖3   JVM的體系結構

JVM的每一個實例都有一個它本身的方法域和一個堆,運行於JVM內的全部的線程都共享這些區域;當虛擬機裝載類文件的時候,它解析其中的二進制數據所包含的類信息,並把它們放到方法域中;當程序運行的時候,JVM把程序初始化的全部對象置於堆上;而每一個線程建立的時候,都會擁有本身的程序計數器和 Java棧,其中程序計數器中的值指向下一條即將被執行的指令,線程的Java棧則存儲爲該線程調用Java方法的狀態;本地方法調用的狀態被存儲在本地方法棧,該方法棧依賴於具體的實現。

下面分別對這幾個部分進行說明。

執行引擎處於JVM的核心位置,在Java虛擬機規範中,它的行爲是由指令集所決定的。儘管對於每條指令,規範很詳細地說明了當JVM執行字節碼遇到指令時,它的實現應該作什麼,但對於怎麼作卻言之甚少。Java虛擬機支持大約248個字節碼。每一個字節碼執行一種基本的CPU運算,例如,把一個整數加到寄存器,子程序轉移等。Java指令集至關於Java程序的彙編語言。

Java指令集中的指令包含一個單字節的操做符,用於指定要執行的操做,還有0個或多個操做數,提供操做所需的參數或數據。許多指令沒有操做數,僅由一個單字節的操做符構成。

    虛擬機的內層循環的執行過程以下:

do{

   取一個操做符字節;

   根據操做符的值執行一個動做;

}while(程序未結束)

因爲指令系統的簡單性,使得虛擬機執行的過程十分簡單,從而有利於提升執行的效率。指令中操做數的數量和大小是由操做符決定的。若是操做數比一個字節大,那麼它存儲的順序是高位字節優先。例如,一個16位的參數存放時佔用兩個字節,其值爲:

第一個字節*256+第二個字節字節碼。

指令流通常只是字節對齊的。指令tableswitch和lookup是例外,在這兩條指令內部要求強制的4字節邊界對齊。

對於本地方法接口,實現JVM並不要求必定要有它的支持,甚至能夠徹底沒有。Sun公司實現Java本地接口(JNI)是出於可移植性的考慮,固然咱們也能夠設計出其它的本地接口來代替Sun公司的JNI。可是這些設計與實現是比較複雜的事情,須要確保垃圾回收器不會將那些正在被本地方法調用的對象釋放掉。

Java的堆是一個運行時數據區,類的實例(對象)從中分配空間,它的管理是由垃圾回收來負責的:不給程序員顯式釋放對象的能力。Java不規定具體使用的垃圾回收算法,能夠根據系統的需求使用各類各樣的算法。

Java方法區與傳統語言中的編譯後代碼或是Unix進程中的正文段相似。它保存方法代碼(編譯後的java代碼)和符號表。在當前的Java實現中,方法代碼不包括在垃圾回收堆中,但計劃在未來的版本中實現。每一個類文件包含了一個Java類或一個Java界面的編譯後的代碼。能夠說類文件是 Java語言的執行代碼文件。爲了保證類文件的平臺無關性,Java虛擬機規範中對類文件的格式也做了詳細的說明。其具體細節請參考Sun公司的Java 虛擬機規範。

Java虛擬機的寄存器用於保存機器的運行狀態,與微處理器中的某些專用寄存器相似。Java虛擬機的寄存器有四種:

1. pc: Java程序計數器;

2. optop: 指向操做數棧頂端的指針;

3. frame: 指向當前執行方法的執行環境的指針;。

4. vars: 指向當前執行方法的局部變量區第一個變量的指針。

在上述體系結構圖中,咱們所說的是第一種,即程序計數器,每一個線程一旦被建立就擁有了本身的程序計數器。當線程執行Java方法的時候,它包含該線程正在被執行的指令的地址。可是若線程執行的是一個本地的方法,那麼程序計數器的值就不會被定義。

Java虛擬機的棧有三個區域:局部變量區、運行環境區、操做數區。

局部變量區

每一個Java方法使用一個固定大小的局部變量集。它們按照與vars寄存器的字偏移量來尋址。局部變量都是32位的。長整數和雙精度浮點數佔據了兩個局部變量的空間,卻按照第一個局部變量的索引來尋址。(例如,一個具備索引n的局部變量,若是是一個雙精度浮點數,那麼它實際佔據了索引n和n+1所表明的存儲空間)虛擬機規範並不要求在局部變量中的64位的值是64位對齊的。虛擬機提供了把局部變量中的值裝載到操做數棧的指令,也提供了把操做數棧中的值寫入局部變量的指令。

運行環境區

在運行環境中包含的信息用於動態連接,正常的方法返回以及異常捕捉。

動態連接

運行環境包括對指向當前類和當前方法的解釋器符號表的指針,用於支持方法代碼的動態連接。方法的class文件代碼在引用要調用的方法和要訪問的變量時使用符號。動態連接把符號形式的方法調用翻譯成實際方法調用,裝載必要的類以解釋尚未定義的符號,並把變量訪問翻譯成與這些變量運行時的存儲結構相應的偏移地址。動態連接方法和變量使得方法中使用的其它類的變化不會影響到本程序的代碼。

正常的方法返回

若是當前方法正常地結束了,在執行了一條具備正確類型的返回指令時,調用的方法會獲得一個返回值。執行環境在正常返回的狀況下用於恢復調用者的寄存器,並把調用者的程序計數器增長一個恰當的數值,以跳過已執行過的方法調用指令,而後在調用者的執行環境中繼續執行下去。

異常捕捉

異常狀況在Java中被稱做Error(錯誤)或Exception(異常),是Throwable類的子類,在程序中的緣由是:動態連接錯,如沒法找到所需的class文件。運行時錯,如對一個空指針的引用。程序使用了throw語句。

當異常發生時,Java虛擬機採起以下措施:

· 檢查與當前方法相聯繫的catch子句表。每一個catch子句包含其有效指令範圍,可以處理的異常類型,以及處理異常的代碼塊地址。

· 與異常相匹配的catch子句應該符合下面的條件:形成異常的指令在其指令範圍以內,發生的異常類型是其能處理的異常類型的子類型。若是找到了匹配的catch子句,那麼系統轉移到指定的異常處理塊處執行;若是沒有找到異常處理塊,重複尋找匹配的catch子句的過程,直到當前方法的全部嵌套的 catch子句都被檢查過。

· 因爲虛擬機從第一個匹配的catch子句處繼續執行,因此catch子句表中的順序是很重要的。由於Java代碼是結構化的,所以總能夠把某個方法的全部的異常處理器都按序排列到一個表中,對任意可能的程序計數器的值,均可以用線性的順序找到合適的異常處理塊,以處理在該程序計數器值下發生的異常狀況。

· 若是找不到匹配的catch子句,那麼當前方法獲得一個"未截獲異常"的結果並返回到當前方法的調用者,好像異常剛剛在其調用者中發生同樣。若是在調用者中仍然沒有找到相應的異常處理塊,那麼這種錯誤將被傳播下去。若是錯誤被傳播到最頂層,那麼系統將調用一個缺省的異常處理塊。

操做數棧區

機器指令只從操做數棧中取操做數,對它們進行操做,並把結果返回到棧中。選擇棧結構的緣由是:在只有少許寄存器或非通用寄存器的機器(如 Intel486)上,也可以高效地模擬虛擬機的行爲。操做數棧是32位的。它用於給方法傳遞參數,並從方法接收結果,也用於支持操做的參數,並保存操做的結果。例如,iadd指令將兩個整數相加。相加的兩個整數應該是操做數棧頂的兩個字。這兩個字是由先前的指令壓進堆棧的。這兩個整數將從堆棧彈出、相加,並把結果壓回到操做數棧中。

每一個原始數據類型都有專門的指令對它們進行必須的操做。每一個操做數在棧中須要一個存儲位置,除了long和double型,它們須要兩個位置。操做數只能被適用於其類型的操做符所操做。例如,壓入兩個int類型的數,若是把它們看成是一個long類型的數則是非法的。在Sun的虛擬機實現中,這個限制由字節碼驗證器強制實行。可是,有少數操做(操做符dupe和swap),用於對運行時數據區進行操做時是不考慮類型的。

本地方法棧,當一個線程調用本地方法時,它就再也不受到虛擬機關於結構和安全限制方面的約束,它既能夠訪問虛擬機的運行期數據區,也能夠使用本地處理器以及任何類型的棧。例如,本地棧是一個C語言的棧,那麼當C程序調用C函數時,函數的參數以某種順序被壓入棧,結果則返回給調用函數。在實現Java虛擬機時,本地方法接口使用的是C語言的模型棧,那麼它的本地方法棧的調度與使用則徹底與C語言的棧相同。

3   Java虛擬機的運行過程

上面對虛擬機的各個部分進行了比較詳細的說明,下面經過一個具體的例子來分析它的運行過程。

虛擬機經過調用某個指定類的方法main啓動,傳遞給main一個字符串數組參數,使指定的類被裝載,同時連接該類所使用的其它的類型,而且初始化它們。例如對於程序:

class HelloApp{

 public static void main(String[] args){

  System.out.println("Hello World!");

  for (int i = 0; i < args.length; i++ ){

   System.out.println(args[i]);

  }

 }

}

編譯後在命令行模式下鍵入: java HelloApp run virtual machine

將經過調用HelloApp的方法main來啓動java虛擬機,傳遞給main一個包含三個字符串"run"、"virtual"、"machine"的數組。如今咱們略述虛擬機在執行HelloApp時可能採起的步驟。

開始試圖執行類HelloApp的main方法,發現該類並無被裝載,也就是說虛擬機當前不包含該類的二進制表明,因而虛擬機使用 ClassLoader試圖尋找這樣的二進制表明。若是這個進程失敗,則拋出一個異常。類被裝載後同時在main方法被調用以前,必須對類 HelloApp與其它類型進行連接而後初始化。連接包含三個階段:檢驗,準備和解析。檢驗檢查被裝載的主類的符號和語義,準備則建立類或接口的靜態域以及把這些域初始化爲標準的默認值,解析負責檢查主類對其它類或接口的符號引用,在這一步它是可選的。類的初始化是對類中聲明的靜態初始化函數和靜態域的初始化構造方法的執行。一個類在初始化以前它的父類必須被初始化。整個過程以下:


圖4:虛擬機的運行過程

4   結束語

本文經過對JVM的體系結構的深刻研究以及一個Java程序執行時虛擬機的運行過程的詳細分析,意在剖析清楚Java虛擬機的機理。

慢慢琢磨JVM

1 JVM簡介

JVM是咱們Javaer的最基本功底了,剛開始學Java的時候,通常都是從「Hello World」開始的,而後會寫個複雜點class,而後再找一些開源框架,好比Spring,Hibernate等等,再而後就開發企業級的應用,好比網站、企業內部應用、實時交易系統等等,直到某一天忽然發現作的系統咋就這麼慢呢,並且時不時還來個內存溢出什麼的,今天是交易系統報了StackOverflowError,明天是網站系統報了個OutOfMemoryError,這種錯誤又很難重現,只有分析Javacore和dump文件,運氣好點還能分析出個結果,運行遭的點,就直接去廟裏燒香吧!天天接客戶的電話都是戰戰兢兢的,生怕再出什麼幺蛾子了。我想Java作的久一點的都有這樣的經歷,那這些問題的最終根結是在哪呢?—— JVM。

JVM全稱是Java Virtual Machine,Java虛擬機,也就是在計算機上再虛擬一個計算機,這和咱們使用 VMWare不同,那個虛擬的東西你是能夠看到的,這個JVM你是看不到的,它存在內存中。咱們知道計算機的基本構成是:運算器、控制器、存儲器、輸入和輸出設備,那這個JVM也是有這成套的元素,運算器是固然是交給硬件CPU還處理了,只是爲了適應「一次編譯,隨處運行」的狀況,須要作一個翻譯動做,因而就用了JVM本身的命令集,這與彙編的命令集有點相似,每一種彙編命令集針對一個系列的CPU,好比8086系列的彙編也是能夠用在8088上的,可是就不能跑在8051上,而JVM的命令集則是能夠處處運行的,由於JVM作了翻譯,根據不一樣的CPU,翻譯成不一樣的機器語言。

JVM中咱們最須要深刻理解的就是它的存儲部分,存儲?硬盤?NO,NO, JVM是一個內存中的虛擬機,那它的存儲就是內存了,咱們寫的全部類、常量、變量、方法都在內存中,這決定着咱們程序運行的是否健壯、是否高效,接下來的部分就是重點介紹之。

2 JVM的組成部分

咱們先把JVM這個虛擬機畫出來,以下圖所示:



 

 

從這個圖中能夠看到,JVM是運行在操做系統之上的,它與硬件沒有直接的交互。咱們再來看下JVM有哪些組成部分,以下圖所示:



  該圖參考了網上廣爲流傳的JVM構成圖,你們看這個圖,整個JVM分爲四部分:

 Class Loader 類加載器

類加載器的做用是加載類文件到內存,好比編寫一個HelloWord.java程序,而後經過javac編譯成class文件,那怎麼才能加載到內存中被執行呢?Class Loader承擔的就是這個責任,那不可能隨便創建一個.class文件就能被加載的,Class Loader加載的class文件是有格式要求,在《JVM Specification》中式這樣定義Class文件的結構:

    ClassFile {

      u4 magic;

      u2 minor_version;

      u2 major_version;

      u2 constant_pool_count;

      cp_info constant_pool[constant_pool_count-1];

      u2 access_flags;

      u2 this_class;

      u2 super_class;

      u2 interfaces_count;

      u2 interfaces[interfaces_count];

      u2 fields_count;

      field_info fields[fields_count];

      u2 methods_count;

      method_info methods[methods_count];

      u2 attributes_count;

      attribute_info attributes[attributes_count];

    }

須要詳細瞭解的話,能夠仔細閱讀《JVM Specification》的第四章「The class File Format」,這裏再也不詳細說明。

友情提示:Class Loader只管加載,只要符合文件結構就加載,至於說能不能運行,則不是它負責的,那是由Execution Engine負責的。

 Execution Engine 執行引擎

執行引擎也叫作解釋器(Interpreter),負責解釋命令,提交操做系統執行。

 Native Interface本地接口

本地接口的做用是融合不一樣的編程語言爲Java所用,它的初衷是融合C/C++程序,Java誕生的時候是C/C++橫行的時候,要想立足,必須有一個聰明的、睿智的調用C/C++程序,因而就在內存中專門開闢了一塊區域處理標記爲native的代碼,它的具體作法是Native Method Stack中登記native方法,在Execution Engine執行時加載native libraies。目前該方法使用的是愈來愈少了,除非是與硬件有關的應用,好比經過Java程序驅動打印機,或者Java系統管理生產設備,在企業級應用中已經比較少見,由於如今的異構領域間的通訊很發達,好比能夠使用Socket通訊,也能夠使用Web Service等等,很少作介紹。

 Runtime data area運行數據區

運行數據區是整個JVM的重點。咱們全部寫的程序都被加載到這裏,以後纔開始運行,Java生態系統如此的繁榮,得益於該區域的優良自治,下一章節詳細介紹之。

 

整個JVM框架由加載器加載文件,而後執行器在內存中處理數據,須要與異構系統交互是能夠經過本地接口進行,瞧,一個完整的系統誕生了!

3 JVM的內存管理

全部的數據和程序都是在運行數據區存放,它包括如下幾部分:

 Stack 

棧也叫棧內存,是Java程序的運行區,是在線程建立時建立,它的生命期是跟隨線程的生命期,線程結束棧內存也就釋放,對於棧來講不存在垃圾回收問題,只要線程一結束,該棧就Over。問題出來了:棧中存的是那些數據呢?又什麼是格式呢?

棧中的數據都是以棧幀(Stack Frame)的格式存在,棧幀是一個內存區塊,是一個數據集,是一個有關方法(Method)和運行期數據的數據集,當一個方法A被調用時就產生了一個棧幀F1,並被壓入到棧中,A方法又調用了B方法,因而產生棧幀F2也被壓入棧,執行完畢後,先彈出F2棧幀,再彈出F1棧幀,遵循「先進後出」原則。

那棧幀中到底存在着什麼數據呢?棧幀中主要保存3類數據:本地變量(Local Variables),包括輸入參數和輸出參數以及方法內的變量;棧操做(Operand Stack),記錄出棧、入棧的操做;棧幀數據(Frame Data),包括類文件、方法等等。光說比較枯燥,咱們畫個圖來理解一下Java棧,以下圖所示:



  圖示在一個棧中有兩個棧幀,棧幀2是最早被調用的方法,先入棧,而後方法2又調用了方法1,棧幀1處於棧頂的位置,棧幀2處於棧底,執行完畢後,依次彈出棧幀1和棧幀2,線程結束,棧釋放。

 Heap 堆內存

一個JVM實例只存在一個堆類存,堆內存的大小是能夠調節的。類加載器讀取了類文件後,須要把類、方法、常變量放到堆內存中,以方便執行器執行,堆內存分爲三部分:

Permanent Space 永久存儲區

永久存儲區是一個常駐內存區域,用於存放JDK自身所攜帶的Class,Interface的元數據,也就是說它存儲的是運行環境必須的類信息,被裝載進此區域的數據是不會被垃圾回收器回收掉的,關閉JVM纔會釋放此區域所佔用的內存。

Young Generation Space 新生區

新生區是類的誕生、成長、消亡的區域,一個類在這裏產生,應用,最後被垃圾回收器收集,結束生命。新生區又分爲兩部分:伊甸區(Eden space)和倖存者區(Survivor pace),全部的類都是在伊甸區被new出來的。倖存區有兩個: 0區(Survivor 0 space)和1區(Survivor 1 space)。當伊甸園的空間用完時,程序又須要建立對象,JVM的垃圾回收器將對伊甸園區進行垃圾回收,將伊甸園區中的再也不被其餘對象所引用的對象進行銷燬。而後將伊甸園中的剩餘對象移動到倖存0區。若倖存0區也滿了,再對該區進行垃圾回收,而後移動到1區。那若是1區也滿了呢?再移動到養老區。

Tenure generation space養老區

養老區用於保存重新生區篩選出來的JAVA對象,通常池對象都在這個區域活躍。   三個區的示意圖以下:



  Method Area 方法區

方法區是被全部線程共享,該區域保存全部字段和方法字節碼,以及一些特殊方法如構造函數,接口代碼也在此定義。

 PC Register 程序計數器

每一個線程都有一個程序計數器,就是一個指針,指向方法區中的方法字節碼,由執行引擎讀取下一條指令。

 Native Method Stack 本地方法棧

 

 

 

4 JVM相關問題

問:堆和棧有什麼區別

 答:堆是存放對象的,可是對象內的臨時變量是存在棧內存中,如例子中的methodVar是在運行期存放到棧中的。

棧是跟隨線程的,有線程就有棧,堆是跟隨JVM的,有JVM就有堆內存。

 

問:堆內存中到底存在着什麼東西?

 答:對象,包括對象變量以及對象方法。

 

問:類變量和實例變量有什麼區別?

 答:靜態變量是類變量,非靜態變量是實例變量,直白的說,有static修飾的變量是靜態變量,沒有static修飾的變量是實例變量。靜態變量存在方法區中,實例變量存在堆內存中。

 

問:我據說類變量是在JVM啓動時就初始化好的,和你這說的不一樣呀!

答:那你是道聽途說,信個人,沒錯。

 

問:Java的方法(函數)究竟是傳值仍是傳址?

 答:都不是,是以傳值的方式傳遞地址,具體的說原生數據類型傳遞的值,引用類型傳遞的地址。對於原始數據類型,JVM的處理方法是從Method Area或Heap中拷貝到Stack,而後運行frame中的方法,運行完畢後再把變量指拷貝回去。

 

問:爲何會產生OutOfMemory產生?

 答:一句話:Heap內存中沒有足夠的可用內存了。這句話要好好理解,不是說Heap沒有內存了,是說新申請內存的對象大於Heap空閒內存,好比如今Heap還空閒1M,可是新申請的內存須要1.1M,因而就會報OutOfMemory了,可能之後的對象申請的內存都只要0.9M,因而就只出現一次OutOfMemory,GC也正常了,看起來像偶發事件,就是這麼回事。       但若是此時GC沒有回收就會產生掛起狀況,系統不響應了。

 

問:我產生的對象很少呀,爲何還會產生OutOfMemory?

 答:你繼承層次忒多了,Heap中 產生的對象是先產生 父類,而後才產生子類,明白不?

 

問:OutOfMemory錯誤分幾種?

 答:分兩種,分別是「OutOfMemoryError:java heap size」和」OutOfMemoryError: PermGen space」,兩種都是內存溢出,heap size是說申請不到新的內存了,這個很常見,檢查應用或調整堆內存大小。

 「PermGen space」是由於永久存儲區滿了,這個也很常見,通常在熱發佈的環境中出現,是由於每次發佈應用系統都不重啓,長此以往永久存儲區中的死對象太多致使新對象沒法申請內存,通常從新啓動一下便可。

 

問:爲何會產生StackOverflowError?

 答:由於一個線程把Stack內存所有耗盡了,通常是遞歸函數形成的。

 

問:一個機器上能夠看多個JVM嗎?JVM之間能夠互訪嗎?

 答:能夠多個JVM,只要機器承受得了。JVM之間是不能夠互訪,你不能在A-JVM中訪問B-JVM的Heap內存,這是不可能的。在之前老版本的JVM中,會出現A-JVM Crack後影響到B-JVM,如今版本很是少見。

 

問:爲何Java要採用垃圾回收機制,而不採用C/C++的顯式內存管理?

 答:爲了簡單,內存管理不是每一個程序員都能折騰好的。

 

問:爲何你沒有詳細介紹垃圾回收機制?

 答:垃圾回收機制每一個JVM都不一樣,JVM Specification只是定義了要自動釋放內存,也就是說它只定義了垃圾回收的抽象方法,具體怎麼實現各個廠商都不一樣,算法各異,這東西實在不必深刻。

 

問:JVM中到底哪些區域是共享的?哪些是私有的?

 答:Heap和Method Area是共享的,其餘都是私有的,

 

問:什麼是JIT,你怎麼沒說?

 答:JIT是指Just In Time,有的文檔把JIT做爲JVM的一個部件來介紹,有的是做爲執行引擎的一部分來介紹,這都能理解。Java剛誕生的時候是一個解釋性語言,別噓,即便編譯成了字節碼(byte code)也是針對JVM的,它須要再次翻譯成原生代碼(native code)才能被機器執行,因而效率的擔心就提出來了。Sun爲了解決該問題提出了一套新的機制,好,你想編譯成原生代碼,沒問題,我在JVM上提供一個工具,把字節碼編譯成原生碼,下次你來訪問的時候直接訪問原生碼就成了,因而JIT就誕生了,就這麼回事。

 

問:JVM還有哪些部分是你沒有提到的?

 答:JVM是一個異常複雜的東西,寫一本磚頭書都不爲過,還有幾個要說明的:

常量池(constant pool):按照順序存放程序中的常量,而且進行索引編號的區域。好比int i =100,這個100就放在常量池中。

安全管理器(Security Manager):提供Java運行期的安全控制,防止惡意攻擊,好比指定讀取文件,寫入文件權限,網絡訪問,建立進程等等,Class Loader在Security Manager認證經過後才能加載class文件的。

方法索引表(Methods table),記錄的是每一個method的地址信息,Stack和Heap中的地址指針實際上是指向Methods table地址。

      

問:爲何不建議在程序中顯式的生命System.gc()?

 答:由於顯式聲明是作堆內存全掃描,也就是Full GC,是須要中止全部的活動的(Stop  The World Collection),你的應用能承受這個嗎?

 

問:JVM有哪些調整參數?

 答:很是多,本身去找,堆內存、棧內存的大小均可以定義,甚至是堆內存的三個部分、新生代的各個比例都能調整。

JVM內存管理:深刻Java內存區域與OOM

Java與C++之間有一堵由內存動態分配和垃圾收集技術所圍成的高牆,牆外面的人想進去,牆裏面的人卻想出來。

 

概述:

對於從事C、C++程序開發的開發人員來講,在內存管理領域,他們便是擁有最高權力的皇帝又是執行最基礎工做的勞動人民——擁有每個對象的「全部權」,又擔負着每個對象生命開始到終結的維護責任。

 

對於Java程序員來講,不須要在爲每個new操做去寫配對的delete/free,不容易出現內容泄漏和內存溢出錯誤,看起來由JVM管理內存一切都很美好。不過,也正是由於Java程序員把內存控制的權力交給了JVM,一旦出現泄漏和溢出,若是不瞭解JVM是怎樣使用內存的,那排查錯誤將會是一件很是困難的事情。

 

VM運行時數據區域

JVM執行Java程序的過程當中,會使用到各類數據區域,這些區域有各自的用途、建立和銷燬時間。根據《Java虛擬機規範(第二版)》(下文稱VM Spec)的規定,JVM包括下列幾個運行時數據區域:

 

1.程序計數器(Program Counter Register):

 

每個Java線程都有一個程序計數器來用於保存程序執行到當前方法的哪個指令,對於非Native方法,這個區域記錄的是正在執行的VM原語的地址,若是正在執行的是Natvie方法,這個區域則爲空(undefined)。此內存區域是惟一一個在VM Spec中沒有規定任何OutOfMemoryError狀況的區域。

 

2.Java虛擬機棧(Java Virtual Machine Stacks)

與程序計數器同樣,VM棧的生命週期也是與線程相同。VM棧描述的是Java方法調用的內存模型:每一個方法被執行的時候,都會同時建立一個幀(Frame)用於存儲本地變量表、操做棧、動態連接、方法出入口等信息。每個方法的調用至完成,就意味着一個幀在VM棧中的入棧至出棧的過程。在後文中,咱們將着重討論VM棧中本地變量表部分。

常常有人把Java內存簡單的區分爲堆內存(Heap)和棧內存(Stack),實際中的區域遠比這種觀點複雜,這樣劃分只是說明與變量定義密切相關的內存區域是這兩塊。其中所指的「堆」後面會專門描述,而所指的「棧」就是VM棧中各個幀的本地變量表部分。本地變量表存放了編譯期可知的各類標量類型(boolean、byte、char、short、int、float、long、double)、對象引用(不是對象自己,僅僅是一個引用指針)、方法返回地址等。其中long和double會佔用2個本地變量空間(32bit),其他佔用1個。本地變量表在進入方法時進行分配,當進入一個方法時,這個方法須要在幀中分配多大的本地變量是一件徹底肯定的事情,在方法運行期間不改變本地變量表的大小。

在VM Spec中對這個區域規定了2中異常情況:若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError異常;若是VM棧能夠動態擴展(VM Spec中容許固定長度的VM棧),當擴展時沒法申請到足夠內存則拋出OutOfMemoryError異常。

3.本地方法棧(Native Method Stacks)

本地方法棧與VM棧所發揮做用是相似的,只不過VM棧爲虛擬機運行VM原語服務,而本地方法棧是爲虛擬機使用到的Native方法服務。它的實現的語言、方式與結構並無強制規定,甚至有的虛擬機(譬如Sun Hotspot虛擬機)直接就把本地方法棧和VM棧合二爲一。和VM棧同樣,這個區域也會拋出StackOverflowError和OutOfMemoryError異常。


4.Java堆(Java Heap)

對於絕大多數應用來講,Java堆是虛擬機管理最大的一塊內存。Java堆是被全部線程共享的,在虛擬機啓動時建立。Java堆的惟一目的就是存放對象實例,絕大部分的對象實例都在這裏分配。這一點在VM Spec中的描述是:全部的實例以及數組都在堆上分配(原文:The heap is the runtime data area from which memory for all class instances and arrays is allocated),可是在逃逸分析和標量替換優化技術出現後,VM Spec的描述就顯得並不那麼準確了。

Java堆內還有更細緻的劃分:新生代、老年代,再細緻一點的:eden、from survivor、to survivor,甚至更細粒度的本地線程分配緩衝(TLAB)等,不管對Java堆如何劃分,目的都是爲了更好的回收內存,或者更快的分配內存,在本章中咱們僅僅針對內存區域的做用進行討論,Java堆中的上述各個區域的細節,可參見本文第二章《JVM內存管理:深刻垃圾收集器與內存分配策略》。

根據VM Spec的要求,Java堆能夠處於物理上不連續的內存空間,它邏輯上是連續的便可,就像咱們的磁盤空間同樣。實現時能夠選擇實現成固定大小的,也能夠是可擴展的,不過當前全部商業的虛擬機都是按照可擴展來實現的(經過-Xmx和-Xms控制)。若是在堆中沒法分配內存,而且堆也沒法再擴展時,將會拋出OutOfMemoryError異常。

5.方法區(Method Area)

叫「方法區」可能認識它的人還不太多,若是叫永久代(Permanent Generation)它的粉絲也許就多了。它還有個別名叫作Non-Heap(非堆),可是VM Spec上則描述方法區爲堆的一個邏輯部分(原文:the method area is logically part of the heap),這個名字的問題還真容易使人產生誤解,咱們在這裏就不糾結了。

方法區中存放了每一個Class的結構信息,包括常量池、字段描述、方法描述等等。VM Space描述中對這個區域的限制很是寬鬆,除了和Java堆同樣不須要連續的內存,也能夠選擇固定大小或者可擴展外,甚至能夠選擇不實現垃圾收集。相對來講,垃圾收集行爲在這個區域是相對比較少發生的,但並非某些描述那樣永久代不會發生GC(至少對當前主流的商業JVM實現來講是如此),這裏的GC主要是對常量池的回收和對類的卸載,雖然回收的「成績」通常也比較差強人意,尤爲是類卸載,條件至關苛刻。

6.運行時常量池(Runtime Constant Pool)

Class文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量表(constant_pool table),用於存放編譯期已可知的常量,這部份內容將在類加載後進入方法區(永久代)存放。可是Java語言並不要求常量必定只有編譯期預置入Class的常量表的內容才能進入方法區常量池,運行期間也可將新內容放入常量池(最典型的String.intern()方法)。

運行時常量池是方法區的一部分,天然受到方法區內存的限制,當常量池沒法在申請到內存時會拋出OutOfMemoryError異常。

 

7.本機直接內存(Direct Memory)

直接內存並非虛擬機運行時數據區的一部分,它根本就是本機內存而不是VM直接管理的區域。可是這部份內存也會致使OutOfMemoryError異常出現,所以咱們放到這裏一塊兒描述。

在JDK1.4中新加入了NIO類,引入一種基於渠道與緩衝區的I/O方式,它能夠經過本機Native函數庫直接分配本機內存,而後經過一個存儲在Java堆裏面的DirectByteBuffer對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在Java對和本機堆中來回複製數據。

顯然本機直接內存的分配不會受到Java堆大小的限制,可是即然是內存那確定仍是要受到本機物理內存(包括SWAP區或者Windows虛擬內存)的限制的,通常服務器管理員配置JVM參數時,會根據實際內存設置-Xmx等參數信息,但常常忽略掉直接內存,使得各個內存區域總和大於物理內存限制(包括物理的和操做系統級的限制),而致使動態擴展時出現OutOfMemoryError異常。

 

實戰OutOfMemoryError

上述區域中,除了程序計數器,其餘在VM Spec中都描述了產生OutOfMemoryError(下稱OOM)的情形,那咱們就實戰模擬一下,經過幾段簡單的代碼,令對應的區域產生OOM異常以便加深認識,同時初步介紹一些與內存相關的虛擬機參數。下文的代碼都是基於Sun Hotspot虛擬機1.6版的實現,對於不一樣公司的不一樣版本的虛擬機,參數與程序運行結果可能結果會有所差異。

 

Java堆

 

Java堆存放的是對象實例,所以只要不斷創建對象,而且保證GC Roots到對象之間有可達路徑便可產生OOM異常。測試中限制Java堆大小爲20M,不可擴展,經過參數-XX:+HeapDumpOnOutOfMemoryError讓虛擬機在出現OOM異常的時候Dump出內存映像以便分析。(關於Dump映像文件分析方面的內容,可參見本文第三章《JVM內存管理:深刻JVM內存異常分析與調優》。)

清單1:Java堆OOM測試

/**

 * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

 * @author zzm

 */

public class HeapOOM {

 

       static class OOMObject {

       }

 

       public static void main(String[] args) {

              List<OOMObject> list = new ArrayList<OOMObject>();

 

              while (true) {

                     list.add(new OOMObject());

              }

       }

}

 

運行結果:

java.lang.OutOfMemoryError: Java heap space

Dumping heap to java_pid3404.hprof ...

Heap dump file created [22045981 bytes in 0.663 secs]

 

 

VM棧和本地方法棧

 

Hotspot虛擬機並不區分VM棧和本地方法棧,所以-Xoss參數其實是無效的,棧容量只由-Xss參數設定。關於VM棧和本地方法棧在VM Spec描述了兩種異常:StackOverflowError與OutOfMemoryError,當棧空間沒法繼續分配分配時,究竟是內存過小仍是棧太大其實某種意義上是對同一件事情的兩種描述而已,在筆者的實驗中,對於單線程應用嘗試下面3種方法均沒法讓虛擬機產生OOM,所有嘗試結果都是得到SOF異常。

 

1.使用-Xss參數削減棧內存容量。結果:拋出SOF異常時的堆棧深度相應縮小。

2.定義大量的本地變量,增大此方法對應幀的長度。結果:拋出SOF異常時的堆棧深度相應縮小。

3.建立幾個定義不少本地變量的複雜對象,打開逃逸分析和標量替換選項,使得JIT編譯器容許對象拆分後在棧中分配。結果:實際效果同第二點。

 

清單2:VM棧和本地方法棧OOM測試(僅做爲第1點測試程序)

/**

 * VM Args:-Xss128k

 * @author zzm

 */

public class JavaVMStackSOF {

 

       private int stackLength = 1;

 

       public void stackLeak() {

              stackLength++;

              stackLeak();

       }

 

       public static void main(String[] args) throws Throwable {

              JavaVMStackSOF oom = new JavaVMStackSOF();

              try {

                     oom.stackLeak();

              } catch (Throwable e) {

                     System.out.println("stack length:" + oom.stackLength);

                     throw e;

              }

       }

}

 

運行結果:

stack length:2402

Exception in thread "main" java.lang.StackOverflowError

        at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:20)

        at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21)

        at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21)

 

若是在多線程環境下,不斷創建線程卻是能夠產生OOM異常,可是基本上這個異常和VM棧空間夠不夠關係沒有直接關係,甚至是給每一個線程的VM棧分配的內存越多反而越容易產生這個OOM異常。

 

緣由其實很好理解,操做系統分配給每一個進程的內存是有限制的,譬如32位Windows限制爲2G,Java堆和方法區的大小JVM有參數能夠限制最大值,那剩餘的內存爲2G(操做系統限制)-Xmx(最大堆)-MaxPermSize(最大方法區),程序計數器消耗內存很小,能夠忽略掉,那虛擬機進程自己耗費的內存不計算的話,剩下的內存就供每個線程的VM棧和本地方法棧瓜分了,那天然每一個線程中VM棧分配內存越多,就越容易把剩下的內存耗盡。

 

清單3:建立線程致使OOM異常

/**

 * VM Args:-Xss2M (這時候不妨設大些)

 * @author zzm

 */

public class JavaVMStackOOM {

 

       private void dontStop() {

              while (true) {

              }

       }

 

       public void stackLeakByThread() {

              while (true) {

                     Thread thread = new Thread(new Runnable() {

                            @Override

                            public void run() {

                                   dontStop();

                            }

                     });

                     thread.start();

              }

       }

 

       public static void main(String[] args) throws Throwable {

              JavaVMStackOOM oom = new JavaVMStackOOM();

              oom.stackLeakByThread();

       }

}

 

特別提示一下,若是讀者要運行上面這段代碼,記得要存盤當前工做,上述代碼執行時有很大令操做系統卡死的風險。

 

運行結果:

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread

 

運行時常量池

 

要在常量池裏添加內容,最簡單的就是使用String.intern()這個Native方法。因爲常量池分配在方法區內,咱們只須要經過-XX:PermSize和-XX:MaxPermSize限制方法區大小便可限制常量池容量。實現代碼以下:

 

清單4:運行時常量池致使的OOM異常

/**

 * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M

 * @author zzm

 */

public class RuntimeConstantPoolOOM {

 

       public static void main(String[] args) {

              // 使用List保持着常量池引用,壓制Full GC回收常量池行爲

              List<String> list = new ArrayList<String>();

              // 10M的PermSize在integer範圍內足夠產生OOM了

              int i = 0;

              while (true) {

                     list.add(String.valueOf(i++).intern());

              }

       }

}

 

運行結果:

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space

       at java.lang.String.intern(Native Method)

       at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)

 

 

方法區

 

上文講過,方法區用於存放Class相關信息,因此這個區域的測試咱們藉助CGLib直接操做字節碼動態生成大量的Class,值得注意的是,這裏咱們這個例子中模擬的場景其實常常會在實際應用中出現:當前不少主流框架,如Spring、Hibernate對類進行加強時,都會使用到CGLib這類字節碼技術,當加強的類越多,就須要越大的方法區用於保證動態生成的Class能夠加載入內存。

 

清單5:藉助CGLib使得方法區出現OOM異常

/**

 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M

 * @author zzm

 */

public class JavaMethodAreaOOM {

 

       public static void main(String[] args) {

              while (true) {

                     Enhancer enhancer = new Enhancer();

                     enhancer.setSuperclass(OOMObject.class);

                     enhancer.setUseCache(false);

                     enhancer.setCallback(new MethodInterceptor() {

                            public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {

                                   return proxy.invokeSuper(obj, args);

                            }

                     });

                     enhancer.create();

              }

       }

 

       static class OOMObject {

 

       }

}

 

運行結果:

Caused by: java.lang.OutOfMemoryError: PermGen space

       at java.lang.ClassLoader.defineClass1(Native Method)

       at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)

       at java.lang.ClassLoader.defineClass(ClassLoader.java:616)

       ... 8 more

 

本機直接內存

 

DirectMemory容量可經過-XX:MaxDirectMemorySize指定,不指定的話默認與Java堆(-Xmx指定)同樣,下文代碼越過了DirectByteBuffer,直接經過反射獲取Unsafe實例進行內存分配(Unsafe類的getUnsafe()方法限制了只有引導類加載器纔會返回實例,也就是基本上只有rt.jar裏面的類的才能使用),由於DirectByteBuffer也會拋OOM異常,但拋出異常時實際上並無真正向操做系統申請分配內存,而是經過計算得知沒法分配既會拋出,真正申請分配的方法是unsafe.allocateMemory()。

 

/**

 * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M

 * @author zzm

 */

public class DirectMemoryOOM {

 

       private static final int _1MB = 1024 * 1024;

 

       public static void main(String[] args) throws Exception {

              Field unsafeField = Unsafe.class.getDeclaredFields()[0];

              unsafeField.setAccessible(true);

              Unsafe unsafe = (Unsafe) unsafeField.get(null);

              while (true) {

                     unsafe.allocateMemory(_1MB);

              }

       }

}

 

運行結果:

Exception in thread "main" java.lang.OutOfMemoryError

       at sun.misc.Unsafe.allocateMemory(Native Method)

       at org.fenixsoft.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:20)

 

 

總結

到此爲止,咱們弄清楚虛擬機裏面的內存是如何劃分的,哪部分區域,什麼樣的代碼、操做可能致使OOM異常。雖然Java有垃圾收集機制,但OOM仍然離咱們並不遙遠,本章內容咱們只是知道各個區域OOM異常出現的緣由,下一章咱們將看看Java垃圾收集機制爲了不OOM異常出現,作出了什麼樣的努力。

java線程安全總結

最近想將java基礎的一些東西都整理整理,寫下來,這是對知識的總結,也是一種樂趣。已經擬好了提綱,大概分爲這幾個主題: java線程安全,java垃圾收集,java併發包詳細介紹,java profile和jvm性能調優 慢慢寫吧。本人jameswxx原創文章,轉載請註明出處,我費了不少心血,多謝了。關於java線程安全,網上有不少資料,我只想從本身的角度總結對這方面的考慮,有時候寫東西是很痛苦的,知道一些東西,但想用文字說清楚,卻不是那麼容易。我認爲要認識java線程安全,必須瞭解兩個主要的點:java的內存模型,java的線程同步機制。特別是內存模型,java的線程同步機制很大程度上都是基於內存模型而設定的。後面我還會寫java併發包的文章,詳細總結如何利用java併發包編寫高效安全的多線程併發程序。暫時寫得比較倉促,後面會慢慢補充完善。

 

淺談java內存模型


       不一樣的平臺,內存模型是不同的,可是jvm的內存模型規範是統一的。其實java的多線程併發問題最終都會反映在java的內存模型上,所謂線程安全無非是要控制多個線程對某個資源的有序訪問或修改。總結java的內存模型,要解決兩個主要的問題:可見性和有序性。咱們都知道計算機有高速緩存的存在,處理器並非每次處理數據都是取內存的。JVM定義了本身的內存模型,屏蔽了底層平臺內存管理細節,對於java開發人員,要清楚在jvm內存模型的基礎上,若是解決多線程的可見性和有序性。
       那麼,何謂可見性? 多個線程之間是不能互相傳遞數據通訊的,它們之間的溝通只能經過共享變量來進行。Java內存模型(JMM)規定了jvm有主內存,主內存是多個線程共享的。當new一個對象的時候,也是被分配在主內存中,每一個線程都有本身的工做內存,工做內存存儲了主存的某些對象的副本,固然線程的工做內存大小是有限制的。當線程操做某個對象時,執行順序以下:
  (1) 從主存複製變量到當前工做內存 (read and load)
  (2) 執行代碼,改變共享變量值 (use and assign)
  (3) 用工做內存數據刷新主存相關內容 (store and write)

JVM規範定義了線程對主存的操做指令:read,load,use,assign,store,write。當一個共享變量在多個線程的工做內存中都有副本時,若是一個線程修改了這個共享變量,那麼其餘線程應該可以看到這個被修改後的值,這就是多線程的可見性問題。
        那麼,什麼是有序性呢 ?線程在引用變量時不能直接從主內存中引用,若是線程工做內存中沒有該變量,則會從主內存中拷貝一個副本到工做內存中,這個過程爲read-load,完成後線程會引用該副本。當同一線程再度引用該字段時,有可能從新從主存中獲取變量副本(read-load-use),也有可能直接引用原來的副本(use),也就是說 read,load,use順序能夠由JVM實現系統決定。
        線程不能直接爲主存中中字段賦值,它會將值指定給工做內存中的變量副本(assign),完成後這個變量副本會同步到主存儲區(store-write),至於什麼時候同步過去,根據JVM實現系統決定.有該字段,則會從主內存中將該字段賦值到工做內存中,這個過程爲read-load,完成後線程會引用該變量副本,當同一線程屢次重複對字段賦值時,好比:

Java代碼 

  1. for(int i=0;i<10;i++)  

  2.  a++;  

 


線程有可能只對工做內存中的副本進行賦值,只到最後一次賦值後才同步到主存儲區,因此assign,store,weite順序能夠由JVM實現系統決定。假設有一個共享變量x,線程a執行x=x+1。從上面的描述中能夠知道x=x+1並非一個原子操做,它的執行過程以下:
1 從主存中讀取變量x副本到工做內存
2 給x加1
3 將x加1後的值寫回主 存
若是另一個線程b執行x=x-1,執行過程以下:
1 從主存中讀取變量x副本到工做內存
2 給x減1
3 將x減1後的值寫回主存 
那麼顯然,最終的x的值是不可靠的。假設x如今爲10,線程a加1,線程b減1,從表面上看,彷佛最終x仍是爲10,可是多線程狀況下會有這種狀況發生:
1:線程a從主存讀取x副本到工做內存,工做內存中x值爲10
2:線程b從主存讀取x副本到工做內存,工做內存中x值爲10
3:線程a將工做內存中x加1,工做內存中x值爲11
4:線程a將x提交主存中,主存中x爲11
5:線程b將工做內存中x值減1,工做內存中x值爲9
6:線程b將x提交到中主存中,主存中x爲9 
一樣,x有可能爲11,若是x是一個銀行帳戶,線程a存款,線程b扣款,顯然這樣是有嚴重問題的,要解決這個問題,必須保證線程a和線程b是有序執行的,而且每一個線程執行的加1或減1是一個原子操做。看看下面代碼:

Java代碼 

  1. public class Account {  

  2.   

  3.     private int balance;  

  4.   

  5.     public Account(int balance) {  

  6.         this.balance = balance;  

  7.     }  

  8.   

  9.     public int getBalance() {  

  10.         return balance;  

  11.     }  

  12.   

  13.     public void add(int num) {  

  14.         balance = balance + num;  

  15.     }  

  16.   

  17.     public void withdraw(int num) {  

  18.         balance = balance - num;  

  19.     }  

  20.   

  21.     public static void main(String[] args) throws InterruptedException {  

  22.         Account account = new Account(1000);  

  23.         Thread a = new Thread(new AddThread(account, 20), "add");  

  24.         Thread b = new Thread(new WithdrawThread(account, 20), "withdraw");  

  25.         a.start();  

  26.         b.start();  

  27.         a.join();  

  28.         b.join();  

  29.         System.out.println(account.getBalance());  

  30.     }  

  31.   

  32.     static class AddThread implements Runnable {  

  33.         Account account;  

  34.         int     amount;  

  35.   

  36.         public AddThread(Account account, int amount) {  

  37.             this.account = account;  

  38.             this.amount = amount;  

  39.         }  

  40.   

  41.         public void run() {  

  42.             for (int i = 0; i < 200000; i++) {  

  43.                 account.add(amount);  

  44.             }  

  45.         }  

  46.     }  

  47.   

  48.     static class WithdrawThread implements Runnable {  

  49.         Account account;  

  50.         int     amount;  

  51.   

  52.         public WithdrawThread(Account account, int amount) {  

  53.             this.account = account;  

  54.             this.amount = amount;  

  55.         }  

  56.   

  57.         public void run() {  

  58.             for (int i = 0; i < 100000; i++) {  

  59.                 account.withdraw(amount);  

  60.             }  

  61.         }  

  62.     }  

  63. }  

 


第一次執行結果爲10200,第二次執行結果爲1060,每次執行的結果都是不肯定的,由於線程的執行順序是不可預見的。這是java同步產生的根源,synchronized關鍵字保證了多個線程對於同步塊是互斥的,synchronized做爲一種同步手段,解決java多線程的執行有序性和內存可見性,而volatile關鍵字之解決多線程的內存可見性問題。後面將會詳細介紹。

 


synchronized關鍵字


        上面說了,java用synchronized關鍵字作爲多線程併發環境的執行有序性的保證手段之一。當一段代碼會修改共享變量,這一段代碼成爲互斥區或臨界區,爲了保證共享變量的正確性,synchronized標示了臨界區。典型的用法以下:

Java代碼 

  1. synchronized(鎖){  

  2.      臨界區代碼  

  3. }   

 


爲了保證銀行帳戶的安全,能夠操做帳戶的方法以下:

Java代碼 

  1. public synchronized void add(int num) {  

  2.      balance = balance + num;  

  3. }  

  4. public synchronized void withdraw(int num) {  

  5.      balance = balance - num;  

  6. }  

 


剛纔不是說了synchronized的用法是這樣的嗎:

Java代碼 

  1. synchronized(鎖){  

  2. 臨界區代碼  

  3. }  

 


那麼對於public synchronized void add(int num)這種狀況,意味着什麼呢?其實這種狀況,鎖就是這個方法所在的對象。同理,若是方法是public  static synchronized void add(int num),那麼鎖就是這個方法所在的class。
        理論上,每一個對象均可以作爲鎖,但一個對象作爲鎖時,應該被多個線程共享,這樣才顯得有意義,在併發環境下,一個沒有共享的對象做爲鎖是沒有意義的。假若有這樣的代碼:

Java代碼 

  1. public class ThreadTest{  

  2.   public void test(){  

  3.      Object lock=new Object();  

  4.      synchronized (lock){  

  5.         //do something  

  6.      }  

  7.   }  

  8. }  

 


lock變量做爲一個鎖存在根本沒有意義,由於它根本不是共享對象,每一個線程進來都會執行Object lock=new Object();每一個線程都有本身的lock,根本不存在鎖競爭。
        每一個鎖對象都有兩個隊列,一個是就緒隊列,一個是阻塞隊列,就緒隊列存儲了將要得到鎖的線程,阻塞隊列存儲了被阻塞的線程,當一個被線程被喚醒(notify)後,纔會進入到就緒隊列,等待cpu的調度。當一開始線程a第一次執行account.add方法時,jvm會檢查鎖對象account的就緒隊列是否已經有線程在等待,若是有則代表account的鎖已經被佔用了,因爲是第一次運行,account的就緒隊列爲空,因此線程a得到了鎖,執行account.add方法。若是剛好在這個時候,線程b要執行account.withdraw方法,由於線程a已經得到了鎖尚未釋放,因此線程b要進入account的就緒隊列,等到獲得鎖後才能夠執行。
一個線程執行臨界區代碼過程以下:
1 得到同步鎖
2 清空工做內存
3 從主存拷貝變量副本到工做內存
4 對這些變量計算
5 將變量從工做內存寫回到主存
6 釋放鎖
可見,synchronized既保證了多線程的併發有序性,又保證了多線程的內存可見性。


生產者/消費者模式


        生產者/消費者模式實際上是一種很經典的線程同步模型,不少時候,並非光保證多個線程對某共享資源操做的互斥性就夠了,每每多個線程之間都是有協做的。
        假設有這樣一種狀況,有一個桌子,桌子上面有一個盤子,盤子裏只能放一顆雞蛋,A專門往盤子裏放雞蛋,若是盤子裏有雞蛋,則一直等到盤子裏沒雞蛋,B專門從盤子裏拿雞蛋,若是盤子裏沒雞蛋,則等待直到盤子裏有雞蛋。其實盤子就是一個互斥區,每次往盤子放雞蛋應該都是互斥的,A的等待其實就是主動放棄鎖,B等待時還要提醒A放雞蛋。
如何讓線程主動釋放鎖
很簡單,調用鎖的wait()方法就好。wait方法是從Object來的,因此任意對象都有這個方法。看這個代碼片斷:

Java代碼 

  1. Object lock=new Object();//聲明瞭一個對象做爲鎖  

  2.    synchronized (lock) {  

  3.        balance = balance - num;  

  4.        //這裏放棄了同步鎖,好不容易獲得,又放棄了  

  5.        lock.wait();  

  6. }  

 


若是一個線程得到了鎖lock,進入了同步塊,執行lock.wait(),那麼這個線程會進入到lock的阻塞隊列。若是調用lock.notify()則會通知阻塞隊列的某個線程進入就緒隊列。
聲明一個盤子,只能放一個雞蛋

Java代碼 

  1. package com.jameswxx.synctest;  

  2. public class Plate{  

  3.   List<Object> eggs=new ArrayList<Object>();  

  4.   public synchronized  Object getEgg(){  

  5.      if(eggs.size()==0){  

  6.         try{  

  7.             wait();  

  8.         }catch(InterruptedException e){  

  9.         }  

  10.      }  

  11.   

  12.     Object egg=eggs.get(0);  

  13.     eggs.clear();//清空盤子  

  14.     notify();//喚醒阻塞隊列的某線程到就緒隊列  

  15.     return egg;  

  16. }  

  17.   

  18.  public synchronized  void putEgg(Object egg){  

  19.     If(eggs.size()>0){  

  20.       try{  

  21.          wait();  

  22.       }catch(InterruptedException e){  

  23.       }  

  24.     }  

  25.     eggs.add(egg);//往盤子裏放雞蛋  

  26.     notify();//喚醒阻塞隊列的某線程到就緒隊列  

  27.   }  

  28. }  

 


聲明一個Plate對象爲plate,被線程A和線程B共享,A專門放雞蛋,B專門拿雞蛋。假設
1 開始,A調用plate.putEgg方法,此時eggs.size()爲0,所以順利將雞蛋放到盤子,還執行了notify()方法,喚醒鎖的阻塞隊列的線程,此時阻塞隊列尚未線程。
2 又有一個A線程對象調用plate.putEgg方法,此時eggs.size()不爲0,調用wait()方法,本身進入了鎖對象的阻塞隊列。
3 此時,來了一個B線程對象,調用plate.getEgg方法,eggs.size()不爲0,順利的拿到了一個雞蛋,還執行了notify()方法,喚醒鎖的阻塞隊列的線程,此時阻塞隊列有一個A線程對象,喚醒後,它進入到就緒隊列,就緒隊列也就它一個,所以立刻獲得鎖,開始往盤子裏放雞蛋,此時盤子是空的,所以放雞蛋成功。
4 假設接着來了線程A,就重複2;假設來料線程B,就重複3。 
整個過程都保證了放雞蛋,拿雞蛋,放雞蛋,拿雞蛋。

 


volatile關鍵字


       volatile是java提供的一種同步手段,只不過它是輕量級的同步,爲何這麼說,由於volatile只能保證多線程的內存可見性,不能保證多線程的執行有序性。而最完全的同步要保證有序性和可見性,例如synchronized。任何被volatile修飾的變量,都不拷貝副本到工做內存,任何修改都及時寫在主存。所以對於Valatile修飾的變量的修改,全部線程立刻就能看到,可是volatile不能保證對變量的修改是有序的。什麼意思呢?假若有這樣的代碼:

Java代碼 

  1. public class VolatileTest{  

  2.   public volatile int a;  

  3.   public void add(int count){  

  4.        a=a+count;  

  5.   }  

  6. }  

 


        當一個VolatileTest對象被多個線程共享,a的值不必定是正確的,由於a=a+count包含了好幾步操做,而此時多個線程的執行是無序的,由於沒有任何機制來保證多個線程的執行有序性和原子性。volatile存在的意義是,任何線程對a的修改,都會立刻被其餘線程讀取到,由於直接操做主存,沒有線程對工做內存和主存的同步。因此,volatile的使用場景是有限的,在有限的一些情形下能夠使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時知足下面兩個條件:
1)對變量的寫操做不依賴於當前值。
2)該變量沒有包含在具備其餘變量的不變式中 
volatile只保證了可見性,因此Volatile適合直接賦值的場景,如

Java代碼 

  1. public class VolatileTest{  

  2.   public volatile int a;  

  3.   public void setA(int a){  

  4.       this.a=a;  

  5.   }  

  6. }  

 


在沒有volatile聲明時,多線程環境下,a的最終值不必定是正確的,由於this.a=a;涉及到給a賦值和將a同步回主存的步驟,這個順序可能被打亂。若是用volatile聲明瞭,讀取主存副本到工做內存和同步a到主存的步驟,至關因而一個原子操做。因此簡單來講,volatile適合這種場景:一個變量被多個線程共享,線程直接給這個變量賦值。這是一種很簡單的同步場景,這時候使用volatile的開銷將會很是小。

 

JVM內存管理:深刻垃圾收集器與內存分配策略

 

Java與C++之間有一堵由內存動態分配和垃圾收集技術所圍成的高牆,牆外面的人想進去,牆裏面的人卻想出來。 

概述:

  提及垃圾收集(Garbage Collection,下文簡稱GC),大部分人都把這項技術當作Java語言的伴生產物。事實上GC的歷史遠遠比Java來得久遠,在1960年誕生於MIT的Lisp是第一門真正使用內存動態分配和垃圾收集技術的語言。當Lisp還在胚胎時期,人們就在思考GC須要完成的3件事情:哪些內存須要回收?何時回收?怎麼樣回收? 

  通過半個世紀的發展,目前的內存分配策略與垃圾回收技術已經至關成熟,一切看起來都進入「自動化」的時代,那爲何咱們還要去了解GC和內存分配?答案很簡單:當須要排查各類內存溢出、泄漏問題時,當垃圾收集成爲系統達到更高併發量的瓶頸時,咱們就須要對這些「自動化」的技術有必要的監控、調節手段。 

  把時間從1960年撥回如今,回到咱們熟悉的Java語言。本文第一章中介紹了Java內存運行時區域的各個部分,其中程序計數器、VM棧、本地方法棧三個區域隨線程而生,隨線程而滅;棧中的幀隨着方法進入、退出而有條不紊的進行着出棧入棧操做;每個幀中分配多少內存基本上是在Class文件生成時就已知的(可能會由JIT動態晚期編譯進行一些優化,但大致上能夠認爲是編譯期可知的),所以這幾個區域的內存分配和回收具有很高的肯定性,所以在這幾個區域不須要過多考慮回收的問題。而Java堆和方法區(包括運行時常量池)則不同,咱們必須等到程序實際運行期間才能知道會建立哪些對象,這部份內存的分配和回收都是動態的,咱們本文後續討論中的「內存」分配與回收僅僅指這一部份內存。 

對象已死?

  在堆裏面存放着Java世界中幾乎全部的對象,在回收前首先要肯定這些對象之中哪些還在存活,哪些已經「死去」了,即不可能再被任何途徑使用的對象。 

引用計數算法(Reference Counting) 

  最初的想法,也是不少教科書判斷對象是否存活的算法是這樣的:給對象中添加一個引用計數器,當有一個地方引用它,計數器加1,當引用失效,計數器減1,任什麼時候刻計數器爲0的對象就是不可能再被使用的。 

  客觀的說,引用計數算法實現簡單,斷定效率很高,在大部分狀況下它都是一個不錯的算法,但引用計數算法沒法解決對象循環引用的問題。舉個簡單的例子:對象A和B分別有字段b、a,令A.b=B和B.a=A,除此以外這2個對象再無任何引用,那實際上這2個對象已經不可能再被訪問,可是引用計數算法卻沒法回收他們。 

根搜索算法(GC Roots Tracing) 

  在實際生產的語言中(Java、C#、甚至包括前面提到的Lisp),都是使用根搜索算法斷定對象是否存活。算法基本思路就是經過一系列的稱爲「GC Roots」的點做爲起始進行向下搜索,當一個對象到GC Roots沒有任何引用鏈(Reference Chain)相連,則證實此對象是不可用的。在Java語言中,GC Roots包括: 

  1.在VM棧(幀中的本地變量)中的引用 
  2.方法區中的靜態引用 
  3.JNI(即通常說的Native方法)中的引用 

生存仍是死亡? 

  斷定一個對象死亡,至少經歷兩次標記過程:若是對象在進行根搜索後,發現沒有與GC Roots相鏈接的引用鏈,那它將會被第一次標記,並在稍後執行他的finalize()方法(若是它有的話)。這裏所謂的「執行」是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束。這點是必須的,不然一個對象在finalize()方法執行緩慢,甚至有死循環什麼的將會很容易致使整個系統崩潰。finalize()方法是對象最後一次逃脫死亡命運的機會,稍後GC將進行第二次規模稍小的標記,若是在finalize()中對象成功拯救本身(只要從新創建到GC Roots的鏈接便可,譬如把本身賦值到某個引用上),那在第二次標記時它將被移除出「即將回收」的集合,若是對象這時候尚未逃脫,那基本上它就真的離死不遠了。 

  須要特別說明的是,這裏對finalize()方法的描述可能帶點悲情的藝術加工,並不表明筆者鼓勵你們去使用這個方法來拯救對象。相反,筆者建議你們儘可能避免使用它,這個不是C/C++裏面的析構函數,它運行代價高昂,不肯定性大,沒法保證各個對象的調用順序。須要關閉外部資源之類的事情,基本上它能作的使用try-finally能夠作的更好。 

關於方法區 

  方法區即後文提到的永久代,不少人認爲永久代是沒有GC的,《Java虛擬機規範》中確實說過能夠不要求虛擬機在這區實現GC,並且這區GC的「性價比」通常比較低:在堆中,尤爲是在新生代,常規應用進行一次GC能夠通常能夠回收70%~95%的空間,而永久代的GC效率遠小於此。雖然VM Spec不要求,但當前生產中的商業JVM都有實現永久代的GC,主要回收兩部份內容:廢棄常量與無用類。這兩點回收思想與Java堆中的對象回收很相似,都是搜索是否存在引用,常量的相對很簡單,與對象相似的斷定便可。而類的回收則比較苛刻,須要知足下面3個條件: 

  1.該類全部的實例都已經被GC,也就是JVM中不存在該Class的任何實例。 
  2.加載該類的ClassLoader已經被GC。 
  3.該類對應的java.lang.Class 對象沒有在任何地方被引用,如不能在任何地方經過反射訪問該類的方法。 

  是否對類進行回收可以使用-XX:+ClassUnloading參數進行控制,還能夠使用-verbose:class或者-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看類加載、卸載信息。 

  在大量使用反射、動態代理、CGLib等bytecode框架、動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都須要JVM具有類卸載的支持以保證永久代不會溢出。 

垃圾收集算法 

  在這節裏不打算大量討論算法實現,只是簡單的介紹一下基本思想以及發展過程。最基礎的蒐集算法是「標記-清除算法」(Mark-Sweep),如它的名字同樣,算法分層「標記」和「清除」兩個階段,首先標記出全部須要回收的對象,而後回收全部須要回收的對象,整個過程其實前一節講對象標記斷定的時候已經基本介紹完了。說它是最基礎的收集算法緣由是後續的收集算法都是基於這種思路並優化其缺點獲得的。它的主要缺點有兩個,一是效率問題,標記和清理兩個過程效率都不高,二是空間問題,標記清理以後會產生大量不連續的內存碎片,空間碎片太多可能會致使後續使用中沒法找到足夠的連續內存而提早觸發另外一次的垃圾蒐集動做。

  爲了解決效率問題,一種稱爲「複製」(Copying)的蒐集算法出現,它將可用內存劃分爲兩塊,每次只使用其中的一塊,當半區內存用完了,僅將還存活的對象複製到另一塊上面,而後就把原來整塊內存空間一次過清理掉。這樣使得每次內存回收都是對整個半區的回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存就能夠了,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲原來的一半,未免過高了一點。 

  如今的商業虛擬機中都是用了這一種收集算法來回收新生代,IBM有專門研究代表新生代中的對象98%是朝生夕死的,因此並不須要按照1:1的比例來劃份內存空間,而是將內存分爲一塊較大的eden空間和2塊較少的survivor空間,每次使用eden和其中一塊survivor,當回收時將eden和survivor還存活的對象一次過拷貝到另一塊survivor空間上,而後清理掉eden和用過的survivor。Sun Hotspot虛擬機默認eden和survivor的大小比例是8:1,也就是每次只有10%的內存是「浪費」的。固然,98%的對象可回收只是通常場景下的數據,咱們沒有辦法保證每次回收都只有10%之內的對象存活,當survivor空間不夠用時,須要依賴其餘內存(譬如老年代)進行分配擔保(Handle Promotion)。 

  複製收集算法在對象存活率高的時候,效率有所降低。更關鍵的是,若是不想浪費50%的空間,就須要有額外的空間進行分配擔保用於應付半區內存中全部對象都100%存活的極端狀況,因此在老年代通常不能直接選用這種算法。所以人們提出另一種「標記-整理」(Mark-Compact)算法,標記過程仍然同樣,但後續步驟不是進行直接清理,而是令全部存活的對象一端移動,而後直接清理掉這端邊界之外的內存。 

  當前商業虛擬機的垃圾收集都是採用「分代收集」(Generational Collecting)算法,這種算法並無什麼新的思想出現,只是根據對象不一樣的存活週期將內存劃分爲幾塊。通常是把Java堆分做新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法,譬如新生代每次GC都有大批對象死去,只有少許存活,那就選用複製算法只須要付出少許存活對象的複製成本就能夠完成收集。 

垃圾收集器 

  垃圾收集器就是收集算法的具體實現,不一樣的虛擬機會提供不一樣的垃圾收集器。而且提供參數供用戶根據本身的應用特色和要求組合各個年代所使用的收集器。本文討論的收集器基於Sun Hotspot虛擬機1.6版。 

圖1.Sun JVM1.6的垃圾收集器 


  圖1展現了1.6中提供的6種做用於不一樣年代的收集器,兩個收集器之間存在連線的話就說明它們能夠搭配使用。在介紹着些收集器以前,咱們先明確一個觀點:沒有最好的收集器,也沒有萬能的收集器,只有最合適的收集器。 

1.Serial收集器 
  單線程收集器,收集時會暫停全部工做線程(咱們將這件事情稱之爲Stop The World,下稱STW),使用複製收集算法,虛擬機運行在Client模式時的默認新生代收集器。 

2.ParNew收集器 
  ParNew收集器就是Serial的多線程版本,除了使用多條收集線程外,其他行爲包括算法、STW、對象分配規則、回收策略等都與Serial收集器一摸同樣。對應的這種收集器是虛擬機運行在Server模式的默認新生代收集器,在單CPU的環境中,ParNew收集器並不會比Serial收集器有更好的效果。 

3.Parallel Scavenge收集器 
  Parallel Scavenge收集器(下稱PS收集器)也是一個多線程收集器,也是使用複製算法,但它的對象分配規則與回收策略都與ParNew收集器有所不一樣,它是以吞吐量最大化(即GC時間佔總運行時間最小)爲目標的收集器實現,它容許較長時間的STW換取總吞吐量最大化。 

4.Serial Old收集器 
  Serial Old是單線程收集器,使用標記-整理算法,是老年代的收集器,上面三種都是使用在新生代收集器。 

5.Parallel Old收集器 
  老年代版本吞吐量優先收集器,使用多線程和標記-整理算法,JVM 1.6提供,在此以前,新生代使用了PS收集器的話,老年代除Serial Old外別無選擇,由於PS沒法與CMS收集器配合工做。 

6.CMS(Concurrent Mark Sweep)收集器 
  CMS是一種以最短停頓時間爲目標的收集器,使用CMS並不能達到GC效率最高(整體GC時間最小),但它能儘量下降GC時服務的停頓時間,這一點對於實時或者高交互性應用(譬如證券交易)來講相當重要,這類應用對於長時間STW通常是不可容忍的。CMS收集器使用的是標記-清除算法,也就是說它在運行期間會產生空間碎片,因此虛擬機提供了參數開啓CMS收集結束後再進行一次內存壓縮。 
內存分配與回收策略 

  瞭解GC其中很重要一點就是了解JVM的內存分配策略:即對象在哪裏分配和對象何時回收。 

  關於對象在哪裏分配,往大方向講,主要就在堆上分配,但也可能通過JIT進行逃逸分析後進行標量替換拆散爲原子類型在棧上分配,也可能分配在DirectMemory中(詳見本文第一章)。往細節處講,對象主要分配在新生代eden上,也可能會直接老年代中,分配的細節決定於當前使用的垃圾收集器類型與VM相關參數設置。咱們能夠經過下面代碼來驗證一下Serial收集器(ParNew收集器的規則與之徹底一致)的內存分配和回收的策略。讀者看完Serial收集器的分析後,不妨本身根據JVM參數文檔寫一些程序去實踐一下其它幾種收集器的分配策略。 

清單1:內存分配測試代碼 

Java代碼 

  1. public class YoungGenGC {  

  2.   

  3.     private static final int _1MB = 1024 * 1024;  

  4.   

  5.     public static void main(String[] args) {  

  6.         // testAllocation();  

  7.         testHandlePromotion();  

  8.         // testPretenureSizeThreshold();  

  9.         // testTenuringThreshold();  

  10.         // testTenuringThreshold2();  

  11.     }  

  12.   

  13.     /** 

  14.      * VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 

  15.      */  

  16.     @SuppressWarnings("unused")  

  17.     public static void testAllocation() {  

  18.         byte[] allocation1, allocation2, allocation3, allocation4;  

  19.         allocation1 = new byte[2 * _1MB];  

  20.         allocation2 = new byte[2 * _1MB];  

  21.         allocation3 = new byte[2 * _1MB];  

  22.         allocation4 = new byte[4 * _1MB];  // 出現一次Minor GC  

  23.     }  

  24.   

  25.     /** 

  26.      * VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 

  27.      * -XX:PretenureSizeThreshold=3145728 

  28.      */  

  29.     @SuppressWarnings("unused")  

  30.     public static void testPretenureSizeThreshold() {  

  31.         byte[] allocation;  

  32.         allocation = new byte[4 * _1MB];  //直接分配在老年代中  

  33.     }  

  34.   

  35.     /** 

  36.      * VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 

  37.      * -XX:+PrintTenuringDistribution 

  38.      */  

  39.     @SuppressWarnings("unused")  

  40.     public static void testTenuringThreshold() {  

  41.         byte[] allocation1, allocation2, allocation3;  

  42.         allocation1 = new byte[_1MB / 4];  // 何時進入老年代決定於XX:MaxTenuringThreshold設置  

  43.         allocation2 = new byte[4 * _1MB];  

  44.         allocation3 = new byte[4 * _1MB];  

  45.         allocation3 = null;  

  46.         allocation3 = new byte[4 * _1MB];  

  47.     }  

  48.   

  49.     /** 

  50.      * VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 

  51.      * -XX:+PrintTenuringDistribution 

  52.      */  

  53.     @SuppressWarnings("unused")  

  54.     public static void testTenuringThreshold2() {  

  55.         byte[] allocation1, allocation2, allocation3, allocation4;  

  56.         allocation1 = new byte[_1MB / 4];   // allocation1+allocation2大於survivo空間一半  

  57.         allocation2 = new byte[_1MB / 4];    

  58.         allocation3 = new byte[4 * _1MB];  

  59.         allocation4 = new byte[4 * _1MB];  

  60.         allocation4 = null;  

  61.         allocation4 = new byte[4 * _1MB];  

  62.     }  

  63.   

  64.     /** 

  65.      * VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:-HandlePromotionFailure 

  66.      */  

  67.     @SuppressWarnings("unused")  

  68.     public static void testHandlePromotion() {  

  69.         byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7;  

  70.         allocation1 = new byte[2 * _1MB];  

  71.         allocation2 = new byte[2 * _1MB];  

  72.         allocation3 = new byte[2 * _1MB];  

  73.         allocation1 = null;  

  74.         allocation4 = new byte[2 * _1MB];  

  75.         allocation5 = new byte[2 * _1MB];  

  76.         allocation6 = new byte[2 * _1MB];  

  77.         allocation4 = null;  

  78.         allocation5 = null;  

  79.         allocation6 = null;  

  80.         allocation7 = new byte[2 * _1MB];  

  81.     }  

  82. }  



規則一:一般狀況下,對象在eden中分配。當eden沒法分配時,觸發一次Minor GC。 

  執行testAllocation()方法後輸出了GC日誌以及內存分配情況。-Xms20M -Xmx20M -Xmn10M這3個參數肯定了Java堆大小爲20M,不可擴展,其中10M分配給新生代,剩下的10M即爲老年代。-XX:SurvivorRatio=8決定了新生代中eden與survivor的空間比例是1:8,從輸出的結果也清晰的看到「eden space 8192K、from space 1024K、to space 1024K」的信息,新生代總可用空間爲9216K(eden+1個survivor)。 

  咱們也注意到在執行testAllocation()時出現了一次Minor GC,GC的結果是新生代6651K變爲148K,而總佔用內存則幾乎沒有減小(由於幾乎沒有可回收的對象)。此次GC是發生的緣由是爲allocation4分配內存的時候,eden已經被佔用了6M,剩餘空間已不足分配allocation4所需的4M內存,所以發生Minor GC。GC期間虛擬機發現已有的3個2M大小的對象所有沒法放入survivor空間(survivor空間只有1M大小),因此直接轉移到老年代去。GC後4M的allocation4對象分配在eden中。 

清單2:testAllocation()方法輸出結果 

[GC [DefNew: 6651K->148K(9216K), 0.0070106 secs] 6651K->6292K(19456K), 0.0070426 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap 
def new generation   total 9216K, used 4326K [0x029d0000, 0x033d0000, 0x033d0000) 
  eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000) 
  from space 1024K,  14% used [0x032d0000, 0x032f5370, 0x033d0000) 
  to   space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000) 
tenured generation   total 10240K, used 6144K [0x033d0000, 0x03dd0000, 0x03dd0000) 
   the space 10240K,  60% used [0x033d0000, 0x039d0030, 0x039d0200, 0x03dd0000) 
compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000) 
   the space 12288K,  17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000) 
No shared spaces configured. 

規則二:配置了PretenureSizeThreshold的狀況下,對象大於設置值將直接在老年代分配。 

  執行testPretenureSizeThreshold()方法後,咱們看到eden空間幾乎沒有被使用,而老年代的10M控件被使用了40%,也就是4M的allocation對象直接就分配在老年代中,則是由於PretenureSizeThreshold被設置爲3M,所以超過3M的對象都會直接從老年代分配。 

清單3: 

Heap 
def new generation   total 9216K, used 671K [0x029d0000, 0x033d0000, 0x033d0000) 
  eden space 8192K,   8% used [0x029d0000, 0x02a77e98, 0x031d0000) 
  from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000) 
  to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000) 
tenured generation   total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000) 
   the space 10240K,  40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000) 
compacting perm gen  total 12288K, used 2107K [0x03dd0000, 0x049d0000, 0x07dd0000) 
   the space 12288K,  17% used [0x03dd0000, 0x03fdefd0, 0x03fdf000, 0x049d0000) 
No shared spaces configured. 

規則三:在eden通過GC後存活,而且survivor能容納的對象,將移動到survivor空間內,若是對象在survivor中繼續熬過若干次回收(默認爲15次)將會被移動到老年代中。回收次數由MaxTenuringThreshold設置。 

  分別以-XX:MaxTenuringThreshold=1和-XX:MaxTenuringThreshold=15兩種設置來執行testTenuringThreshold(),方法中allocation1對象須要256K內存,survivor空間能夠容納。當MaxTenuringThreshold=1時,allocation1對象在第二次GC發生時進入老年代,新生代已使用的內存GC後很是乾淨的變成0KB。而MaxTenuringThreshold=15時,第二次GC發生後,allocation1對象則還留在新生代survivor空間,這時候新生代仍然有404KB被佔用。 

清單4: 
MaxTenuringThreshold=1 

[GC [DefNew 
Desired survivor size 524288 bytes, new threshold 1 (max 1) 
- age   1:     414664 bytes,     414664 total 
: 4859K->404K(9216K), 0.0065012 secs] 4859K->4500K(19456K), 0.0065283 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[GC [DefNew 
Desired survivor size 524288 bytes, new threshold 1 (max 1) 
: 4500K->0K(9216K), 0.0009253 secs] 8596K->4500K(19456K), 0.0009458 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap 
def new generation   total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000) 
  eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000) 
  from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000) 
  to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000) 
tenured generation   total 10240K, used 4500K [0x033d0000, 0x03dd0000, 0x03dd0000) 
   the space 10240K,  43% used [0x033d0000, 0x03835348, 0x03835400, 0x03dd0000) 
compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000) 
   the space 12288K,  17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000) 
No shared spaces configured. 

MaxTenuringThreshold=15 
[GC [DefNew 
Desired survivor size 524288 bytes, new threshold 15 (max 15) 
- age   1:     414664 bytes,     414664 total 
: 4859K->404K(9216K), 0.0049637 secs] 4859K->4500K(19456K), 0.0049932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC [DefNew 
Desired survivor size 524288 bytes, new threshold 15 (max 15) 
- age   2:     414520 bytes,     414520 total 
: 4500K->404K(9216K), 0.0008091 secs] 8596K->4500K(19456K), 0.0008305 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap 
def new generation   total 9216K, used 4582K [0x029d0000, 0x033d0000, 0x033d0000) 
  eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000) 
  from space 1024K,  39% used [0x031d0000, 0x03235338, 0x032d0000) 
  to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000) 
tenured generation   total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000) 
   the space 10240K,  40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000) 
compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000) 
   the space 12288K,  17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000) 
No shared spaces configured. 

規則四:若是在survivor空間中相同年齡全部對象大小的累計值大於survivor空間的一半,大於或等於個年齡的對象就能夠直接進入老年代,無需達到MaxTenuringThreshold中要求的年齡。 

執行testTenuringThreshold2()方法,並將設置-XX:MaxTenuringThreshold=15,發現運行結果中survivor佔用仍然爲0%,而老年代比預期增長了6%,也就是說allocation一、allocation2對象都直接進入了老年代,而沒有等待到15歲的臨界年齡。由於這2個對象加起來已經到達了512K,而且它們是同年的,知足同年對象達到survivor空間的一半規則。咱們只要註釋掉其中一個對象new操做,就會發現另一個就不會晉升到老年代中去了。 

清單5: 
[GC [DefNew 
Desired survivor size 524288 bytes, new threshold 1 (max 15) 
- age   1:     676824 bytes,     676824 total 
: 5115K->660K(9216K), 0.0050136 secs] 5115K->4756K(19456K), 0.0050443 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] 
[GC [DefNew 
Desired survivor size 524288 bytes, new threshold 15 (max 15) 
: 4756K->0K(9216K), 0.0010571 secs] 8852K->4756K(19456K), 0.0011009 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap 
def new generation   total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000) 
  eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000) 
  from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000) 
  to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000) 
tenured generation   total 10240K, used 4756K [0x033d0000, 0x03dd0000, 0x03dd0000) 
   the space 10240K,  46% used [0x033d0000, 0x038753e8, 0x03875400, 0x03dd0000) 
compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000) 
   the space 12288K,  17% used [0x03dd0000, 0x03fe09a0, 0x03fe0a00, 0x049d0000) 
No shared spaces configured. 

規則五:在Minor GC觸發時,會檢測以前每次晉升到老年代的平均大小是否大於老年代的剩餘空間,若是大於,改成直接進行一次Full GC,若是小於則查看HandlePromotionFailure設置看看是否容許擔保失敗,若是容許,那仍然進行Minor GC,若是不容許,則也要改成進行一次Full GC。 

前面提到過,新生代纔有複製收集算法,但爲了內存利用率,只使用其中一個survivor空間來做爲輪換備份,所以當出現大量對象在GC後仍然存活的狀況(最極端就是GC後全部對象都存活),就須要老年代進行分配擔保,把survivor沒法容納的對象直接放入老年代。與生活中貸款擔保相似,老年代要進行這樣的擔保,前提就是老年代自己還有容納這些對象的剩餘空間,一共有多少對象在GC以前是沒法明確知道的,因此取以前每一次GC晉升到老年代對象容量的平均值與老年代的剩餘空間進行比較決定是否進行Full GC來讓老年代騰出更多空間。 

取平均值進行比較其實仍然是一種動態機率的手段,也就是說若是某次Minor GC存活後的對象突增,大大高於平均值的話,依然會致使擔保失敗,這樣就只好在失敗後從新進行一次Full GC。雖然擔保失敗時作的繞的圈子是最大的,但大部分狀況下都仍是會將HandlePromotionFailure打開,避免Full GC過於頻繁。 

清單6: 
HandlePromotionFailure = false 

[GC [DefNew: 6651K->148K(9216K), 0.0078936 secs] 6651K->4244K(19456K), 0.0079192 secs] [Times: user=0.00 sys=0.02, real=0.02 secs] 
[GC [DefNew: 6378K->6378K(9216K), 0.0000206 secs][Tenured: 4096K->4244K(10240K), 0.0042901 secs] 10474K->4244K(19456K), [Perm : 2104K->2104K(12288K)], 0.0043613 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

HandlePromotionFailure = true 

[GC [DefNew: 6651K->148K(9216K), 0.0054913 secs] 6651K->4244K(19456K), 0.0055327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC [DefNew: 6378K->148K(9216K), 0.0006584 secs] 10474K->4244K(19456K), 0.0006857 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

總結 

  本章介紹了垃圾收集的算法、6款主要的垃圾收集器,以及經過代碼實例具體介紹了新生代串行收集器對內存分配及回收的影響。 

  GC在不少時候都是系統併發度的決定性因素,虛擬機之因此提供多種不一樣的收集器,提供大量的調節參數,是由於只有根據實際應用需求、實現方式選擇最優的收集方式才能獲取最好的性能。沒有固定收集器、參數組合,也沒有最優的調優方法,虛擬機也沒有什麼必然的行爲。筆者看過一些文章,撇開具體場景去談論老年代達到92%會觸發Full GC(92%應當來自CMS收集器觸發的默認臨界點)、98%時間在進行垃圾收集系統會拋出OOM異常(98%應該來自parallel收集器收集時間比率的默認臨界點)其實意義並不太大。所以學習GC若是要到實踐調優階段,必須瞭解每一個具體收集器的行爲、優點劣勢、調節參數。 

JVM調優總結(一)

 

數據類型

Java虛擬機中,數據類型能夠分爲兩類:基本類型引用類型。基本類型的變量保存原始值,即:他表明的值就是數值自己;而引用類型的變量保存引用值。「引用值」表明了某個對象的引用,而不是對象自己,對象自己存放在這個引用值所表示的地址的位置。

基本類型包括:byte,short,int,long,char,float,double,Boolean,returnAddress

引用類型包括:類類型接口類型數組

堆與棧

  

堆和棧是程序運行的關鍵,頗有必要把他們的關係說清楚。

 

   

    棧是運行時的單位,而堆是存儲的單位

  棧解決程序的運行問題,即程序如何執行,或者說如何處理數據;堆解決的是數據存儲的問題,即數據怎麼放、放在哪兒。

在Java中一個線程就會相應有一個線程棧與之對應,這點很容易理解,由於不一樣的線程執行邏輯有所不一樣,所以須要一個獨立的線程棧。而堆則是全部線程共享的。棧由於是運行單位,所以裏面存儲的信息都是跟當前線程(或程序)相關信息的。包括局部變量、程序運行狀態、方法返回值等等;而堆只負責存儲對象信息。

    爲何要把堆和棧區分出來呢?棧中不是也能夠存儲數據嗎

第一,從軟件設計的角度看,棧表明了處理邏輯,而堆表明了數據。這樣分開,使得處理邏輯更爲清晰。分而治之的思想。這種隔離、模塊化的思想在軟件設計的方方面面都有體現。

第二,堆與棧的分離,使得堆中的內容能夠被多個棧共享(也能夠理解爲多個線程訪問同一個對象)。這種共享的收益是不少的。一方面這種共享提供了一種有效的數據交互方式(如:共享內存),另外一方面,堆中的共享常量和緩存能夠被全部棧訪問,節省了空間。

第三,棧由於運行時的須要,好比保存系統運行的上下文,須要進行地址段的劃分。因爲棧只能向上增加,所以就會限制住棧存儲內容的能力。而堆不一樣,堆中的對象是能夠根據須要動態增加的,所以棧和堆的拆分,使得動態增加成爲可能,相應棧中只需記錄堆中的一個地址便可。

第四,面向對象就是堆和棧的完美結合。其實,面向對象方式的程序與之前結構化的程序在執行上沒有任何區別。可是,面向對象的引入,使得對待問題的思考方式發生了改變,而更接近於天然方式的思考。當咱們把對象拆開,你會發現,對象的屬性其實就是數據,存放在堆中;而對象的行爲(方法),就是運行邏輯,放在棧中。咱們在編寫對象的時候,其實即編寫了數據結構,也編寫的處理數據的邏輯。不得不認可,面向對象的設計,確實很美。

    在Java中,Main函數就是棧的起始點,也是程序的起始點

程序要運行老是有一個起點的。同C語言同樣,java中的Main就是那個起點。不管什麼java程序,找到main就找到了程序執行的入口:)

    堆中存什麼?棧中存什麼

堆中存的是對象。棧中存的是基本數據類型堆中對象的引用。一個對象的大小是不可估計的,或者說是能夠動態變化的,可是在棧中,一個對象只對應了一個4btye的引用(堆棧分離的好處:))。

爲何不把基本類型放堆中呢?由於其佔用的空間通常是1~8個字節——須要空間比較少,並且由於是基本類型,因此不會出現動態增加的狀況——長度固定,所以棧中存儲就夠了,若是把他存在堆中是沒有什麼意義的(還會浪費空間,後面說明)。能夠這麼說,基本類型和對象的引用都是存放在棧中,並且都是幾個字節的一個數,所以在程序運行時,他們的處理方式是統一的。可是基本類型、對象引用和對象自己就有所區別了,由於一個是棧中的數據一個是堆中的數據。最多見的一個問題就是,Java中參數傳遞時的問題。

    Java中的參數傳遞時傳值呢?仍是傳引用

要說明這個問題,先要明確兩點:

 1. 不要試圖與C進行類比,Java中沒有指針的概念

  2. 程序運行永遠都是在棧中進行的,於是參數傳遞時,只存在傳遞基本類型和對象引用的問題。不會直接傳對象自己。

明確以上兩點後。Java在方法調用傳遞參數時,由於沒有指針,因此它都是進行傳值調用(這點能夠參考C的傳值調用)。所以,不少書裏面都說Java是進行傳值調用,這點沒有問題,並且也簡化的C中複雜性。

可是傳引用的錯覺是如何形成的呢?在運行棧中,基本類型和引用的處理是同樣的,都是傳值,因此,若是是傳引用的方法調用,也同時能夠理解爲「傳引用值」的傳值調用,即引用的處理跟基本類型是徹底同樣的。可是當進入被調用方法時,被傳遞的這個引用的值,被程序解釋(或者查找)到堆中的對象,這個時候纔對應到真正的對象。若是此時進行修改,修改的是引用對應的對象,而不是引用自己,即:修改的是堆中的數據。因此這個修改是能夠保持的了。

對象,從某種意義上說,是由基本類型組成的。能夠把一個對象看做爲一棵樹,對象的屬性若是仍是對象,則仍是一顆樹(即非葉子節點),基本類型則爲樹的葉子節點。程序參數傳遞時,被傳遞的值自己都是不能進行修改的,可是,若是這個值是一個非葉子節點(即一個對象引用),則能夠修改這個節點下面的全部內容。

 

堆和棧中,棧是程序運行最根本的東西。程序運行能夠沒有堆,可是不能沒有棧。而堆是爲棧進行數據存儲服務,說白了堆就是一塊共享的內存。不過,正是由於堆和棧的分離的思想,才使得Java的垃圾回收成爲可能。

Java中,棧的大小經過-Xss來設置,當棧中存儲數據比較多時,須要適當調大這個值,不然會出現java.lang.StackOverflowError異常。常見的出現這個異常的是沒法返回的遞歸,由於此時棧中保存的信息都是方法返回的記錄點。

JVM調優總結(二)

Java對象的大小

基本數據的類型的大小是固定的,這裏就很少說了。對於非基本類型的Java對象,其大小就值得商榷。

在Java中,一個空Object對象的大小是8byte,這個大小隻是保存堆中一個沒有任何屬性的對象的大小。看下面語句:

Object ob = new Object();

這樣在程序中完成了一個Java對象的生命,可是它所佔的空間爲:4byte+8byte。4byte是上面部分所說的Java棧中保存引用的所須要的空間。而那8byte則是Java堆中對象的信息。由於全部的Java非基本類型的對象都須要默認繼承Object對象,所以不論什麼樣的Java對象,其大小都必須是大於8byte。

有了Object對象的大小,咱們就能夠計算其餘對象的大小了。

Class NewObject {

    int count;

    boolean flag;

    Object ob;

}

其大小爲:空對象大小(8byte)+int大小(4byte)+Boolean大小(1byte)+空Object引用的大小(4byte)=17byte。可是由於Java在對對象內存分配時都是以8的整數倍來分,所以大於17byte的最接近8的整數倍的是24,所以此對象的大小爲24byte。

這裏須要注意一下基本類型的包裝類型的大小。由於這種包裝類型已經成爲對象了,所以須要把他們做爲對象來看待。包裝類型的大小至少是12byte(聲明一個空Object至少須要的空間),並且12byte沒有包含任何有效信息,同時,由於Java對象大小是8的整數倍,所以一個基本類型包裝類的大小至少是16byte。這個內存佔用是很恐怖的,它是使用基本類型的N倍(N>2),有些類型的內存佔用更是誇張(隨便想下就知道了)。所以,可能的話應儘可能少使用包裝類。在JDK5.0之後,由於加入了自動類型裝換,所以,Java虛擬機會在存儲方面進行相應的優化。

引用類型

對象引用類型分爲強引用、軟引用、弱引用和虛引用

 

強引用:就是咱們通常聲明對象是時虛擬機生成的引用,強引用環境下,垃圾回收時須要嚴格判斷當前對象是否被強引用,若是被強引用,則不會被垃圾回收

 

軟引用:軟引用通常被作爲緩存來使用。與強引用的區別是,軟引用在垃圾回收時,虛擬機會根據當前系統的剩餘內存來決定是否對軟引用進行回收。若是剩餘內存比較緊張,則虛擬機會回收軟引用所引用的空間;若是剩餘內存相對富裕,則不會進行回收。換句話說,虛擬機在發生OutOfMemory時,確定是沒有軟引用存在的。

 

弱引用:弱引用與軟引用相似,都是做爲緩存來使用。但與軟引用不一樣,弱引用在進行垃圾回收時,是必定會被回收掉的,所以其生命週期只存在於一個垃圾回收週期內。

 

強引用不用說,咱們系統通常在使用時都是用的強引用。而「軟引用」和「弱引用」比較少見。他們通常被做爲緩存使用,並且通常是在內存大小比較受限的狀況下作爲緩存。由於若是內存足夠大的話,能夠直接使用強引用做爲緩存便可,同時可控性更高。於是,他們常見的是被使用在桌面應用系統的緩存。

JVM調優總結(三)-基本垃圾回收算法

能夠從不一樣的的角度去劃分垃圾回收算法:

按照基本回收策略分

引用計數(Reference Counting):

比較古老的回收算法。原理是此對象有一個引用,即增長一個計數,刪除一個引用則減小一個計數。垃圾回收時,只用收集計數爲0的對象。此算法最致命的是沒法處理循環引用的問題。

 

標記-清除(Mark-Sweep):


 

 

此算法執行分兩階段。第一階段從引用根節點開始標記全部被引用的對象,第二階段遍歷整個堆,把未標記的對象清除。此算法須要暫停整個應用,同時,會產生內存碎片。

 

複製(Copying):


 

 

此算法把內存空間劃爲兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的對象複製到另一個區域中。次算法每次只處理正在使用中的對象,所以複製成本比較小,同時複製過去之後還能進行相應的內存整理,不會出現「碎片」問題。固然,此算法的缺點也是很明顯的,就是須要兩倍內存空間。

 

標記-整理(Mark-Compact):


 

 

此算法結合了「標記-清除」和「複製」兩個算法的優勢。也是分兩階段,第一階段從根節點開始標記全部被引用對象,第二階段遍歷整個堆,把清除未標記對象而且把存活對象「壓縮」到堆的其中一塊,按順序排放。此算法避免了「標記-清除」的碎片問題,同時也避免了「複製」算法的空間問題。

按分區對待的方式分

增量收集(Incremental Collecting):實時垃圾回收算法,即:在應用進行的同時進行垃圾回收。不知道什麼緣由JDK5.0中的收集器沒有使用這種算法的。

 

分代收集(Generational Collecting):基於對對象生命週期分析後得出的垃圾回收算法。把對象分爲年青代、年老代、持久代,對不一樣生命週期的對象使用不一樣的算法(上述方式中的一個)進行回收。如今的垃圾回收器(從J2SE1.2開始)都是使用此算法的。

 

按系統線程分

串行收集:串行收集使用單線程處理全部垃圾回收工做,由於無需多線程交互,實現容易,並且效率比較高。可是,其侷限性也比較明顯,即沒法使用多處理器的優點,因此此收集適合單處理器機器。固然,此收集器也能夠用在小數據量(100M左右)狀況下的多處理器機器上。

 

並行收集:並行收集使用多線程處理垃圾回收工做,於是速度快,效率高。並且理論上CPU數目越多,越能體現出並行收集器的優點。

 

併發收集:相對於串行收集和並行收集而言,前面兩個在進行垃圾回收工做時,須要暫停整個運行環境,而只有垃圾回收程序在運行,所以,系統在垃圾回收時會有明顯的暫停,並且暫停時間會由於堆越大而越長。

JVM調優總結(四)-垃圾回收面臨的問題

如何區分垃圾

 

上面說到的「引用計數」法,經過統計控制生成對象和刪除對象時的引用數來判斷。垃圾回收程序收集計數爲0的對象便可。可是這種方法沒法解決循環引用。因此,後來實現的垃圾判斷算法中,都是從程序運行的根節點出發,遍歷整個對象引用,查找存活的對象。那麼在這種方式的實現中,垃圾回收從哪兒開始的呢?即,從哪兒開始查找哪些對象是正在被當前系統使用的。上面分析的堆和棧的區別,其中棧是真正進行程序執行地方,因此要獲取哪些對象正在被使用,則須要從Java棧開始。同時,一個棧是與一個線程對應的,所以,若是有多個線程的話,則必須對這些線程對應的全部的棧進行檢查。


同時,除了棧外,還有系統運行時的寄存器等,也是存儲程序運行數據的。這樣,以棧或寄存器中的引用爲起點,咱們能夠找到堆中的對象,又從這些對象找到對堆中其餘對象的引用,這種引用逐步擴展,最終以null引用或者基本類型結束,這樣就造成了一顆以Java棧中引用所對應的對象爲根節點的一顆對象樹,若是棧中有多個引用,則最終會造成多顆對象樹。在這些對象樹上的對象,都是當前系統運行所須要的對象,不能被垃圾回收。而其餘剩餘對象,則能夠視爲沒法被引用到的對象,能夠被當作垃圾進行回收。

所以,垃圾回收的起點是一些根對象(java棧, 靜態變量, 寄存器...)。而最簡單的Java棧就是Java程序執行的main函數。這種回收方式,也是上面提到的「標記-清除」的回收方式

 

 

如何處理碎片

因爲不一樣Java對象存活時間是不必定的,所以,在程序運行一段時間之後,若是不進行內存整理,就會出現零散的內存碎片。碎片最直接的問題就是會致使沒法分配大塊的內存空間,以及程序運行效率下降。因此,在上面提到的基本垃圾回收算法中,「複製」方式和「標記-整理」方式,均可以解決碎片的問題。

 

 

如何解決同時存在的對象建立和對象回收問題

垃圾回收線程是回收內存的,而程序運行線程則是消耗(或分配)內存的,一個回收內存,一個分配內存,從這點看,二者是矛盾的。所以,在現有的垃圾回收方式中,要進行垃圾回收前,通常都須要暫停整個應用(即:暫停內存的分配),而後進行垃圾回收,回收完成後再繼續應用。這種實現方式是最直接,並且最有效的解決兩者矛盾的方式。

可是這種方式有一個很明顯的弊端,就是當堆空間持續增大時,垃圾回收的時間也將會相應的持續增大,對應應用暫停的時間也會相應的增大。一些對相應時間要求很高的應用,好比最大暫停時間要求是幾百毫秒,那麼當堆空間大於幾個G時,就頗有可能超過這個限制,在這種狀況下,垃圾回收將會成爲系統運行的一個瓶頸。爲解決這種矛盾,有了併發垃圾回收算法,使用這種算法,垃圾回收線程與程序運行線程同時運行。在這種方式下,解決了暫停的問題,可是由於須要在新生成對象的同時又要回收對象,算法複雜性會大大增長,系統的處理能力也會相應下降,同時,「碎片」問題將會比較難解決。

JVM調優總結(五)-分代垃圾回收詳述1

爲何要分代

分代的垃圾回收策略,是基於這樣一個事實:不一樣的對象的生命週期是不同的。所以,不一樣生命週期的對象能夠採起不一樣的收集方式,以便提升回收效率。

 

在Java程序運行的過程當中,會產生大量的對象,其中有些對象是與業務信息相關,好比Http請求中的Session對象、線程、Socket鏈接,這類對象跟業務直接掛鉤,所以生命週期比較長。可是還有一些對象,主要是程序運行過程當中生成的臨時變量,這些對象生命週期會比較短,好比:String對象,因爲其不變類的特性,系統會產生大量的這些對象,有些對象甚至只用一次便可回收。

 

試想,在不進行對象存活時間區分的狀況下,每次垃圾回收都是對整個堆空間進行回收,花費時間相對會長,同時,由於每次回收都須要遍歷全部存活對象,但實際上,對於生命週期長的對象而言,這種遍歷是沒有效果的,由於可能進行了不少次遍歷,可是他們依舊存在。所以,分代垃圾回收採用分治的思想,進行代的劃分,把不一樣生命週期的對象放在不一樣代上,不一樣代上採用最適合它的垃圾回收方式進行回收。

 

如何分代


 

如圖所示:

 

虛擬機中的共劃分爲三個代:年輕代(Young Generation)、年老點(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java類的類信息,與垃圾收集要收集的Java對象關係不大。年輕代和年老代的劃分是對垃圾收集影響比較大的。

 

 

年輕代:

全部新生成的對象首先都是放在年輕代的。年輕代的目標就是儘量快速的收集掉那些生命週期短的對象。年輕代分三個區。一個Eden區,兩個Survivor區(通常而言)。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被複制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活對象將被複制到另一個Survivor區,當這個Survivor去也滿了的時候,從第一個Survivor區複製過來的而且此時還存活的對象,將被複制「年老區(Tenured)」。須要注意,Survivor的兩個區是對稱的,沒前後關係,因此同一個區中可能同時存在從Eden複製過來 對象,和從前一個Survivor複製過來的對象,而複製到年老區的只有從第一個Survivor去過來的對象。並且,Survivor區總有一個是空的。同時,根據程序須要,Survivor區是能夠配置爲多個的(多於兩個),這樣能夠增長對象在年輕代中的存在時間,減小被放到年老代的可能。

 

年老代:

在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。所以,能夠認爲年老代中存放的都是一些生命週期較長的對象。

 

持久代:

用於存放靜態文件,現在Java類、方法等。持久代對垃圾回收沒有顯著影響,可是有些應用可能動態生成或者調用一些class,例如Hibernate等,在這種時候須要設置一個比較大的持久代空間來存放這些運行過程當中新增的類。持久代大小經過-XX:MaxPermSize=<N>進行設置。

 

什麼狀況下觸發垃圾回收

因爲對象進行了分代處理,所以垃圾回收區域、時間也不同。GC有兩種類型:Scavenge GCFull GC

 

Scavenge GC

通常狀況下,當新對象生成,而且在Eden申請空間失敗時,就會觸發Scavenge GC,對Eden區域進行GC,清除非存活對象,而且把尚且存活的對象移動到Survivor區。而後整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。由於大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,因此Eden區的GC會頻繁進行。於是,通常在這裏須要使用速度快、效率高的算法,使Eden去能儘快空閒出來。

 

Full GC

對整個堆進行整理,包括Young、Tenured和Perm。Full GC由於須要對整個對進行回收,因此比Scavenge GC要慢,所以應該儘量減小Full GC的次數。在對JVM調優的過程當中,很大一部分工做就是對於FullGC的調節。有以下緣由可能致使Full GC:

· 年老代(Tenured)被寫滿

· 持久代(Perm)被寫滿 

· System.gc()被顯示調用 

·上一次GC以後Heap的各域分配策略動態變化

JVM調優總結(六)-分代垃圾回收詳述2

分代垃圾回收流程示意





 

 

選擇合適的垃圾收集算法

串行收集器


 

用單線程處理全部垃圾回收工做,由於無需多線程交互,因此效率比較高。可是,也沒法使用多處理器的優點,因此此收集器適合單處理器機器。固然,此收集器也能夠用在小數據量(100M左右)狀況下的多處理器機器上。能夠使用-XX:+UseSerialGC打開。

 

 

 

並行收集器


 

 

對年輕代進行並行垃圾回收,所以能夠減小垃圾回收時間。通常在多線程多處理器機器上使用。使用-XX:+UseParallelGC.打開。並行收集器在J2SE5.0第六6更新上引入,在Java SE6.0中進行了加強--能夠對年老代進行並行收集。若是年老代不使用併發收集的話,默認是使用單線程進行垃圾回收,所以會制約擴展能力。使用-XX:+UseParallelOldGC打開。

使用-XX:ParallelGCThreads=<N>設置並行垃圾回收的線程數。此值能夠設置與機器處理器數量相等。

此收集器能夠進行以下配置:

最大垃圾回收暫停:指定垃圾回收時的最長暫停時間,經過-XX:MaxGCPauseMillis=<N>指定。<N>爲毫秒.若是指定了此值的話,堆大小和垃圾回收相關參數會進行調整以達到指定值。設定此值可能會減小應用的吞吐量。

吞吐量:吞吐量爲垃圾回收時間與非垃圾回收時間的比值,經過-XX:GCTimeRatio=<N>來設定,公式爲1/(1+N)。例如,-XX:GCTimeRatio=19時,表示5%的時間用於垃圾回收。默認狀況爲99,即1%的時間用於垃圾回收。

 

 

 

併發收集器

能夠保證大部分工做都併發進行(應用不中止),垃圾回收只暫停不多的時間,此收集器適合對響應時間要求比較高的中、大規模應用。使用-XX:+UseConcMarkSweepGC打開。


併發收集器主要減小年老代的暫停時間,他在應用不中止的狀況下使用獨立的垃圾回收線程,跟蹤可達對象。在每一個年老代垃圾回收週期中,在收集初期併發收集器 會對整個應用進行簡短的暫停,在收集中還會再暫停一次。第二次暫停會比第一次稍長,在此過程當中多個線程同時進行垃圾回收工做。

併發收集器使用處理器換來短暫的停頓時間。在一個N個處理器的系統上,併發收集部分使用K/N個可用處理器進行回收,通常狀況下1<=K<=N/4。

在只有一個處理器的主機上使用併發收集器,設置爲incremental mode模式也可得到較短的停頓時間。

 

浮動垃圾:因爲在應用運行的同時進行垃圾回收,因此有些垃圾可能在垃圾回收進行完成時產生,這樣就形成了「Floating Garbage」,這些垃圾須要在下次垃圾回收週期時才能回收掉。因此,併發收集器通常須要20%的預留空間用於這些浮動垃圾。

 

Concurrent Mode Failure:併發收集器在應用運行時進行收集,因此須要保證堆在垃圾回收的這段時間有足夠的空間供程序使用,不然,垃圾回收還未完成,堆空間先滿了。這種狀況下將會發生「併發模式失敗」,此時整個應用將會暫停,進行垃圾回收。

 

啓動併發收集器:由於併發收集在應用運行時進行收集,因此必須保證收集完成以前有足夠的內存空間供程序使用,不然會出現「Concurrent Mode Failure」。經過設置-XX:CMSInitiatingOccupancyFraction=<N>指定還有多少剩餘堆時開始執行併發收集

 

 

小結

串行處理器:

--適用狀況:數據量比較小(100M左右);單處理器下而且對響應時間無要求的應用。 
--缺點:只能用於小型應用

 

並行處理器:

--適用狀況:「對吞吐量有高要求」,多CPU、對應用響應時間無要求的中、大型應用。舉例:後臺處理、科學計算。 
--缺點:垃圾收集過程當中應用響應時間可能加長

 

併發處理器:

--適用狀況:「對響應時間有高要求」,多CPU、對應用響應時間有較高要求的中、大型應用。舉例:Web服務器/應用服務器、電信交換、集成開發環境。

 

JVM調優總結(七)-典型配置舉例1

如下配置主要針對分代垃圾回收算法而言。

 

堆大小設置

年輕代的設置很關鍵

JVM中最大堆大小有三方面限制:相關操做系統的數據模型(32-bt仍是64-bit)限制;系統的可用虛擬內存限制;系統的可用物理內存限制。32位系統下,通常限制在1.5G~2G;64爲操做系統對內存無限制。在Windows Server 2003 系統,3.5G物理內存,JDK5.0下測試,最大可設置爲1478m。

典型設置:

java -Xmx3550m -Xms3550m -Xmn2g –Xss128k

-Xmx3550m:設置JVM最大可用內存爲3550M。

-Xms3550m:設置JVM促使內存爲3550m。此值能夠設置與-Xmx相同,以免每次垃圾回收完成後JVM從新分配內存。

-Xmn2g:設置年輕代大小爲2G。整個堆大小=年輕代大小 + 年老代大小 + 持久代大小。持久代通常固定大小爲64m,因此增大年輕代後,將會減少年老代大小。此值對系統性能影響較大,Sun官方推薦配置爲整個堆的3/8。

-Xss128k:設置每一個線程的堆棧大小。JDK5.0之後每一個線程堆棧大小爲1M,之前每一個線程堆棧大小爲256K。更具應用的線程所需內存大小進行調整。在相同物理內存下,減少這個值能生成更多的線程。可是操做系統對一個進程內的線程數仍是有限制的,不能無限生成,經驗值在3000~5000左右。

 

java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0

-XX:NewRatio=4:設置年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代)。設置爲4,則年輕代與年老代所佔比值爲1:4,年輕代佔整個堆棧的1/5

-XX:SurvivorRatio=4:設置年輕代中Eden區與Survivor區的大小比值。設置爲4,則兩個Survivor區與一個Eden區的比值爲2:4,一個Survivor區佔整個年輕代的1/6

-XX:MaxPermSize=16m:設置持久代大小爲16m。

-XX:MaxTenuringThreshold=0:設置垃圾最大年齡。若是設置爲0的話,則年輕代對象不通過Survivor區,直接進入年老代。對於年老代比較多的應用,能夠提升效率。若是將此值設置爲一個較大值,則年輕代對象會在Survivor區進行屢次複製,這樣能夠增長對象再年輕代的存活時間,增長在年輕代即被回收的概論。

 

回收器選擇

JVM給了三種選擇:串行收集器、並行收集器、併發收集器,可是串行收集器只適用於小數據量的狀況,因此這裏的選擇主要針對並行收集器和併發收集器。默認狀況下,JDK5.0之前都是使用串行收集器,若是想使用其餘收集器須要在啓動時加入相應參數。JDK5.0之後,JVM會根據當前系統配置進行判斷。

吞吐量優先的並行收集器

如上文所述,並行收集器主要以到達必定的吞吐量爲目標,適用於科學技術和後臺處理等。

典型配置:

java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20

-XX:+UseParallelGC:選擇垃圾收集器爲並行收集器。此配置僅對年輕代有效。即上述配置下,年輕代使用併發收集,而年老代仍舊使用串行收集。

-XX:ParallelGCThreads=20:配置並行收集器的線程數,即:同時多少個線程一塊兒進行垃圾回收。此值最好配置與處理器數目相等。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC

-XX:+UseParallelOldGC:配置年老代垃圾收集方式爲並行收集。JDK6.0支持對年老代並行收集。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100

-XX:MaxGCPauseMillis=100:設置每次年輕代垃圾回收的最長時間,若是沒法知足此時間,JVM會自動調全年輕代大小,以知足此值。

n java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy

-XX:+UseAdaptiveSizePolicy:設置此選項後,並行收集器會自動選擇年輕代區大小和相應的Survivor區比例,以達到目標系統規定的最低相應時間或者收集頻率等,此值建議使用並行收集器時,一直打開。

 

響應時間優先的併發收集器

如上文所述,併發收集器主要是保證系統的響應時間,減小垃圾收集時的停頓時間。適用於應用服務器、電信領域等。

典型配置:

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC

-XX:+UseConcMarkSweepGC:設置年老代爲併發收集。測試中配置這個之後,-XX:NewRatio=4的配置失效了,緣由不明。因此,此時年輕代大小最好用-Xmn設置。

-XX:+UseParNewGC: 設置年輕代爲並行收集。可與CMS收集同時使用。JDK5.0以上,JVM會根據系統配置自行設置,因此無需再設置此值。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection

-XX:CMSFullGCsBeforeCompaction:因爲併發收集器不對內存空間進行壓縮、整理,因此運行一段時間之後會產生「碎片」,使得運行效率下降。此值設置運行多少次GC之後對內存空間進行壓縮、整理。

-XX:+UseCMSCompactAtFullCollection:打開對年老代的壓縮。可能會影響性能,可是能夠消除碎片

 

輔助信息

JVM提供了大量命令行參數,打印信息,供調試使用。主要有如下一些:

-XX:+PrintGC:輸出形式:[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]

-XX:+PrintGCDetails:輸出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

-XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可與上面兩個混合使用 
輸出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]

-XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中斷的執行時間。可與上面混合使用。輸出形式:Application time: 0.5291524 seconds

-XX:+PrintGCApplicationStoppedTime:打印垃圾回收期間程序暫停的時間。可與上面混合使用。輸出形式:Total time for which application threads were stopped: 0.0468229 seconds

-XX:PrintHeapAtGC: 打印GC先後的詳細堆棧信息。輸出形式:

34.702: [GC {Heap before gc invocations=7:

def new generation   total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)

eden space 49152K,  99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)

from space 6144K,  55% used [0x221d0000, 0x22527e10, 0x227d0000)

to   space 6144K,   0% used [0x21bd0000, 0x21bd0000, 0x221d0000)

tenured generation   total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)

the space 69632K,   3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)

compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)

   the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)

ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)

rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)

34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:

def new generation   total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)

eden space 49152K,   0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)

  from space 6144K,  55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)

  to   space 6144K,   0% used [0x221d0000, 0x221d0000, 0x227d0000)

tenured generation   total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)

the space 69632K,   4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)

compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)

   the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)

   ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)

   rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)

}

, 0.0757599 secs]

-Xloggc:filename:與上面幾個配合使用,把相關日誌信息記錄到文件以便分析。

JVM調優總結(八)-典型配置舉例2

常見配置彙總

 

堆設置

-Xms:初始堆大小

-Xmx:最大堆大小

-XX:NewSize=n:設置年輕代大小

-XX:NewRatio=n:設置年輕代和年老代的比值。如:爲3,表示年輕代與年老代比值爲1:3,年輕代佔整個年輕代年老代和的1/4

-XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區佔整個年輕代的1/5

-XX:MaxPermSize=n:設置持久代大小

收集器設置

-XX:+UseSerialGC:設置串行收集器

-XX:+UseParallelGC:設置並行收集器

-XX:+UseParalledlOldGC:設置並行年老代收集器

-XX:+UseConcMarkSweepGC:設置併發收集器

垃圾回收統計信息

-XX:+PrintGC

   -XX:+PrintGCDetails

   -XX:+PrintGCTimeStamps

   -Xloggc:filename

並行收集器設置

-XX:ParallelGCThreads=n:設置並行收集器收集時使用的CPU數。並行收集線程數。

-XX:MaxGCPauseMillis=n:設置並行收集最大暫停時間

-XX:GCTimeRatio=n:設置垃圾回收時間佔程序運行時間的百分比。公式爲1/(1+n)

併發收集器設置

-XX:+CMSIncrementalMode:設置爲增量模式。適用於單CPU狀況。

-XX:ParallelGCThreads=n:設置併發收集器年輕代收集方式爲並行收集時,使用的CPU數。並行收集線程數。

 

調優總結

年輕代大小選擇

響應時間優先的應用:儘量設大,直到接近系統的最低響應時間限制(根據實際狀況選擇)。在此種狀況下,年輕代收集發生的頻率也是最小的。同時,減小到達年老代的對象。

吞吐量優先的應用:儘量的設置大,可能到達Gbit的程度。由於對響應時間沒有要求,垃圾收集能夠並行進行,通常適合8CPU以上的應用。

 

 

年老代大小選擇

 

響應時間優先的應用:年老代使用併發收集器,因此其大小須要當心設置,通常要考慮併發會話率會話持續時間等一些參數。若是堆設置小了,能夠會形成內存碎片、高回收頻率以及應用暫停而使用傳統的標記清除方式;若是堆大了,則須要較長的收集時間。最優化的方案,通常須要參考如下數據得到:

1. 併發垃圾收集信息

2. 持久代併發收集次數

3. 傳統GC信息

4. 花在年輕代和年老代回收上的時間比例

減小年輕代和年老代花費的時間,通常會提升應用的效率

 

 

吞吐量優先的應用

通常吞吐量優先的應用都有一個很大的年輕代和一個較小的年老代。緣由是,這樣能夠儘量回收掉大部分短時間對象,減小中期的對象,而年老代盡存放長期存活對象。

 

 

較小堆引發的碎片問題

由於年老代的併發收集器使用標記、清除算法,因此不會對堆進行壓縮。當收集器回收時,他會把相鄰的空間進行合併,這樣能夠分配給較大的對象。可是,當堆空間較小時,運行一段時間之後,就會出現「碎片」,若是併發收集器找不到足夠的空間,那麼併發收集器將會中止,而後使用傳統的標記、清除方式進行回收。若是出現「碎片」,可能須要進行以下配置:

1. -XX:+UseCMSCompactAtFullCollection:使用併發收集器時,開啓對年老代的壓縮。

2. -XX:CMSFullGCsBeforeCompaction=0:上面配置開啓的狀況下,這裏設置多少次Full GC後,對年老代進行壓縮

JVM調優總結(九)-新一代的垃圾回收算法

垃圾回收的瓶頸

傳統分代垃圾回收方式,已經在必定程度上把垃圾回收給應用帶來的負擔降到了最小,把應用的吞吐量推到了一個極限。可是他沒法解決的一個問題,就是Full GC所帶來的應用暫停。在一些對實時性要求很高的應用場景下,GC暫停所帶來的請求堆積和請求失敗是沒法接受的。這類應用可能要求請求的返回時間在幾百甚至幾十毫秒之內,若是分代垃圾回收方式要達到這個指標,只能把最大堆的設置限制在一個相對較小範圍內,可是這樣有限制了應用自己的處理能力,一樣也是不可接收的。

分代垃圾回收方式確實也考慮了實時性要求而提供了併發回收器,支持最大暫停時間的設置,可是受限於分代垃圾回收的內存劃分模型,其效果也不是很理想。

爲了達到實時性的要求(其實Java語言最初的設計也是在嵌入式系統上的),一種新垃圾回收方式呼之欲出,它既支持短的暫停時間,又支持大的內存空間分配。能夠很好的解決傳統分代方式帶來的問題。

 

 

增量收集的演進

增量收集的方式在理論上能夠解決傳統分代方式帶來的問題。增量收集把對堆空間劃分紅一系列內存塊,使用時,先使用其中一部分(不會所有用完),垃圾收集時把以前用掉的部分中的存活對象再放到後面沒有用的空間中,這樣能夠實現一直邊使用邊收集的效果,避免了傳統分代方式整個使用完了再暫停的回收的狀況。

固然,傳統分代收集方式也提供了併發收集,可是他有一個很致命的地方,就是把整個堆作爲一個內存塊,這樣一方面會形成碎片(沒法壓縮),另外一方面他的每次收集都是對整個堆的收集,沒法進行選擇,在暫停時間的控制上仍是很弱。而增量方式,經過內存空間的分塊,偏偏能夠解決上面問題。

 

 

Garbage Firest(G1)

這部分的內容主要參考這裏,這篇文章算是對G1算法論文的解讀。我也沒加什麼東西了。

 

 

目標

從設計目標看G1徹底是爲了大型應用而準備的。

支持很大的堆

高吞吐量

--支持多CPU和垃圾回收線程

--在主線程暫停的狀況下,使用並行收集

--在主線程運行的狀況下,使用併發收集

實時目標:可配置在N毫秒內最多隻佔用M毫秒的時間進行垃圾回收

固然G1要達到實時性的要求,相對傳統的分代回收算法,在性能上會有一些損失。

 

 

算法詳解


G1可謂博採衆家之長,力求到達一種完美。他吸收了增量收集優勢,把整個堆劃分爲一個一個等大小的區域(region)。內存的回收和劃分都以region爲單位;同時,他也吸收了CMS的特色,把這個垃圾回收過程分爲幾個階段,分散一個垃圾回收過程;並且,G1也認同分代垃圾回收的思想,認爲不一樣對象的生命週期不一樣,能夠採起不一樣收集方式,所以,它也支持分代的垃圾回收。爲了達到對回收時間的可預計性,G1在掃描了region之後,對其中的活躍對象的大小進行排序,首先會收集那些活躍對象小的region,以便快速回收空間(要複製的活躍對象少了),由於活躍對象小,裏面能夠認爲多數都是垃圾,因此這種方式被稱爲Garbage First(G1)的垃圾回收算法,即:垃圾優先的回收。

 

 

回收步驟:

 

初始標記(Initial Marking)

G1對於每一個region都保存了兩個標識用的bitmap,一個爲previous marking bitmap,一個爲next marking bitmap,bitmap中包含了一個bit的地址信息來指向對象的起始點。

開始Initial Marking以前,首先併發的清空next marking bitmap,而後中止全部應用線程,並掃描標識出每一個region中root可直接訪問到的對象,將region中top的值放入next top at mark start(TAMS)中,以後恢復全部應用線程。

觸發這個步驟執行的條件爲:

G1定義了一個JVM Heap大小的百分比的閥值,稱爲h,另外還有一個H,H的值爲(1-h)*Heap Size,目前這個h的值是固定的,後續G1也許會將其改成動態的,根據jvm的運行狀況來動態的調整,在分代方式下,G1還定義了一個u以及soft limit,soft limit的值爲H-u*Heap Size,當Heap中使用的內存超過了soft limit值時,就會在一次clean up執行完畢後在應用容許的GC暫停時間範圍內儘快的執行此步驟;

在pure方式下,G1將marking與clean up組成一個環,以便clean up能充分的使用marking的信息,當clean up開始回收時,首先回收可以帶來最多內存空間的regions,當通過屢次的clean up,回收到沒多少空間的regions時,G1從新初始化一個新的marking與clean up構成的環。

 

併發標記(Concurrent Marking)

按照以前Initial Marking掃描到的對象進行遍歷,以識別這些對象的下層對象的活躍狀態,對於在此期間應用線程併發修改的對象的以來關係則記錄到remembered set logs中,新建立的對象則放入比top值更高的地址區間中,這些新建立的對象默認狀態即爲活躍的,同時修改top值。

 

 

最終標記暫停(Final Marking Pause)

當應用線程的remembered set logs未滿時,是不會放入filled RS buffers中的,在這樣的狀況下,這些remebered set logs中記錄的card的修改就會被更新了,所以須要這一步,這一步要作的就是把應用線程中存在的remembered set logs的內容進行處理,並相應的修改remembered sets,這一步須要暫停應用,並行的運行。

 

 

存活對象計算及清除(Live Data Counting and Cleanup)

值得注意的是,在G1中,並非說Final Marking Pause執行完了,就確定執行Cleanup這步的,因爲這步須要暫停應用,G1爲了可以達到準實時的要求,須要根據用戶指定的最大的GC形成的暫停時間來合理的規劃何時執行Cleanup,另外還有幾種狀況也是會觸發這個步驟的執行的:

G1採用的是複製方法來進行收集,必須保證每次的」to space」的空間都是夠的,所以G1採起的策略是當已經使用的內存空間達到了H時,就執行Cleanup這個步驟;

對於full-young和partially-young的分代模式的G1而言,則還有狀況會觸發Cleanup的執行,full-young模式下,G1根據應用可接受的暫停時間、回收young regions須要消耗的時間來估算出一個yound regions的數量值,當JVM中分配對象的young regions的數量達到此值時,Cleanup就會執行;partially-young模式下,則會盡可能頻繁的在應用可接受的暫停時間範圍內執行Cleanup,並最大限度的去執行non-young regions的Cleanup。

 

 

展望

之後JVM的調優或許跟多須要針對G1算法進行調優了。

JVM調優總結(十)-調優方法

JVM調優工具

Jconsole,jProfile,VisualVM

Jconsole : jdk自帶,功能簡單,可是能夠在系統有必定負荷的狀況下使用。對垃圾回收算法有很詳細的跟蹤。詳細說明參考這裏

 

JProfiler:商業軟件,須要付費。功能強大。詳細說明參考這裏

 

VisualVM:JDK自帶,功能強大,與JProfiler相似。推薦。

 

如何調優

觀察內存釋放狀況、集合類檢查、對象樹

上面這些調優工具都提供了強大的功能,可是總的來講通常分爲如下幾類功能

 

堆信息查看


可查看堆空間大小分配(年輕代、年老代、持久代分配)

提供即時的垃圾回收功能

垃圾監控(長時間監控回收狀況)

 


查看堆內類、對象信息查看:數量、類型等

 


對象引用狀況查看

 

有了堆信息查看方面的功能,咱們通常能夠順利解決如下問題:

--年老代年輕代大小劃分是否合理

--內存泄漏

--垃圾回收算法設置是否合理

 

線程監控


線程信息監控:系統線程數量。

線程狀態監控:各個線程都處在什麼樣的狀態下

 


Dump線程詳細信息:查看線程內部運行狀況

死鎖檢查

 

熱點分析


 

 

CPU熱點:檢查系統哪些方法佔用的大量CPU時間

內存熱點:檢查哪些對象在系統中數量最大(必定時間內存活對象和銷燬對象一塊兒統計)

 

這兩個東西對於系統優化頗有幫助。咱們能夠根據找到的熱點,有針對性的進行系統的瓶頸查找和進行系統優化,而不是漫無目的的進行全部代碼的優化。

 

 

快照

快照是系統運行到某一時刻的一個定格。在咱們進行調優的時候,不可能用眼睛去跟蹤全部系統變化,依賴快照功能,咱們就能夠進行系統兩個不一樣運行時刻,對象(或類、線程等)的不一樣,以便快速找到問題

舉例說,我要檢查系統進行垃圾回收之後,是否還有該收回的對象被遺漏下來的了。那麼,我能夠在進行垃圾回收先後,分別進行一次堆狀況的快照,而後對比兩次快照的對象狀況。

 

內存泄漏檢查

內存泄漏是比較常見的問題,並且解決方法也比較通用,這裏能夠重點說一下,而線程、熱點方面的問題則是具體問題具體分析了。

內存泄漏通常能夠理解爲系統資源(各方面的資源,堆、棧、線程等)在錯誤使用的狀況下,致使使用完畢的資源沒法回收(或沒有回收),從而致使新的資源分配請求沒法完成,引發系統錯誤。

內存泄漏對系統危害比較大,由於他能夠直接致使系統的崩潰。

須要區別一下,內存泄漏和系統超負荷二者是有區別的,雖然可能致使的最終結果是同樣的。內存泄漏是用完的資源沒有回收引發錯誤,而系統超負荷則是系統確實沒有那麼多資源能夠分配了(其餘的資源都在使用)。

 

 

年老代堆空間被佔滿

異常: java.lang.OutOfMemoryError: Java heap space

說明:


 

這是最典型的內存泄漏方式,簡單說就是全部堆空間都被沒法回收的垃圾對象佔滿,虛擬機沒法再在分配新空間。

如上圖所示,這是很是典型的內存泄漏的垃圾回收狀況圖。全部峯值部分都是一次垃圾回收點,全部谷底部分表示是一次垃圾回收後剩餘的內存。鏈接全部谷底的點,能夠發現一條由底到高的線,這說明,隨時間的推移,系統的堆空間被不斷佔滿,最終會佔滿整個堆空間。所以能夠初步認爲系統內部可能有內存泄漏。(上面的圖僅供示例,在實際狀況下收集數據的時間須要更長,好比幾個小時或者幾天)

 

解決:

這種方式解決起來也比較容易,通常就是根據垃圾回收先後狀況對比,同時根據對象引用狀況(常見的集合對象引用)分析,基本均可以找到泄漏點。

 

 

持久代被佔滿

異常:java.lang.OutOfMemoryError: PermGen space

說明:

Perm空間被佔滿。沒法爲新的class分配存儲空間而引起的異常。這個異常之前是沒有的,可是在Java反射大量使用的今天這個異常比較常見了。主要緣由就是大量動態反射生成的類不斷被加載,最終致使Perm區被佔滿。

更可怕的是,不一樣的classLoader即使使用了相同的類,可是都會對其進行加載,至關於同一個東西,若是有N個classLoader那麼他將會被加載N次。所以,某些狀況下,這個問題基本視爲無解。固然,存在大量classLoader和大量反射類的狀況其實也很少。

解決:

1. -XX:MaxPermSize=16m

2. 換用JDK。好比JRocket。

 

 

堆棧溢出

異常:java.lang.StackOverflowError

說明:這個就很少說了,通常就是遞歸沒返回,或者循環調用形成

 

 

線程堆棧滿

異常:Fatal: Stack size too small

說明:java中一個線程的空間大小是有限制的。JDK5.0之後這個值是1M。與這個線程相關的數據將會保存在其中。可是當線程空間滿了之後,將會出現上面異常。

解決:增長線程棧大小。-Xss2m。但這個配置沒法解決根本問題,還要看代碼部分是否有形成泄漏的部分。

 

系統內存被佔滿

異常:java.lang.OutOfMemoryError: unable to create new native thread

說明

  這個異常是因爲操做系統沒有足夠的資源來產生這個線程形成的。系統建立線程時,除了要在Java堆中分配內存外,操做系統自己也須要分配資源來建立線程。所以,當線程數量大到必定程度之後,堆中或許還有空間,可是操做系統分配不出資源來了,就出現這個異常了。

分配給Java虛擬機的內存愈多,系統剩餘的資源就越少,所以,當系統內存固定時,分配給Java虛擬機的內存越多,那麼,系統總共可以產生的線程也就越少,二者成反比的關係。同時,能夠經過修改-Xss來減小分配給單個線程的空間,也能夠增長系統總共內生產的線程數。

解決:

1. 從新設計系統減小線程數量。

    2. 線程數量不能減小的狀況下,經過-Xss減少單個線程大小。以便能生產更多的線程。

JVM 幾個重要的參數

<本文提供的設置僅僅是在高壓力, 多CPU, 高內存環境下設置> 

最近對JVM的參數從新看了下, 把應用的JVM參數調整了下。  幾個重要的參數

-server -Xmx3g -Xms3g -XX:MaxPermSize=128m 
-XX:NewRatio=1  eden/old 的比例
-XX:SurvivorRatio=8  s/e的比例 
-XX:+UseParallelGC 
-XX:ParallelGCThreads=8  
-XX:+UseParallelOldGC  這個是JAVA 6出現的參數選項 
-XX:LargePageSizeInBytes=128m 內存頁的大小, 不可設置過大, 會影響Perm的大小。 
-XX:+UseFastAccessorMethods 原始類型的快速優化 
-XX:+DisableExplicitGC  關閉System.gc()



另外 -Xss 是線程棧的大小, 這個參數須要嚴格的測試, 通常小的應用, 若是棧不是很深, 應該是128k夠用的, 不過,咱們的應用調用深度比較大, 還須要作詳細的測試。 這個選項對性能的影響比較大。 建議使用256K的大小.

例子:

-server -Xmx3g -Xms3g -Xmn=1g -XX:MaxPermSize=128m -Xss256k  -XX:MaxTenuringThreshold=10 -XX:+DisableExplicitGC -XX:+UseParallelGC -XX:+UseParallelOld GC -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+AggressiveOpts -XX:+UseBiasedLocking 

 

-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCTimeStamps -XX:+PrintGCDetails 打印參數

=================================================================

另外對於大內存設置的要求:

Linux : 
Large page support is included in 2.6 kernel. Some vendors have backported the code to their 2.4 based releases. To check if your system can support large page memory, try the following:   

# cat /proc/meminfo | grep Huge
HugePages_Total: 0
HugePages_Free: 0
Hugepagesize: 2048 kB
#

If the output shows the three "Huge" variables then your system can support large page memory, but it needs to be configured. If the command doesn't print out anything, then large page support is not available. To configure the system to use large page memory, one must log in as root, then:

1. Increase SHMMAX value. It must be larger than the Java heap size. On a system with 4 GB of physical RAM (or less) the following will make all the memory sharable:

# echo 4294967295 > /proc/sys/kernel/shmmax

2. Specify the number of large pages. In the following example 3 GB of a 4 GB system are reserved for large pages (assuming a large page size of 2048k, then 3g = 3 x 1024m = 3072m = 3072 * 1024k = 3145728k, and 3145728k / 2048k = 1536): 

# echo 1536 > /proc/sys/vm/nr_hugepages

Note the /proc values will reset after reboot so you may want to set them in an init script (e.g. rc.local or sysctl.conf).

=============================================
這個設置, 目前觀察下來的結果是EDEN區域收集明顯速度比較快, 最多幾個ms, 可是,對於FGC, 大約須要0。9, 可是發生時間很是的長, 應該是影響不大。 可是對於非web應用的中間件服務, 這個設置很要不得, 可能致使很嚴重延遲效果. 所以, CMS必然須要被使用, 下面是CMS的重要參數介紹

關於CMS的設置:

使用CMS的前提條件是你有比較的長生命對象, 好比有200M以上的OLD堆佔用。 那麼這個威力很是猛, 能夠極大的提升的FGC的收集能力。 若是你的OLD佔用很是的少, 別用了, 絕對下降你性能, 由於CMS收集有2個STOP WORLD的行爲。 OLD少的清狀況, 根據個人測試, 使用並行收集參數會比較好。


-XX:+UseConcMarkSweepGC   使用CMS內存收集
-XX:+AggressiveHeap 特別說明下:(我感受對於作java cache應用有幫助)

· 試圖是使用大量的物理內存

· 長時間大內存使用的優化,能檢查計算資源(內存, 處理器數量)

· 至少須要256MB內存

· 大量的CPU/內存, (在1.4.1在4CPU的機器上已經顯示有提高)

-XX:+UseParNewGC 容許多線程收集新生代
-XX:+CMSParallelRemarkEnabled  下降標記停頓

-XX+UseCMSCompactAtFullCollection  在FULL GC的時候, 壓縮內存, CMS是不會移動內存的, 所以, 這個很是容易產生碎片, 致使內存不夠用, 所以, 內存的壓縮這個時候就會被啓用。 增長這個參數是個好習慣。 

 

 

壓力測試下合適結果:

-server -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xmx2g -Xms2g -Xmn256m -XX:PermSize=128m -Xss256k -XX:MaxTenuringThreshold=31 -XX:+DisableExplicitGC  -XX:+UseConcMarkSweepGC -XX:+UseParNewGC  -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m  -XX:+UseFastAccessorMethods

 

因爲Jdk1.5.09及以前的bug, 所以, CMS下的GC, 在這些版本的表現是十分糟糕的。  須要另外2個參數來控制cms的啓動時間:

-XX:+UseCMSInitiatingOccupancyOnly   僅僅使用手動定義初始化定義開始CMS收集

-XX:CMSInitiatingOccupancyFraction=70  CMS堆上, 使用70%後開始CMS收集。

 

使用CMS的好處是用盡可能少的新生代、,個人經驗值是128M-256M, 而後老生代利用CMS並行收集, 這樣能保證系統低延遲的吞吐效率。 實際上cms的收集停頓時間很是的短,2G的內存, 大約20-80ms的應用程序停頓時間。

 

=========系統狀況介紹========================

這個例子是測試系統12小時運行後的狀況:

$uname -a

2.4.21-51.EL3.customsmp #1 SMP Fri Jun 27 10:44:12 CST 2008 i686 i686 i386 GNU/Linux

 

$ free -m
             total       used       free     shared    buffers     cached
Mem:          3995       3910         85          0        162       1267
-/+ buffers/cache:       2479       1515
Swap:         2047          0       2047

 

$ jstat -gcutil 23959 1000

 S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT   
 59.06   0.00  45.77  44.45  56.88  15204  324.023    66    1.668  325.691
  0.00  39.66  27.53  44.73  56.88  15205  324.046    66    1.668  325.715
 53.42   0.00  22.80  44.73  56.88  15206  324.073    66    1.668  325.741
  0.00  44.90  13.73  44.76  56.88  15207  324.094    66    1.668  325.762
 51.70   0.00  19.03  44.76  56.88  15208  324.118    66    1.668  325.786
  0.00  61.62  19.44  44.98  56.88  15209  324.148    66    1.668  325.816
 53.03   0.00  14.00  45.09  56.88  15210  324.172    66    1.668  325.840
 53.03   0.00  87.87  45.09  56.88  15210  324.172    66    1.668  325.840
  0.00  50.49  72.00  45.22  56.88  15211  324.198    66    1.668  325.866

 

GC參數配置:

JAVA_OPTS=" -server -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xmx2g -Xms2g -Xmn256m -XX:PermSize=128m -Xss256k -XX:MaxTenuringThreshold=31 -XX:+DisableExplicitGC  -XX:+UseConcMarkSweepGC -XX:+UseParNewGC  -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m  -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 "

實際上咱們能夠看到並行young gc執行時間是: 324.198s/15211=20ms, cms的執行時間是 1.668/66=25ms. 固然嚴格來講, 這麼算是不對的, 世界停頓的時間要比這是數據稍微大5-10ms. 對咱們來講若是不輸出日誌, 對咱們是有參考意義的。

 

32位系統下, 設置成2G, 很是危險, 除非你肯定你的應用佔用的native內存不多, 否則可能致使jvm直接crash。

 

-XX:+AggressiveOpts 加快編譯

-XX:+UseBiasedLocking 鎖機制的性能改善。

 

  

JVM調優總結(十二)-參考資料

能整理出上面一些東西,也是由於站在巨人的肩上。下面是一些參考資料,供你們學習,你們有更好的,能夠繼續完善:)

 

· Java 理論與實踐: 垃圾收集簡史

 

· Java SE 6 HotSpot[tm] Virtual Machine Garbage Collection Tuning

 

· Improving Java Application Performance and Scalability by Reducing Garbage Collection Times and Sizing Memory Using JDK 1.4.1

 

· Hotspot memory management whitepaper

 

· Java Tuning White Paper

 

· Diagnosing a Garbage Collection problem

 

· Java HotSpot VM Options

 

· A Collection of JVM Options

 

· Garbage-First Garbage Collection

 

· Frequently Asked Questions about Garbage Collection in the HotspotTM JavaTM Virtual Machine

· JProfiler試用手記

 

· Java6 JVM參數選項大全

 

· 《深刻Java虛擬機》。雖然過去了不少年,但這本書依舊是經典。

 

 

 這裏是本系列的最後一篇了,很高興你們可以喜歡這系列的文章。期間也提了不少問題,其中有些是我以前沒有想到的或者考慮欠妥的,感謝提出這些問題的朋友,我也學到的很多東西。

相關文章
相關標籤/搜索