日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

认识JVM--第一篇-对象分配回收算法

發(fā)布時(shí)間:2024/9/30 编程问答 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 认识JVM--第一篇-对象分配回收算法 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

http://blog.csdn.net/xieyuooo/article/details/6553588


本來標(biāo)題黨想寫成《深入JVM》,不過不太敢寫,我想一小篇博客我想還不足以說明JVM,在本文中,會就我所知給大家介紹JVM的很多內(nèi)部知識,概念會相對較粗,因?yàn)樘?xì)的內(nèi)容要寫,這里肯定寫不出來;本文主要偏重理論,沒有什么實(shí)踐,中間除一些官方資料外,還有部分自身的理解,所以請大家不要完全信任本文內(nèi)容;另外本文會有一小部分糾正以前一篇文章對于intern()使用方法的錯(cuò)誤,本文會在其中說明使用錯(cuò)誤的原因,大致文章內(nèi)容有以下幾個(gè)部分:

?

1、JVM虛擬內(nèi)存組成及操作系統(tǒng)地址表

2、新生成對象在HeapSize是如何變化的

3、虛擬機(jī)如何定義回收算法

4、JVM占用的空間除HeapSize還會占用什么,OutOfMemory種類!

5、糾正錯(cuò)誤:intern()的使用上的錯(cuò)誤

?

?

好,現(xiàn)在開始話題吧:

1、JVM虛擬內(nèi)存組成及操作系統(tǒng)地址表

?

?????1.1.虛擬地址大致概念:在OS層面一般是由邏輯地址映射到線性地址,如果線性地址管理,如果啟動了分頁,那么線性地址就會轉(zhuǎn)換到相應(yīng)的物理地址上,否則就直接認(rèn)為是物理地址;程序設(shè)計(jì)中所用到的地址單元就是邏輯單元,如在C語言中的&表示指定的地址就是邏輯地址;而物理地址也并非我們所認(rèn)為的RAM,還應(yīng)該包括網(wǎng)卡、顯存、SWAP等相關(guān)內(nèi)容,也就是由OS所管理所有可以通過頂層邏輯單元映射到的目標(biāo)地點(diǎn),不過絕大部分情況下只需要考慮RAM即可,尤其是在服務(wù)器上;JVM的虛擬內(nèi)存地址和操作系統(tǒng)的虛擬內(nèi)存地址不是一個(gè)概念,操作系統(tǒng)的虛擬內(nèi)存地址相當(dāng)于在磁盤上劃分的一個(gè)SWAP交換區(qū),用于內(nèi)存,內(nèi)存與之做page out和page in的操作,這種用于物理內(nèi)存本身不夠,而地址空間夠用的情況,一旦程序出現(xiàn)page out這些情況的時(shí)候,程序?qū)兊梅浅>徛?#xff0c;而JVM的虛擬內(nèi)存是在有效的空間內(nèi)分配一個(gè)連續(xù)的線性地址空間,因?yàn)镴VM想要自己管理內(nèi)存,分配的堆內(nèi)存都是在自己的heapSize內(nèi)部,因?yàn)樗獙?shí)現(xiàn)一些脫離于存儲器本身對非連續(xù)堆處理的管理而導(dǎo)致的復(fù)雜性,也就是JVM去初始化的時(shí)候就會加載一塊很大的內(nèi)存單元,然后內(nèi)部的操作都是內(nèi)部自己完成的。


????1.2.內(nèi)存分配:一般C語言分配內(nèi)存是初始化將相應(yīng)的基本內(nèi)容和代碼段進(jìn)行加載,但是不會加載運(yùn)行時(shí)候的堆棧內(nèi)存分配,也就是在運(yùn)行到某個(gè)具體的函數(shù)時(shí)通過malloc、callloc、realloc等方方申請的區(qū)域,這些區(qū)域必須從操作系統(tǒng)中重新來分配,使用完成后必須進(jìn)行free,C++中必須使用delete方法來釋放,大家發(fā)現(xiàn)沒有,OS的堆在內(nèi)存不斷申請和釋放的過程中,必然會產(chǎn)生許多的內(nèi)存碎片,從而導(dǎo)致你在申請一塊大內(nèi)存的時(shí)候,需要進(jìn)行邏輯連接,導(dǎo)致在申請的速度減小,當(dāng)然LINUX采用了將內(nèi)存塊劃分為多個(gè)不同大小的板塊,來較好的處理這個(gè)問題,不過片段還是存在的,不過這種思想的確是很好的,而JVM是如何完成碎片的處理的呢,后面章節(jié)會說到;JVM在初始化的時(shí)候就會向OS申請一塊大內(nèi)存,JVM要求這塊內(nèi)存在地址空間上是連續(xù)的(物理上未必連續(xù)),讓所有的程序在這個(gè)內(nèi)部區(qū)分配,由自己來管理,所以它內(nèi)部相當(dāng)于做了一個(gè)小的OS對內(nèi)存的管理,所以JVM是想讓java程序員不用關(guān)心在哪一個(gè)平臺上寫代碼,但是你一定要關(guān)心java怎么管理內(nèi)存的;

??? 線性地址隨著實(shí)際物理內(nèi)存的增加,將會導(dǎo)致頁表非常大,甚至于導(dǎo)致多層頁表,如內(nèi)存達(dá)到96G這一類,那么這樣管理起來將會非常麻煩(正常情況下一個(gè)頁只有4K,可以自己算一下需要多少個(gè)管理地址來指向這個(gè)4K,這個(gè)管理地址太大的時(shí)候,又需要其他的管理地址來管理這個(gè)地址,就會導(dǎo)致多層地址,可能到最后,一個(gè)大內(nèi)存有40%都是用于管理內(nèi)存的,真正使用的可想而知),所以在LINUX高版本中對于內(nèi)存尋址方面做了改進(jìn),就是支持大頁面來支持(其實(shí)是通過一個(gè)套件完成的,并非OS本身),如一個(gè)頁的大小為1M這樣的,但是有一些風(fēng)險(xiǎn)在里面,它要求大頁面內(nèi)存要么放得下你的內(nèi)存,但是你不能將你的進(jìn)程一部分放在大頁面內(nèi)存中,一部分放在OS管理的小頁面內(nèi)存中,也就是說要么這塊放得下,要么就放在其他地方,可能會導(dǎo)致兩邊正好都差那么一點(diǎn)點(diǎn)的問題,在OS這邊可以使用SWAP,但是系統(tǒng)會很慢,而且SWAP很多的情況下肯定會宕機(jī)掉。


