簡單明了帶你認(rèn)識JVM

前言

如果在文中用詞或者理解方面出現(xiàn)問題示绊,歡迎指出。此文旨在提及而不深究剧包,但會盡量效率地把知識點(diǎn)都拋出來

一肿嘲、JVM的基本介紹

JVM 是 Java Virtual Machine 的縮寫,它是一個虛構(gòu)出來的計(jì)算機(jī)次乓,一種規(guī)范吓歇。通過在實(shí)際的計(jì)算機(jī)上仿真模擬各類計(jì)算機(jī)功能實(shí)現(xiàn)···

好,其實(shí)拋開這么專業(yè)的句子不說票腰,就知道JVM其實(shí)就類似于一臺小電腦運(yùn)行在windows或者linux這些操作系統(tǒng)環(huán)境下即可城看。它直接和操作系統(tǒng)進(jìn)行交互,與硬件不直接交互杏慰,可操作系統(tǒng)可以幫我們完成和硬件進(jìn)行交互的工作测柠。

1.1 Java文件是如何被運(yùn)行的

比如我們現(xiàn)在寫了一個 HelloWorld.java 好了,那這個 HelloWorld.java 拋開所有東西不談缘滥,那是不是就類似于一個文本文件轰胁,只是這個文本文件它寫的都是英文,而且有一定的縮進(jìn)而已朝扼。

那我們的?JVM?是不認(rèn)識文本文件的赃阀,所以它需要一個?編譯?,讓其成為一個它會讀二進(jìn)制文件的?HelloWorld.class

① 類加載器

如果?JVM?想要執(zhí)行這個?.class?文件吟税,我們需要將其裝進(jìn)一個?類加載器?中凹耙,它就像一個搬運(yùn)工一樣姿现,會把所有的?.class?文件全部搬進(jìn)JVM里面來。

② 方法區(qū)

方法區(qū)?是用于存放類似于元數(shù)據(jù)信息方面的數(shù)據(jù)的肖抱,比如類信息备典,常量,靜態(tài)變量意述,編譯后代碼···等

類加載器將 .class 文件搬過來就是先丟到這一塊上

③ 堆

?主要放了一些存儲的數(shù)據(jù)提佣,比如對象實(shí)例,數(shù)組···等荤崇,它和方法區(qū)都同屬于?線程共享區(qū)域?拌屏。也就是說它們都是?線程不安全?的

④ 棧

?這是我們的代碼運(yùn)行空間。我們編寫的每一個方法都會放到??里面運(yùn)行术荤。

我們會聽說過 本地方法棧 或者 本地方法接口 這兩個名詞倚喂,不過我們基本不會涉及這兩塊的內(nèi)容,它倆底層是使用C來進(jìn)行工作的瓣戚,和Java沒有太大的關(guān)系端圈。

⑤ 程序計(jì)數(shù)器

主要就是完成一個加載工作,類似于一個指針一樣的子库,指向下一行我們需要執(zhí)行的代碼舱权。和棧一樣,都是?線程獨(dú)享?的仑嗅,就是說每一個線程都會有自己對應(yīng)的一塊區(qū)域而不會存在并發(fā)和多線程的問題宴倍。

小總結(jié)

Java文件經(jīng)過編譯后變成 .class 字節(jié)碼文件

字節(jié)碼文件通過類加載器被搬運(yùn)到 JVM 虛擬機(jī)中

虛擬機(jī)主要的5大塊:方法區(qū),堆都為線程共享區(qū)域仓技,有線程安全問題鸵贬,棧和本地方法棧和計(jì)數(shù)器都是獨(dú)享區(qū)域,不存在線程安全問題浑彰,而 JVM 的調(diào)優(yōu)主要就是圍繞堆恭理,棧兩大塊進(jìn)行

1.2 簡單的代碼例子

一個簡單的學(xué)生類

一個main方法

執(zhí)行main方法的步驟如下:

編譯好 App.java 后得到 App.class 后,執(zhí)行 App.class郭变,系統(tǒng)會啟動一個 JVM 進(jìn)程颜价,從 classpath 路徑中找到一個名為 App.class 的二進(jìn)制文件,將 App 的類信息加載到運(yùn)行時數(shù)據(jù)區(qū)的方法區(qū)內(nèi)诉濒,這個過程叫做 App 類的加載

JVM 找到 App 的主程序入口周伦,執(zhí)行main方法

這個main中的第一條語句為 Student student = new Student("tellUrDream") ,就是讓 JVM 創(chuàng)建一個Student對象未荒,但是這個時候方法區(qū)中是沒有 Student 類的信息的专挪,所以 JVM 馬上加載 Student 類,把 Student 類的信息放到方法區(qū)中

加載完 Student 類后,JVM 在堆中為一個新的 Student 實(shí)例分配內(nèi)存寨腔,然后調(diào)用構(gòu)造函數(shù)初始化 Student 實(shí)例速侈,這個 Student 實(shí)例持有?指向方法區(qū)中的 Student 類的類型信息?的引用

執(zhí)行student.sayName();時,JVM 根據(jù) student 的引用找到 student 對象迫卢,然后根據(jù) student 對象持有的引用定位到方法區(qū)中 student 類的類型信息的方法表倚搬,獲得 sayName() 的字節(jié)碼地址。

執(zhí)行sayName()

其實(shí)也不用管太多乾蛤,只需要知道對象實(shí)例初始化時會去方法區(qū)中找類信息每界,完成后再到棧那里去運(yùn)行方法。找方法就在方法表中找家卖。

二眨层、類加載器的介紹

