原本標題黨想寫成《深刻JVM》,不過不太敢寫,我想一小篇博客我想還不足以說明JVM,在本文中,會就我所知給你們介紹JVM的不少內部知識,概念會相對較粗,由於太細的內容要寫,這裏確定寫不出來;本文主要偏重理論,沒有什麼實踐,中間除一些官方資料外,還有部分自身的理解,因此請你們不要徹底信任本文內容;另外本文會有一小部分糾正之前一篇文章對於intern使用方法的錯誤,本文會在其中說明使用錯誤的緣由,大體文章內容有如下幾個部分:java
一、JVM虛擬內存組成及操做系統地址表程序員
二、新生成對象在HeapSize是如何變化的算法
三、虛擬機如何定義回收算法sql
四、JVM佔用的空間除HeapSize還會佔用什麼,OutOfMemory種類!數據庫
五、糾正錯誤:intern的使用上的錯誤數組
好,如今開始話題吧:服務器
一、JVM虛擬內存組成及操做系統地址表數據結構
1.1.虛擬地址大體概念:在OS層面通常是由邏輯地址映射到線性地址,若是線性地址管理,若是啓動了分頁,那麼線性地址就會轉換到相應的物理地址上,不然就直接認爲是物理地址;程序設計中所用到的地址單元就是邏輯單元,如在C語言中的&表示指定的地址就是邏輯地址;而物理地址也並不是咱們所認爲的RAM,還應該包括網卡、顯存、SWAP等相關內容,也就是由OS所管理全部能夠經過頂層邏輯單元映射到的目標地點,不過絕大部分狀況下只須要考慮RAM便可,尤爲是在服務器上;JVM的虛擬內存地址和操做系統的虛擬內存地址不是一個概念,操做系統的虛擬內存地址至關於在磁盤上劃分的一個SWAP交換區,用於內存,內存與之作page out和page in的操做,這種用於物理內存自己不夠,而地址空間夠用的狀況,一旦程序出現page out這些狀況的時候,程序將會變得很是緩慢,而JVM的虛擬內存是在有效的空間內分配一個連續的線性地址空間,由於JVM想要本身管理內存,分配的堆內存都是在本身的heapSize內部,由於它要實現一些脫離於存儲器自己對非連續堆處理的管理而致使的複雜性,也就是JVM去初始化的時候就會加載一塊很大的內存單元,而後內部的操做都是內部本身完成的。多線程
1.2.內存分配:通常C語言分配內存是初始化將相應的基本內容和代碼段進行加載,可是不會加載運行時候的堆棧內存分配,也就是在運行到某個具體的函數時經過malloc、callloc、realloc等方方申請的區域,這些區域必須從操做系統中從新來分配,使用完成後必須進行free,C++中必須使用delete方法來釋放,你們發現沒有,OS的堆在內存不斷申請和釋放的過程當中,必然會產生許多的內存碎片,從而致使你在申請一塊大內存的時候,須要進行邏輯鏈接,致使在申請的速度減少,固然LINUX採用了將內存塊劃分爲多個不一樣大小的板塊,來較好的處理這個問題,不過片斷仍是存在的,不過這種思想的確是很好的,而JVM是如何完成碎片的處理的呢,後面章節會說到;JVM在初始化的時候就會向OS申請一塊大內存,JVM要求這塊內存在地址空間上是連續的(物理上未必連續),讓全部的程序在這個內部區分配,由本身來管理,因此它內部至關於作了一個小的OS對內存的管理,因此JVM是想讓java程序員不用關心在哪個平臺上寫代碼,可是你必定要關心java怎麼管理內存的;架構
線性地址隨着實際物理內存的增長,將會致使頁表很是大,甚至於致使多層頁表,如內存達到96G這一類,那麼這樣管理起來將會很是麻煩(正常狀況下一個頁只有4K,能夠本身算一下須要多少個管理地址來指向這個4K,這個管理地址太大的時候,又須要其餘的管理地址來管理這個地址,就會致使多層地址,可能到最後,一個大內存有40%都是用於管理內存的,真正使用的可想而知),因此在LINUX高版本中對於內存尋址方面作了改進,就是支持大頁面來支持(實際上是經過一個套件完成的,並不是OS自己),如一個頁的大小爲1M這樣的,可是有一些風險在裏面,它要求大頁面內存要麼放得下你的內存,可是你不能將你的進程一部分放在大頁面內存中,一部分放在OS管理的小頁面內存中,也就是說要麼這塊放得下,要麼就放在其餘地方,可能會致使兩邊正好都差那麼一點點的問題,在OS這邊可使用SWAP,可是系統會很慢,並且SWAP不少的狀況下確定會宕機掉。
1.3.內存分配狀態:一個大的進程若是初始化須要分配一塊大的內存空間,內存空間通常會經歷兩個狀態的轉換過程,首先內存必須是free狀態才能夠被分配,若是的確是該狀態而且空間是夠用的,那麼它首先會佔用那麼大一個坑,在java的heapSize中,就是-Xmx參數指定的,也就是JVM虛擬機最大的內存空間(注意這裏-Xmx並無包含PermSize的空間),這個坑是不容許其它進程所佔用的,內存的狀態爲:reserved的狀態,當須要使用的空間時,內存將會被commited狀態,在JVM初始化時也就是-Xms狀態的內存空間,處於這個狀態的內存若是發現不夠使用(物理內存),此時就會發生swap區域,程序將會變得很是緩慢,可是不會形成宕機,而不少時候在這個時候定位不出緣由,因此咱們爲了讓物理內存不夠用的現象暴露出來能夠被發現,至於能夠定位不是程序代碼的問題,咱們就直接將swap內存禁用掉;有個問題就是既然被reserved的內存就不能被其餘進程所佔用,爲何要在這兩個狀態之間來回倒騰呢?這不是多一個開銷嗎?JVM在來回倒騰的過程當中會致使每一個區域的容量發生相應的變化,必然致使的是FullGC的過程,那麼JVM通常在服務器端如何設置呢?文章後面逐步細化說明。
1.4.JVM內存組織:關於JVM內存組織方面,前面在講述Java垃圾回收的時候已經說起到了,可是講得不太細,有些部分可能算是有錯誤的,因此這裏根據上述操做系統知識以及官方部分資料繼續深刻,不敢說徹底正確,不過至少比之前要更加深刻得多,首先來看下ORACLE官方給出來的一個JVM內存單元的組織圖形:
其實我看過不少次這個圖看得很暈,由於之前不了內存分配中commited與reserved的區別,以致於我當時認爲這副圖是說java的HeapSize是由N多個部分組成的,而且還包含HeapSize的,其實在通過不少資料查閱後,尤爲是看到一些監控工具後,才知道看官方資料也有誤區,呵呵,經過簡化,我本身畫的這副圖但願可以幫助你們理解JVM的大體的內存劃分(這裏僅僅說起JVM本身的內存,也就是HeapSize和PermSize的部分,其他的文章後面說明),這裏僅僅將上面的圖形立起來畫了,當時看起來要方便理解得不少(我的感受):
也就是說,你首先須要將JVM的兩個大板塊分開,一個是HeapSize,也就是上圖左側的部分,右邊部分爲PermSize的尺寸,HeapSize也劃分爲大區域爲Young和Old區域,Young區域內部劃分爲三個部分,一個是Eden和兩個一樣尺寸大小的survivor區域,注意到的人會發現爲何每一個區域內部還有一個virtual區域,這就是咱們上面說的沒有通過commited當時已經佔用了地址列表,它不能被其餘進程所佔用,當時操做系統通常的提示會認爲這是塊剩餘空間,可是其實是隻能被本身使用的,這部分上面已經說起,至於爲何咱們後面來解釋,這裏再提出一些問題,就是爲何JVM要提出這麼多區域劃分來管理呢?若是一個區域能夠管理爲何還要搞得那麼麻煩呢?這麼多區域有什麼用處,咱們在第二章對象的分配中將詳細說明這部份內容。
二、新生成對象在HeapSize是如何變化的
2.1.java新建立對象的方法有哪些:首先學習過java的人可能沒有人不知道new 這個關鍵字,也就是新建立一個對象的關鍵字,當發生new操做時,jvm爲你作了什麼?咱們先把這個問題放下,對於jvm初始化加載專門處理,這裏先說除了new以外還有什麼方式,就是經過java.lang.Class.forName進行動態狀態後,獲取一個新的實例,固然方法有重載,也經過經過ClassLoader進行動態狀態,什麼是動態裝載?爲何有了new還要有動態裝載?而jvm初始化作了什麼?動態裝載和new的區別是什麼?這也是咱們下面要討論的問題,也是PermSize中內容的一大塊部分。
2.2.jvm初始化須要作什麼?Jvm在向OS請求了一塊地址列表後,而後就須要初始化了,初始化要作什麼呢?jvm啓動至關於一個進程,固然它能夠再啓動子進程,這裏我咱們只考慮單個進程,進程啓動必然須要初始化一些內容,C語言或者C++它會將相應的全局變量以及代碼段等內容在內存中進行編譯爲相應的指令集;而jvm作了什麼呢?jvm它也須要作一些操做;首先每個進程都必須最少一個引導進程,也就是咱們說的main,經過引導進程所關聯,以及關聯的關聯(也就是import),jvm會將這些關聯關係的內容造成一個大的jvm網狀結構用於關係於class之間並保證每個class有一份本身的私有池,他們放在哪裏,他們就是放在PermSize,也就是不少中文翻譯中的永久代,每個Class都有本身獨立的私有池去管理自身的結構,對一個java程序源文件,編寫的是對於程序的描述信息,生成class也就是描述信息的byte格式(在這個過程當中會自動完成一些簡單邏輯合併工做),byte格式是字節碼格式,也就是按照每8個bit位組成的計算機基本格式,只要字符集統一,則爲每個操做系統所認知的格式,JVM須要作的是將這些統一認知的格式信息翻譯爲對應操做系統的指令或硬件指令,因此JVM真正的意義就是爲每個操做系統編寫了一個統一的JRE,即:java運行時環境,而編譯環境是全部系統均可以使用的;初始化將class的定義加載到內存中會進行相應的轉換和壓縮,總之會造成原有對類型描述和執行順序,而不會出現混亂,但並非對應的操做系統指令(對應的操做系統指令是運行時知道的),如描述類型、做用域、訪問權限等等內容,這部分空間大小決定於class的多少,也就是你的工程的大小,PermSize還包含了其餘的內容,而且只是在通常狀況下不會發生GC,可是有些時候仍是會發生GC的,在後面繼續說明;這個加載完成後,他們在池中天然有本身的內存首地址,要尋找他必然要有對應列表,列表的基礎確定是屬於符號向量了,也就是基於名稱的一個符號向量,那麼當發生new時,它會在符號向量中尋找對應的class,找到後將符號地址轉換爲對應的class地址,而且這個內容只會被轉載一次,之後能夠直接被利用,從中找到了class的定義,在堆中分配內存時將其定義部分的某些組織單元放置與對象的頭部,這些代碼段對於對象來講是彼此獨立,就像你在方法體前面增長synchronize關鍵字,對於非靜態方法來講,不一樣的對象這個關鍵字是相互不會影響的,也就是說,若是多個線程調用的對象不是同一個,僅僅在方法(非靜態方法)體上面增長synchronized這對於多線程同步是無效的(更多關於多線程的知識,如關鎖方面的Lock、Atomic等方面的知識不是本文的內容,這裏再也不展開討論);注意,這裏尚未談到申請對象以及動態裝載,動態裝載的class通常是不會JVM初始化的時候轉入Perm的,而是運行時動態裝載進去的,就像JDBC驅動同樣,你們幾乎都用動態裝載來實現動態加載不一樣數據庫鏈接的目的;也就是咱們上一節提出的問題,動態裝載作什麼?它負責的是運行時裝載一些類的定義,而不是初始化,固然,當你經過全名去加載的時候,他們會從符號向量中尋找這個類是否已經加載,若是已經加載則直接使用,不然從相應的包中獲取這個class定義,而後裝載起來,裝載的單位也是以class爲單位,並非以jar包爲單位,這裏請你們若是不要濫用動態加載,一個是形成Perm的不穩定,另外一個是它的效率確定沒有new高,由於它須要先去經過符號向量尋找是否存在,不存在再加載,而後再經過newInstance實例化一個或多個實例,固然在某些特殊的時候,利用它能夠爲你的程序帶來極高的靈活性。
2.2.內存申請時的指針與實例:內存申請時上一節已經說到地址空間的和符號引用獲得對應數據結構的方法,這裏再也不說起,這裏就將對象做爲總體,在堆中;在JVM的初衷中,它但願新申請的內存是連續的,雖然堆的定義是讓內存是隨機分配的,可是對於整個JVM來講,它但願分配的內存是較爲連續的,也就是按照較爲條帶化的方式進行分配,好處有好幾個,一個是這樣很是的簡單,通過精簡後的狀況目前一個new翻譯爲機器碼只須要10條左右的指令碼,近乎與C語言,因此在高版本的jdk中,new的開銷再也不是java虛擬機慢的一個緣由,你們也沒有必要去儘可能減小new,可是也不要濫用,業績雖亂定義沒必要要的對象;其次,另外一個好處,當內存較爲連續後,內存在分配上就沒有相似的大量碎片的問題,形成運行一段時間後,大量碎片,當須要申請一個大內存的時候,須要尋找很是多的地方纔能將其邏輯上組成,而致使分配空間上沒必要要的浪費;而一個簡單內存分配Stringa =new String("abc");,這樣一條代碼,會作什麼動做呢?a至關因而對象的一個指針同樣的東西,這個空間的大小爲一個long的長度,也就是能夠支持到能夠想象的任何內存大小,它並非存放在heapSize中的,而是放在stack中的,由OS來調度管理,也就是當a的做用區域完成,這個指針將會斷開,java中的String再也不是C或者C++中的一個指針指向的一個字符數組,而是一個被包裝後的對象,也就是java爲何說本身都是對象,由於它把原生態的內容進行了包裝,讓程序編寫更加簡單;這裏順便說起一下:在較早期的jdk中,jvm並非由一個指針直接指向分配堆中的首地址,而是先有一個handle空間,這個空間存放了開始說的一些對象的定義和結構信息,也就是找到該位置,而後由該位置轉換到對應的對象上,可是那個時候的對象頭部信息就沒有如今的那麼全,也就是之前是將一部分handle內容放置在獨立的空間上,如今的jdk已經沒有那樣的了。
2.3.內存分配後放在哪裏,如何移動?
終於回到上面的話題,內存分配後,在堆中的什麼位置?就是咱們上面說的heapSize中的Young區域的Eden區域中,也就是new的對象絕大部分會放在這裏(排除一種很是大的對象的特殊狀況),在java設計的看來有一個特別有意思的地方,就是它在新生成的對象中它認爲你絕大部分對象都是應該須要被銷燬掉的,就像在作java WEB應用上同樣,一個列表請求過來,可能請求的內容有2K的內容,請求完成後,這個內容通常說來天然就不須要了,也就是在他原始的考慮下它沒有考慮你本身在應用級別去作page cache的操做;好,那麼當內存不夠的時候,這裏指被commited的空間不夠的狀況下,此時java就會作一個動做,就是會對Young空間進行回收,因爲新生成的對象,java認爲這塊空間不會很大,並且絕大部分應該是被幹掉的內容,因此不少時候java會採用單線程的複製算法(固然你也能夠設置爲多線程),關於算法的核心在第三章中會說到,這裏總之先理解找到了活着的對象,將其拷貝到其中一個survivor區域中,當下一次作操做時,就會將Eden中活着的以及前一個surivor活着的一塊兒拷貝到另外一個survivor中,這就是爲何要設置兩個survivor區域,而拷貝後,Eden區域爲空、另外一個survivor也爲空,能夠徹底直接總體清除掉,因此很是快速,而拷貝的目標也會被連續化,新生成的對象又從Eden的初始位置開始分配空間。
當對象每次(活着)被拷貝到一個survivor時,Java虛擬機就會記錄下來對象被移動的次數,當次數達到必定的程度,也就是官方文檔所說的足夠老的狀況,這塊內存就認爲它不太容易被註銷掉,此時就會被移動到第二個區域Tenured區域,這個次數也能夠由本身來控制。
另外在通常默認的狀況下當回收後的內存仍然佔用實際目前commited內存的70%以上,那麼此時虛擬機將會開始擴展這些內存,而當回收後的內存小於40%後,虛擬機將會下降這部份內存,可是其餘線程仍然不能使用(固然這個參數也是可配置的,在文章最後有說明),這樣收縮和擴展必然致使一些問題,可是java的初衷是想讓你再沒有使用這塊地址表的時候,回收內存的大小會小一些,由於young區域的通常是使用單線程的回收方式,這個時間段是會被暫停的,因此它認爲內存使用較少的時候回收就內存的速度應該加快;可是,和實際相反的是,咱們正好須要的是內存使用較大的時候,才但願加快回收的速度,內存使用小的時候,回收都是無所謂的;因此咱們在不少時候建議將-Xms和-Xmx設置成同樣的大小,不用這麼來回倒騰。
在說明下,如下三種狀況對象會被晉升到old區域:
一、在eden和survivor中能夠來回被minor gc屢次,這個次數超過了-XX:MaxTenuringThreshold
二、在發生minor gc時,發現to survivor沒法放下這些對象,就會進入old。
三、在新申請對象,大於eden區域的一半大小時直接進入old,也能夠專門設置參數-XX:PretenureSizeThreshold這個參數指定當超過這個值就直接進入old。
當上面的對象被移動到了Tenured區域,這個區域通常很是大,佔用了HeapSize的絕大部分空間,此時若它發生一次內存回收,就不能像剛纔那樣來回拷貝了,那樣代價太大,並且這個區域能夠說是經得起考驗的對象纔會被移動過來,在機率上是不容易被銷燬掉的對象纔會被移動過來;那麼,咱們很此時想到的就是反過來計算,也就是找到須要銷燬的對象,將其銷燬,關於算法也是下面第三章要說的內容,總之對象會在這裏存放着。
爲何java不論在Young中的區域會來回倒騰,而在Tenured區域也會不斷去作壓縮,就是咱們前面說的,它但願內存相對較爲連續而作的;java在Yong的區域,它認爲能夠剩下的內容不會不少,因此拷貝的代價並不大,因此它認爲來回拷貝是一種合適的方法,而Tenured區域它採用了清除後,必定次數後進行壓縮的方式,固然這個次數你能夠本身去設置,在文章的最後是有參數的;而它沒有采用相似操做系統同樣的按照板塊大小等一系列算法來完成,這也是我比較納悶的事情,不過整體說來這種算法仍是可行的;但願在劃分區域一些策略上能有更大的靈活性,這樣能夠在更多的應用中發揮得更加靈活,這樣就更好了;比較困惑的就是這樣的架構本身若是作頻繁度不高不低的page cache,性能很差估量,也許比不作cache更低,這個要根據具體狀況而定了。
2.3.Perm通常還會存放什麼內容?Perm除了存放上面的Class定義外,還通常會存放的內容有靜態代碼段、final static類型的類變量、String常量以及String被intern後的內容,也是最後一章中所要說起之前我本身寫錯的內容;如何應對好常量池,以及常量池是否會被GC,也是咱們所須要說明的內容;關於Perm永久代中存放的內容,應當如何配置以致於它能夠去回收,在文章的最後有相應的說明,請自行查閱;不過對於Perm的大小,通常仍是不建議去作GC的,也就是合理的去使用Perm,在程序運行中佔用Perm最多的就是String常量,尤爲是若是大量使用intern的時候,就會形成大量Perm膨脹,也是最後一部分須要說明的內容,不過intern也並不是一無可取,由於你能夠這樣說:若是它沒有用處的話,java沒有必要再把String的常量放在單獨的一個地方,它有不少好處,只要在適當的時候利用好常量池這個區域在必要的時候能夠提升性能,具體在最後一章有所講解。
三、虛擬機如何定義回收算法
3.1.首先虛擬的回收算法會分紅兩個部分,一個部分是對象的查找算法,一個是真正如何回收的方法。通常對於查找有如下兩種:
a)引用計數:原本在本文中我不想說起引用計數,由於這是最原始也是最垃圾的算法,也是較低版本jdk慢得出奇的緣由,可是爲了說明後面的問題不得不簡單說明一下,引用計數就是經過java虛擬機專門爲每一個對象記錄它被指針指向的個數,當發生指針指向它或者被賦值,計數器將會被加1,而但指向它的指針=null或者脫離了做用區域,jvm就會將相應的計數器減小1,這樣簡單,可是慢死了,不只僅操做上出奇的慢,由於要作一個簡單的賦值操做要到多個地方去找一大堆東西;還有一個就會引發很難檢測到的內存泄露,那就是當兩個或者多個對象存在循環交叉引用的時候,此時他們的引用計數將永遠不會等於0(如使用雙向鏈表或使用複雜的集合類後,相互之間的引用),也就是垃圾收集器將永遠不會認爲這是垃圾(固然要用複雜的算法能夠解決,可是這個算法的確很複雜,可能垃圾回收會更加慢),最後就是這個垃圾回收方式必然致使內存的遍歷操做過程。引用計數的示意圖以下圖所示:
b)引用樹遍歷:實際上是一個圖,只是有根而已,它沿着對象的根句柄向下查找到活着的節點,並標記下來,其他沒有被標記的節點就是死掉的節點,這些對象就是能夠被回收的,或者說活着的節點就是能夠被拷貝走的,具體要看所在heapSize中的區域以及算法,它的大體示意圖以下圖所示(對象:B、G、D、J、K、L、F都是垃圾對象,雖然他們也有相互指向,可是不是被根節點能遍歷到的,注意這裏是指針是單向的):
3.2.內存回收:上面的方法咱們能夠找到內存能夠被使用的,或者說那些內存是能夠回收,更多的時候咱們確定願意作更少的事情達到一樣的目的,咱們會根據通常的狀況設置不一樣的算法來讓系統的性能達到較好的程度,首先來了解下內存回收的算法或者它的經歷有哪些?
a):標記清除算法,這算是比較原始的算法,也就是經過上面的查找標記後,咱們對沒有標記的對象進行空間釋放的過程,這個算法雖然很原始,可是是後來全部算法的基礎,好處的簡單,缺陷是形成和其餘語言同樣的內存碎片,要經過更加複雜的算法來解決這些碎片;另外一缺陷就是它這個過程若是用於較大的內存將會致使長時間的對外服務中止(固然這個中止也不是傳說中那麼長,只是相對計算機來講比較長,至於多長是還和jdk的版本以及廠商有關係,BEA曾經在1G的JVM下面測試,有300M空間屬於可用空間,據測試結果爲30ms的中止服務時間,我想這個時間應該能夠接受,不過它有本身的測試場景,不能徹底說明問題,而通常狀況下在單線程引用下,常規的回收起碼會比這個時間要長好幾倍甚至於10倍以上)。
b):標記清楚壓縮,這個算法是也是較爲原始的,它的出現是爲了解決上面一種算法中不能壓縮空間的問題,可是並不是取代,由於它致使的另外一個問題就是更長時間的服務中止,由於壓縮就是空間拷貝到一個較爲連續的地方,而並不是對數據自己進行壓縮,因此不少時候他們是配合使用的,如多少次清除後進行一次壓縮。
c)複製回收:也就是在jvm發展的過程當中出現的算法,如今基本都只能看到一些思想影子在裏面,可是沒有這個方式,也就是將其劃分爲2個相同的大小,而後將活着的節點來回拷貝,這樣形成的內存浪費的很是大的,不只僅是一半的浪費問題,並且每次拷貝的開銷也是很是大的,由於都是涉及到整個jvm活着節點的拷貝過程。
d)增量回收:這算是現代垃圾回收的一個前身,它作的事情就是爲了解決複製回收算法中的一個問題,就是每次複製形成的空間開銷很是大的問題,此時它將內存中切分爲逐個板塊,這些板塊,每一個內部使用了複製算法,也就是並無解決空間浪費的問題,回收的過程當中沒有進行細化,雖然回收速度較快速,並且只會形成局部的中止服務,可是對於不一樣板塊大小、不一樣生命週期的對象仍是沒有劃分開。
e)分代收集器:分代收集器是增量收集的另外一個化身,或者說延續吧,它將板塊按照生命週期劃分爲上面所說的板塊,每個板塊能夠採用不一樣的算法進行回收,這也是和增量回收最大的區別,此時可讓jvm的回收達到更好的效果,不過因爲jvm按照生命週期劃分後都是指定板塊的,因此根據內存大小劃分自定義板塊是不可能的,至少如今好像尚未,因此在回收過程當中若是內存大了回收起來同樣很吃力,尤爲是對Old區域的回收,因此併發回收不得不出現了。
f)併發回收:所謂併發回收是指外部在訪問的同時,java回收器依然在作着回收工做,原早我認爲併發回收是不可能的,由於你須要知道內存是須要回收的,就不能讓內存繼續的被申請和釋放,可是SUN的人仍是比較天才的,仍是有辦法儘可能讓他併發去作的;併發回收器其實也會暫停,可是時間很是短,它並不會在從開始回收尋找、標記、清楚、壓縮或拷貝等方式過程徹底暫停服務,它發現有幾個時間比較長,一個就是標記,由於這個回收通常面對的是老年代,這個區域通常很大,而通常來講絕大部分對象應該是活着的,因此標記時間很長,還有一個時間是壓縮,可是壓縮並不必定非要每一次作完GC都去壓縮的,而拷貝呢通常不會用在老年代,因此暫時不考慮;因此他們想出來的辦法就是:第一次短暫停機是將全部對象的根指針找到,這個很是容易找到,並且很是快速,找到後,此時GC開始從這些根節點標記活着的節點(這裏能夠採用並行),而後待標記完成後,此時可能有新的 內存申請以及被拋棄(java自己沒有內存釋放這一律念),此時JVM會記錄下這個過程當中的增量信息,而對於老年代來講,必需要通過屢次在survivor倒騰後纔會進入老年代,因此它在這段時間增量通常來講會很是少,並且它被釋放的機率前面也說並不大(JVM若是不是徹底作Cache,本身作pageCache並且發生機率不大不小的pageout和pagein是不適合的);JVM根據這些增量信息快速標記出內部的節點,也是很是快速的,就能夠開始回收了,因爲須要殺掉的節點並很少,因此這個過程也很是快,壓縮在必定時間後會專門作一次操做,有關暫停時間在Hotspot版本,也就是SUN的jdk中都是能夠配置的,當在指定時間範圍內沒法回收時,JVM將會對相應尺寸進行調整,若是你不想讓它調整,在設置各個區域的大小時,就使用定量,而不要使用比例來控制;當採用併發回收算法的時候,通常對於老年代區域,不會等待內存小於10%左右的時候纔會發起回收,由於併發回收是容許在回收的時候被分配,那樣就有可能來不及了,因此併發回收的時候,JVM可能會在68%左右的時候就開始啓動對老年代GC了。
d)並行回收:並行回收指利用多個CPU對JVM進行並行垃圾回收的過程,並行度都是能夠設置的,能夠分別對年輕代和老年代配置是否使用並行回收。
好了,回收算法就說到這裏,那麼如何利用好回收算法,在看了上面的介紹後,是否對JVM有了一個大體的瞭解,具體細節,能夠慢慢實踐,在文章最後給出一些經常使用的java虛擬機內存設置參數的說明,不過並不權威,須要根據實際狀況而定才能夠。
下面說下java虛擬機除了消耗基本內存外還會消耗什麼內存?
四、JVM佔用的空間除HeapSize還會佔用什麼?
通常來講,對於不少學了好幾年,甚至於不少年java人來講,一旦看到OutOfMemeory(簡稱OOM),就認爲HeapSize不夠,而後瘋狂的增長-Xmx的值,可是HeapSize只是其中一個部分,當你去作一個實驗,也就是java啓動時直接在程序中瘋狂的new 一些線程出來,直到內存溢出,當-Xms -Xmx設置得越大的時候,獲得的線程個數會越少,爲何呢?由於OOM並非HeapSize不夠而致使的,而由不少種狀況。
首先看下操做系統如何劃份內存給應用系統,其實在Win 3二、Linux 32的系統中,地址總線爲32位的理論上應該能夠支持4G內存空間,可是當你在Win 32上設置初始化內存若是達到2G,就會報錯,說這個塊空間無法作,首先默認的Win32系統,會按照50%比例給予給Kernel使用,而另外一部分給應用內存,也就是說操做系統內核部分不管是否使用,這一半是不會給你的,而還有2G呢,它在系統擴展的部分,也就是並不是Kernel的部分,有不少靜態區域和字典表的內容,因此要劃分一個連續的2G內存給JVM在Win 32上是不可能的,Win 32提出了一種Win 32 3G模式,貌似能夠劃分3G空間,其實它只是將內核部分縮小也就是管理部分縮小,也就是將一部分劃分到外部來使用,並且Win 32習慣在內存2G的位置作一些手腳,讓你分配連續2G沒有可能性,通常來講在Win 32平臺上,在物理內存足夠的狀況下給JVM劃分的空間通常是1.4~1.5G左右,具體數據沒有測試過;而Linux 32相似於Win 32 3G模式,可是它仍是通常狀況下分佈不凌亂的狀況下,通常能夠給JVM劃分到2G的大小。Linux 32 Hugemem是一個擴展版本,能夠劃分更大的空間,可是須要付出一些其餘的代價,理論上能夠支持到4G給應用,也就是Kenel是獨立的;Solaris x86-32和AIX 32等系統,也相似於Linux 32平臺同樣。
爲何還要預留一些空間出來呢?這些空間給誰?
當你申請一個線程的時候,它的除了線程內部對象的開銷外,線程自己的開銷,是須要OS來調度完成,通常來講,會在OS的線程與虛擬機內部有都有一個一一對應的,可是會根據操做系統不一樣有所變化,有些可能只有一個,總之heapSize外的那部分空間是跑不掉的,它放在哪裏呢?就是放在Stack中的,因此上文中的-Xss就是設置這個的,在jdk 1.5之後,每一個線程的大小被默認設置爲1M的stack開銷,咱們習慣將這個開銷下降。
好了知道了指針、線程是在heapSize外部的,還有什麼呢?
當你本身使用native方法,也就是JNI的時候,調用本地其餘語言,如C、C++在程序中使用了malloc等相似方法開闢的內存,都不是在heapSize中的,而是在本地OS所掌控的,另外這部分空間若是沒有相應的釋放命令,就須要在對應finalize方法內部調用其餘的native方法來完成對相應對象的釋放,不然這部分將成爲OS級別的內存泄露,直到JVM進程重啓或者宕機爲止(操做系統會記錄下進程和相應線程和堆內存的關聯關係,可是進程再沒有釋放前,OS也是不會回收這部份內存的)。
另外在使用JavaNIO以及JDBC、流等系列操做時,當造成與終端交互時,會在另外一個位置造成一個內存區域,這些內存區域都不在HeapSize中。
因此常見的OOM現象有如下幾種:
一、heapSize溢出,這個須要設置Java虛擬機的內存狀況
二、PermSize溢出,須要設置Perm相關參數以及檢查內存中的常量狀況。
三、OS地址空間不夠,也就是沒有那麼多內存分配,這個通常是啓動時報錯。
四、Swap空間頻繁交互,進程直接被crash掉,在不一樣操做系統中會體現不一樣的狀況。
五、native Thread溢出,注意線程Stack的大小,以及自己操做系統的限制。
六、DirectByteBuffer溢出,這一類通常是在作一些NIO操做的時候,或在某種狀況下使用ByteBuffer,在分配內存時使用了allocateDirect以及使用一些框架間接調用了相似方法,致使直接內存的分配(如mina中使用IoByte去調用,當參數設置爲true的時候就分配爲直接內存,所謂直接內存就是又OS定義的內存,而不須要從程序間接拷貝一次再輸出的過程,提升性能,可是若是沒有手動回收是回收不掉的),致使的Buffer問題,如輸出大量的內容,輸入大量的內容,此時須要儘可能去嘗試限制它的大小。
使用很是多的工具區檢測Java的內存如:jstat(只能看HeapSize和PermSize)、jmap(很細的東西)、jps(java的ps -ef呵呵)、jdb(這個不是監控工具哈,這個是debug工具)、jprofile(圖形支持,可是能夠遠程鏈接)等等;jconsole(能夠看到heapsize、permsize+native mem size(這這裏叫作:non-heapsize)等等的使用的趨勢圖)、visualvm(極爲推薦的東西,圖形化查看,你能夠查看到內存單元分配、交換、回收、移動等等整個過程,很是清晰展示jvm的全局資源)、另外pmap能夠展示很是清晰的資料,能夠精確到某一個java進程內部的每個細節,並且能夠看到heapsize只是其中很小一部分(在solaris操做系統上看得最齊全,LINUX下有些進程可能看不太懂);也能夠在/proc/進程號/maps中查看(這裏能夠看到內存地址單元的起始地址,包含了reserved的地址範圍和commited的地址範圍),全局資源使用操做系統top命令和free命令看;IBM有一個GCMV免費下載工具也很好;Win32有一個WMMap工具都是很好的工具
使用相應的工具觀察相應的內容,當觀察到內存的使用從無到有,上升,而後處於一個平穩趨勢,那麼這個JVM應該是較爲穩定的;若是發現它通過一段平滑期後,又出現飆升,這個必然是有問題的,至於什麼問題,根據前面的學下和實際狀況咱們能夠去分析;當它開始後,平滑過程,出現緩慢上升的過程,可是始終會上升到極點,那麼一個是須要知道物理內存時候可用,另外一個就是少許的內存泄露(JVM現代也有內存泄露,只是它的內存泄露並不是C、C++中的內存泄露)。
五、糾正錯誤:intern的使用上的錯誤
最後一章節,我本身糾正一下我本身的錯誤,之前的文章中,也就是關於intern的使用,最近對他作了一些深刻研究,由於之前也是和不少同窗同樣,聽到別人推薦什麼就瘋狂的使用,知道點原理也是點大概,沒有深刻研究內部的內容。
我曾經在文章中說到任何系統最多使用的數據類型必然是String,無論作什麼,因此在String的處理上頗有研究,推薦使用java的朋友在大量使用對比的時候不要用equals,而推薦使用intern,可是我最近發現我錯了,我這裏給你們道歉,由於可能會誤導不少朋友;下面說明下這個東西爲何?
首先我開始本身懷疑本身的時候是想說,若是intern能夠作到高效,那麼equals是否是在String中就沒有存在的必要了呢,當時對於我理解僅僅爲常量池的一個地址對比,比如是兩個數字的compare,僅僅須要CPU的單個指令便可完成;因而我開始作了兩個實驗,一個是最原始,最初級的方法採用單線程循環1000000次調用equals與intern等值對比,而且採用了不一樣長度的字符串去作比較,發現equals居然比intern要快,並且隨着字符串長度的增長,equals會明顯快與intern,而後使用多線程測試也是獲得同樣的效果,我首先很不敢相信本身堅持的理論被完全和諧了,後來冷靜下來必須須要面對,經過不少權威資料的閱讀,我發現我對JVM常量池的理解還只是一點點皮毛而已,因此我作了更加深刻的研究。
原來intern方法被調用時是在Perm中的String私有化常量池中尋找相應的內容,而尋找雖然能夠經過hash定位到某些較小的鏈表中,可是仍是須要在鏈表中逐個對比,對比的方法仍然是equals,也就是拋開hash的開銷,intern最少要與裏面的0到多個對象進行equals操做,並且若是不存在,還要在常量池開闢一塊空間來記錄,若是存在則返回地址,也就是常量池保證每一個String常量是惟一的,這個開銷固然大了,並且若是使用在業務代碼中將會致使Perm區域的不斷增長;
因而,我又反過來想了:既然equals比他效率高,爲啥還要用intern呢?並且equals的那個算法對於長字符串逐個字符對比的過程我實在是難以入目;並且也實在是以爲不甘心本身的理論就這麼容易被和諧掉,由於本身已經在很多程序中這樣用過,這樣我豈不是犯下大錯了,由於本身參與過的項目的確太多了,並且有相似的代碼我寫入了框架中,最終發現我可能錯了一半,也就是歷史上的記錄可能我有一半相似的代碼是錯誤的;爲何呢?intern仍是有用的,我先作了一個測試,那就是,用一個已經intern好的對象,讓他與一個常量作等值,循環次數和上面同樣,結果我預料的結果發生了,那就是比equals快出了N多倍數,隨着長度的增長,會體現出更加明顯的優點,由於intern對比的始終是地址,和長度無關,因而我想到了如何使用它,就是在程序中返回經過字符串相似於數字同樣的類型斷定時,如:作一個sqlparser的時候,常常根據數據類型作不一樣的動做,這樣若是用equals會在每次循環時付出不少開銷,尤爲是不少數據庫的類型很是多,最壞的是從上到下每一個字符串匹配一次,固然長度不等開銷很小,長度相等開銷就大了;intern我就將這些schema信息預先intern掉,也就是他們已經指向了常量池,當再真正匹配時,就不須要用intern了,而是直接匹配,也就是將這個開銷放在初始化的過程當中,運行時咱們不去增長它的開銷。
因此,我的是犯下一個錯誤,而且之前還很張揚的處處宣傳,呵呵,如今以爲有點傻,但願在看到某些推薦用什麼新東西的時候,千萬不要在沒有研究明白他就去用它,甚至於濫用它,至少要通過一些簡單的測試,不過對於現代不少複雜的東西,一些簡單的測試已經不足以說明問題,就像Lock與Synchronize的開銷同樣,若是採用簡單的循環的話,你會發現新版本的Lock的開銷將會比Synchronized的開銷更加大,它適合的是併發,讀寫的併發,因此真正要弄清楚仍是研究內在。
最後說下,我我的對JVM的指望,JVM作到了不少個板塊之間使用不一樣的算法,而JVM不但願程序員去關心內存,可是有些特殊的應用須要JVM提供多的支持,固然有些公司對JVM內核進行了改造來適合特殊的應用,可是咱們更加但願標準的JVM可以提供更加靈活的內存管理機制,而不只侷限於配置,由於配置適中是死的,在不少時候會面臨擴展性的限制;如不少時候咱們認爲能夠斷定不少的對象自己就是不會被回收或者根本不容易被回收的,就不用到Young的空間和其餘的業務套在一塊兒倒騰了;對於常常作page cache的系統,而page cache的命中率不是特別高(95%以上就很高),也不是很低(如80%如下),這個時候,置換到快不慢的,而會致使在老年代的回收的頻繁起來,就我我的但願這些空間都能獨立出來,甚至於能夠由程序去控制和指定,固然JVM能夠自身去默認;尤爲是按照一些特殊的對象等級類型或者說對象的大小,這些細節均可以採用一些相應的默認GC手段來完成,也能夠人工的指定,固然也在默認狀況下能夠按照原有的模式進行架構,這樣JVM的內存調節的靈活將會更加寬鬆,使得它能在各種場合下只要使用相對應的手段配置和程序調整都是能夠打到目的的。
本文包含大量我的看法,若有不是之處,請你們多多指教!本文到此完結,內容粗而不深刻,細節問題,細節討論。
常見參數JVM參數配置(java vm Hotspot TM 1.6):
•-Xms爲初始化爲HeapSize的空間,即被Commited的尺寸。
•-Xmx爲最大的HeapSize空間,有些還沒有被Commited,可是已經被進程所Reserved,當如今已經被Commit的空間長期處於(jdk1.1還有一個-mx爲包含handler表的空間)。
•-Xmn設置Young的空間大小,此時NewSize和MaxNewSize一致,或者分別設置-XX:NewSize=128m
•-XX:PermSize = 64M及-XX:MaxPermSize= 64M爲永久代的初始大小和最大大小。
•-XX:NewRatio= 3爲Tenured:Young的初始尺寸比例(設置了大小就再也不設置此值),此時Young佔用整個HeapSize的1/4大小。
•-XX:SurvivorRatio= 6:爲Eden:Survivor比例大小,此時一個Survivor佔用Young的1/8大小,而Eden佔用3/4大小。
•-Xss=256k爲ThreadStack空間大小,jdk 1.5之後默認是1M,在IBM的jdk中還有-Xoss參數(此時每一個線程佔用的stack空間爲256K大小)
•-XX:MaxTenuringThreshold=3:通常一個對象在Young通過多少次GC後會被移動到OLD區。
-XX:+UseParNewGC:對Yong區域啓用並行回收算法。
•-XX:+UseParallelGC:一種較老的並行回收算法。
•-XX:+UseParallelOldGC:對Tenured區域使用並行回收算法。
•-XX:ParallelGCThread=10:並行的個數,通常和CPU個數相對應。
•-XX:+UseAdaptiveSizepollcy:收集器自動根據實際狀況進行一些比例以及回收算法調整。
•-XX:CMSFullGCsBeforeCompaction= 3:多少次GC後會進行壓縮碎片
•-XX:+UseCmsFullCompactAtFullCollction:打開老年代壓縮
-XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled
-XX:+CMSPermGenSweepingEnabled對永久帶進行相應的回收,在jdk1.6中不須要數:-XX:+CMSPermGenSweepingEnabled
-XX:MinHeapFreeRatio這是指剩餘空間百分比多少時,開始減少commited的內存;
-XX:MaxHeapFreeRatio指剩餘空間百分比多少時,開始增長commited的內存,直到-Xmx大小。
-XX:MaxGCPauseMillis指GC最大的暫停時間,當超過這個時間,那麼JVM會適當調整內存比例(前提是使用的是基於比例的YONG和設置)。
-XX:+UseConcMarkSweepGC啓動併發GC,通常針對Tenured區域。
-XX:+CMSIncrementalMode增量GC,將內存切塊,分佈在多個局部去GC。
-XX:CMSInitiatingOccupancyFraction在併發GC下,因爲一邊使用,一遍GC,就不能在不夠用的時候GC,默認狀況下是在使用了68%的時候進行GC,經過該參數能夠調整實際的值。
大體的參數設置就這些,可是GC自己的參數還有不少,尤爲是和應用或者和具體硬件結合起來的時候,而BEA和IBM也有本身的JDK,這裏有些參數他們支持,有些參數不支持,在某些平臺和甚至於硬件上能夠支持特殊的參數來控制(如在部分intel系列的多CPU機器上,經過它的NUMA架構,能夠設置對應參數支撐,節點和CPU之間能夠實現分工負載、常規服務上都是SMP的,而大型機上多半是MPP);相似於上面的併發GC在通常狀況下是不會進行compact壓縮的,由於它但願回收的時間短,可是充滿compact的壓縮時間必然不是那麼短,因此在部分特殊應用下有些使用定寬度的內存尺寸,回收後無論空餘內存,由於每一個內存的尺寸都是那麼大,這樣來處理,固然這樣必然會致使不少的內存浪費,可是它的好處是能夠沒有compact而不存在說要分配的內存分配不到的問題。