????1.3.內(nèi)存分配狀態(tài):一個(gè)大的進(jìn)程如果初始化需要分配一塊大的內(nèi)存空間,內(nèi)存空間一般會經(jīng)歷兩個(gè)狀態(tài)的轉(zhuǎn)換過程,首先內(nèi)存必須是free狀態(tài)才可以被分配,如果的確是該狀態(tài)并且空間是夠用的,那么它首先會占用那么大一個(gè)坑,在java的heapSize中,就是-Xmx參數(shù)指定的,也就是JVM虛擬機(jī)最大的內(nèi)存空間(注意這里-Xmx并沒有包含PermSize的空間),這個(gè)坑是不允許其它進(jìn)程所占用的,內(nèi)存的狀態(tài)為:reserved的狀態(tài),當(dāng)需要使用的空間時(shí),內(nèi)存將會被commited狀態(tài),在JVM初始化時(shí)也就是-Xms狀態(tài)的內(nèi)存空間,處于這個(gè)狀態(tài)的內(nèi)存如果發(fā)現(xiàn)不夠使用(物理內(nèi)存),此時(shí)就會發(fā)生swap區(qū)域,程序?qū)兊梅浅>徛?#xff0c;但是不會造成宕機(jī),而很多時(shí)候在這個(gè)時(shí)候定位不出原因,所以我們?yōu)榱俗屛锢韮?nèi)存不夠用的現(xiàn)象暴露出來可以被發(fā)現(xiàn),至于可以定位不是程序代碼的問題,我們就直接將swap內(nèi)存禁用掉;有個(gè)問題就是既然被reserved的內(nèi)存就不能被其他進(jìn)程所占用,為什么要在這兩個(gè)狀態(tài)之間來回倒騰呢?這不是多一個(gè)開銷嗎?JVM在來回倒騰的過程中會導(dǎo)致每個(gè)區(qū)域的容量發(fā)生相應(yīng)的變化,必然導(dǎo)致的是FullGC的過程,那么JVM一般在服務(wù)器端如何設(shè)置呢?文章后面逐步細(xì)化說明。


????1.4.JVM內(nèi)存組織:關(guān)于JVM內(nèi)存組織方面,前面在講述Java垃圾回收的時(shí)候已經(jīng)提及到了,但是講得不太細(xì),有些部分可能算是有錯(cuò)誤的,所以這里根據(jù)上述操作系統(tǒng)知識以及官方部分資料繼續(xù)深入,不敢說完全正確,不過至少比以前要更加深入得多,首先來看下ORACLE官方給出來的一個(gè)JVM內(nèi)存單元的組織圖形:

?

?

其實(shí)我看過很多次這個(gè)圖看得很暈,因?yàn)橐郧安涣藘?nèi)存分配中commited與reserved的區(qū)別,以至于我當(dāng)時(shí)認(rèn)為這副圖是說java的HeapSize是由N多個(gè)部分組成的,并且還包含HeapSize的,其實(shí)在經(jīng)過很多資料查閱后,尤其是看到一些監(jiān)控工具后,才知道看官方資料也有誤區(qū),呵呵,通過簡化,我自己畫的這副圖希望能夠幫助大家理解JVM的大致的內(nèi)存劃分(這里僅僅提及JVM自己的內(nèi)存,也就是HeapSize和PermSize的部分,其余的文章后面說明),這里僅僅將上面的圖形立起來畫了,當(dāng)時(shí)看起來要方便理解得很多(個(gè)人感覺):

?

?

也就是說,你首先需要將JVM的兩個(gè)大板塊分開,一個(gè)是HeapSize,也就是上圖左側(cè)的部分,右邊部分為PermSize的尺寸,HeapSize也劃分為大區(qū)域?yàn)閅oung和Old區(qū)域,Young區(qū)域內(nèi)部劃分為三個(gè)部分,一個(gè)是Eden和兩個(gè)同樣尺寸大小的survivor區(qū)域,注意到的人會發(fā)現(xiàn)為什么每個(gè)區(qū)域內(nèi)部還有一個(gè)virtual區(qū)域,這就是我們上面說的沒有經(jīng)過commited當(dāng)時(shí)已經(jīng)占用了地址列表,它不能被其他進(jìn)程所占用,當(dāng)時(shí)操作系統(tǒng)一般的提示會認(rèn)為這是塊剩余空間,但是實(shí)際上是只能被自己使用的,這部分上面已經(jīng)提及,至于為什么我們后面來解釋,這里再提出一些問題,就是為什么JVM要提出這么多區(qū)域劃分來管理呢?如果一個(gè)區(qū)域可以管理為什么還要搞得那么麻煩呢?這么多區(qū)域有什么用處,我們在第二章對象的分配中將詳細(xì)說明這部分內(nèi)容。

?

?

2、新生成對象在HeapSize是如何變化的

??? 2.1.java新創(chuàng)建對象的方法有哪些:首先學(xué)習(xí)過java的人可能沒有人不知道new 這個(gè)關(guān)鍵字,也就是新創(chuàng)建一個(gè)對象的關(guān)鍵字,當(dāng)發(fā)生new操作時(shí),jvm為你做了什么?我們先把這個(gè)問題放下,對于jvm初始化加載專門處理,這里先說除了new之外還有什么方式,就是通過java.lang.Class.forName進(jìn)行動態(tài)狀態(tài)后,獲取一個(gè)新的實(shí)例,當(dāng)然方法有重載,也通過通過ClassLoader進(jìn)行動態(tài)狀態(tài),什么是動態(tài)裝載?為什么有了new還要有動態(tài)裝載?而jvm初始化做了什么?動態(tài)裝載和new的區(qū)別是什么?這也是我們下面要討論的問題,也是PermSize中內(nèi)容的一大塊部分。

?

???2.2.jvm初始化需要做什么?Jvm在向OS請求了一塊地址列表后,然后就需要初始化了,初始化要做什么呢?jvm啟動相當(dāng)于一個(gè)進(jìn)程,當(dāng)然它可以再啟動子進(jìn)程,這里我我們只考慮單個(gè)進(jìn)程,進(jìn)程啟動必然需要初始化一些內(nèi)容,C語言或者C++它會將相應(yīng)的全局變量以及代碼段等內(nèi)容在內(nèi)存中進(jìn)行編譯為相應(yīng)的指令集;而jvm做了什么呢?jvm它也需要做一些操作;首先每一個(gè)進(jìn)程都必須最少一個(gè)引導(dǎo)進(jìn)程,也就是我們說的main,通過引導(dǎo)進(jìn)程所關(guān)聯(lián),以及關(guān)聯(lián)的關(guān)聯(lián)(也就是import),jvm會將這些關(guān)聯(lián)關(guān)系的內(nèi)容形成一個(gè)大的jvm網(wǎng)狀結(jié)構(gòu)用于關(guān)系于class之間并保證每一個(gè)class有一份自己的私有池,他們放在哪里,他們就是放在PermSize,也就是很多中文翻譯中的永久代,每一個(gè)Class都有自己獨(dú)立的私有池去管理自身的結(jié)構(gòu),對一個(gè)java程序源文件,編寫的是對于程序的描述信息,生成class也就是描述信息的byte格式(在這個(gè)過程中會自動完成一些簡單邏輯合并工作),byte格式是字節(jié)碼格式,也就是按照每8個(gè)bit位組成的計(jì)算機(jī)基本格式,只要字符集統(tǒng)一,則為每一個(gè)操作系統(tǒng)所認(rèn)知的格式,JVM需要做的是將這些統(tǒng)一認(rèn)知的格式信息翻譯為對應(yīng)操作系統(tǒng)的指令或硬件指令,所以JVM真正的意義就是為每一個(gè)操作系統(tǒng)編寫了一個(gè)統(tǒng)一的JRE,即:java運(yùn)行時(shí)環(huán)境,而編譯環(huán)境是所有系統(tǒng)都可以使用的;初始化將class的定義加載到內(nèi)存中會進(jìn)行相應(yīng)的轉(zhuǎn)換和壓縮,總之會形成原有對類型描述和執(zhí)行順序,而不會出現(xiàn)混亂,但并不是對應(yīng)的操作系統(tǒng)指令(對應(yīng)的操作系統(tǒng)指令是運(yùn)行時(shí)知道的),如描述類型、作用域、訪問權(quán)限等等內(nèi)容,這部分空間大小決定于class的多少,也就是你的工程的大小,PermSize還包含了其他的內(nèi)容,并且只是在一般情況下不會發(fā)生GC,但是有些時(shí)候還是會發(fā)生GC的,在后面繼續(xù)說明;這個(gè)加載完成后,他們在池中自然有自己的內(nèi)存首地址,要尋找他必然要有對應(yīng)列表,列表的基礎(chǔ)肯定是屬于符號向量了,也就是基于名稱的一個(gè)符號向量,那么當(dāng)發(fā)生new時(shí),它會在符號向量中尋找對應(yīng)的class,找到后將符號地址轉(zhuǎn)換為對應(yīng)的class地址,并且這個(gè)內(nèi)容只會被轉(zhuǎn)載一次,以后可以直接被利用,從中找到了class的定義,在堆中分配內(nèi)存時(shí)將其定義部分的某些組織單元放置與對象的頭部,這些代碼段對于對象來說是彼此獨(dú)立,就像你在方法體前面增加synchronize關(guān)鍵字,對于非靜態(tài)方法來說,不同的對象這個(gè)關(guān)鍵字是相互不會影響的,也就是說,如果多個(gè)線程調(diào)用的對象不是同一個(gè),僅僅在方法(非靜態(tài)方法)體上面增加synchronized這對于多線程同步是無效的(更多關(guān)于多線程的知識,如關(guān)鎖方面的Lock、Atomic等方面的知識不是本文的內(nèi)容,這里不再展開討論);注意,這里還沒有談到申請對象以及動態(tài)裝載,動態(tài)裝載的class一般是不會JVM初始化的時(shí)候轉(zhuǎn)入Perm的,而是運(yùn)行時(shí)動態(tài)裝載進(jìn)去的,就像JDBC驅(qū)動一樣,大家?guī)缀醵加脛討B(tài)裝載來實(shí)現(xiàn)動態(tài)加載不同數(shù)據(jù)庫連接的目的;也就是我們上一節(jié)提出的問題,動態(tài)裝載做什么?它負(fù)責(zé)的是運(yùn)行時(shí)裝載一些類的定義,而不是初始化,當(dāng)然,當(dāng)你通過全名去加載的時(shí)候,他們會從符號向量中尋找這個(gè)類是否已經(jīng)加載,如果已經(jīng)加載則直接使用,否則從相應(yīng)的包中獲取這個(gè)class定義,然后裝載起來,裝載的單位也是以class為單位,并不是以jar包為單位,這里請大家如果不要濫用動態(tài)加載,一個(gè)是造成Perm的不穩(wěn)定,另一個(gè)是它的效率肯定沒有new高,因?yàn)樗枰热ネㄟ^符號向量尋找是否存在,不存在再加載,然后再通過newInstance實(shí)例化一個(gè)或多個(gè)實(shí)例,當(dāng)然在某些特殊的時(shí)候,利用它可以為你的程序帶來極高的靈活性。