之前也提到了它是負(fù)責(zé)加載.class文件的,它們在文件開頭會有特定的文件標(biāo)示上荡,將class文件字節(jié)碼內(nèi)容加載到內(nèi)存中趴樱,并將這些內(nèi)容轉(zhuǎn)換成方法區(qū)中的運(yùn)行時數(shù)據(jù)結(jié)構(gòu),并且ClassLoader只負(fù)責(zé)class文件的加載榛臼,而是否能夠運(yùn)行則由 Execution Engine 來決定

2.1 類加載器的流程

從類被加載到虛擬機(jī)內(nèi)存中開始伊佃,到釋放內(nèi)存總共有7個步驟:加載,驗(yàn)證沛善,準(zhǔn)備,解析塞祈,初始化金刁,使用,卸載议薪。其中驗(yàn)證尤蛮,準(zhǔn)備,解析三個部分統(tǒng)稱為連接

2.1.1 加載

將class文件加載到內(nèi)存

將靜態(tài)數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)化成方法區(qū)中運(yùn)行時的數(shù)據(jù)結(jié)構(gòu)

在堆中生成一個代表這個類的 java.lang.Class對象作為數(shù)據(jù)訪問的入口

2.1.2 連接

驗(yàn)證:確保加載的類符合 JVM 規(guī)范和安全斯议,保證被校驗(yàn)類的方法在運(yùn)行時不會做出危害虛擬機(jī)的事件产捞,其實(shí)就是一個安全檢查

準(zhǔn)備:為static變量在方法區(qū)中分配內(nèi)存空間,設(shè)置變量的初始值哼御,例如 static int a = 3 (注意:準(zhǔn)備階段只設(shè)置類中的靜態(tài)變量(方法區(qū)中)坯临,不包括實(shí)例變量(堆內(nèi)存中),實(shí)例變量是對象初始化時賦值的)

解析:虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程(符號引用比如我現(xiàn)在import java.util.ArrayList這就算符號引用恋昼,直接引用就是指針或者對象地址看靠,注意引用對象一定是在內(nèi)存進(jìn)行)

2.1.3 初始化

初始化其實(shí)就是一個賦值的操作,它會執(zhí)行一個類構(gòu)造器的<clinit>()方法液肌。由編譯器自動收集類中所有變量的賦值動作挟炬,此時準(zhǔn)備階段時的那個 static int a = 3 的例子,在這個時候就正式賦值為3

2.1.4 卸載

GC將無用對象從內(nèi)存中卸載

2.2 類加載器的加載順序

加載一個Class類的順序也是有優(yōu)先級的,類加載器從最底層開始往上的順序是這樣的

BootStrap ClassLoader:rt.jar

Extention ClassLoader: 加載擴(kuò)展的jar包

App ClassLoader:指定的classpath下面的jar包

Custom ClassLoader:自定義的類加載器

2.3 雙親委派機(jī)制

當(dāng)一個類收到了加載請求時谤祖,它是不會先自己去嘗試加載的婿滓,而是委派給父類去完成,比如我現(xiàn)在要new一個Person粥喜,這個Person是我們自定義的類空幻,如果我們要加載它,就會先委派App ClassLoader容客,只有當(dāng)父類加載器都反饋?zhàn)约簾o法完成這個請求(也就是父類加載器都沒有找到加載所需的Class)時秕铛,子類加載器才會自行嘗試加載

這樣做的好處是,加載位于rt.jar包中的類時不管是哪個加載器加載缩挑,最終都會委托到BootStrap ClassLoader進(jìn)行加載但两,這樣保證了使用不同的類加載器得到的都是同一個結(jié)果。

其實(shí)這個也是一個隔離的作用供置,避免了我們的代碼影響了JDK的代碼谨湘,比如我現(xiàn)在要來一個

public?class?String(){

public?static?void?main(){sout;}

}

這種時候,我們的代碼肯定會報(bào)錯芥丧,因?yàn)樵诩虞d的時候其實(shí)是找到了rt.jar中的String.class紧阔,然后發(fā)現(xiàn)這也沒有main方法

三、運(yùn)行時數(shù)據(jù)區(qū)

3.1 本地方法棧和程序計(jì)數(shù)器

比如說我們現(xiàn)在點(diǎn)開Thread類的源碼续担,會看到它的start0方法帶有一個native關(guān)鍵字修飾擅耽,而且不存在方法體,這種用native修飾的方法就是本地方法物遇,這是使用C來實(shí)現(xiàn)的乖仇,然后一般這些方法都會放到一個叫做本地方法棧的區(qū)域。

程序計(jì)數(shù)器其實(shí)就是一個指針询兴,它指向了我們程序中下一句需要執(zhí)行的指令乃沙,它也是內(nèi)存區(qū)域中唯一一個不會出現(xiàn)OutOfMemoryError的區(qū)域,而且占用內(nèi)存空間小到基本可以忽略不計(jì)诗舰。這個內(nèi)存僅代表當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器警儒,字節(jié)碼解析器通過改變這個計(jì)數(shù)器的值選取下一條需要執(zhí)行的字節(jié)碼指令。

如果執(zhí)行的是native方法眶根,那這個指針就不工作了蜀铲。

3.2 方法區(qū)

方法區(qū)主要的作用技術(shù)存放類的元數(shù)據(jù)信息,常量和靜態(tài)變量···等汛闸。當(dāng)它存儲的信息過大時蝙茶,會在無法滿足內(nèi)存分配時報(bào)錯。

3.3 虛擬機(jī)棧和虛擬機(jī)堆

一句話便是:棧管運(yùn)行诸老,堆管存儲隆夯。則虛擬機(jī)棧負(fù)責(zé)運(yùn)行代碼钳恕,而虛擬機(jī)堆負(fù)責(zé)存儲數(shù)據(jù)。

