如果在文中用詞或者理解方面出現(xiàn)問(wèn)題,歡迎指出。此文旨在提及而不深究,但會(huì)盡量效率地把知識(shí)點(diǎn)都拋出來(lái)
JVM 是 Java Virtual Machine 的縮寫(xiě),它是一個(gè)虛構(gòu)出來(lái)的計(jì)算機(jī),一種規(guī)范。通過(guò)在實(shí)際的計(jì)算機(jī)上仿真模擬各類(lèi)計(jì)算機(jī)功能實(shí)現(xiàn)···
好,其實(shí)拋開(kāi)這么專(zhuān)業(yè)的句子不說(shuō),就知道JVM其實(shí)就類(lèi)似于一臺(tái)小電腦運(yùn)行在windows或者linux這些操作系統(tǒng)環(huán)境下即可。它直接和操作系統(tǒng)進(jìn)行交互,與硬件不直接交互,可操作系統(tǒng)可以幫我們完成和硬件進(jìn)行交互的工作。
比如我們現(xiàn)在寫(xiě)了一個(gè) HelloWorld.java 好了,那這個(gè) HelloWorld.java 拋開(kāi)所有東西不談,那是不是就類(lèi)似于一個(gè)文本文件,只是這個(gè)文本文件它寫(xiě)的都是英文,而且有一定的縮進(jìn)而已。
那我們的 JVM 是不認(rèn)識(shí)文本文件的,所以它需要一個(gè) 編譯 ,讓其成為一個(gè)它會(huì)讀二進(jìn)制文件的 HelloWorld.class
如果 JVM 想要執(zhí)行這個(gè) .class 文件,我們需要將其裝進(jìn)一個(gè) 類(lèi)加載器 中,它就像一個(gè)搬運(yùn)工一樣,會(huì)把所有的 .class 文件全部搬進(jìn)JVM里面來(lái)。
方法區(qū) 是用于存放類(lèi)似于元數(shù)據(jù)信息方面的數(shù)據(jù)的,比如類(lèi)信息,常量,靜態(tài)變量,編譯后代碼···等
類(lèi)加載器將 .class 文件搬過(guò)來(lái)就是先丟到這一塊上
堆 主要放了一些存儲(chǔ)的數(shù)據(jù),比如對(duì)象實(shí)例,數(shù)組···等,它和方法區(qū)都同屬于 線程共享區(qū)域 。也就是說(shuō)它們都是 線程不安全 的
棧 這是我們的代碼運(yùn)行空間。我們編寫(xiě)的每一個(gè)方法都會(huì)放到 棧 里面運(yùn)行。
我們會(huì)聽(tīng)說(shuō)過(guò) 本地方法棧 或者 本地方法接口 這兩個(gè)名詞,不過(guò)我們基本不會(huì)涉及這兩塊的內(nèi)容,它倆底層是使用C來(lái)進(jìn)行工作的,和Java沒(méi)有太大的關(guān)系。
主要就是完成一個(gè)加載工作,類(lèi)似于一個(gè)指針一樣的,指向下一行我們需要執(zhí)行的代碼。和棧一樣,都是 線程獨(dú)享 的,就是說(shuō)每一個(gè)線程都會(huì)有自己對(duì)應(yīng)的一塊區(qū)域而不會(huì)存在并發(fā)和多線程的問(wèn)題。
Java文件經(jīng)過(guò)編譯后變成 .class 字節(jié)碼文件
字節(jié)碼文件通過(guò)類(lèi)加載器被搬運(yùn)到 JVM 虛擬機(jī)中
虛擬機(jī)主要的5大塊:方法區(qū),堆都為線程共享區(qū)域,有線程安全問(wèn)題,棧和本地方法棧和計(jì)數(shù)器都是獨(dú)享區(qū)域,不存在線程安全問(wèn)題,而 JVM 的調(diào)優(yōu)主要就是圍繞堆,棧兩大塊進(jìn)行
一個(gè)簡(jiǎn)單的學(xué)生類(lèi)
一個(gè)main方法
執(zhí)行main方法的步驟如下:
編譯好 App.java 后得到 App.class 后,執(zhí)行 App.class,系統(tǒng)會(huì)啟動(dòng)一個(gè) JVM 進(jìn)程,從 classpath 路徑中找到一個(gè)名為 App.class 的二進(jìn)制文件,將 App 的類(lèi)信息加載到運(yùn)行時(shí)數(shù)據(jù)區(qū)的方法區(qū)內(nèi),這個(gè)過(guò)程叫做 App 類(lèi)的加載
JVM 找到 App 的主程序入口,執(zhí)行main方法
這個(gè)main中的第一條語(yǔ)句為 Student student = new Student("tellUrDream") ,就是讓 JVM 創(chuàng)建一個(gè)Student對(duì)象,但是這個(gè)時(shí)候方法區(qū)中是沒(méi)有 Student 類(lèi)的信息的,所以 JVM 馬上加載 Student 類(lèi),把 Student 類(lèi)的信息放到方法區(qū)中
加載完 Student 類(lèi)后,JVM 在堆中為一個(gè)新的 Student 實(shí)例分配內(nèi)存,然后調(diào)用構(gòu)造函數(shù)初始化 Student 實(shí)例,這個(gè) Student 實(shí)例持有 指向方法區(qū)中的 Student 類(lèi)的類(lèi)型信息 的引用
執(zhí)行student.sayName();時(shí),JVM 根據(jù) student 的引用找到 student 對(duì)象,然后根據(jù) student 對(duì)象持有的引用定位到方法區(qū)中 student 類(lèi)的類(lèi)型信息的方法表,獲得 sayName() 的字節(jié)碼地址。
執(zhí)行sayName()
其實(shí)也不用管太多,只需要知道對(duì)象實(shí)例初始化時(shí)會(huì)去方法區(qū)中找類(lèi)信息,完成后再到棧那里去運(yùn)行方法。找方法就在方法表中找。
之前也提到了它是負(fù)責(zé)加載.class文件的,它們?cè)谖募_(kāi)頭會(huì)有特定的文件標(biāo)示,將class文件字節(jié)碼內(nèi)容加載到內(nèi)存中,并將這些內(nèi)容轉(zhuǎn)換成方法區(qū)中的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu),并且ClassLoader只負(fù)責(zé)class文件的加載,而是否能夠運(yùn)行則由 Execution Engine 來(lái)決定
從類(lèi)被加載到虛擬機(jī)內(nèi)存中開(kāi)始,到釋放內(nèi)存總共有7個(gè)步驟:加載,驗(yàn)證,準(zhǔn)備,解析,初始化,使用,卸載。其中驗(yàn)證,準(zhǔn)備,解析三個(gè)部分統(tǒng)稱為連接
將class文件加載到內(nèi)存
將靜態(tài)數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)化成方法區(qū)中運(yùn)行時(shí)的數(shù)據(jù)結(jié)構(gòu)
在堆中生成一個(gè)代表這個(gè)類(lèi)的 java.lang.Class對(duì)象作為數(shù)據(jù)訪問(wèn)的入口
驗(yàn)證:確保加載的類(lèi)符合 JVM 規(guī)范和安全,保證被校驗(yàn)類(lèi)的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)的事件,其實(shí)就是一個(gè)安全檢查
準(zhǔn)備:為static變量在方法區(qū)中分配內(nèi)存空間,設(shè)置變量的初始值,例如 static int a = 3 (注意:準(zhǔn)備階段只設(shè)置類(lèi)中的靜態(tài)變量(方法區(qū)中),不包括實(shí)例變量(堆內(nèi)存中),實(shí)例變量是對(duì)象初始化時(shí)賦值的)
解析:虛擬機(jī)將常量池內(nèi)的符號(hào)引用替換為直接引用的過(guò)程(符號(hào)引用比如我現(xiàn)在import java.util.ArrayList這就算符號(hào)引用,直接引用就是指針或者對(duì)象地址,注意引用對(duì)象一定是在內(nèi)存進(jìn)行)
初始化其實(shí)就是一個(gè)賦值的操作,它會(huì)執(zhí)行一個(gè)類(lèi)構(gòu)造器的()方法。由編譯器自動(dòng)收集類(lèi)中所有變量的賦值動(dòng)作,此時(shí)準(zhǔn)備階段時(shí)的那個(gè) static int a = 3 的例子,在這個(gè)時(shí)候就正式賦值為3
GC將無(wú)用對(duì)象從內(nèi)存中卸載
加載一個(gè)Class類(lèi)的順序也是有優(yōu)先級(jí)的,類(lèi)加載器從最底層開(kāi)始往上的順序是這樣的
BootStrap ClassLoader:rt.jar
Extention ClassLoader: 加載擴(kuò)展的jar包
App ClassLoader:指定的classpath下面的jar包
Custom ClassLoader:自定義的類(lèi)加載器
當(dāng)一個(gè)類(lèi)收到了加載請(qǐng)求時(shí),它是不會(huì)先自己去嘗試加載的,而是委派給父類(lèi)去完成,比如我現(xiàn)在要new一個(gè)Person,這個(gè)Person是我們自定義的類(lèi),如果我們要加載它,就會(huì)先委派App ClassLoader,只有當(dāng)父類(lèi)加載器都反饋?zhàn)约簾o(wú)法完成這個(gè)請(qǐng)求(也就是父類(lèi)加載器都沒(méi)有找到加載所需的Class)時(shí),子類(lèi)加載器才會(huì)自行嘗試加載
這樣做的好處是,加載位于rt.jar包中的類(lèi)時(shí)不管是哪個(gè)加載器加載,最終都會(huì)委托到BootStrap ClassLoader進(jìn)行加載,這樣保證了使用不同的類(lèi)加載器得到的都是同一個(gè)結(jié)果。
其實(shí)這個(gè)也是一個(gè)隔離的作用,避免了我們的代碼影響了JDK的代碼,比如我現(xiàn)在要來(lái)一個(gè)
public class String(){ public static void main(){sout;} }
這種時(shí)候,我們的代碼肯定會(huì)報(bào)錯(cuò),因?yàn)樵诩虞d的時(shí)候其實(shí)是找到了rt.jar中的String.class,然后發(fā)現(xiàn)這也沒(méi)有main方法
比如說(shuō)我們現(xiàn)在點(diǎn)開(kāi)Thread類(lèi)的源碼,會(huì)看到它的start0方法帶有一個(gè)native關(guān)鍵字修飾,而且不存在方法體,這種用native修飾的方法就是本地方法,這是使用C來(lái)實(shí)現(xiàn)的,然后一般這些方法都會(huì)放到一個(gè)叫做本地方法棧的區(qū)域。
程序計(jì)數(shù)器其實(shí)就是一個(gè)指針,它指向了我們程序中下一句需要執(zhí)行的指令,它也是內(nèi)存區(qū)域中唯一一個(gè)不會(huì)出現(xiàn)OutOfMemoryError的區(qū)域,而且占用內(nèi)存空間小到基本可以忽略不計(jì)。這個(gè)內(nèi)存僅代表當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器,字節(jié)碼解析器通過(guò)改變這個(gè)計(jì)數(shù)器的值選取下一條需要執(zhí)行的字節(jié)碼指令。
如果執(zhí)行的是native方法,那這個(gè)指針就不工作了。
方法區(qū)主要的作用技術(shù)存放類(lèi)的元數(shù)據(jù)信息,常量和靜態(tài)變量···等。當(dāng)它存儲(chǔ)的信息過(guò)大時(shí),會(huì)在無(wú)法滿足內(nèi)存分配時(shí)報(bào)錯(cuò)。
一句話便是:棧管運(yùn)行,堆管存儲(chǔ)。則虛擬機(jī)棧負(fù)責(zé)運(yùn)行代碼,而虛擬機(jī)堆負(fù)責(zé)存儲(chǔ)數(shù)據(jù)。
它是Java方法執(zhí)行的內(nèi)存模型。里面會(huì)對(duì)局部變量,動(dòng)態(tài)鏈表,方法出口,棧的操作(入棧和出棧)進(jìn)行存儲(chǔ),且線程獨(dú)享。同時(shí)如果我們聽(tīng)到局部變量表,那也是在說(shuō)虛擬機(jī)棧
public class Person{ int a = 1; public void doSomething(){ int b = 2; } }
如果線程請(qǐng)求的棧的深度大于虛擬機(jī)棧的最大深度,就會(huì)報(bào) StackOverflowError (這種錯(cuò)誤經(jīng)常出現(xiàn)在遞歸中)。Java虛擬機(jī)也可以動(dòng)態(tài)擴(kuò)展,但隨著擴(kuò)展會(huì)不斷地申請(qǐng)內(nèi)存,當(dāng)無(wú)法申請(qǐng)足夠內(nèi)存時(shí)就會(huì)報(bào)錯(cuò) OutOfMemoryError。
對(duì)于棧來(lái)說(shuō),不存在垃圾回收。只要程序運(yùn)行結(jié)束,棧的空間自然就會(huì)釋放了。棧的生命周期和所處的線程是一致的。
這里補(bǔ)充一句:8種基本類(lèi)型的變量+對(duì)象的引用變量+實(shí)例方法都是在棧里面分配內(nèi)存。
我們經(jīng)常說(shuō)的棧幀數(shù)據(jù),說(shuō)白了在JVM中叫棧幀,放到Java中其實(shí)就是方法,它也是存放在棧中的。
棧中的數(shù)據(jù)都是以棧幀的格式存在,它是一個(gè)關(guān)于方法和運(yùn)行期數(shù)據(jù)的數(shù)據(jù)集。比如我們執(zhí)行一個(gè)方法a,就會(huì)對(duì)應(yīng)產(chǎn)生一個(gè)棧幀A1,然后A1會(huì)被壓入棧中。同理方法b會(huì)有一個(gè)B1,方法c會(huì)有一個(gè)C1,等到這個(gè)線程執(zhí)行完畢后,棧會(huì)先彈出C1,后B1,A1。它是一個(gè)先進(jìn)后出,后進(jìn)先出原則。
局部變量表用于存放方法參數(shù)和方法內(nèi)部所定義的局部變量。它的容量是以Slot為最小單位,一個(gè)slot可以存放32位以內(nèi)的數(shù)據(jù)類(lèi)型。
虛擬機(jī)通過(guò)索引定位的方式使用局部變量表,范圍為[0,局部變量表的slot的數(shù)量]。方法中的參數(shù)就會(huì)按一定順序排列在這個(gè)局部變量表中,至于怎么排的我們可以先不關(guān)心。而為了節(jié)省棧幀空間,這些slot是可以復(fù)用的,當(dāng)方法執(zhí)行位置超過(guò)了某個(gè)變量,那么這個(gè)變量的slot可以被其它變量復(fù)用。當(dāng)然如果需要復(fù)用,那我們的垃圾回收自然就不會(huì)去動(dòng)這些內(nèi)存。
JVM內(nèi)存會(huì)劃分為堆內(nèi)存和非堆內(nèi)存,堆內(nèi)存中也會(huì)劃分為年輕代和老年代,而非堆內(nèi)存則為永久代。年輕代又會(huì)分為Eden和Survivor區(qū)。Survivor也會(huì)分為FromPlace和ToPlace,toPlace的survivor區(qū)域是空的。Eden,F(xiàn)romPlace和ToPlace的默認(rèn)占比為 8:1:1。當(dāng)然這個(gè)東西其實(shí)也可以通過(guò)一個(gè) -XX:+UsePSAdaptiveSurvivorSizePolicy 參數(shù)來(lái)根據(jù)生成對(duì)象的速率動(dòng)態(tài)調(diào)整
堆內(nèi)存中存放的是對(duì)象,垃圾收集就是收集這些對(duì)象然后交給GC算法進(jìn)行回收。非堆內(nèi)存其實(shí)我們已經(jīng)說(shuō)過(guò)了,就是方法區(qū)。在1.8中已經(jīng)移除永久代,替代品是一個(gè)元空間(MetaSpace),最大區(qū)別是metaSpace是不存在于JVM中的,它使用的是本地內(nèi)存。并有兩個(gè)參數(shù)
MetaspaceSize:初始化元空間大小,控制發(fā)生GC MaxMetaspaceSize:限制元空間大小上限,防止占用過(guò)多物理內(nèi)存。
移除的原因可以大致了解一下:融合HotSpot JVM和JRockit VM而做出的改變,因?yàn)镴Rockit是沒(méi)有永久代的,不過(guò)這也間接性地解決了永久代的OOM問(wèn)題。
當(dāng)我們new一個(gè)對(duì)象后,會(huì)先放到Eden劃分出來(lái)的一塊作為存儲(chǔ)空間的內(nèi)存,但是我們知道對(duì)堆內(nèi)存是線程共享的,所以有可能會(huì)出現(xiàn)兩個(gè)對(duì)象共用一個(gè)內(nèi)存的情況。這里JVM的處理是每個(gè)線程都會(huì)預(yù)先申請(qǐng)好一塊連續(xù)的內(nèi)存空間并規(guī)定了對(duì)象存放的位置,而如果空間不足會(huì)再申請(qǐng)多塊內(nèi)存空間。這個(gè)操作我們會(huì)稱作TLAB,有興趣可以了解一下。
當(dāng)Eden空間滿了之后,會(huì)觸發(fā)一個(gè)叫做Minor GC(就是一個(gè)發(fā)生在年輕代的GC)的操作,存活下來(lái)的對(duì)象移動(dòng)到Survivor0區(qū)。Survivor0區(qū)滿后觸發(fā) Minor GC,就會(huì)將存活對(duì)象移動(dòng)到Survivor1區(qū),此時(shí)還會(huì)把from和to兩個(gè)指針交換,這樣保證了一段時(shí)間內(nèi)總有一個(gè)survivor區(qū)為空且to所指向的survivor區(qū)為空。經(jīng)過(guò)多次的 Minor GC后仍然存活的對(duì)象(這里的存活判斷是15次,對(duì)應(yīng)到虛擬機(jī)參數(shù)為 -XX:MaxTenuringThreshold 。為什么是15,因?yàn)镠otSpot會(huì)在對(duì)象投中的標(biāo)記字段里記錄年齡,分配到的空間僅有4位,所以最多只能記錄到15)會(huì)移動(dòng)到老年代。老年代是存儲(chǔ)長(zhǎng)期存活的對(duì)象的,占滿時(shí)就會(huì)觸發(fā)我們最常聽(tīng)說(shuō)的Full GC,期間會(huì)停止所有線程等待GC的完成。所以對(duì)于響應(yīng)要求高的應(yīng)用應(yīng)該盡量去減少發(fā)生Full GC從而避免響應(yīng)超時(shí)的問(wèn)題。
而且當(dāng)老年區(qū)執(zhí)行了full gc之后仍然無(wú)法進(jìn)行對(duì)象保存的操作,就會(huì)產(chǎn)生OOM,這時(shí)候就是虛擬機(jī)中的堆內(nèi)存不足,原因可能會(huì)是堆內(nèi)存設(shè)置的大小過(guò)小,這個(gè)可以通過(guò)參數(shù)-Xms、-Xmx來(lái)調(diào)整。也可能是代碼中創(chuàng)建的對(duì)象大且多,而且它們一直在被引用從而長(zhǎng)時(shí)間垃圾收集無(wú)法收集它們。
圖中程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧,3個(gè)區(qū)域隨著線程的生存而生存的。內(nèi)存分配和回收都是確定的。隨著線程的結(jié)束內(nèi)存自然就被回收了,因此不需要考慮垃圾回收的問(wèn)題。而Java堆和方法區(qū)則不一樣,各線程共享,內(nèi)存的分配和回收都是動(dòng)態(tài)的。因此垃圾收集器所關(guān)注的都是堆和方法這部分內(nèi)存。
在進(jìn)行回收前就要判斷哪些對(duì)象還存活,哪些已經(jīng)死去。下面介紹兩個(gè)基礎(chǔ)的計(jì)算方法
1.引用計(jì)數(shù)器計(jì)算:給對(duì)象添加一個(gè)引用計(jì)數(shù)器,每次引用這個(gè)對(duì)象時(shí)計(jì)數(shù)器加一,引用失效時(shí)減一,計(jì)數(shù)器等于0時(shí)就是不會(huì)再次使用的。不過(guò)這個(gè)方法有一種情況就是出現(xiàn)對(duì)象的循環(huán)引用時(shí)GC沒(méi)法回收。
2.可達(dá)性分析計(jì)算:這是一種類(lèi)似于二叉樹(shù)的實(shí)現(xiàn),將一系列的GC ROOTS作為起始的存活對(duì)象集,從這個(gè)節(jié)點(diǎn)往下搜索,搜索所走過(guò)的路徑成為引用鏈,把能被該集合引用到的對(duì)象加入到集合中。搜索當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有使用任何引用鏈時(shí),則說(shuō)明該對(duì)象是不可用的。主流的商用程序語(yǔ)言,例如Java,C#等都是靠這招去判定對(duì)象是否存活的。
(了解一下即可)在Java語(yǔ)言匯總能作為GC Roots的對(duì)象分為以下幾種:
虛擬機(jī)棧(棧幀中的本地方法表)中引用的對(duì)象(局部變量)
方法區(qū)中靜態(tài)變量所引用的對(duì)象(靜態(tài)變量)
方法區(qū)中常量引用的對(duì)象
本地方法棧(即native修飾的方法)中JNI引用的對(duì)象(JNI是Java虛擬機(jī)調(diào)用對(duì)應(yīng)的C函數(shù)的方式,通過(guò)JNI函數(shù)也可以創(chuàng)建新的Java對(duì)象。且JNI對(duì)于對(duì)象的局部引用或者全局引用都會(huì)把它們指向的對(duì)象都標(biāo)記為不可回收)
已啟動(dòng)的且未終止的Java線程
這種方法的優(yōu)點(diǎn)是能夠解決循環(huán)引用的問(wèn)題,可它的實(shí)現(xiàn)需要耗費(fèi)大量資源和時(shí)間,也需要GC(它的分析過(guò)程引用關(guān)系不能發(fā)生變化,所以需要停止所有進(jìn)程)
首先必須要提到的是一個(gè)名叫 finalize() 的方法
finalize()是Object類(lèi)的一個(gè)方法、一個(gè)對(duì)象的finalize()方法只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次,經(jīng)過(guò)finalize()方法逃脫死亡的對(duì)象,第二次不會(huì)再調(diào)用。
補(bǔ)充一句:并不提倡在程序中調(diào)用finalize()來(lái)進(jìn)行自救。建議忘掉Java程序中該方法的存在。因?yàn)樗鼒?zhí)行的時(shí)間不確定,甚至是否被執(zhí)行也不確定(Java程序的不正常退出),而且運(yùn)行代價(jià)高昂,無(wú)法保證各個(gè)對(duì)象的調(diào)用順序(甚至有不同線程中調(diào)用)。在Java9中已經(jīng)被標(biāo)記為 deprecated ,且java.lang.ref.Cleaner(也就是強(qiáng)、軟、弱、幻象引用的那一套)中已經(jīng)逐步替換掉它,會(huì)比f(wàn)inalize來(lái)的更加的輕量及可靠。
判斷一個(gè)對(duì)象的死亡至少需要兩次標(biāo)記
如果對(duì)象進(jìn)行可達(dá)性分析之后沒(méi)發(fā)現(xiàn)與GC Roots相連的引用鏈,那它將會(huì)第一次標(biāo)記并且進(jìn)行一次篩選。判斷的條件是決定這個(gè)對(duì)象是否有必要執(zhí)行finalize()方法。如果對(duì)象有必要執(zhí)行finalize()方法,則被放入F-Queue隊(duì)列中。
GC對(duì)F-Queue隊(duì)列中的對(duì)象進(jìn)行二次標(biāo)記。如果對(duì)象在finalize()方法中重新與引用鏈上的任何一個(gè)對(duì)象建立了關(guān)聯(lián),那么二次標(biāo)記時(shí)則會(huì)將它移出“即將回收”集合。如果此時(shí)對(duì)象還沒(méi)成功逃脫,那么只能被回收了。
如果確定對(duì)象已經(jīng)死亡,我們又該如何回收這些垃圾呢
不會(huì)非常詳細(xì)的展開(kāi),常用的有標(biāo)記清除,復(fù)制,標(biāo)記整理和分代收集算法
標(biāo)記清除算法就是分為“標(biāo)記”和“清除”兩個(gè)階段。標(biāo)記出所有需要回收的對(duì)象,標(biāo)記結(jié)束后統(tǒng)一回收。這個(gè)套路很簡(jiǎn)單,也存在不足,后續(xù)的算法都是根據(jù)這個(gè)基礎(chǔ)來(lái)加以改進(jìn)的。
其實(shí)它就是把已死亡的對(duì)象標(biāo)記為空閑內(nèi)存,然后記錄在一個(gè)空閑列表中,當(dāng)我們需要new一個(gè)對(duì)象時(shí),內(nèi)存管理模塊會(huì)從空閑列表中尋找空閑的內(nèi)存來(lái)分給新的對(duì)象。
不足的方面就是標(biāo)記和清除的效率比較低下。且這種做法會(huì)讓內(nèi)存中的碎片非常多。這個(gè)導(dǎo)致了如果我們需要使用到較大的內(nèi)存塊時(shí),無(wú)法分配到足夠的連續(xù)內(nèi)存。比如下圖
此時(shí)可使用的內(nèi)存塊都是零零散散的,導(dǎo)致了剛剛提到的大內(nèi)存對(duì)象問(wèn)題
為了解決效率問(wèn)題,復(fù)制算法就出現(xiàn)了。它將可用內(nèi)存按容量劃分成兩等分,每次只使用其中的一塊。和survivor一樣也是用from和to兩個(gè)指針這樣的玩法。fromPlace存滿了,就把存活的對(duì)象copy到另一塊toPlace上,然后交換指針的內(nèi)容。這樣就解決了碎片的問(wèn)題。
這個(gè)算法的代價(jià)就是把內(nèi)存縮水了,這樣堆內(nèi)存的使用效率就會(huì)變得十分低下了
不過(guò)它們分配的時(shí)候也不是按照1:1這樣進(jìn)行分配的,就類(lèi)似于Eden和Survivor也不是等價(jià)分配是一個(gè)道理。
復(fù)制算法在對(duì)象存活率高的時(shí)候會(huì)有一定的效率問(wèn)題,標(biāo)記過(guò)程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉邊界以外的內(nèi)存
這種算法并沒(méi)有什么新的思想,只是根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴āT谛律?,每次垃圾收集時(shí)都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集。而老年代中因?yàn)閷?duì)象存活率高、沒(méi)有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用“標(biāo)記-清理”或者“標(biāo)記-整理”算法來(lái)進(jìn)行回收。
說(shuō)白了就是八仙過(guò)海各顯神通,具體問(wèn)題具體分析了而已。
HotSpot VM中的垃圾回收器,以及適用場(chǎng)景
到j(luò)dk8為止,默認(rèn)的垃圾收集器是Parallel Scavenge 和 Parallel Old
從jdk9開(kāi)始,G1收集器成為默認(rèn)的垃圾收集器
目前來(lái)看,G1回收器停頓時(shí)間最短而且沒(méi)有明顯缺點(diǎn),非常適合Web應(yīng)用。在jdk8中測(cè)試Web應(yīng)用,堆內(nèi)存6G,新生代4.5G的情況下,Parallel Scavenge 回收新生代停頓長(zhǎng)達(dá)1.5秒。G1回收器回收同樣大小的新生代只停頓0.2秒。
JVM的參數(shù)非常之多,這里只列舉比較重要的幾個(gè),通過(guò)各種各樣的搜索引擎也可以得知這些信息。
參數(shù)名稱 | 含義 | 默認(rèn)值 | 說(shuō)明 |
---|---|---|---|
-Xms | 初始堆大小 | 物理內(nèi)存的1/64(<1GB) | 默認(rèn)(MinHeapFreeRatio參數(shù)可以調(diào)整)空余堆內(nèi)存小于40%時(shí),JVM就會(huì)增大堆直到-Xmx的最大限制. |
-Xmx | 最大堆大小 | 物理內(nèi)存的1/4(<1GB) | 默認(rèn)(MaxHeapFreeRatio參數(shù)可以調(diào)整)空余堆內(nèi)存大于70%時(shí),JVM會(huì)減少堆直到 -Xms的最小限制 |
-Xmn | 年輕代大小(1.4or lator) | 注意:此處的大小是(eden+ 2 survivor space).與jmap -heap中顯示的New gen是不同的。整個(gè)堆大小=年輕代大小 + 老年代大小 + 持久代(永久代)大小.增大年輕代后,將會(huì)減小年老代大小.此值對(duì)系統(tǒng)性能影響較大,Sun官方推薦配置為整個(gè)堆的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 | 每個(gè)線程的堆棧大小 | JDK5.0以后每個(gè)線程堆棧大小為1M,以前每個(gè)線程堆棧大小為256K.更具應(yīng)用的線程所需內(nèi)存大小進(jìn)行 調(diào)整.在相同物理內(nèi)存下,減小這個(gè)值能生成更多的線程.但是操作系統(tǒng)對(duì)一個(gè)進(jìn)程內(nèi)的線程數(shù)還是有限制的,不能無(wú)限生成,經(jīng)驗(yàn)值在3000~5000左右一般小的應(yīng)用, 如果棧不是很深, 應(yīng)該是128k夠用的 大的應(yīng)用建議使用256k。這個(gè)選項(xiàng)對(duì)性能影響比較大,需要嚴(yán)格的測(cè)試。(校長(zhǎng))和threadstacksize選項(xiàng)解釋很類(lèi)似,官方文檔似乎沒(méi)有解釋,在論壇中有這樣一句話:-Xss is translated in a VM flag named ThreadStackSize”一般設(shè)置這個(gè)值就可以了 | |
-XX:NewRatio | 年輕代(包括Eden和兩個(gè)Survivor區(qū))與年老代的比值(除去持久代) | -XX:NewRatio=4表示年輕代與年老代所占比值為1:4,年輕代占整個(gè)堆棧的1/5Xms=Xmx并且設(shè)置了Xmn的情況下,該參數(shù)不需要進(jìn)行設(shè)置。 | |
-XX:SurvivorRatio | Eden區(qū)與Survivor區(qū)的大小比值 | 設(shè)置為8,則兩個(gè)Survivor區(qū)與一個(gè)Eden區(qū)的比值為2:8,一個(gè)Survivor區(qū)占整個(gè)年輕代的1/10 | |
-XX:+DisableExplicitGC | 關(guān)閉System.gc() | 這個(gè)參數(shù)需要嚴(yán)格的測(cè)試 | |
-XX:PretenureSizeThreshold | 對(duì)象超過(guò)多大是直接在舊生代分配 | 0 | 單位字節(jié) 新生代采用Parallel ScavengeGC時(shí)無(wú)效另一種直接在舊生代分配的情況是大的數(shù)組對(duì)象,且數(shù)組中無(wú)外部引用對(duì)象. |
-XX:ParallelGCThreads | 并行收集器的線程數(shù) | 此值最好配置與處理器數(shù)目相等 同樣適用于CMS | |
-XX:MaxGCPauseMillis | 每次年輕代垃圾回收的最長(zhǎng)時(shí)間(最大暫停時(shí)間) | 如果無(wú)法滿足此時(shí)間,JVM會(huì)自動(dòng)調(diào)整年輕代大小,以滿足此值. |
其實(shí)還有一些打印及CMS方面的參數(shù),這里就不以一一列舉了
根據(jù)剛剛涉及的jvm的知識(shí)點(diǎn),我們可以嘗試對(duì)JVM進(jìn)行調(diào)優(yōu),主要就是堆內(nèi)存那塊
所有線程共享數(shù)據(jù)區(qū)大小=新生代大小 + 年老代大小 + 持久代大小。持久代一般固定大小為64m。所以java堆中增大年輕代后,將會(huì)減小年老代大小(因?yàn)槔夏甏那謇硎鞘褂胒ullgc,所以老年代過(guò)小的話反而是會(huì)增多fullgc的)。此值對(duì)系統(tǒng)性能影響較大,Sun官方推薦配置為java堆的3/8。
-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%時(shí),JVM就會(huì)增大堆直到-Xmx的最大限制.,默認(rèn)(MaxHeapFreeRatio參數(shù)可以調(diào)整)空余堆內(nèi)存大于70%時(shí),JVM會(huì)減少堆直到 -Xms的最小限制。簡(jiǎn)單點(diǎn)來(lái)說(shuō),你不停地往堆內(nèi)存里面丟數(shù)據(jù),等它剩余大小小于40%了,JVM就會(huì)動(dòng)態(tài)申請(qǐng)內(nèi)存空間不過(guò)會(huì)小于-Xmx,如果剩余大小大于70%,又會(huì)動(dòng)態(tài)縮小不過(guò)不會(huì)小于–Xms。就這么簡(jiǎn)單
開(kāi)發(fā)過(guò)程中,通常會(huì)將 -Xms 與 -Xmx兩個(gè)參數(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è)置一個(gè)VM options的參數(shù)
-Xmx20m -Xms5m -XX:+PrintGCDetails
再次啟動(dòng)main方法
這里GC彈出了一個(gè)Allocation Failure分配失敗,這個(gè)事情發(fā)生在PSYoungGen,也就是年輕代中
這時(shí)候申請(qǐng)到的內(nèi)存為18M,空閑內(nèi)存為4.214195251464844M
我們此時(shí)創(chuàng)建一個(gè)字節(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");
此時(shí)free memory就又縮水了,不過(guò)total memory是沒(méi)有變化的。Java會(huì)盡可能將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)前可用的總空間
這時(shí)候我們創(chuàng)建了一個(gè)10M的字節(jié)數(shù)據(jù),這時(shí)候最小堆內(nèi)存是頂不住的。我們會(huì)發(fā)現(xiàn)現(xiàn)在的total memory已經(jīng)變成了15M,這就是已經(jīng)申請(qǐng)了一次內(nèi)存的結(jié)果。
此時(shí)我們?cè)倥芤幌逻@個(gè)代碼
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)前可用的總空間
此時(shí)我們手動(dòng)執(zhí)行了一次fullgc,此時(shí)total memory的內(nèi)存空間又變回5.5M了,此時(shí)又是把申請(qǐng)的內(nèi)存釋放掉的結(jié)果。
-XX:NewRatio --- 新生代(eden+2*Survivor)和老年代(不包含永久區(qū))的比值
例如:-XX:NewRatio=4,表示新生代:老年代=1:4,即新生代占整個(gè)堆的1/5。在Xms=Xmx并且設(shè)置了Xmn的情況下,該參數(shù)不需要進(jìn)行設(shè)置。
-XX:SurvivorRatio(幸存代)--- 設(shè)置兩個(gè)Survivor區(qū)和eden的比值
例如:8,表示兩個(gè)Survivor:eden=2:8,即一個(gè)Survivor占年輕代的1/10
-XX:NewSize --- 設(shè)置年輕代大小
-XX:MaxNewSize --- 設(shè)置年輕代最大值
可以通過(guò)設(shè)置不同參數(shù)來(lái)測(cè)試不同的情況,反正最優(yōu)解當(dāng)然就是官方的Eden和Survivor的占比為8:1:1,然后在剛剛介紹這些參數(shù)的時(shí)候都已經(jīng)附帶了一些說(shuō)明,感興趣的也可以看看。反正最大堆內(nèi)存和最小堆內(nèi)存如果數(shù)值不同會(huì)導(dǎo)致多次的gc,需要注意。
根據(jù)實(shí)際事情調(diào)整新生代和幸存代的大小,官方推薦新生代占java堆的3/8,幸存代占新生代的1/10
在OOM時(shí),記得Dump出堆,確??梢耘挪楝F(xiàn)場(chǎng)問(wèn)題,通過(guò)下面命令你可以輸出一個(gè).dump文件,這個(gè)文件可以使用VisualVM或者Java自帶的Java VisualVM工具。
-Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=你要輸出的日志路徑
一般我們也可以通過(guò)編寫(xiě)腳本的方式來(lái)讓OOM出現(xiàn)時(shí)給我們報(bào)個(gè)信,可以通過(guò)發(fā)送郵件或者重啟程序等來(lái)解決。
-XX:PermSize -XX:MaxPermSize
初始空間(默認(rèn)為物理內(nèi)存的1/64)和最大空間(默認(rèn)為物理內(nèi)存的1/4)。也就是說(shuō),jvm啟動(dòng)時(shí),永久區(qū)一開(kāi)始就占用了PermSize大小的空間,如果空間還不夠,可以繼續(xù)擴(kuò)展,但是不能超過(guò)MaxPermSize,否則會(huì)OOM。
tips:如果堆空間沒(méi)有用完也拋出了OOM,有可能是永久區(qū)導(dǎo)致的。堆空間實(shí)際占用非常少,但是永久區(qū)溢出 一樣拋出OOM。
可以通過(guò)-Xss:調(diào)整每個(gè)線程??臻g的大小
JDK5.0以后每個(gè)線程堆棧大小為1M,以前每個(gè)線程堆棧大小為256K。在相同物理內(nèi)存下,減小這個(gè)值能生成更多的線程。但是操作系統(tǒng)對(duì)一個(gè)進(jìn)程內(nèi)的線程數(shù)還是有限制的,不能無(wú)限生成,經(jīng)驗(yàn)值在3000~5000左右
-XXThreadStackSize: 設(shè)置線程棧的大小(0 means use default stack size)
這些參數(shù)都是可以通過(guò)自己編寫(xiě)程序去簡(jiǎn)單測(cè)試的,這里礙于篇幅問(wèn)題就不再提供demo了
形形色色的參數(shù)很多,就不會(huì)說(shuō)把所有都扯個(gè)遍了,因?yàn)榇蠹移鋵?shí)也不會(huì)說(shuō)一定要去深究到底。
-XXThreadStackSize: 設(shè)置內(nèi)存頁(yè)的大小,不可設(shè)置過(guò)大,會(huì)影響Perm的大小復(fù)制代碼
-XX:+UseFastAccessorMethods: 設(shè)置原始類(lèi)型的快速優(yōu)化
-XX:+DisableExplicitGC: 設(shè)置關(guān)閉System.gc()(這個(gè)參數(shù)需要嚴(yán)格的測(cè)試)
-XX:MaxTenuringThreshold 設(shè)置垃圾最大年齡。如果設(shè)置為0的話,則年輕代對(duì)象不經(jīng)過(guò)Survivor區(qū),直接進(jìn)入年老代. 對(duì)于年老代比較多的應(yīng)用,可以提高效率。如果將此值設(shè)置為一個(gè)較大值, 則年輕代對(duì)象會(huì)在Survivor區(qū)進(jìn)行多次復(fù)制,這樣可以增加對(duì)象再年輕代的存活時(shí)間, 增加在年輕代即被回收的概率。該參數(shù)只有在串行GC時(shí)才有效.
-XX:+AggressiveOpts
加快編譯速度
-XX:+UseBiasedLocking
-Xnoclassgc
-XX:SoftRefLRUPolicyMSPerMB 設(shè)置每兆堆空閑空間中SoftReference的存活時(shí)間,默認(rèn)值是1s。
-XX:PretenureSizeThreshold 設(shè)置對(duì)象超過(guò)多大時(shí)直接在老年代分配,默認(rèn)值是0。
-XX:TLABWasteTargetPercent 設(shè)置TLAB占eden區(qū)的百分比,默認(rèn)值是1% 。
-XX:+CollectGen0First 設(shè)置FullGC時(shí)是否先YGC,默認(rèn)值是false。
作者:說(shuō)出你的愿望吧
鏈接:大白話帶你認(rèn)識(shí)JVM
來(lái)源:掘金
著作權(quán)歸作者所有。非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。
聯(lián)系客服