?

??2.2.內(nèi)存申請時(shí)的指針與實(shí)例:內(nèi)存申請時(shí)上一節(jié)已經(jīng)說到地址空間的和符號引用得到對應(yīng)數(shù)據(jù)結(jié)構(gòu)的方法,這里不再提及,這里就將對象作為整體,在堆中;在JVM的初衷中,它希望新申請的內(nèi)存是連續(xù)的,雖然堆的定義是讓內(nèi)存是隨機(jī)分配的,但是對于整個(gè)JVM來說,它希望分配的內(nèi)存是較為連續(xù)的,也就是按照較為條帶化的方式進(jìn)行分配,好處有好幾個(gè),一個(gè)是這樣非常的簡單,經(jīng)過精簡后的情況目前一個(gè)new翻譯為機(jī)器碼只需要10條左右的指令碼,近乎與C語言,所以在高版本的jdk中,new的開銷不再是java虛擬機(jī)慢的一個(gè)原因,大家也沒有必要去盡量減少new,但是也不要濫用,業(yè)績雖亂定義不必要的對象;其次,另一個(gè)好處,當(dāng)內(nèi)存較為連續(xù)后,內(nèi)存在分配上就沒有類似的大量碎片的問題,造成運(yùn)行一段時(shí)間后,大量碎片,當(dāng)需要申請一個(gè)大內(nèi)存的時(shí)候,需要尋找非常多的地方才能將其邏輯上組成,而導(dǎo)致分配空間上不必要的浪費(fèi);而一個(gè)簡單內(nèi)存分配String?a =?new?String("abc");,這樣一條代碼,會做什么動作呢?a相當(dāng)于是對象的一個(gè)指針一樣的東西,這個(gè)空間的大小為一個(gè)long的長度,也就是可以支持到可以想象的任何內(nèi)存大小,它并不是存放在heapSize中的,而是放在stack中的,由OS來調(diào)度管理,也就是當(dāng)a的作用區(qū)域完成,這個(gè)指針將會斷開,java中的String不再是C或者C++中的一個(gè)指針指向的一個(gè)字符數(shù)組,而是一個(gè)被包裝后的對象,也就是java為什么說自己都是對象,因?yàn)樗言鷳B(tài)的內(nèi)容進(jìn)行了包裝,讓程序編寫更加簡單;這里順便提及一下:在較早期的jdk中,jvm并不是由一個(gè)指針直接指向分配堆中的首地址,而是先有一個(gè)handle空間,這個(gè)空間存放了開始說的一些對象的定義和結(jié)構(gòu)信息,也就是找到該位置,然后由該位置轉(zhuǎn)換到對應(yīng)的對象上,但是那個(gè)時(shí)候的對象頭部信息就沒有現(xiàn)在的那么全,也就是以前是將一部分handle內(nèi)容放置在獨(dú)立的空間上,現(xiàn)在的jdk已經(jīng)沒有那樣的了。

?

???2.3.內(nèi)存分配后放在哪里,如何移動

終于回到上面的話題,內(nèi)存分配后,在堆中的什么位置?就是我們上面說的heapSize中的Young區(qū)域的Eden區(qū)域中,也就是new的對象絕大部分會放在這里(排除一種非常大的對象的特殊情況),在java設(shè)計(jì)的看來有一個(gè)特別有意思的地方,就是它在新生成的對象中它認(rèn)為你絕大部分對象都是應(yīng)該需要被銷毀掉的,就像在做java WEB應(yīng)用上一樣,一個(gè)列表請求過來,可能請求的內(nèi)容有2K的內(nèi)容,請求完成后,這個(gè)內(nèi)容一般說來自然就不需要了,也就是在他原始的考慮下它沒有考慮你自己在應(yīng)用級別去做page cache的操作;好,那么當(dāng)內(nèi)存不夠的時(shí)候,這里指被commited的空間不夠的情況下,此時(shí)java就會做一個(gè)動作,就是會對Young空間進(jìn)行回收,由于新生成的對象,java認(rèn)為這塊空間不會很大,而且絕大部分應(yīng)該是被干掉的內(nèi)容,所以很多時(shí)候java會采用單線程的復(fù)制算法(當(dāng)然你也可以設(shè)置為多線程),關(guān)于算法的核心在第三章中會說到,這里總之先理解找到了活著的對象,將其拷貝到其中一個(gè)survivor區(qū)域中,當(dāng)下一次做操作時(shí),就會將Eden中活著的以及前一個(gè)surivor活著的一起拷貝到另一個(gè)survivor中,這就是為什么要設(shè)置兩個(gè)survivor區(qū)域,而拷貝后,Eden區(qū)域?yàn)榭铡⒘硪粋€(gè)survivor也為空,可以完全直接整體清除掉,所以非常快速,而拷貝的目標(biāo)也會被連續(xù)化,新生成的對象又從Eden的初始位置開始分配空間。