3.3.1 虛擬機(jī)棧的概念

它是Java方法執(zhí)行的內(nèi)存模型蹄衷。里面會對局部變量忧额,動態(tài)鏈表,方法出口愧口,棧的操作(入棧和出棧)進(jìn)行存儲睦番,且線程獨(dú)享。同時如果我們聽到局部變量表耍属,那也是在說虛擬機(jī)棧

public?class?Person{

int?a?=?1;

public?void?doSomething(){

int?b?=?2;

}

}

3.3.2 虛擬機(jī)棧存在的異常

如果線程請求的棧的深度大于虛擬機(jī)棧的最大深度托嚣,就會報(bào)?StackOverflowError?(這種錯誤經(jīng)常出現(xiàn)在遞歸中)。Java虛擬機(jī)也可以動態(tài)擴(kuò)展厚骗,但隨著擴(kuò)展會不斷地申請內(nèi)存示启,當(dāng)無法申請足夠內(nèi)存時就會報(bào)錯?OutOfMemoryError

3.3.3 虛擬機(jī)棧的生命周期

對于棧來說领舰,不存在垃圾回收夫嗓。只要程序運(yùn)行結(jié)束,棧的空間自然就會釋放了冲秽。棧的生命周期和所處的線程是一致的舍咖。

這里補(bǔ)充一句:8種基本類型的變量+對象的引用變量+實(shí)例方法都是在棧里面分配內(nèi)存。

3.3.4 虛擬機(jī)棧的執(zhí)行

我們經(jīng)常說的棧幀數(shù)據(jù)锉桑,說白了在JVM中叫棧幀排霉,放到Java中其實(shí)就是方法,它也是存放在棧中的刨仑。

棧中的數(shù)據(jù)都是以棧幀的格式存在郑诺,它是一個關(guān)于方法和運(yùn)行期數(shù)據(jù)的數(shù)據(jù)集。比如我們執(zhí)行一個方法a杉武,就會對應(yīng)產(chǎn)生一個棧幀A1,然后A1會被壓入棧中辙售。同理方法b會有一個B1轻抱,方法c會有一個C1,等到這個線程執(zhí)行完畢后旦部,棧會先彈出C1祈搜,后B1,A1。它是一個先進(jìn)后出士八,后進(jìn)先出原則容燕。

3.3.5 局部變量的復(fù)用

局部變量表用于存放方法參數(shù)和方法內(nèi)部所定義的局部變量。它的容量是以Slot為最小單位婚度,一個slot可以存放32位以內(nèi)的數(shù)據(jù)類型蘸秘。

虛擬機(jī)通過索引定位的方式使用局部變量表,范圍為[0,局部變量表的slot的數(shù)量]。方法中的參數(shù)就會按一定順序排列在這個局部變量表中醋虏,至于怎么排的我們可以先不關(guān)心寻咒。而為了節(jié)省棧幀空間,這些slot是可以復(fù)用的颈嚼,當(dāng)方法執(zhí)行位置超過了某個變量毛秘,那么這個變量的slot可以被其它變量復(fù)用。當(dāng)然如果需要復(fù)用阻课,那我們的垃圾回收自然就不會去動這些內(nèi)存叫挟。

3.3.6 虛擬機(jī)堆的概念

JVM內(nèi)存會劃分為堆內(nèi)存和非堆內(nèi)存,堆內(nèi)存中也會劃分為年輕代老年代限煞,而非堆內(nèi)存則為永久代抹恳。年輕代又會分為EdenSurvivor區(qū)。Survivor也會分為FromPlaceToPlace晰骑,toPlace的survivor區(qū)域是空的适秩。Eden,F(xiàn)romPlace和ToPlace的默認(rèn)占比為?8:1:1硕舆。當(dāng)然這個東西其實(shí)也可以通過一個 -XX:+UsePSAdaptiveSurvivorSizePolicy 參數(shù)來根據(jù)生成對象的速率動態(tài)調(diào)整

堆內(nèi)存中存放的是對象秽荞,垃圾收集就是收集這些對象然后交給GC算法進(jìn)行回收。非堆內(nèi)存其實(shí)我們已經(jīng)說過了抚官,就是方法區(qū)扬跋。在1.8中已經(jīng)移除永久代,替代品是一個元空間(MetaSpace)凌节,最大區(qū)別是metaSpace是不存在于JVM中的钦听,它使用的是本地內(nèi)存。并有兩個參數(shù)

MetaspaceSize:初始化元空間大小倍奢,控制發(fā)生GC

MaxMetaspaceSize:限制元空間大小上限朴上,防止占用過多物理內(nèi)存。

移除的原因可以大致了解一下:融合HotSpot JVM和JRockit VM而做出的改變卒煞,因?yàn)镴Rockit是沒有永久代的痪宰,不過這也間接性地解決了永久代的OOM問題。

3.3.7 Eden年輕代的介紹

當(dāng)我們new一個對象后畔裕,會先放到Eden劃分出來的一塊作為存儲空間的內(nèi)存衣撬,但是我們知道對堆內(nèi)存是線程共享的,所以有可能會出現(xiàn)兩個對象共用一個內(nèi)存的情況扮饶。這里JVM的處理是每個線程都會預(yù)先申請好一塊連續(xù)的內(nèi)存空間并規(guī)定了對象存放的位置具练,而如果空間不足會再申請多塊內(nèi)存空間。這個操作我們會稱作TLAB甜无,有興趣可以了解一下扛点。

當(dāng)Eden空間滿了之后丈秩,會觸發(fā)一個叫做Minor GC(就是一個發(fā)生在年輕代的GC)的操作鹊杖,存活下來的對象移動到Survivor0區(qū)穷当。Survivor0區(qū)滿后觸發(fā) Minor GC磨取,就會將存活對象移動到Survivor1區(qū),此時還會把from和to兩個指針交換畔乙,這樣保證了一段時間內(nèi)總有一個survivor區(qū)為空且to所指向的survivor區(qū)為空君仆。經(jīng)過多次的 Minor GC后仍然存活的對象(這里的存活判斷是15次,對應(yīng)到虛擬機(jī)參數(shù)為 -XX:MaxTenuringThreshold 牲距。為什么是15返咱,因?yàn)镠otSpot會在對象投中的標(biāo)記字段里記錄年齡,分配到的空間僅有4位牍鞠,所以最多只能記錄到15)會移動到老年代咖摹。老年代是存儲長期存活的對象的,占滿時就會觸發(fā)我們最常聽說的Full GC难述,期間會停止所有線程等待GC的完成萤晴。所以對于響應(yīng)要求高的應(yīng)用應(yīng)該盡量去減少發(fā)生Full GC從而避免響應(yīng)超時的問題。

而且當(dāng)老年區(qū)執(zhí)行了full gc之后仍然無法進(jìn)行對象保存的操作胁后,就會產(chǎn)生OOM店读,這時候就是虛擬機(jī)中的堆內(nèi)存不足,原因可能會是堆內(nèi)存設(shè)置的大小過小攀芯,這個可以通過參數(shù)-Xms屯断、-Xmx來調(diào)整。也可能是代碼中創(chuàng)建的對象大且多侣诺,而且它們一直在被引用從而長時間垃圾收集無法收集它們殖演。

3.3.8 如何判斷一個對象需要被干掉

圖中程序計(jì)數(shù)器、虛擬機(jī)棧年鸳、本地方法棧趴久,3個區(qū)域隨著線程的生存而生存的。內(nèi)存分配和回收都是確定的搔确。隨著線程的結(jié)束內(nèi)存自然就被回收了朋鞍,因此不需要考慮垃圾回收的問題。而Java堆和方法區(qū)則不一樣妥箕,各線程共享,內(nèi)存的分配和回收都是動態(tài)的更舞。因此垃圾收集器所關(guān)注的都是堆和方法這部分內(nèi)存畦幢。

在進(jìn)行回收前就要判斷哪些對象還存活,哪些已經(jīng)死去缆蝉。下面介紹兩個基礎(chǔ)的計(jì)算方法

1.引用計(jì)數(shù)器計(jì)算:給對象添加一個引用計(jì)數(shù)器宇葱,每次引用這個對象時計(jì)數(shù)器加一瘦真,引用失效時減一,計(jì)數(shù)器等于0時就是不會再次使用的黍瞧。不過這個方法有一種情況就是出現(xiàn)對象的循環(huán)引用時GC沒法回收诸尽。

2.可達(dá)性分析計(jì)算:這是一種類似于二叉樹的實(shí)現(xiàn),將一系列的GC ROOTS作為起始的存活對象集印颤,從這個節(jié)點(diǎn)往下搜索您机,搜索所走過的路徑成為引用鏈,把能被該集合引用到的對象加入到集合中年局。搜索當(dāng)一個對象到GC Roots沒有使用任何引用鏈時际看,則說明該對象是不可用的。主流的商用程序語言矢否,例如Java仲闽,C#等都是靠這招去判定對象是否存活的。

(了解一下即可)在Java語言匯總能作為GC Roots的對象分為以下幾種:

虛擬機(jī)棧(棧幀中的本地方法表)中引用的對象(局部變量)

方法區(qū)中靜態(tài)變量所引用的對象(靜態(tài)變量)

方法區(qū)中常量引用的對象

本地方法棧(即native修飾的方法)中JNI引用的對象(JNI是Java虛擬機(jī)調(diào)用對應(yīng)的C函數(shù)的方式僵朗,通過JNI函數(shù)也可以創(chuàng)建新的Java對象赖欣。且JNI對于對象的局部引用或者全局引用都會把它們指向的對象都標(biāo)記為不可回收)

已啟動的且未終止的Java線程

這種方法的優(yōu)點(diǎn)是能夠解決循環(huán)引用的問題,可它的實(shí)現(xiàn)需要耗費(fèi)大量資源和時間验庙,也需要GC(它的分析過程引用關(guān)系不能發(fā)生變化顶吮,所以需要停止所有進(jìn)程)

3.3.9 如何宣告一個對象的真正死亡

首先必須要提到的是一個名叫?finalize()?的方法

finalize()是Object類的一個方法、一個對象的finalize()方法只會被系統(tǒng)自動調(diào)用一次壶谒,經(jīng)過finalize()方法逃脫死亡的對象云矫,第二次不會再調(diào)用。

補(bǔ)充一句:并不提倡在程序中調(diào)用finalize()來進(jìn)行自救汗菜。建議忘掉Java程序中該方法的存在让禀。因?yàn)樗鼒?zhí)行的時間不確定,甚至是否被執(zhí)行也不確定(Java程序的不正常退出)陨界,而且運(yùn)行代價高昂巡揍,無法保證各個對象的調(diào)用順序(甚至有不同線程中調(diào)用)。在Java9中已經(jīng)被標(biāo)記為?deprecated?菌瘪,且java.lang.ref.Cleaner(也就是強(qiáng)腮敌、軟、弱俏扩、幻象引用的那一套)中已經(jīng)逐步替換掉它糜工,會比finalize來的更加的輕量及可靠。