????? 當(dāng)對象每次(活著)被拷貝到一個(gè)survivor時(shí),Java虛擬機(jī)就會記錄下來對象被移動的次數(shù),當(dāng)次數(shù)達(dá)到一定的程度,也就是官方文檔所說的足夠老的情況,這塊內(nèi)存就認(rèn)為它不太容易被注銷掉,此時(shí)就會被移動到第二個(gè)區(qū)域Tenured區(qū)域,這個(gè)次數(shù)也可以由自己來控制。

????? 另外在一般默認(rèn)的情況下當(dāng)回收后的內(nèi)存仍然占用實(shí)際目前commited內(nèi)存的70%以上,那么此時(shí)虛擬機(jī)將會開始擴(kuò)展這些內(nèi)存,而當(dāng)回收后的內(nèi)存小于40%后,虛擬機(jī)將會降低這部分內(nèi)存,但是其他線程仍然不能使用(當(dāng)然這個(gè)參數(shù)也是可配置的,在文章最后有說明),這樣收縮和擴(kuò)展必然導(dǎo)致一些問題,但是java的初衷是想讓你再沒有使用這塊地址表的時(shí)候,回收內(nèi)存的大小會小一些,因?yàn)閥oung區(qū)域的一般是使用單線程的回收方式,這個(gè)時(shí)間段是會被暫停的,所以它認(rèn)為內(nèi)存使用較少的時(shí)候回收就內(nèi)存的速度應(yīng)該加快;但是,和實(shí)際相反的是,我們正好需要的是內(nèi)存使用較大的時(shí)候,才希望加快回收的速度,內(nèi)存使用小的時(shí)候,回收都是無所謂的;所以我們在很多時(shí)候建議將-Xms和-Xmx設(shè)置成一樣的大小,不用這么來回倒騰。

???? 在說明下,以下三種情況對象會被晉升到old區(qū)域:

???? 1、在eden和survivor中可以來回被minor gc多次,這個(gè)次數(shù)超過了-XX:MaxTenuringThreshold

?????2、在發(fā)生minor gc時(shí),發(fā)現(xiàn)to survivor無法放下這些對象,就會進(jìn)入old。

???? 3、在新申請對象,大于eden區(qū)域的一半大小時(shí)直接進(jìn)入old,也可以專門設(shè)置參數(shù)-XX:PretenureSizeThreshold這個(gè)參數(shù)指定當(dāng)超過這個(gè)值就直接進(jìn)入old。

?

?

???? 當(dāng)上面的對象被移動到了Tenured區(qū)域,這個(gè)區(qū)域一般非常大,占用了HeapSize的絕大部分空間,此時(shí)若它發(fā)生一次內(nèi)存回收,就不能像剛才那樣來回拷貝了,那樣代價(jià)太大,而且這個(gè)區(qū)域可以說是經(jīng)得起考驗(yàn)的對象才會被移動過來,在概率上是不容易被銷毀掉的對象才會被移動過來;那么,我們很此時(shí)想到的就是反過來計(jì)算,也就是找到需要銷毀的對象,將其銷毀,關(guān)于算法也是下面第三章要說的內(nèi)容,總之對象會在這里存放著。

?

???? 為什么java不論在Young中的區(qū)域會來回倒騰,而在Tenured區(qū)域也會不斷去做壓縮,就是我們前面說的,它希望內(nèi)存相對較為連續(xù)而做的;java在Yong的區(qū)域,它認(rèn)為可以剩下的內(nèi)容不會很多,所以拷貝的代價(jià)并不大,所以它認(rèn)為來回拷貝是一種合適的方法,而Tenured區(qū)域它采用了清除后,一定次數(shù)后進(jìn)行壓縮的方式,當(dāng)然這個(gè)次數(shù)你可以自己去設(shè)置,在文章的最后是有參數(shù)的;而它沒有采用類似操作系統(tǒng)一樣的按照板塊大小等一系列算法來完成,這也是我比較納悶的事情,不過總體說來這種算法還是可行的;希望在劃分區(qū)域一些策略上能有更大的靈活性,這樣可以在更多的應(yīng)用中發(fā)揮得更加靈活,這樣就更好了;比較困惑的就是這樣的架構(gòu)自己如果做頻繁度不高不低的page cache,性能不好估量,也許比不做cache更低,這個(gè)要根據(jù)具體情況而定了。


????2.3.Perm一般還會存放什么內(nèi)容?Perm除了存放上面的Class定義外,還一般會存放的內(nèi)容有靜態(tài)代碼段、final static類型的類變量、String常量以及String被intern后的內(nèi)容,也是最后一章中所要提及以前我自己寫錯(cuò)的內(nèi)容;如何應(yīng)對好常量池,以及常量池是否會被GC,也是我們所需要說明的內(nèi)容;關(guān)于Perm永久代中存放的內(nèi)容,應(yīng)當(dāng)如何配置以至于它可以去回收,在文章的最后有相應(yīng)的說明,請自行查閱;不過對于Perm的大小,一般還是不建議去做GC的,也就是合理的去使用Perm,在程序運(yùn)行中占用Perm最多的就是String常量,尤其是如果大量使用intern的時(shí)候,就會造成大量Perm膨脹,也是最后一部分需要說明的內(nèi)容,不過intern也并非一無是處,因?yàn)槟憧梢赃@樣說:如果它沒有用處的話,java沒有必要再把String的常量放在單獨(dú)的一個(gè)地方,它有很多好處,只要在適當(dāng)?shù)臅r(shí)候利用好常量池這個(gè)區(qū)域在必要的時(shí)候可以提高性能,具體在最后一章有所講解。

?

?

?

3、虛擬機(jī)如何定義回收算法

??? 3.1.首先虛擬的回收算法會分成兩個(gè)部分,一個(gè)部分是對象的查找算法,一個(gè)是真正如何回收的方法。一般對于查找有以下兩種:

??? a)引用計(jì)數(shù):本來在本文中我不想提及引用計(jì)數(shù),因?yàn)檫@是最原始也是最垃圾的算法,也是較低版本jdk慢得出奇的原因,但是為了說明后面的問題不得不簡單說明一下,引用計(jì)數(shù)就是通過java虛擬機(jī)專門為每個(gè)對象記錄它被指針指向的個(gè)數(shù),當(dāng)發(fā)生指針指向它或者被賦值,計(jì)數(shù)器將會被加1,而但指向它的指針=null或者脫離了作用區(qū)域,jvm就會將相應(yīng)的計(jì)數(shù)器減少1,這樣簡單,但是慢死了,不僅僅操作上出奇的慢,因?yàn)橐鲆粋€(gè)簡單的賦值操作要到多個(gè)地方去找一大堆東西;還有一個(gè)就會引起很難檢測到的內(nèi)存泄露,那就是當(dāng)兩個(gè)或者多個(gè)對象存在循環(huán)交叉引用的時(shí)候,此時(shí)他們的引用計(jì)數(shù)將永遠(yuǎn)不會等于0(如使用雙向鏈表或使用復(fù)雜的集合類后,相互之間的引用),也就是垃圾收集器將永遠(yuǎn)不會認(rèn)為這是垃圾(當(dāng)然要用復(fù)雜的算法可以解決,但是這個(gè)算法的確很復(fù)雜,可能垃圾回收會更加慢),最后就是這個(gè)垃圾回收方式必然導(dǎo)致內(nèi)存的遍歷操作過程。引用計(jì)數(shù)的示意圖如下圖所示:

?

?

?