判斷一個對象的死亡至少需要兩次標(biāo)記

如果對象進(jìn)行可達(dá)性分析之后沒發(fā)現(xiàn)與GC Roots相連的引用鏈录淡,那它將會第一次標(biāo)記并且進(jìn)行一次篩選捌木。判斷的條件是決定這個對象是否有必要執(zhí)行finalize()方法。如果對象有必要執(zhí)行finalize()方法嫉戚,則被放入F-Queue隊(duì)列中刨裆。

GC對F-Queue隊(duì)列中的對象進(jìn)行二次標(biāo)記澈圈。如果對象在finalize()方法中重新與引用鏈上的任何一個對象建立了關(guān)聯(lián),那么二次標(biāo)記時則會將它移出“即將回收”集合帆啃。如果此時對象還沒成功逃脫瞬女,那么只能被回收了。

如果確定對象已經(jīng)死亡努潘,我們又該如何回收這些垃圾呢

3.4 垃圾回收算法

不會非常詳細(xì)的展開诽偷,常用的有標(biāo)記清除,復(fù)制慈俯,標(biāo)記整理和分代收集算法

3.4.1 標(biāo)記清除算法

標(biāo)記清除算法就是分為“標(biāo)記”和“清除”兩個階段渤刃。標(biāo)記出所有需要回收的對象,標(biāo)記結(jié)束后統(tǒng)一回收贴膘。這個套路很簡單卖子,也存在不足,后續(xù)的算法都是根據(jù)這個基礎(chǔ)來加以改進(jìn)的刑峡。

其實(shí)它就是把已死亡的對象標(biāo)記為空閑內(nèi)存洋闽,然后記錄在一個空閑列表中,當(dāng)我們需要new一個對象時突梦,內(nèi)存管理模塊會從空閑列表中尋找空閑的內(nèi)存來分給新的對象诫舅。

不足的方面就是標(biāo)記和清除的效率比較低下。且這種做法會讓內(nèi)存中的碎片非常多宫患。這個導(dǎo)致了如果我們需要使用到較大的內(nèi)存塊時刊懈,無法分配到足夠的連續(xù)內(nèi)存。比如下圖

此時可使用的內(nèi)存塊都是零零散散的娃闲,導(dǎo)致了剛剛提到的大內(nèi)存對象問題

3.4.2 復(fù)制算法

為了解決效率問題虚汛,復(fù)制算法就出現(xiàn)了。它將可用內(nèi)存按容量劃分成兩等分皇帮,每次只使用其中的一塊卷哩。和survivor一樣也是用from和to兩個指針這樣的玩法。fromPlace存滿了属拾,就把存活的對象copy到另一塊toPlace上将谊,然后交換指針的內(nèi)容。這樣就解決了碎片的問題渐白。

這個算法的代價就是把內(nèi)存縮水了尊浓,這樣堆內(nèi)存的使用效率就會變得十分低下了

不過它們分配的時候也不是按照1:1這樣進(jìn)行分配的,就類似于Eden和Survivor也不是等價分配是一個道理纯衍。

3.4.3 標(biāo)記整理算法

復(fù)制算法在對象存活率高的時候會有一定的效率問題眠砾,標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象進(jìn)行清理,而是讓所有存活的對象都向一端移動褒颈,然后直接清理掉邊界以外的內(nèi)存

3.4.4 分代收集算法

這種算法并沒有什么新的思想,只是根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊励堡。一般是把Java堆分為新生代和老年代谷丸,這樣就可以根據(jù)各個年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴āT谛律杏幔看卫占瘯r都發(fā)現(xiàn)有大批對象死去刨疼,只有少量存活,那就選用復(fù)制算法鹅龄,只需要付出少量存活對象的復(fù)制成本就可以完成收集揩慕。而老年代中因?yàn)閷ο蟠婊盥矢摺]有額外空間對它進(jìn)行分配擔(dān)保扮休,就必須使用“標(biāo)記-清理”或者“標(biāo)記-整理”算法來進(jìn)行回收迎卤。

說白了就是八仙過海各顯神通,具體問題具體分析了而已玷坠。

3.5 (了解)各種各樣的垃圾回收器

HotSpot VM中的垃圾回收器蜗搔,以及適用場景

到j(luò)dk8為止,默認(rèn)的垃圾收集器是Parallel Scavenge 和 Parallel Old

從jdk9開始八堡,G1收集器成為默認(rèn)的垃圾收集器目前來看樟凄,G1回收器停頓時間最短而且沒有明顯缺點(diǎn),非常適合Web應(yīng)用兄渺。在jdk8中測試Web應(yīng)用缝龄,堆內(nèi)存6G,新生代4.5G的情況下挂谍,Parallel Scavenge 回收新生代停頓長達(dá)1.5秒叔壤。G1回收器回收同樣大小的新生代只停頓0.2秒。

3.6 (了解)JVM的常用參數(shù)

JVM的參數(shù)非常之多凳兵,這里只列舉比較重要的幾個百新,通過各種各樣的搜索引擎也可以得知這些信息。

參數(shù)名稱 含義 默認(rèn)值 說明 -Xms 初始堆大小 物理內(nèi)存的1/64(<1GB) 默認(rèn)(MinHeapFreeRatio參數(shù)可以調(diào)整)空余堆內(nèi)存小于40%時庐扫,JVM就會增大堆直到-Xmx的最大限制. -Xmx 最大堆大小 物理內(nèi)存的1/4(<1GB) 默認(rèn)(MaxHeapFreeRatio參數(shù)可以調(diào)整)空余堆內(nèi)存大于70%時饭望,JVM會減少堆直到 -Xms的最小限制 -Xmn 年輕代大小(1.4or lator) 注意:此處的大小是(eden+ 2 survivor space).與jmap -heap中顯示的New gen是不同的。整個堆大小=年輕代大小 + 老年代大小 + 持久代(永久代)大小.增大年輕代后,將會減小年老代大小.此值對系統(tǒng)性能影響較大,Sun官方推薦配置為整個堆的3/8 -XX:NewSize 設(shè)置年輕代大小(for 1.3/1.4) -XX:MaxNewSize 年輕代最大值(for 1.3/1.4) -XX:PermSize 設(shè)置持久代(perm gen)初始值 物理內(nèi)存的1/64 -XX:MaxPermSize 設(shè)置持久代最大值 物理內(nèi)存的1/4 -Xss 每個線程的堆棧大小 JDK5.0以后每個線程堆棧大小為1M,以前每個線程堆棧大小為256K.更具應(yīng)用的線程所需內(nèi)存大小進(jìn)行 調(diào)整.在相同物理內(nèi)存下,減小這個值能生成更多的線程.但是操作系統(tǒng)對一個進(jìn)程內(nèi)的線程數(shù)還是有限制的,不能無限生成,經(jīng)驗(yàn)值在3000~5000左右一般小的應(yīng)用形庭, 如果棧不是很深铅辞, 應(yīng)該是128k夠用的 大的應(yīng)用建議使用256k。這個選項(xiàng)對性能影響比較大萨醒,需要嚴(yán)格的測試斟珊。(校長)和threadstacksize選項(xiàng)解釋很類似,官方文檔似乎沒有解釋,在論壇中有這樣一句話:-Xss is translated in a VM flag named ThreadStackSize”一般設(shè)置這個值就可以了 -XX:NewRatio 年輕代(包括Eden和兩個Survivor區(qū))與年老代的比值(除去持久代) -XX:NewRatio=4表示年輕代與年老代所占比值為1:4,年輕代占整個堆棧的1/5Xms=Xmx并且設(shè)置了Xmn的情況下,該參數(shù)不需要進(jìn)行設(shè)置富纸。 -XX:SurvivorRatio Eden區(qū)與Survivor區(qū)的大小比值 設(shè)置為8,則兩個Survivor區(qū)與一個Eden區(qū)的比值為2:8,一個Survivor區(qū)占整個年輕代的1/10 -XX:+DisableExplicitGC 關(guān)閉System.gc() 這個參數(shù)需要嚴(yán)格的測試 -XX:PretenureSizeThreshold 對象超過多大是直接在舊生代分配 0 單位字節(jié) 新生代采用Parallel ScavengeGC時無效另一種直接在舊生代分配的情況是大的數(shù)組對象,且數(shù)組中無外部引用對象. -XX:ParallelGCThreads 并行收集器的線程數(shù) 此值最好配置與處理器數(shù)目相等 同樣適用于CMS -XX:MaxGCPauseMillis 每次年輕代垃圾回收的最長時間(最大暫停時間) 如果無法滿足此時間,JVM會自動調(diào)整年輕代大小,以滿足此值.

其實(shí)還有一些打印及CMS方面的參數(shù)囤踩,這里就不以一一列舉了

四旨椒、關(guān)于JVM調(diào)優(yōu)的一些方面

根據(jù)剛剛涉及的jvm的知識點(diǎn),我們可以嘗試對JVM進(jìn)行調(diào)優(yōu)堵漱,主要就是堆內(nèi)存那塊

所有線程共享數(shù)據(jù)區(qū)大小=新生代大小 + 年老代大小 + 持久代大小综慎。持久代一般固定大小為64m。所以java堆中增大年輕代后勤庐,將會減小年老代大惺揪(因?yàn)槔夏甏那謇硎鞘褂胒ullgc,所以老年代過小的話反而是會增多fullgc的)愉镰。此值對系統(tǒng)性能影響較大米罚,Sun官方推薦配置為java堆的3/8。

4.1 調(diào)整最大堆內(nèi)存和最小堆內(nèi)存

-Xmx –Xms:指定java堆最大值(默認(rèn)值是物理內(nèi)存的1/4(<1GB))和初始java堆最小值(默認(rèn)值是物理內(nèi)存的1/64(<1GB))

默認(rèn)(MinHeapFreeRatio參數(shù)可以調(diào)整)空余堆內(nèi)存小于40%時丈探,JVM就會增大堆直到-Xmx的最大限制.录择,默認(rèn)(MaxHeapFreeRatio參數(shù)可以調(diào)整)空余堆內(nèi)存大于70%時,JVM會減少堆直到 -Xms的最小限制类嗤。簡單點(diǎn)來說糊肠,你不停地往堆內(nèi)存里面丟數(shù)據(jù),等它剩余大小小于40%了遗锣,JVM就會動態(tài)申請內(nèi)存空間不過會小于-Xmx货裹,如果剩余大小大于70%,又會動態(tài)縮小不過不會小于–Xms精偿。就這么簡單

開發(fā)過程中弧圆,通常會將 -Xms 與 -Xmx兩個參數(shù)的配置相同的值,其目的是為了能夠在java垃圾回收機(jī)制清理完堆區(qū)后不需要重新分隔計(jì)算堆區(qū)的大小而浪費(fèi)資源笔咽。

我們執(zhí)行下面的代碼

System.out.println("Xmx="?+?Runtime.getRuntime().maxMemory()?/?1024.0?/?1024?+?"M");????//系統(tǒng)的最大空間

System.out.println("free?mem="?+?Runtime.getRuntime().freeMemory()?/?1024.0?/?1024?+?"M");??//系統(tǒng)的空閑空間