????b)引用樹遍歷:其實(shí)是一個(gè)圖,只是有根而已,它沿著對象的根句柄向下查找到活著的節(jié)點(diǎn),并標(biāo)記下來,其余沒有被標(biāo)記的節(jié)點(diǎn)就是死掉的節(jié)點(diǎn),這些對象就是可以被回收的,或者說活著的節(jié)點(diǎn)就是可以被拷貝走的,具體要看所在heapSize中的區(qū)域以及算法,它的大致示意圖如下圖所示(對象:B、G、D、J、K、L、F都是垃圾對象,雖然他們也有相互指向,但是不是被根節(jié)點(diǎn)能遍歷到的,注意這里是指針是單向的):

?

?

?

????3.2.內(nèi)存回收:上面的方法我們可以找到內(nèi)存可以被使用的,或者說那些內(nèi)存是可以回收,更多的時(shí)候我們肯定愿意做更少的事情達(dá)到同樣的目的,我們會根據(jù)一般的情況設(shè)置不同的算法來讓系統(tǒng)的性能達(dá)到較好的程度,首先來了解下內(nèi)存回收的算法或者它的經(jīng)歷有哪些?

????a):標(biāo)記清除算法,這算是比較原始的算法,也就是通過上面的查找標(biāo)記后,我們對沒有標(biāo)記的對象進(jìn)行空間釋放的過程,這個(gè)算法雖然很原始,但是是后來所有算法的基礎(chǔ),好處的簡單,缺陷是造成和其他語言一樣的內(nèi)存碎片,要通過更加復(fù)雜的算法來解決這些碎片;另一缺陷就是它這個(gè)過程如果用于較大的內(nèi)存將會導(dǎo)致長時(shí)間的對外服務(wù)停止(當(dāng)然這個(gè)停止也不是傳說中那么長,只是相對計(jì)算機(jī)來說比較長,至于多長是還和jdk的版本以及廠商有關(guān)系,BEA曾經(jīng)在1G的JVM下面測試,有300M空間屬于可用空間,據(jù)測試結(jié)果為30ms的停止服務(wù)時(shí)間,我想這個(gè)時(shí)間應(yīng)該可以接受,不過它有自己的測試場景,不能完全說明問題,而一般情況下在單線程引用下,常規(guī)的回收起碼會比這個(gè)時(shí)間要長好幾倍甚至于10倍以上)。

????b):標(biāo)記清楚壓縮,這個(gè)算法是也是較為原始的,它的出現(xiàn)是為了解決上面一種算法中不能壓縮空間的問題,但是并非取代,因?yàn)樗鼘?dǎo)致的另一個(gè)問題就是更長時(shí)間的服務(wù)停止,因?yàn)閴嚎s就是空間拷貝到一個(gè)較為連續(xù)的地方,而并非對數(shù)據(jù)本身進(jìn)行壓縮,所以很多時(shí)候他們是配合使用的,如多少次清除后進(jìn)行一次壓縮。

???c)復(fù)制回收:也就是在jvm發(fā)展的過程中出現(xiàn)的算法,現(xiàn)在基本都只能看到一些思想影子在里面,但是沒有這個(gè)方式,也就是將其劃分為2個(gè)相同的大小,然后將活著的節(jié)點(diǎn)來回拷貝,這樣造成的內(nèi)存浪費(fèi)的非常大的,不僅僅是一半的浪費(fèi)問題,而且每次拷貝的開銷也是非常大的,因?yàn)槎际巧婕暗秸麄€(gè)jvm活著節(jié)點(diǎn)的拷貝過程。

???d)增量回收:這算是現(xiàn)代垃圾回收的一個(gè)前身,它做的事情就是為了解決復(fù)制回收算法中的一個(gè)問題,就是每次復(fù)制造成的空間開銷非常大的問題,此時(shí)它將內(nèi)存中切分為逐個(gè)板塊,這些板塊,每個(gè)內(nèi)部使用了復(fù)制算法,也就是并沒有解決空間浪費(fèi)的問題,回收的過程中沒有進(jìn)行細(xì)化,雖然回收速度較快速,而且只會造成局部的停止服務(wù),但是對于不同板塊大小、不同生命周期的對象還是沒有劃分開。

???e)分代收集器:分代收集器是增量收集的另一個(gè)化身,或者說延續(xù)吧,它將板塊按照生命周期劃分為上面所說的板塊,每一個(gè)板塊可以采用不同的算法進(jìn)行回收,這也是和增量回收最大的區(qū)別,此時(shí)可以讓jvm的回收達(dá)到更好的效果,不過由于jvm按照生命周期劃分后都是指定板塊的,所以根據(jù)內(nèi)存大小劃分自定義板塊是不可能的,至少現(xiàn)在好像還沒有,所以在回收過程中如果內(nèi)存大了回收起來一樣很吃力,尤其是對Old區(qū)域的回收,所以并發(fā)回收不得不出現(xiàn)了。

???f)并發(fā)回收:所謂并發(fā)回收是指外部在訪問的同時(shí),java回收器依然在做著回收工作,原早我認(rèn)為并發(fā)回收是不可能的,因?yàn)槟阈枰纼?nèi)存是需要回收的,就不能讓內(nèi)存繼續(xù)的被申請和釋放,但是SUN的人還是比較天才的,還是有辦法盡量讓他并發(fā)去做的;并發(fā)回收器其實(shí)也會暫停,但是時(shí)間非常短,它并不會在從開始回收尋找、標(biāo)記、清楚、壓縮或拷貝等方式過程完全暫停服務(wù),它發(fā)現(xiàn)有幾個(gè)時(shí)間比較長,一個(gè)就是標(biāo)記,因?yàn)檫@個(gè)回收一般面對的是老年代,這個(gè)區(qū)域一般很大,而一般來說絕大部分對象應(yīng)該是活著的,所以標(biāo)記時(shí)間很長,還有一個(gè)時(shí)間是壓縮,但是壓縮并不一定非要每一次做完GC都去壓縮的,而拷貝呢一般不會用在老年代,所以暫時(shí)不考慮;所以他們想出來的辦法就是:第一次短暫停機(jī)是將所有對象的根指針找到,這個(gè)非常容易找到,而且非常快速,找到后,此時(shí)GC開始從這些根節(jié)點(diǎn)標(biāo)記活著的節(jié)點(diǎn)(這里可以采用并行),然后待標(biāo)記完成后,此時(shí)可能有新的 內(nèi)存申請以及被拋棄(java本身沒有內(nèi)存釋放這一概念),此時(shí)JVM會記錄下這個(gè)過程中的增量信息,而對于老年代來說,必須要經(jīng)過多次在survivor倒騰后才會進(jìn)入老年代,所以它在這段時(shí)間增量一般來說會非常少,而且它被釋放的概率前面也說并不大(JVM如果不是完全做Cache,自己做pageCache而且發(fā)生概率不大不小的pageout和pagein是不適合的);JVM根據(jù)這些增量信息快速標(biāo)記出內(nèi)部的節(jié)點(diǎn),也是非常快速的,就可以開始回收了,由于需要?dú)⒌舻墓?jié)點(diǎn)并不多,所以這個(gè)過程也非常快,壓縮在一定時(shí)間后會專門做一次操作,有關(guān)暫停時(shí)間在Hotspot版本,也就是SUN的jdk中都是可以配置的,當(dāng)在指定時(shí)間范圍內(nèi)無法回收時(shí),JVM將會對相應(yīng)尺寸進(jìn)行調(diào)整,如果你不想讓它調(diào)整,在設(shè)置各個(gè)區(qū)域的大小時(shí),就使用定量,而不要使用比例來控制;當(dāng)采用并發(fā)回收算法的時(shí)候,一般對于老年代區(qū)域,不會等待內(nèi)存小于10%左右的時(shí)候才會發(fā)起回收,因?yàn)椴l(fā)回收是允許在回收的時(shí)候被分配,那樣就有可能來不及了,所以并發(fā)回收的時(shí)候,JVM可能會在68%左右的時(shí)候就開始啟動對老年代GC了。