System.out.println("total?mem="?+?Runtime.getRuntime().totalMemory()?/?1024.0?/?1024?+?"M");??//當(dāng)前可用的總空間

注意:此處設(shè)置的是Java堆大小搔预,也就是新生代大小 + 老年代大小

設(shè)置一個VM options的參數(shù)

-Xmx20m?-Xms5m?-XX:+PrintGCDetails

再次啟動main方法

這里GC彈出了一個Allocation Failure分配失敗,這個事情發(fā)生在PSYoungGen叶组,也就是年輕代中

這時候申請到的內(nèi)存為18M拯田,空閑內(nèi)存為4.214195251464844M

我們此時創(chuàng)建一個字節(jié)數(shù)組看看,執(zhí)行下面的代碼

byte[]?b?=?new?byte[1?*?1024?*?1024];

System.out.println("分配了1M空間給數(shù)組");

System.out.println("Xmx="?+?Runtime.getRuntime().maxMemory()?/?1024.0?/?1024?+?"M");??//系統(tǒng)的最大空間

System.out.println("free?mem="?+?Runtime.getRuntime().freeMemory()?/?1024.0?/?1024?+?"M");??//系統(tǒng)的空閑空間

System.out.println("total?mem="?+?Runtime.getRuntime().totalMemory()?/?1024.0?/?1024?+?"M");

此時free memory就又縮水了甩十,不過total memory是沒有變化的船庇。Java會盡可能將total mem的值維持在最小堆內(nèi)存大小

byte[]?b?=?new?byte[10?*?1024?*?1024];

System.out.println("分配了10M空間給數(shù)組");

System.out.println("Xmx="?+?Runtime.getRuntime().maxMemory()?/?1024.0?/?1024?+?"M");??//系統(tǒng)的最大空間

System.out.println("free?mem="?+?Runtime.getRuntime().freeMemory()?/?1024.0?/?1024?+?"M");??//系統(tǒng)的空閑空間

System.out.println("total?mem="?+?Runtime.getRuntime().totalMemory()?/?1024.0?/?1024?+?"M");??//當(dāng)前可用的總空間

這時候我們創(chuàng)建了一個10M的字節(jié)數(shù)據(jù),這時候最小堆內(nèi)存是頂不住的侣监。我們會發(fā)現(xiàn)現(xiàn)在的total memory已經(jīng)變成了15M鸭轮,這就是已經(jīng)申請了一次內(nèi)存的結(jié)果。

此時我們再跑一下這個代碼

System.gc();

System.out.println("Xmx="?+?Runtime.getRuntime().maxMemory()?/?1024.0?/?1024?+?"M");????//系統(tǒng)的最大空間

System.out.println("free?mem="?+?Runtime.getRuntime().freeMemory()?/?1024.0?/?1024?+?"M");??//系統(tǒng)的空閑空間

System.out.println("total?mem="?+?Runtime.getRuntime().totalMemory()?/?1024.0?/?1024?+?"M");??//當(dāng)前可用的總空間

此時我們手動執(zhí)行了一次fullgc橄霉,此時total memory的內(nèi)存空間又變回5.5M了窃爷,此時又是把申請的內(nèi)存釋放掉的結(jié)果。

4.2 調(diào)整新生代和老年代的比值

-XX:NewRatio --- 新生代(eden+2*Survivor)和老年代(不包含永久區(qū))的比值

例如:-XX:NewRatio=4,表示新生代:老年代=1:4按厘,即新生代占整個堆的1/5医吊。在Xms=Xmx并且設(shè)置了Xmn的情況下,該參數(shù)不需要進(jìn)行設(shè)置刻剥。

4.3 調(diào)整Survivor區(qū)和Eden區(qū)的比值

-XX:SurvivorRatio(幸存代)--- 設(shè)置兩個Survivor區(qū)和eden的比值

例如:8遮咖,表示兩個Survivor:eden=2:8,即一個Survivor占年輕代的1/10

4.4 設(shè)置年輕代和老年代的大小

-XX:NewSize --- 設(shè)置年輕代大小

-XX:MaxNewSize --- 設(shè)置年輕代最大值

可以通過設(shè)置不同參數(shù)來測試不同的情況造虏,反正最優(yōu)解當(dāng)然就是官方的Eden和Survivor的占比為8:1:1,然后在剛剛介紹這些參數(shù)的時候都已經(jīng)附帶了一些說明麦箍,感興趣的也可以看看漓藕。反正最大堆內(nèi)存和最小堆內(nèi)存如果數(shù)值不同會導(dǎo)致多次的gc,需要注意挟裂。

4.5 小總結(jié)

根據(jù)實(shí)際事情調(diào)整新生代和幸存代的大小享钞,官方推薦新生代占java堆的3/8,幸存代占新生代的1/10

在OOM時诀蓉,記得Dump出堆栗竖,確保可以排查現(xiàn)場問題渠啤,通過下面命令你可以輸出一個.dump文件狐肢,這個文件可以使用VisualVM或者Java自帶的Java VisualVM工具。

-Xmx20m?-Xms5m?-XX:+HeapDumpOnOutOfMemoryError?-XX:HeapDumpPath=你要輸出的日志路徑

一般我們也可以通過編寫腳本的方式來讓OOM出現(xiàn)時給我們報(bào)個信沥曹,可以通過發(fā)送郵件或者重啟程序等來解決份名。

4.6 永久區(qū)的設(shè)置

-XX:PermSize?-XX:MaxPermSize