???d)并行回收:并行回收指利用多個(gè)CPU對JVM進(jìn)行并行垃圾回收的過程,并行度都是可以設(shè)置的,可以分別對年輕代和老年代配置是否使用并行回收。

?

好了,回收算法就說到這里,那么如何利用好回收算法,在看了上面的介紹后,是否對JVM有了一個(gè)大致的了解,具體細(xì)節(jié),可以慢慢實(shí)踐,在文章最后給出一些常用的java虛擬機(jī)內(nèi)存設(shè)置參數(shù)的說明,不過并不權(quán)威,需要根據(jù)實(shí)際情況而定才可以。

?

?

?

?

下面說下java虛擬機(jī)除了消耗基本內(nèi)存外還會消耗什么內(nèi)存?

?

?

?

4、JVM占用的空間除HeapSize還會占用什么?

?

?

??? 一般來說,對于很多學(xué)了好幾年,甚至于很多年java人來說,一旦看到OutOfMemeory(簡稱OOM),就認(rèn)為HeapSize不夠,然后瘋狂的增加-Xmx的值,但是HeapSize只是其中一個(gè)部分,當(dāng)你去做一個(gè)實(shí)驗(yàn),也就是java啟動時(shí)直接在程序中瘋狂的new 一些線程出來,直到內(nèi)存溢出,當(dāng)-Xms -Xmx設(shè)置得越大的時(shí)候,得到的線程個(gè)數(shù)會越少,為什么呢?因?yàn)镺OM并不是HeapSize不夠而導(dǎo)致的,而由很多種情況。

?

??? 首先看下操作系統(tǒng)如何劃分內(nèi)存給應(yīng)用系統(tǒng),其實(shí)在Win 32、Linux 32的系統(tǒng)中,地址總線為32位的理論上應(yīng)該可以支持4G內(nèi)存空間,但是當(dāng)你在Win 32上設(shè)置初始化內(nèi)存如果達(dá)到2G,就會報(bào)錯(cuò),說這個(gè)塊空間沒法做,首先默認(rèn)的Win32系統(tǒng),會按照50%比例給予給Kernel使用,而另一部分給應(yīng)用內(nèi)存,也就是說操作系統(tǒng)內(nèi)核部分不論是否使用,這一半是不會給你的,而還有2G呢,它在系統(tǒng)擴(kuò)展的部分,也就是并非Kernel的部分,有很多靜態(tài)區(qū)域和字典表的內(nèi)容,所以要劃分一個(gè)連續(xù)的2G內(nèi)存給JVM在Win 32上是不可能的,Win 32提出了一種Win 32 3G模式,貌似可以劃分3G空間,其實(shí)它只是將內(nèi)核部分縮小也就是管理部分縮小,也就是將一部分劃分到外部來使用,而且Win 32習(xí)慣在內(nèi)存2G的位置做一些手腳,讓你分配連續(xù)2G沒有可能性,一般來說在Win 32平臺上,在物理內(nèi)存足夠的情況下給JVM劃分的空間一般是1.4~1.5G左右,具體數(shù)據(jù)沒有測試過;而Linux 32類似于Win 32 3G模式,但是它還是一般情況下分布不凌亂的情況下,一般可以給JVM劃分到2G的大小。Linux 32 Hugemem是一個(gè)擴(kuò)展版本,可以劃分更大的空間,但是需要付出一些其他的代價(jià),理論上可以支持到4G給應(yīng)用,也就是Kenel是獨(dú)立的;Solaris x86-32和AIX 32等系統(tǒng),也類似于Linux 32平臺一樣。

?

??為什么還要預(yù)留一些空間出來呢?這些空間給誰?

? 當(dāng)你申請一個(gè)線程的時(shí)候,它的除了線程內(nèi)部對象的開銷外,線程本身的開銷,是需要OS來調(diào)度完成,一般來說,會在OS的線程與虛擬機(jī)內(nèi)部有都有一個(gè)一一對應(yīng)的,但是會根據(jù)操作系統(tǒng)不同有所變化,有些可能只有一個(gè),總之heapSize外的那部分空間是跑不掉的,它放在哪里呢?就是放在Stack中的,所以上文中的-Xss就是設(shè)置這個(gè)的,在jdk 1.5以后,每個(gè)線程的大小被默認(rèn)設(shè)置為1M的stack開銷,我們習(xí)慣將這個(gè)開銷降低。

?

??好了知道了指針、線程是在heapSize外部的,還有什么呢?

? 當(dāng)你自己使用native方法,也就是JNI的時(shí)候,調(diào)用本地其他語言,如C、C++在程序中使用了malloc等類似方法開辟的內(nèi)存,都不是在heapSize中的,而是在本地OS所掌控的,另外這部分空間如果沒有相應(yīng)的釋放命令,就需要在對應(yīng)finalize方法內(nèi)部調(diào)用其他的native方法來完成對相應(yīng)對象的釋放,否則這部分將成為OS級別的內(nèi)存泄露,直到JVM進(jìn)程重啟或者宕機(jī)為止(操作系統(tǒng)會記錄下進(jìn)程和相應(yīng)線程和堆內(nèi)存的關(guān)聯(lián)關(guān)系,但是進(jìn)程再沒有釋放前,OS也是不會回收這部分內(nèi)存的)。

??另外在使用JavaNIO以及JDBC、流等系列操作時(shí),當(dāng)形成與終端交互時(shí),會在另一個(gè)位置形成一個(gè)內(nèi)存區(qū)域,這些內(nèi)存區(qū)域都不在HeapSize中。

? 所以常見的OOM現(xiàn)象有以下幾種:

? 1、heapSize溢出,這個(gè)需要設(shè)置Java虛擬機(jī)的內(nèi)存情況

? 2、PermSize溢出,需要設(shè)置Perm相關(guān)參數(shù)以及檢查內(nèi)存中的常量情況。

? 3、OS地址空間不夠,也就是沒有那么多內(nèi)存分配,這個(gè)一般是啟動時(shí)報(bào)錯(cuò)。

? 4、Swap空間頻繁交互,進(jìn)程直接被crash掉,在不同操作系統(tǒng)中會體現(xiàn)不同的情況。

? 5、native Thread溢出,注意線程Stack的大小,以及本身操作系統(tǒng)的限制。

??6、DirectByteBuffer溢出,這一類一般是在做一些NIO操作的時(shí)候,或在某種情況下使用ByteBuffer,在分配內(nèi)存時(shí)使用了allocateDirect以及使用一些框架間接調(diào)用了類似方法,導(dǎo)致直接內(nèi)存的分配(如mina中使用IoByte去調(diào)用,當(dāng)參數(shù)設(shè)置為true的時(shí)候就分配為直接內(nèi)存,所謂直接內(nèi)存就是又OS定義的內(nèi)存,而不需要從程序間接拷貝一次再輸出的過程,提高性能,但是如果沒有手動回收是回收不掉的),導(dǎo)致的Buffer問題,如輸出大量的內(nèi)容,輸入大量的內(nèi)容,此時(shí)需要盡量去嘗試限制它的大小。