初始空間(默認(rèn)為物理內(nèi)存的1/64)和最大空間(默認(rèn)為物理內(nèi)存的1/4)。也就是說妓美,jvm啟動時僵腺,永久區(qū)一開始就占用了PermSize大小的空間,如果空間還不夠壶栋,可以繼續(xù)擴(kuò)展辰如,但是不能超過MaxPermSize,否則會OOM贵试。

tips:如果堆空間沒有用完也拋出了OOM琉兜,有可能是永久區(qū)導(dǎo)致的。堆空間實(shí)際占用非常少锡移,但是永久區(qū)溢出 一樣拋出OOM呕童。

4.7 JVM的棧參數(shù)調(diào)優(yōu)

4.7.1 調(diào)整每個線程棧空間的大小

可以通過-Xss:調(diào)整每個線程椣海空間的大小

JDK5.0以后每個線程堆棧大小為1M夺饲,以前每個線程堆棧大小為256K。在相同物理內(nèi)存下,減小這個值能生成更多的線程。但是操作系統(tǒng)對一個進(jìn)程內(nèi)的線程數(shù)還是有限制的往声,不能無限生成擂找,經(jīng)驗(yàn)值在3000~5000左右

4.7.2 設(shè)置線程棧的大小

-XXThreadStackSize:

設(shè)置線程棧的大小(0?means?use?default?stack?size)

這些參數(shù)都是可以通過自己編寫程序去簡單測試的,這里礙于篇幅問題就不再提供demo了

4.8 (可以直接跳過了)JVM其他參數(shù)介紹

形形色色的參數(shù)很多浩销,就不會說把所有都扯個遍了贯涎,因?yàn)榇蠹移鋵?shí)也不會說一定要去深究到底。

4.8.1 設(shè)置內(nèi)存頁的大小

-XXThreadStackSize:

設(shè)置內(nèi)存頁的大小慢洋,不可設(shè)置過大塘雳,會影響Perm的大小

4.8.2 設(shè)置原始類型的快速優(yōu)化

-XX:+UseFastAccessorMethods:

設(shè)置原始類型的快速優(yōu)化

4.8.3 設(shè)置關(guān)閉手動GC

-XX:+DisableExplicitGC:

設(shè)置關(guān)閉System.gc()(這個參數(shù)需要嚴(yán)格的測試)

4.8.4 設(shè)置垃圾最大年齡

-XX:MaxTenuringThreshold

設(shè)置垃圾最大年齡。如果設(shè)置為0的話,則年輕代對象不經(jīng)過Survivor區(qū),直接進(jìn)入年老代.

對于年老代比較多的應(yīng)用,可以提高效率普筹。如果將此值設(shè)置為一個較大值,

則年輕代對象會在Survivor區(qū)進(jìn)行多次復(fù)制,這樣可以增加對象再年輕代的存活時間,

增加在年輕代即被回收的概率败明。該參數(shù)只有在串行GC時才有效.

4.8.5 加快編譯速度

-XX:+AggressiveOpts

加快編譯速度

4.8.6 改善鎖機(jī)制性能

-XX:+UseBiasedLocking

4.8.7 禁用垃圾回收

-Xnoclassgc

4.8.8 設(shè)置堆空間存活時間

-XX:SoftRefLRUPolicyMSPerMB

設(shè)置每兆堆空閑空間中SoftReference的存活時間,默認(rèn)值是1s太防。

4.8.9 設(shè)置對象直接分配在老年代

-XX:PretenureSizeThreshold

設(shè)置對象超過多大時直接在老年代分配妻顶,默認(rèn)值是0。

4.8.10 設(shè)置TLAB占eden區(qū)的比例

-XX:TLABWasteTargetPercent

設(shè)置TLAB占eden區(qū)的百分比蜒车,默認(rèn)值是1%?讳嘱。

4.8.11設(shè)置是否優(yōu)先YGC

-XX:+CollectGen0First

設(shè)置FullGC時是否先YGC,默認(rèn)值是false酿愧。

finally

完事

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末沥潭,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子寓娩,更是在濱河造成了極大的恐慌叛氨,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棘伴,死亡現(xiàn)場離奇詭異寞埠,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)焊夸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進(jìn)店門仁连,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人阱穗,你說我怎么就攤上這事饭冬。” “怎么了揪阶?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵昌抠,是天一觀的道長。 經(jīng)常有香客問我鲁僚,道長炊苫,這世上最難降的妖魔是什么裁厅? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮侨艾,結(jié)果婚禮上执虹,老公的妹妹穿的比我還像新娘。我一直安慰自己唠梨,他們只是感情好袋励,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著当叭,像睡著了一般茬故。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蚁鳖,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天均牢,我揣著相機(jī)與錄音,去河邊找鬼才睹。 笑死,一個胖子當(dāng)著我的面吹牛甘邀,可吹牛的內(nèi)容都是我干的琅攘。 我是一名探鬼主播,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼松邪,長吁一口氣:“原來是場噩夢啊……” “哼坞琴!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起逗抑,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤剧辐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后邮府,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體荧关,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年褂傀,在試婚紗的時候發(fā)現(xiàn)自己被綠了忍啤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡仙辟,死狀恐怖同波,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情叠国,我是刑警寧澤未檩,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站粟焊,受9級特大地震影響冤狡,放射性物質(zhì)發(fā)生泄漏孙蒙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一筒溃、第九天 我趴在偏房一處隱蔽的房頂上張望马篮。 院中可真熱鬧,春花似錦怜奖、人聲如沸浑测。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽迁央。三九已至,卻和暖如春滥崩,著一層夾襖步出監(jiān)牢的瞬間岖圈,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工钙皮, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蜂科,地道東北人。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓短条,卻偏偏與公主長得像导匣,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子茸时,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評論 2 351