?

使用非常多的工具區(qū)檢測Java的內(nèi)存如:jstat(只能看HeapSize和PermSize)、jmap(很細(xì)的東西)、jps(java的ps -ef呵呵)、jdb(這個(gè)不是監(jiān)控工具哈,這個(gè)是debug工具)、jprofile(圖形支持,但是可以遠(yuǎn)程連接)等等;jconsole(可以看到heapsize、permsize+native mem size(這這里叫做:non-heapsize)等等的使用的趨勢圖)、visualvm(極為推薦的東西,圖形化查看,你可以查看到內(nèi)存單元分配、交換、回收、移動等等整個(gè)過程,非常清晰展現(xiàn)jvm的全局資源)、另外pmap可以展現(xiàn)非常清晰的資料,可以精確到某一個(gè)java進(jìn)程內(nèi)部的每一個(gè)細(xì)節(jié),而且可以看到heapsize只是其中很小一部分(在solaris操作系統(tǒng)上看得最齊全,LINUX下有些進(jìn)程可能看不太懂);也可以在/proc/進(jìn)程號/maps中查看(這里可以看到內(nèi)存地址單元的起始地址,包含了reserved的地址范圍和commited的地址范圍),全局資源使用操作系統(tǒng)top命令和free命令看;IBM有一個(gè)GCMV免費(fèi)下載工具也很好;Win32有一個(gè)WMMap工具都是很好的工具

?

?

使用相應(yīng)的工具觀察相應(yīng)的內(nèi)容,當(dāng)觀察到內(nèi)存的使用從無到有,上升,然后處于一個(gè)平穩(wěn)趨勢,那么這個(gè)JVM應(yīng)該是較為穩(wěn)定的;如果發(fā)現(xiàn)它經(jīng)過一段平滑期后,又出現(xiàn)飆升,這個(gè)必然是有問題的,至于什么問題,根據(jù)前面的學(xué)下和實(shí)際情況我們可以去分析;當(dāng)它開始后,平滑過程,出現(xiàn)緩慢上升的過程,但是始終會上升到極點(diǎn),那么一個(gè)是需要知道物理內(nèi)存時(shí)候可用,另一個(gè)就是少量的內(nèi)存泄露(JVM現(xiàn)代也有內(nèi)存泄露,只是它的內(nèi)存泄露并非C、C++中的內(nèi)存泄露)。

?

5、糾正錯(cuò)誤:intern()的使用上的錯(cuò)誤

???? 最后一章節(jié),我自己糾正一下我自己的錯(cuò)誤,以前的文章中,也就是關(guān)于intern的使用,最近對他做了一些深入研究,因?yàn)橐郧耙彩呛秃芏嗤瑢W(xué)一樣,聽到別人推薦什么就瘋狂的使用,知道點(diǎn)原理也是點(diǎn)大概,沒有深入研究內(nèi)部的內(nèi)容。

???? 我曾經(jīng)在文章中說到任何系統(tǒng)最多使用的數(shù)據(jù)類型必然是String,不管做什么,所以在String的處理上很有研究,推薦使用java的朋友在大量使用對比的時(shí)候不要用equals,而推薦使用intern(),但是我最近發(fā)現(xiàn)我錯(cuò)了,我這里給大家道歉,因?yàn)榭赡軙`導(dǎo)很多朋友;下面說明下這個(gè)東西為什么?

???? 首先我開始自己懷疑自己的時(shí)候是想說,如果intern可以做到高效,那么equals是不是在String中就沒有存在的必要了呢,當(dāng)時(shí)對于我理解僅僅為常量池的一個(gè)地址對比,好比是兩個(gè)數(shù)字的compare,僅僅需要CPU的單個(gè)指令即可完成;于是我開始做了兩個(gè)實(shí)驗(yàn),一個(gè)是最原始,最初級的方法采用單線程循環(huán)1000000次調(diào)用equals與intern等值對比,并且采用了不同長度的字符串去做比較,發(fā)現(xiàn)equals竟然比intern要快,而且隨著字符串長度的增加,equals會明顯快與intern,然后使用多線程測試也是得到一樣的效果,我首先很不敢相信自己堅(jiān)持的理論被徹底和諧了,后來冷靜下來必須需要面對,通過很多權(quán)威資料的閱讀,我發(fā)現(xiàn)我對JVM常量池的理解還只是一點(diǎn)點(diǎn)皮毛而已,所以我做了更加深入的研究。

??? 原來intern方法被調(diào)用時(shí)是在Perm中的String私有化常量池中尋找相應(yīng)的內(nèi)容,而尋找雖然可以通過hash定位到某些較小的鏈表中,但是還是需要在鏈表中逐個(gè)對比,對比的方法仍然是equals,也就是拋開hash的開銷,intern最少要與里面的0到多個(gè)對象進(jìn)行equals操作,而且如果不存在,還要在常量池開辟一塊空間來記錄,如果存在則返回地址,也就是常量池保證每個(gè)String常量是唯一的,這個(gè)開銷當(dāng)然大了,而且如果使用在業(yè)務(wù)代碼中將會導(dǎo)致Perm區(qū)域的不斷增加;

??? 于是,我又反過來想了:既然equals比他效率高,為啥還要用intern呢?而且equals的那個(gè)算法對于長字符串逐個(gè)字符對比的過程我實(shí)在是難以入目;而且也實(shí)在是覺得不甘心自己的理論就這么容易被和諧掉,因?yàn)樽约阂呀?jīng)在不少程序中這樣用過,這樣我豈不是犯下大錯(cuò)了,因?yàn)樽约簠⑴c過的項(xiàng)目的確太多了,而且有類似的代碼我寫入了框架中,最終發(fā)現(xiàn)我可能錯(cuò)了一半,也就是歷史上的記錄可能我有一半類似的代碼是錯(cuò)誤的;為什么呢?intern還是有用的,我先做了一個(gè)測試,那就是,用一個(gè)已經(jīng)intern好的對象,讓他與一個(gè)常量做等值,循環(huán)次數(shù)和上面一樣,結(jié)果我預(yù)料的結(jié)果發(fā)生了,那就是比equals快出了N多倍數(shù),隨著長度的增加,會體現(xiàn)出更加明顯的優(yōu)勢,因?yàn)閕ntern對比的始終是地址,和長度無關(guān),于是我想到了如何使用它,就是在程序中返回通過字符串類似于數(shù)字一樣的類型判定時(shí),如:做一個(gè)sqlparser的時(shí)候,經(jīng)常根據(jù)數(shù)據(jù)類型做不同的動作,這樣如果用equals會在每次循環(huán)時(shí)付出很多開銷,尤其是很多數(shù)據(jù)庫的類型非常多,最壞的是從上到下每個(gè)字符串匹配一次,當(dāng)然長度不等開銷很小,長度相等開銷就大了;intern我就將這些schema信息預(yù)先intern掉,也就是他們已經(jīng)指向了常量池,當(dāng)再真正匹配時(shí),就不需要用intern了,而是直接匹配,也就是將這個(gè)開銷放在初始化的過程中,運(yùn)行時(shí)我們不去增加它的開銷。

??? 所以,個(gè)人是犯下一個(gè)錯(cuò)誤,并且以前還很張揚(yáng)的到處宣傳,呵呵,現(xiàn)在覺得有點(diǎn)傻,希望在看到某些推薦用什么新東西的時(shí)候,千萬不要在沒有研究明白他就去用它,甚至于濫用它,至少要經(jīng)過一些簡單的測試,不過對于現(xiàn)代很多復(fù)雜的東西,一些簡單的測試已經(jīng)不足以說明問題,就像Lock與Synchronize的開銷一樣,如果采用簡單的循環(huán)的話,你會發(fā)現(xiàn)新版本的Lock的開銷將會比Synchronized的開銷更加大,它適合的是并發(fā),讀寫的并發(fā),所以真正要弄清楚還是研究內(nèi)在。

?

??? 最后說下,我個(gè)人對JVM的期望,JVM做到了很多個(gè)板塊之間使用不同的算法,而JVM不希望程序員去關(guān)心內(nèi)存,但是有些特殊的應(yīng)用需要JVM提供多的支持,當(dāng)然有些公司對JVM內(nèi)核進(jìn)行了改造來適合特殊的應(yīng)用,但是我們更加希望標(biāo)準(zhǔn)的JVM能夠提供更加靈活的內(nèi)存管理機(jī)制,而不僅局限于配置,因?yàn)榕渲眠m中是死的,在很多時(shí)候會面臨擴(kuò)展性的限制;如很多時(shí)候我們認(rèn)為可以判定很多的對象本身就是不會被回收或者根本不容易被回收的,就不用到Y(jié)oung的空間和其他的業(yè)務(wù)套在一起倒騰了;對于經(jīng)常做page cache的系統(tǒng),而page cache的命中率不是特別高(95%以上就很高),也不是很低(如80%以下),這個(gè)時(shí)候,置換到快不慢的,而會導(dǎo)致在老年代的回收的頻繁起來,就我個(gè)人希望這些空間都能獨(dú)立出來,甚至于可以由程序去控制和指定,當(dāng)然JVM可以自身去默認(rèn);尤其是按照一些特殊的對象等級類型或者說對象的大小,這些細(xì)節(jié)都可以采用一些相應(yīng)的默認(rèn)GC手段來完成,也可以人工的指定,當(dāng)然也在默認(rèn)情況下可以按照原有的模式進(jìn)行架構(gòu),這樣JVM的內(nèi)存調(diào)節(jié)的靈活將會更加寬松,使得它能在各類場合下只要使用相對應(yīng)的手段配置和程序調(diào)整都是可以打到目的的。

?

?? 本文包含大量個(gè)人見解,如有不是之處,請大家多多指教!本文到此完結(jié),內(nèi)容粗而不深入,細(xì)節(jié)問題,細(xì)節(jié)討論。

?

?

?

?

?常見參數(shù)JVM參數(shù)配置(java vm Hotspot TM 1.6):

?

?-Xms為初始化為HeapSize的空間,即被Commited的尺寸。 ?-Xmx為最大的HeapSize空間,有些尚未被Commited,但是已經(jīng)被進(jìn)程所Reserved,當(dāng)現(xiàn)在已經(jīng)被Commit的空間長期處于(jdk1.1還有一個(gè)-mx為包含handler表的空間) ?-Xmn設(shè)置Young的空間大小,此時(shí)NewSizeMaxNewSize一致,或者分別設(shè)置-XX:NewSize=128m ?-XX:PermSize?= 64M-XX:MaxPermSize= 64M為永久代的初始大小和最大大小。 ?-XX:NewRatio= 3?為Tenured:Young的初始尺寸比例(設(shè)置了大小就不再設(shè)置此值),此時(shí)Young占用整個(gè)HeapSize的1/4大小。 ?-XX:SurvivorRatio= 6:為Eden:Survivor比例大小,此時(shí)一個(gè)Survivor占用Young的1/8大小,而Eden占用3/4大小 ?-Xss=256kThreadStack空間大小,jdk?1.5以后默認(rèn)是1M,在IBMjdk中還有-Xoss參數(shù)(此時(shí)每個(gè)線程占用的stack空間為256K大小) ?-XX:MaxTenuringThreshold=3:一般一個(gè)對象在Young經(jīng)過多少次GC后會被移動到OLD區(qū)。
-XX:+UseParNewGC:對Yong區(qū)域啟用并行回收算法。
?-XX:+UseParallelGC:一種較老的并行回收算法 ?-XX:+UseParallelOldGC:對Tenured區(qū)域使用并行回收算法。 ?-XX:ParallelGCThread=10:并行的個(gè)數(shù),一般和CPU個(gè)數(shù)相對應(yīng)。 ?-XX:+UseAdaptiveSizepollcy:收集器自動根據(jù)實(shí)際情況進(jìn)行一些比例以及回收算法調(diào)整。 ?-XX:CMSFullGCsBeforeCompaction= 3:多少次GC后會進(jìn)行壓縮碎片 ?-XX:+UseCmsFullCompactAtFullCollction:打開老年代壓縮 以下3個(gè)參數(shù)為永久帶回收參數(shù):
-XX:+UseConcMarkSweepGC?-XX:+CMSClassUnloadingEnabled

-XX:+CMSPermGenSweepingEnabled對永久帶進(jìn)行相應(yīng)的回收,在jdk1.6中不需要數(shù):-XX:+CMSPermGenSweepingEnabled -XX:MinHeapFreeRatio這是指剩余空間百分比多少時(shí),開始減小commited的內(nèi)存; -XX:MaxHeapFreeRatio指剩余空間百分比多少時(shí),開始增加commited的內(nèi)存,直到-Xmx大小。 -XX:MaxGCPauseMillis指GC最大的暫停時(shí)間,當(dāng)超過這個(gè)時(shí)間,那么JVM會適當(dāng)調(diào)整內(nèi)存比例(前提是使用的是基于比例的YONG和設(shè)置)。 -XX:+UseConcMarkSweepGC?啟動并發(fā)GC,一般針對Tenured區(qū)域。 -XX:+CMSIncrementalMode增量GC,將內(nèi)存切塊,分布在多個(gè)局部去GC。 -XX:CMSInitiatingOccupancyFraction在并發(fā)GC下,由于一邊使用,一遍GC,就不能在不夠用的時(shí)候GC,默認(rèn)情況下是在使用了68%的時(shí)候進(jìn)行GC,通過該參數(shù)可以調(diào)整實(shí)際的值。
大致的參數(shù)設(shè)置就這些,但是GC本身的參數(shù)還有很多,尤其是和應(yīng)用或者和具體硬件結(jié)合起來的時(shí)候,而BEA和IBM也有自己的JDK,這里有些參數(shù)他們支持,有些參數(shù)不支持,在某些平臺和甚至于硬件上可以支持特殊的參數(shù)來控制(如在部分intel系列的多CPU機(jī)器上,通過它的NUMA架構(gòu),可以設(shè)置對應(yīng)參數(shù)支撐,節(jié)點(diǎn)和CPU之間可以實(shí)現(xiàn)分工負(fù)載、常規(guī)服務(wù)上都是SMP的,而大型機(jī)上多半是MPP);類似于上面的并發(fā)GC在一般情況下是不會進(jìn)行compact壓縮的,因?yàn)樗M厥盏臅r(shí)間短,但是充滿compact的壓縮時(shí)間必然不是那么短,所以在部分特殊應(yīng)用下有些使用定寬度的內(nèi)存尺寸,回收后不管空余內(nèi)存,因?yàn)槊總€(gè)內(nèi)存的尺寸都是那么大,這樣來處理,當(dāng)然這樣必然會導(dǎo)致很多的內(nèi)存浪費(fèi),但是它的好處是可以沒有compact而不存在說要分配的內(nèi)存分配不到的問題。

總結(jié)

以上是生活随笔為你收集整理的认识JVM--第一篇-对象分配回收算法的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。