回復(fù)“000”獲取大量電子書 作為java開發(fā)人員,JVM是必備的,今天,我把JVM的核心知識(shí)點(diǎn)進(jìn)行了一個(gè)總結(jié),畫了一張思維導(dǎo)圖。 圖展開太了,需要的加我微信tj20120622,我私發(fā)給你 下面用一個(gè)18問來開頭,先自己試試這18問,你能回答多數(shù)問? 送給你 JVM 的 18 問1.JDK、JRE、JVM有什么關(guān)系? 2.java是怎么編譯的? 3.編譯成的class文件后,JVM是如何加載class的? 4.類加載機(jī)制是什么? 5.類加載器有哪些? 6.如何自定義類加載器? 7.雙親委派模型是什么? 8.雙親委派模型為什么安全? 9.如何破壞? 10.破壞雙親委派模型的經(jīng)典案例? 11.運(yùn)行時(shí)數(shù)據(jù)庫(kù)區(qū)每個(gè)區(qū)域是干啥的? 12.怎么判斷一個(gè)對(duì)象為垃圾對(duì)象? 13.垃圾回收算法有哪些? 14.每個(gè)算法的利弊? 15.實(shí)現(xiàn)垃圾算法的垃圾收集器有哪些? 16.JVM性能調(diào)優(yōu)參數(shù)熟悉哪些? 17.熟悉哪些性能調(diào)優(yōu)工具? 18.關(guān)于JVM相關(guān)調(diào)優(yōu)經(jīng)歷嗎,有的話說說你的看法? ..... 個(gè)人認(rèn)為上面這18問,就難倒很多人。如果你覺得自己沒問題,那就沒必要看此文了。出門右拐看更高級(jí)更牛逼的O(∩_∩)O哈哈~。 反之,建議踏踏實(shí)實(shí)的學(xué)習(xí),每一篇文章老田都是用心寫出來的,真心的希望對(duì)你有所幫助。
認(rèn)識(shí)JDK、JVM、JRE什么是JVMJVM 全稱 Java Virtual Machine(Java 虛擬機(jī)) ,也就是我們耳熟能詳?shù)?Java 虛擬機(jī)。它能識(shí)別 .class后綴的文件,并且能夠解析它的指令,最終調(diào)用操作系統(tǒng)上的函數(shù),完成我們想要的操作。 JDK認(rèn)識(shí)Java Development Kit (JDK) 是Sun公司(已被Oracle收購(gòu))針對(duì)Java開發(fā)員的軟件開發(fā)工具包。自從Java推出以來,JDK已經(jīng)成為使用最廣泛的Java SDK(Software development kit)。 什么是JREJRE全程Java Runtime Environment,是運(yùn)行基于Java語言編寫的程序所不可缺少的運(yùn)行環(huán)境。也是通過它,Java的開發(fā)者才得以將自己開發(fā)的程序發(fā)布到用戶手中,讓用戶使用。 JDK、JVM、JRE關(guān)系從圖中可以得知: 范圍關(guān)系:JDK>JRE>JVM 編譯從java源文件到class文件的整個(gè)流程為: 總結(jié)以下四步:1、詞法分析讀取源代碼,一個(gè)字節(jié)一個(gè)字節(jié)的讀取,找出其中我們定義好的關(guān)鍵字(如Java中的if、else、for、while等關(guān)鍵詞,識(shí)別哪些if是合法的關(guān)鍵詞,哪些不是),這就是詞法分析器進(jìn)行詞法分析的過程,其結(jié)果是從源代碼中找出規(guī)范化的Token流。 2、語法分析通過語法分析器對(duì)詞法分析后Token流進(jìn)行語法分析,這一步檢查這些關(guān)鍵字組合再一次是否符合Java語言規(guī)范(如在if后面是不是緊跟著一個(gè)布爾判斷表達(dá)式),詞法分析的結(jié)果是形成一個(gè)符合Java語言規(guī)范的抽象語法樹。 3、語義分析通過語義分析器進(jìn)行語義分析。語音分析主要是將一些難懂的、復(fù)雜的語法轉(zhuǎn)化成更加簡(jiǎn)單的語法,結(jié)果形成最簡(jiǎn)單的語法(如將foreach轉(zhuǎn)換成for循環(huán) ,好有注解等),最后形成一個(gè)注解過后的抽象語法樹,這個(gè)語法樹更為接近目標(biāo)語言的語法規(guī)則。 4、生成字節(jié)碼通過字節(jié)碼生產(chǎn)器生成字節(jié)碼,根據(jù)經(jīng)過注解的語法抽象樹生成字節(jié)碼,也就是將一個(gè)數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)化為另一個(gè)數(shù)據(jù)結(jié)構(gòu)。最后生成我們想要的.class文件。 類加載如何查找class文件并導(dǎo)入到JVM中(1)通過一個(gè)類的全限定名獲取定義此類的二進(jìn)制字節(jié)流 (2)將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu) (3)在Java堆中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為對(duì)方法區(qū)中這些數(shù)據(jù)的訪問入口 獲取class文件有哪些方式
加載進(jìn)來后就,系統(tǒng)為這個(gè).class文件生成一個(gè)對(duì)應(yīng)的Class對(duì)象。 生成Class對(duì)象的有哪些方式1.對(duì)象獲取:調(diào)用person類的父類方法getClaass(); 2.類名獲取,每個(gè)類型(包括基本類型和引用)都有一個(gè)靜態(tài)屬性,class。 3.Class類的靜態(tài)方法獲取。forName("字符串的類名")寫全名,要帶包名。 (包名.類名) 類加載機(jī)制類加載機(jī)制分為三步:
其中連接又分三個(gè)步驟:驗(yàn)證、準(zhǔn)備、解析。 連接驗(yàn)證首先肯定是要保證被加載類的正確性,也就是做一些
準(zhǔn)備為類的靜態(tài)變量分配內(nèi)存空間,并將其初始化為默認(rèn)值。 比如說User.java中有個(gè)變量int a; public class InitialDemo { 在這個(gè)階段,會(huì)對(duì)這些static修飾的變量進(jìn)行賦值,附一個(gè)初始值,這里就是給
因?yàn)閕nt類型的初始值就是0;如果是String類型,那么初始值就是null。 解析初始值搞定后,還有就是有部分對(duì)象引用的,在.class字節(jié)碼文件中還是符號(hào),得給指定一個(gè)真實(shí)引用地址。 換言之,把符號(hào)引用變成直接引用。 符號(hào)引用符號(hào)引用以一組符號(hào)來描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)能夠無歧義的定位到目標(biāo)即可。 例如,在Class文件中通過javap命令能查看,它以
符號(hào)引用與虛擬機(jī)的內(nèi)存布局無關(guān),引用的目標(biāo)并不一定加載到內(nèi)存中。在Java中,一個(gè)java類將會(huì)編譯成一個(gè)class文件。 在編譯時(shí),java類并不知道所引用的類的實(shí)際地址,因此只能使用符號(hào)引用來代替。 比如: 各種虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局可能有所不同,但是它們能接受的符號(hào)引用都是一致的,因?yàn)榉?hào)引用的字面量形式明確定義在Java虛擬機(jī)規(guī)范的Class文件格式中。 直接引用直接引用可以是以下三種場(chǎng)景: (1)直接指向目標(biāo)的指針(比如,指向“類型”【Class對(duì)象】、類變量、類方法的直接引用可能是指向方法區(qū)的指針) (2)相對(duì)偏移量(比如,指向?qū)嵗兞?、?shí)例方法的直接引用都是偏移量) (3)一個(gè)能間接定位到目標(biāo)的句柄 直接引用是和虛擬機(jī)的布局相關(guān)的,同一個(gè)符號(hào)引用在不同的虛擬機(jī)實(shí)例上翻譯出來的直接引用一般不會(huì)相同。 如果有了直接引用,那引用的目標(biāo)必定已經(jīng)被加載入內(nèi)存中了。 類加載器在裝載(Load)階段,通過類的全限定名獲取其定義的二進(jìn)制字節(jié)流,需要借助類裝載 器完成,顧名思義,就是用來裝載Class文件的。 類裝載器分類Bootstrap ClassLoader負(fù)責(zé)加載$JAVA_HOME中 jre/lib/rt.jar里所有的class或Xbootclassoath選項(xiàng)指定的jar包。由C++實(shí)現(xiàn),不是ClassLoader子類。 Extension ClassLoader負(fù)責(zé)加載Java平臺(tái)中擴(kuò)展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或 -Djava.ext.dirs指定目錄下的jar包。 App ClassLoader負(fù)責(zé)加載classpath中指定的jar包及 Djava.class.path所指定目錄下的類和jar包。 Custom ClassLoader通過java.lang.ClassLoader的子類自定義加載class,屬于應(yīng)用程序根據(jù)自身需要自定義的ClassLoader,如tomcat、jboss都會(huì)根據(jù)j2ee規(guī)范自行實(shí)現(xiàn)ClassLoader。 圖解類加載 加載原則檢查某個(gè)類是否已經(jīng)加載:順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader逐層檢 查,只要某個(gè)Classloader已加載,就視為已加載此類,保證此類只所有ClassLoader加載一次。 加載的順序:先查找是否已經(jīng)加載過,當(dāng)沒有被加載過,則加載的順序是自頂向下,也就是由上層來逐層嘗試加載此類。 java.lang.ClassLoader中很重要的三個(gè)方法:
loadClass方法 public Class<?> loadClass(String name) throws ClassNotFoundException { 正如loadClass方法所展示的,當(dāng)類加載請(qǐng)求到來時(shí),先從緩存中查找該類對(duì)象,如果存在直接返回,如果不存在則交給該類加載去的父加載器去加載,倘若沒有父加載則交給頂級(jí)啟動(dòng)類加載器去加載,最后倘若仍沒有找到,則使用findClass()方法去加載(關(guān)于findClass()稍后會(huì)進(jìn)一步介紹)。從loadClass實(shí)現(xiàn)也可以知道如果不想重新定義加載類的規(guī)則,也沒有復(fù)雜的邏輯,只想在運(yùn)行時(shí)加載自己指定的類,那么我們可以直接使用this.getClass().getClassLoder.loadClass("className"),這樣就可以直接調(diào)用ClassLoader的loadClass方法獲取到class對(duì)象。 findClass方法protected Class<?> findClass(String name) throws ClassNotFoundException { 在JDK1.2之前,在自定義類加載時(shí),總會(huì)去繼承ClassLoader類并重寫loadClass方法,從而實(shí)現(xiàn)自定義的類加載類,但是在JDK1.2之后已不再建議用戶去覆蓋loadClass()方法,而是建議把自定義的類加載邏輯寫在findClass()方法中,從前面的分析可知,findClass()方法是在loadClass()方法中被調(diào)用的,當(dāng)loadClass()方法中父加載器加載失敗后,則會(huì)調(diào)用自己的findClass()方法來完成類加載,這樣就可以保證自定義的類加載器也符合雙親委托模式。 需要注意的是ClassLoader類中并沒有實(shí)現(xiàn)findClass()方法的具體代碼邏輯,取而代之的是拋出ClassNotFoundException異常, 同時(shí)應(yīng)該知道的是findClass方法通常是和defineClass方法一起使用的。 defineClass方法protected final Class<?> defineClass(String name, byte[] b, int off, int len, defineClass()方法是用來將byte字節(jié)流解析成JVM能夠識(shí)別的Class對(duì)象。通過這個(gè)方法不僅能夠通過class文件實(shí)例化class對(duì)象,也可以通過其他方式實(shí)例化class對(duì)象,如通過網(wǎng)絡(luò)接收一個(gè)類的字節(jié)碼,然后轉(zhuǎn)換為byte字節(jié)流創(chuàng)建對(duì)應(yīng)的Class對(duì)象 。 如何自定義類加載器用戶根據(jù)需求自己定義的。需要繼承自ClassLoader,重寫方法findClass()。 如果想要編寫自己的類加載器,只需要兩步:
ClassLoader超類的loadClass方法用于將類的加載操作委托給其父類加載器去進(jìn)行,只有當(dāng)該類尚未加載并且父類加載器也無法加載該類時(shí),才調(diào)用findClass方法。 如果要實(shí)現(xiàn)該方法,必須做到以下幾點(diǎn): 1.為來自本地文件系統(tǒng)或者其他來源的類加載其字節(jié)碼。 2.調(diào)用ClassLoader超類的defineClass方法,向虛擬機(jī)提供字節(jié)碼。 雙親委派模型什么是雙親委派模型如果一個(gè)類加載器在接到加載類的請(qǐng)求時(shí),先查找是否已經(jīng)加載過,如果沒有被加載過,它首先不會(huì)自己嘗試去加載這個(gè)類,而是把這個(gè)請(qǐng)求任務(wù)委托給父類加載器去完成,依次遞歸,如果父類加載器可以完成類加載任務(wù),就成功返回;只有父類加載器無法完成此加載任務(wù)時(shí),才自己去加載。 雙親委派模型有什么好處?Java類隨著加載它的類加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系。 比如,Java中的Object類,它存放在rt.jar之中,無論哪一個(gè)類加載器要加載這個(gè)類,最終都是委派給處于模型最頂端的啟動(dòng)類加載器進(jìn)行加載,因此Object在各種類加載環(huán)境中都是同一個(gè)類。 如果不采用雙親委派模型,那么由各個(gè)類加載器自己取加載的話,那么系統(tǒng)中會(huì)存在多種不同的Object類。 打破雙親委派模型的案例tomcattomcat 通過 war 包進(jìn)行應(yīng)用的發(fā)布,它其實(shí)是違反了雙親委派機(jī)制原則的。簡(jiǎn)單看一下 tomcat 類加載器的層次結(jié)構(gòu)。 對(duì)于一些需要加載的非基礎(chǔ)類,會(huì)由一個(gè)叫作 WebAppClassLoader 的類加載器優(yōu)先加載。等它加載不到的時(shí)候,再交給上層的 ClassLoader 進(jìn)行加載。這個(gè)加載器用來隔絕不同應(yīng)用的 .class 文件,比如你的兩個(gè)應(yīng)用,可能會(huì)依賴同一個(gè)第三方的不同版本,它們是相互沒有影響的。 如何在同一個(gè) JVM 里,運(yùn)行著不兼容的兩個(gè)版本,當(dāng)然是需要自定義加載器才能完成的事。 那么 tomcat 是怎么打破雙親委派機(jī)制的呢?可以看圖中的 WebAppClassLoader,它加載自己目錄下的 .class 文件,并不會(huì)傳遞給父類的加載器。但是,它卻可以使用 SharedClassLoader 所加載的類,實(shí)現(xiàn)了共享和分離的功能。 但是你自己寫一個(gè) ArrayList,放在應(yīng)用目錄里,tomcat 依然不會(huì)加載。它只是自定義的加載器順序不同,但對(duì)于頂層來說,還是一樣的。 OSGiOSGi 曾經(jīng)非常流行,Eclipse 就使用 OSGi 作為插件系統(tǒng)的基礎(chǔ)。OSGi 是服務(wù)平臺(tái)的規(guī)范,旨在用于需要長(zhǎng)運(yùn)行時(shí)間、動(dòng)態(tài)更新和對(duì)運(yùn)行環(huán)境破壞最小的系統(tǒng)。 OSGi 規(guī)范定義了很多關(guān)于包生命周期,以及基礎(chǔ)架構(gòu)和綁定包的交互方式。這些規(guī)則,通過使用特殊 Java 類加載器來強(qiáng)制執(zhí)行,比較霸道。 比如,在一般 Java 應(yīng)用程序中,classpath 中的所有類都對(duì)所有其他類可見,這是毋庸置疑的。但是,OSGi 類加載器基于 OSGi 規(guī)范和每個(gè)綁定包的 manifest.mf 文件中指定的選項(xiàng),來限制這些類的交互,這就讓編程風(fēng)格變得非常的怪異。但我們不難想象,這種與直覺相違背的加載方式,肯定是由專用的類加載器來實(shí)現(xiàn)的。 隨著 jigsaw 的發(fā)展(旨在為 Java SE 平臺(tái)設(shè)計(jì)、實(shí)現(xiàn)一個(gè)標(biāo)準(zhǔn)的模塊系統(tǒng)),我個(gè)人認(rèn)為,現(xiàn)在的 OSGi,意義已經(jīng)不是很大了。OSGi 是一個(gè)龐大的話題,你只需要知道,有這么一個(gè)復(fù)雜的東西,實(shí)現(xiàn)了模塊化,每個(gè)模塊可以獨(dú)立安裝、啟動(dòng)、停止、卸載,就可以了。 SPIJava 中有一個(gè) SPI 機(jī)制,全稱是 Service Provider Interface,是 Java 提供的一套用來被第三方實(shí)現(xiàn)或者擴(kuò)展的 API,它可以用來啟用框架擴(kuò)展和替換組件。 后面會(huì)專門針對(duì)這個(gè)寫一篇文章,這里就不細(xì)說了。 雙親委派模型為什么安全?前面談到雙親委派機(jī)制是為了安全而設(shè)計(jì)的,但是為什么就安全了呢?舉個(gè)例子,ClassLoader加載的class文件來源很多,比如編譯器編譯生成的class、或者網(wǎng)絡(luò)下載的字節(jié)碼。 而一些來源的class文件是不可靠的,比如我可以自定義一個(gè)java.lang.Integer類來覆蓋jdk中默認(rèn)的Integer類,例如下面這樣: package java.lang; 初始化這個(gè)Integer的構(gòu)造器是會(huì)退出JVM,破壞應(yīng)用程序的正常進(jìn)行,如果使用雙親委派機(jī)制的話,該Integer類永遠(yuǎn)不會(huì)被調(diào)用,以為委托BootStrapClassLoader加載后會(huì)加載JDK中的Integer類而不會(huì)加載自定義的這個(gè),運(yùn)行main方法,程序并沒有執(zhí)行System.exit(0); 這里使用的是rt.jar里的Integer,并沒有使用我們自定義的Integer類,這個(gè)案例和前面的能不能自定義一個(gè)String類完全一樣,這樣就保證了安全性。 運(yùn)行數(shù)據(jù)區(qū)運(yùn)行時(shí)數(shù)據(jù)區(qū)的五個(gè)模塊
什么是方法區(qū)方法區(qū)是用于存儲(chǔ)類結(jié)構(gòu)信息的地方,線程共享,包括常量池、靜態(tài)變量、構(gòu)造函數(shù)等類型信息,類型信息是由類加載器在類加載時(shí)從類.class文件中提取出來的。 官網(wǎng)的介紹;
從上面的介紹中,我們大致可以得出以下結(jié)論:
用一段代碼來加深印象: /** User.class類信息,以及靜態(tài)變量a,常量b等信息是存放在方法區(qū)的。方法區(qū)的實(shí)現(xiàn)通常有兩種:JDK8前的永久代,以及JDK8后的元空間。 什么是寄存器?The pc Register 也有的翻譯為pc寄存器。下面是官網(wǎng)對(duì)寄存器的解釋,做了一個(gè)簡(jiǎn)要的翻譯。 The Java Virtual Machine can support many threads of execution at once (JLS §17). 實(shí)際上,程序計(jì)數(shù)器占用的內(nèi)存空間很小,由于Java虛擬機(jī)的多線程是通過線程輪流切換,并分配處理器執(zhí)行時(shí)間的方式來實(shí)現(xiàn)的,在任意時(shí)刻,一個(gè)處理器只會(huì)執(zhí)行一條線程中的指令。因此,為了線程切換后能夠恢復(fù)到正確的執(zhí)行位置,每條線程需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器(線程私有)。 我們都知道一個(gè)JVM進(jìn)程中有多個(gè)線程在執(zhí)行,而線程中的內(nèi)容是否能夠擁有執(zhí)行權(quán),是根據(jù)CPU調(diào)度來的。 假如線程A正在執(zhí)行到某個(gè)地方,突然失去了CPU的執(zhí)行權(quán),切換到線程B了,然后當(dāng)線程A再獲得CPU執(zhí)行權(quán)的時(shí)候,怎么能繼續(xù)執(zhí)行呢? 這就是需要在線程中維護(hù)一個(gè)變量,記錄線程執(zhí)行到的位置,記錄本次已經(jīng)執(zhí)行到哪一行代碼了,當(dāng)CPU切換回來時(shí)候,再?gòu)倪@里繼續(xù)執(zhí)行。 什么是堆?堆是Java虛擬機(jī)所管理內(nèi)存中最大的一塊,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建,被所有線程共享。Java對(duì)象實(shí)例以及數(shù)組基本上都在堆上分配。官網(wǎng)介紹: The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. 在前面類加載階段我們已經(jīng)聊過了,在Java堆中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為對(duì)方法區(qū)中這些數(shù)據(jù)的訪問入口。 堆在JDK1.7和JDK1.8的變化大家都知道,JVM 在運(yùn)行時(shí),會(huì)從操作系統(tǒng)申請(qǐng)大塊的堆內(nèi)內(nèi)存,進(jìn)行數(shù)據(jù)的存儲(chǔ)。但是,堆外內(nèi)存也就是申請(qǐng)后操作系統(tǒng)剩余的內(nèi)存,也會(huì)有部分受到 JVM 的控制。比較典型的就是一些 native 關(guān)鍵詞修飾的方法,以及對(duì)內(nèi)存的申請(qǐng)和處理。 在 JVM中,堆被劃分成兩個(gè)不同的區(qū)域:新生代 ( 新生代 ( Young ) 又被劃分為三個(gè)區(qū)域: 注意:很多的文章或者書籍里也稱 這樣劃分的目的是為了使JVM能夠更好的管理堆內(nèi)存中的對(duì)象,包括內(nèi)存的分配以及回收。 根據(jù)之前對(duì)于Heap的介紹可以知道,一般對(duì)象和數(shù)組的創(chuàng)建會(huì)在堆中分配內(nèi)存空間,關(guān)鍵是堆中有這么多區(qū) 域,那一個(gè)對(duì)象的創(chuàng)建到底在哪個(gè)區(qū)域呢? 對(duì)象創(chuàng)建所在區(qū)域一般情況下,新創(chuàng)建的對(duì)象都會(huì)被分配到Eden區(qū)(朝生夕死),一些特殊的大的對(duì)象會(huì)直接分配到Old區(qū)。 比如有對(duì)象A,B,C等創(chuàng)建在Eden區(qū),但是Eden區(qū)的內(nèi)存空間肯定有限,比如有100M,假如已經(jīng)使用了 100M或者達(dá)到一個(gè)設(shè)定的臨界值,這時(shí)候就需要對(duì)Eden內(nèi)存空間進(jìn)行清理,即垃圾收集(Garbage Collect), 這樣的GC我們稱之為Minor GC,Minor GC指得是 經(jīng)過GC之后,有些對(duì)象就會(huì)被清理掉,有些對(duì)象可能還存活著,對(duì)于存活著的對(duì)象需要將其復(fù)制到Survivor 區(qū),然后再清空Eden區(qū)中的這些對(duì)象。
這個(gè)道理和 Java 語言中的ThreadLocal類似,避免了對(duì)公共區(qū)的操作,以及一些鎖競(jìng)爭(zhēng)。 對(duì)象的分配優(yōu)先在TLAB上 分配,但 TLAB通常都很小,所以對(duì)象相對(duì)比較大的時(shí)候,會(huì)在 Eden 區(qū)的共享區(qū)域進(jìn)行分配。 TLAB是一種優(yōu)化技術(shù),類似的優(yōu)化還有對(duì)象的棧上分配(這可以引出逃逸分析的話題,默認(rèn)開啟)。這屬于非常細(xì)節(jié)的優(yōu)化,不做過多介紹,但偶爾面試也會(huì)被問到。 Survivor區(qū)詳解由圖解可以看出,Survivor區(qū)分為兩塊S0和S1,也可以叫做From和To。在同一個(gè)時(shí)間點(diǎn)上,S0和S1只能有一個(gè)區(qū)有數(shù)據(jù),另外一個(gè)是空的。 接著上面的GC來說,比如一開始只有Eden區(qū)和From中有對(duì)象,To中是空的。 此時(shí)進(jìn)行一次GC操作,F(xiàn)rom區(qū)中對(duì)象的年齡就會(huì)+1,我們知道Eden區(qū)中所有存活的對(duì)象會(huì)被復(fù)制到To區(qū),F(xiàn)rom區(qū)中還能存活的對(duì)象會(huì)有兩個(gè)去處。 若對(duì)象年齡達(dá)到之前設(shè)置好的年齡閾值(默認(rèn)年齡為15歲,可以自行設(shè)置參數(shù) 此時(shí)Eden區(qū)和From區(qū)已經(jīng)被清空(被GC的對(duì)象肯定沒了,沒有被GC的對(duì)象都有了各自的去處)。 這時(shí)候From和To交換角色,之前的From變成了To,之前的To變成了From。也就是說無論如何都要保證名為To的Survivor區(qū)域是空的。 Minor GC會(huì)一直重復(fù)這樣的過程,知道To區(qū)被填滿,然后會(huì)將所有對(duì)象復(fù)制到老年代中。 Old區(qū)從上面的分析可以看出,一般Old區(qū)都是年齡比較大的對(duì)象,或者相對(duì)超過了某個(gè)閾值(-XX:PretenureSizeThreshold,默認(rèn)為15,表示全部進(jìn)Eden區(qū))的對(duì)象。在Old區(qū)也會(huì)有GC的操作,Old區(qū)的GC我們稱作為Major GC。 什么是虛擬機(jī)棧?Java虛擬機(jī)棧,是線程私有。 每一個(gè)線程擁有一個(gè)虛擬機(jī)棧,每一個(gè)棧包含n個(gè)棧幀,每個(gè)棧幀對(duì)應(yīng)一次一個(gè)放調(diào)用, 每個(gè)棧幀里包含:局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口。 官網(wǎng)介紹 Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread. 看一段代碼 public class JavaStackDemo { 啟動(dòng)main方法就是啟動(dòng)了一個(gè)線程,JVM中會(huì)對(duì)應(yīng)給這個(gè)線程創(chuàng)建一個(gè)棧。 從這個(gè)調(diào)用過程很容易發(fā)現(xiàn)是個(gè)先進(jìn)后出的結(jié)構(gòu),剛好棧的結(jié)構(gòu)就是這樣的。java虛擬機(jī)棧就是這么設(shè)計(jì)的: 每個(gè)棧幀表示一個(gè)方法的調(diào)用:進(jìn)入方法表示棧幀入棧,棧幀出棧表示方法調(diào)用結(jié)束。 多線程的話就是這樣了: 從上面這個(gè)圖大家會(huì)不會(huì)覺得這個(gè)棧有問題?其實(shí)也是有問題的,比如說看下面這段代碼 /** 調(diào)用過程如下圖: 是不是覺得很無語,調(diào)用方法就往棧里加入一個(gè)棧幀,這么下去,這個(gè)棧得需要多深才能放下,死循環(huán)和無限遞歸呢,豈不是棧里需要無限深度嗎? Java虛擬機(jī)棧大小肯定是有限的,所以就會(huì)導(dǎo)致一個(gè)大家都聽說過的棧溢出。 運(yùn)行上面的代碼: 如何設(shè)置Java虛擬機(jī)棧的大小呢?我們可以使用虛擬機(jī)參數(shù)-Xss 選項(xiàng)來設(shè)置線程的最大??臻g,棧的大小直接決定了函數(shù)調(diào)用的最大可達(dá)深度;
下面的示例以不同的單位將線程堆棧大小設(shè)置為1024 KB: -Xss1m (1mb) 回到上面的話題。 什么是棧幀?上面提到過,調(diào)用方法就生成一個(gè)棧幀,然后入棧。 看一段代碼 public class JavaStackDemo { 既然是和方法有關(guān),那么就可以聯(lián)想到方法里都有些什么 官網(wǎng)介紹 Each frame has its own array of local variables , its own operand stack (§2.6.2), and a reference to the run-time constant pool of the class of the current method. 每個(gè)棧幀擁有自己的本地變量。比如上面代碼里的 int age、int temp 這些都是本地變量。 每個(gè)棧幀都有自己的操作數(shù)棧 通過javac編譯好JavaStackDemo,然后使用 javap -v JavaStackDemo.class >log.txt 將字節(jié)碼導(dǎo)入到log.txt中,打開 對(duì)getUserType方法里面的字節(jié)碼做一個(gè)解釋。有時(shí)候本地變量通過javap看不到,可以再javac的時(shí)候添加一個(gè)參數(shù)
指令bipush 18 將18壓入操作數(shù)棧 官網(wǎng)
這些都是字節(jié)碼指令。 LocalVariableTable 本地變量表Start Length Slot Name Signature 自己this算一個(gè)本地變量,入?yún)ge算一個(gè)本地變量,方法中的臨時(shí)變量temp也算一個(gè)本地變量。 方法出口return。如果方法不需要返回void的時(shí)候,其實(shí)方法里是默認(rèn)會(huì)為其加上一個(gè)return; 另外方法的返回分兩種:
棧幀總結(jié)
什么是本地方法棧?Native Method Stacks 翻譯過來就是本地方法棧,與Java虛擬機(jī)棧一樣,但這里的棧是針對(duì)native修飾的方法的,比如System、Unsafe、Object類中的相關(guān)native方法。 public class Object { 面試常問:JVM運(yùn)行時(shí)區(qū)那些和線程有直接的關(guān)系和間接的關(guān)系,哪些區(qū)會(huì)發(fā)生OOM? 每個(gè)區(qū)域是否為線程共享,是否會(huì)發(fā)生OOM 如何判斷對(duì)象是垃圾對(duì)象?引用計(jì)數(shù)法給對(duì)象添加一個(gè)引用計(jì)數(shù)器,每當(dāng)一個(gè)地方引用它object時(shí)技術(shù)加1,引用失去以后就減1,計(jì)數(shù)為0說明不再引用。
public class A { 可達(dá)性分析算法當(dāng)一個(gè)對(duì)象到GC Roots沒有引用鏈相連,即就是GC Roots到這個(gè)對(duì)象不可達(dá)時(shí),證明對(duì)象不可用。 GC Roots種類
public class Test{ 對(duì)象的引用類型有哪些?
finalize()方法有什么作用? 這個(gè)方法就有點(diǎn)類似,某個(gè)人被拍了死刑,但是不一定會(huì)死。 即使在可達(dá)性分析算法中不可達(dá)的對(duì)象,也并非一定是“非死不可”的,這時(shí)候他們暫時(shí)處于“緩刑”階段,真正宣告一個(gè)對(duì)象死亡至少要經(jīng)歷兩個(gè)階段: 1、如果對(duì)象在可達(dá)性分析算法中不可達(dá),那么它會(huì)被第一次標(biāo)記并進(jìn)行一次刷選,刷選的條件是是否需要執(zhí)行finalize()方法(當(dāng)對(duì)象沒有覆蓋finalize()或者finalize()方法已經(jīng)執(zhí)行過了(對(duì)象的此方法只會(huì)執(zhí)行一次)),虛擬機(jī)將這兩種情況都會(huì)視為沒有必要執(zhí)行)。 2、如果這個(gè)對(duì)象有必要執(zhí)行finalize()方法會(huì)將其放入F-Queue隊(duì)列中,稍后GC將對(duì)F-Queue隊(duì)列進(jìn)行第二次標(biāo)記,如果在重寫finalize()方法中將對(duì)象自己賦值給某個(gè)類變量或者對(duì)象的成員變量,那么第二次標(biāo)記時(shí)候就會(huì)將它移出“即將回收”的集合。 垃圾回收算法有哪些?標(biāo)記-清除第一步:就是找出活躍的對(duì)象。我們反復(fù)強(qiáng)調(diào) GC 過程是逆向的, 根據(jù) GC Roots 遍歷所有的可達(dá)對(duì)象,這個(gè)過程,就叫作標(biāo)記。 第二部:除了上面標(biāo)記出來的對(duì)象以外,其余的都清楚掉。 復(fù)制新生代使用,新生代分中Eden:S0:S1= 8:1:1,其中后面的1:1就是用來復(fù)制的。 當(dāng)其中一塊內(nèi)存使用完了,就將還存活的對(duì)象復(fù)制到另外一塊上面,然后把已經(jīng)使用過的內(nèi)存空間一次 清除掉。 一般對(duì)象分配都是進(jìn)入新生代的eden區(qū),如果Minor GC還存活則進(jìn)入S0區(qū),S0和S1不斷對(duì)象進(jìn)行復(fù)制。對(duì)象存活年齡最大默認(rèn)是15,大對(duì)象進(jìn)來可能因?yàn)樾律淮嬖谶B續(xù)空間,所以會(huì)直接接入老年代。任何使用都有新生代的10%是空著的。 標(biāo)記整理它的主要思路,就是移動(dòng)所有存活的對(duì)象,且按照內(nèi)存地址順序依次排列,然后將末端內(nèi)存地址以后的內(nèi)存全部回收。 但是需要注意,這只是一個(gè)理想狀態(tài)。對(duì)象的引用關(guān)系一般都是非常復(fù)雜的,我們這里不對(duì)具體的算法進(jìn)行描述。我們只需要了解,從效率上來說,一般整理算法是要低于復(fù)制算法的。這個(gè)算法是規(guī)避了內(nèi)存碎片和內(nèi)存浪費(fèi)。 讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。 從上面的三個(gè)算法來看,其實(shí)沒有絕對(duì)最好的回收算法,只有最適合的算法。 垃圾收集器垃圾收集器就是垃圾回收算法的實(shí)現(xiàn),下面就來聊聊現(xiàn)目前有哪些垃圾收集器。 新生代有哪些垃圾收集器serialSerial收集器是最基本、發(fā)展歷史最悠久的收集器,曾經(jīng)(在JDK1.3.1之前)是虛擬機(jī)新生代收集的唯一選擇。 它是一種單線程收集器,不僅僅意味著它只會(huì)使用一個(gè)CPU或者一條收集線程去完成垃圾收集工作,更重要的是其在進(jìn)行垃圾收集的時(shí)候需要暫停其他線程。 優(yōu)點(diǎn):簡(jiǎn)單高效,擁有很高的單線程收集效率 缺點(diǎn):收集過程需要暫停所有線程 算法:復(fù)制算法 應(yīng)用:Client模式下的默認(rèn)新生代收集器 收集過程: ParNew可以把這個(gè)收集器理解為Serial收集器的多線程版本。 優(yōu)點(diǎn):在多CPU時(shí),比Serial效率高。 缺點(diǎn):收集過程暫停所有應(yīng)用程序線程,單CPU時(shí)比Serial效率差。 算法:復(fù)制算法 應(yīng)用:運(yùn)行在Server模式下的虛擬機(jī)中首選的新生代收集器 收集過程: Parallel ScanvengeParallel Scavenge收集器是一個(gè)新生代收集器,它也是使用復(fù)制算法的收集器,又是并行的多線程收集 器,看上去和ParNew一樣,但是Parallel Scanvenge更關(guān)注 系統(tǒng)的吞吐量 ; 吞吐量 = 運(yùn)行用戶代碼的時(shí)間 / (運(yùn)行用戶代碼的時(shí)間 + 垃圾收集時(shí)間) 比如虛擬機(jī)總共運(yùn)行了120秒,垃圾收集時(shí)間用了1秒,吞吐量=(120-1)/120=99.167%。 若吞吐量越大,意味著垃圾收集的時(shí)間越短,則用戶代碼可以充分利用CPU資源,盡快完成程序的運(yùn)算任務(wù)。 可設(shè)置參數(shù): -XX:MaxGCPauseMillis控制最大的垃圾收集停頓時(shí)間, 老年代有哪些垃圾收集器CMS=Concurrent Mark Sweep特點(diǎn):最短回收停頓時(shí)間, 回收算法:標(biāo)記-清除 回收步驟: 缺點(diǎn):對(duì)CPU資源非常敏感,CPU少于4個(gè)時(shí),CMS歲用戶程序的影響可能變得很大,有此虛擬機(jī)提供了“增量式并發(fā)收集器”;無法回收浮動(dòng)垃圾;采用標(biāo)記清除算法會(huì)產(chǎn)生內(nèi)存碎片,不過可以通過參數(shù)開啟內(nèi)存碎片的合并整理。 收集過程: serial oldSerial Old收集器是Serial收集器的老年代版本,也是一個(gè)單線程收集器,不同的是采用"標(biāo)記-整理算 法",運(yùn)行過程和Serial收集器一樣。 適用場(chǎng)景:JDK1.5前與Parallel Scanvenge配合使用,作為CMS的后備預(yù)案; 收集過程: Parallel oldParallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和"標(biāo)記-整理算法"進(jìn)行垃圾 回收,吞吐量?jī)?yōu)先; 回收算法:標(biāo)記-整理 適用場(chǎng)景:為了替代serial old與Parallel Scanvenge配合使用 收集過程: G1 收集器G1(Garbage first) 收集器是 jdk1.7 才正式引用的商用收集器,現(xiàn)在已經(jīng)成為JDK9 默認(rèn)的收集器。前面幾款收集器收集的范圍都是新生代或者老年代,G1 進(jìn)行垃圾收集的范圍是整個(gè)堆內(nèi)存,它采用 “ 化整為零 ” 的思路,把整個(gè)堆內(nèi)存劃分為多個(gè)大小相等的獨(dú)立區(qū)域(Region),在 G1 收集器中還保留著新生代和老年代的概念,它們分別都是一部分 Region,如下圖: 每一個(gè)方塊就是一個(gè)區(qū)域,每個(gè)區(qū)域可能是 Eden、Survivor、老年代,每種區(qū)域的數(shù)量也不一定。JVM 啟動(dòng)時(shí)會(huì)自動(dòng)設(shè)置每個(gè)區(qū)域的大?。?M ~ 32M,必須是 2 的次冪),最多可以設(shè)置 2048 個(gè)區(qū)域(即支持的最大堆內(nèi)存為 32M*2048 = 64G),假如設(shè)置 -Xmx8g -Xms8g,則每個(gè)區(qū)域大小為 8g/2048=4M。 為了在 GC Roots Tracing 的時(shí)候避免掃描全堆,在每個(gè) Region 中,都有一個(gè) Remembered Set 來實(shí)時(shí)記錄該區(qū)域內(nèi)的引用類型數(shù)據(jù)與其他區(qū)域數(shù)據(jù)的引用關(guān)系(在前面的幾款分代收集中,新生代、老年代中也有一個(gè) Remembered Set 來實(shí)時(shí)記錄與其他區(qū)域的引用關(guān)系),在標(biāo)記時(shí)直接參考這些引用關(guān)系就可以知道這些對(duì)象是否應(yīng)該被清除,而不用掃描全堆的數(shù)據(jù)。 G1 收集器可以 “ 建立可預(yù)測(cè)的停頓時(shí)間模型 ”,它維護(hù)了一個(gè)列表用于記錄每個(gè) Region 回收的價(jià)值大小(回收后獲得的空間大小以及回收所需時(shí)間的經(jīng)驗(yàn)值),這樣可以保證 G1 收集器在有限的時(shí)間內(nèi)可以獲得最大的回收效率。 如下圖所示,G1 收集器收集器收集過程有初始標(biāo)記、并發(fā)標(biāo)記、最終標(biāo)記、篩選回收,和 CMS 收集器前幾步的收集過程很相似: ① 初始標(biāo)記:標(biāo)記出 GC Roots 直接關(guān)聯(lián)的對(duì)象,這個(gè)階段速度較快,需要停止用戶線程,單線程執(zhí)行。 ② 并發(fā)標(biāo)記:從 GC Root 開始對(duì)堆中的對(duì)象進(jìn)行可達(dá)新分析,找出存活對(duì)象,這個(gè)階段耗時(shí)較長(zhǎng),但可以和用戶線程并發(fā)執(zhí)行。 ③ 最終標(biāo)記:修正在并發(fā)標(biāo)記階段引用戶程序執(zhí)行而產(chǎn)生變動(dòng)的標(biāo)記記錄。 ④ 篩選回收:篩選回收階段會(huì)對(duì)各個(gè) Region 的回收價(jià)值和成本進(jìn)行排序,根據(jù)用戶所期望的 GC 停頓時(shí)間來指定回收計(jì)劃(用最少的時(shí)間來回收包含垃圾最多的區(qū)域,這就是 Garbage First 的由來——第一時(shí)間清理垃圾最多的區(qū)塊),這里為了提高回收效率,并沒有采用和用戶線程并發(fā)執(zhí)行的方式,而是停頓用戶線程。 適用場(chǎng)景:要求盡可能可控 GC 停頓時(shí)間;內(nèi)存占用較大的應(yīng)用??梢杂?-XX:+UseG1GC 使用 G1 收集器,jdk9 默認(rèn)使用 ZGC收集器ZGC有什么特點(diǎn)?ZGC 是最新的 JDK1.11 版本中提供的高效垃圾回收算法,ZGC 針對(duì)大堆內(nèi)存設(shè)計(jì)可以支持 TB 級(jí)別的堆,ZGC 非常高效,能夠做到 10ms 以下的回收停頓時(shí)間。 這么快的響應(yīng),ZGC 是如何做到的呢?這是由于 ZGC 具有以下特點(diǎn)。 雖然 ZGC 的大部分時(shí)間是并發(fā)進(jìn)行的,但是還會(huì)有短暫的停頓。來看一下 ZGC 的回收過程。 ZGC 是如何進(jìn)行垃圾收集的?ZGC(Z Garbage Collector)是一款由Oracle公司研發(fā)的,以低延遲為首要目標(biāo)的一款垃圾收集器。它是基于動(dòng)態(tài)Region內(nèi)存布局,(暫時(shí))不設(shè)年齡分代,使用了讀屏障、染色指針和內(nèi)存多重映射等技術(shù)來實(shí)現(xiàn)可并發(fā)的標(biāo)記-整理算法的收集器。 初始狀態(tài)時(shí),整個(gè)堆空間被劃分為大小不等的許多 Region,即圖中綠色的方塊。 開始進(jìn)行回收時(shí),ZGC 首先會(huì)進(jìn)行一個(gè)短暫的 STW(Stop The world),來進(jìn)行 roots 標(biāo)記。這個(gè)步驟非常短,因?yàn)?roots 的總數(shù)通常比較小。 然后就開始進(jìn)行并發(fā)標(biāo)記,如上圖所示,通過對(duì)對(duì)象指針進(jìn)行著色來進(jìn)行標(biāo)記,結(jié)合讀屏障解決單個(gè)對(duì)象的并發(fā)問題。其實(shí),這個(gè)階段在最后還是會(huì)有一個(gè)非常短的 STW 停頓,用來處理一些邊緣情況,這個(gè)階段絕大部分時(shí)間是并發(fā)進(jìn)行的,所以沒有明顯標(biāo)出這個(gè)停頓。 下一個(gè)是清理階段,這個(gè)階段會(huì)把標(biāo)記為不在使用的對(duì)象進(jìn)行回收,如上圖所示,把橘色的不在使用的對(duì)象進(jìn)行了回收。 最后一個(gè)階段是重定位,重定位就是對(duì) GC 后存活的對(duì)象進(jìn)行移動(dòng),來釋放大塊的內(nèi)存空間,解決碎片問題。 重定位最開始會(huì)有一個(gè)短暫的 STW,用來重定位集合中的 root 對(duì)象。暫停時(shí)間取決于 root 的數(shù)量、重定位集與對(duì)象的總活動(dòng)集的比率。 最后是并發(fā)重定位,這個(gè)過程也是通過讀屏障,與應(yīng)用線程并發(fā)進(jìn)行的。 性能調(diào)優(yōu)熟悉哪些JVM調(diào)優(yōu)參數(shù)X或者XX開頭的都是非轉(zhuǎn)標(biāo)準(zhǔn)化參數(shù): 意思就是說準(zhǔn)表化參數(shù)不會(huì)變,非標(biāo)準(zhǔn)化參數(shù)可能在每個(gè)JDK版本中有所變化,但是就目前來看X開頭的非標(biāo)準(zhǔn)化的參數(shù)改變的也是非常少。 格式:-XX:[+-]<name> 表示啟用或者禁用name屬性。 堆設(shè)置注意:在通常情況下,服務(wù)器項(xiàng)目在運(yùn)行過程中,堆空間會(huì)不斷的收縮與擴(kuò)張,勢(shì)必會(huì)造成不必要的系統(tǒng)壓力。所以在生產(chǎn)環(huán)境中,JVM的Xms和Xmx要設(shè)置成一樣的,能夠避免GC在調(diào)整堆大小帶來的不必要的壓力。 Eden:S0:S1=8:1:1 如:-XX:SurvivorRatio=3,表示Eden:Survivor=3:2,一個(gè)Survivor區(qū)占整個(gè)年輕代的1/5。 收集器設(shè)置垃圾回收統(tǒng)計(jì)信息并行收集器設(shè)置并發(fā)收集器設(shè)置其他Dump異??煺?br>堆內(nèi)存出現(xiàn)OOM的概率是所有內(nèi)存耗盡異常中最高的,出錯(cuò)時(shí)的堆內(nèi)信息對(duì)解決問題非常有幫助,所以給JVM設(shè)置這個(gè)參數(shù)(-XX:+HeapDumpOnOutOfMemoryError),讓JVM遇到OOM異常時(shí)能輸出堆內(nèi)信息,并通過(-XX:+HeapDumpPath)參數(shù)設(shè)置堆內(nèi)存溢出快照輸出的文件地址,這對(duì)于特別是對(duì)相隔數(shù)月才出現(xiàn)的OOM異常尤為重要。 -Xms10M -Xmx10M -Xmn2M -XX:SurvivorRatio=8 -XX:+HeapDumpOnOutOfMemoryError 表示發(fā)生OOM后,運(yùn)行jconsole.exe程序。這里可以不用加“”,因?yàn)閖console.exe路徑Program Files含有空格。利用這個(gè)參數(shù),我們可以在系統(tǒng)OOM后,自定義一個(gè)腳本,可以用來發(fā)送郵件告警信息,可以用來重啟系統(tǒng)等等。 -XX:OnOutOfMemoryError="C:\Program Files\Java\jdk1.8.0_151\bin\jconsole.exe" JVM 調(diào)優(yōu)常見目標(biāo)JVM 調(diào)優(yōu)目標(biāo):使用較小的內(nèi)存占用來獲得較高的吞吐量或者較低的延遲。 程序在上線前的測(cè)試或運(yùn)行中有時(shí)會(huì)出現(xiàn)一些大大小小的 JVM 問題,比如 cpu load 過高、請(qǐng)求延遲、tps 降低等,甚至出現(xiàn)內(nèi)存泄漏(每次垃圾收集使用的時(shí)間越來越長(zhǎng),垃圾收集頻率越來越高,每次垃圾收集清理掉的垃圾數(shù)據(jù)越來越少)、內(nèi)存溢出導(dǎo)致系統(tǒng)崩潰,因此需要對(duì) JVM 進(jìn)行調(diào)優(yōu),使得程序在正常運(yùn)行的前提下,獲得更高的用戶體驗(yàn)和運(yùn)行效率。 這里有幾個(gè)比較重要的指標(biāo): 當(dāng)然,和 CAP 原則一樣,同時(shí)滿足一個(gè)程序內(nèi)存占用小、延遲低、高吞吐量是不可能的,程序的目標(biāo)不同,調(diào)優(yōu)時(shí)所考慮的方向也不同,在調(diào)優(yōu)之前,必須要結(jié)合實(shí)際場(chǎng)景,有明確的的優(yōu)化目標(biāo),找到性能瓶頸,對(duì)瓶頸有針對(duì)性的優(yōu)化,最后進(jìn)行測(cè)試,通過各種監(jiān)控工具確認(rèn)調(diào)優(yōu)后的結(jié)果是否符合目標(biāo)。 有哪些調(diào)優(yōu)工具?JPS用 jps(JVM process Status)可以查看虛擬機(jī)啟動(dòng)的所有進(jìn)程、執(zhí)行主類的全名、JVM啟動(dòng)參數(shù),比如當(dāng)執(zhí)行了 JPSTest 類中的 main 方法后(main 方法持續(xù)執(zhí)行),執(zhí)行 jps -l可看到下面的JPSTest類的 pid 為 31354,加上 -v 參數(shù)還可以看到JVM啟動(dòng)參數(shù)。 jstat用 jstat(JVM Statistics Monitoring Tool)監(jiān)視虛擬機(jī)信息 jstat -gc pid 500 10:每 500 毫秒打印一次 Java 堆狀況(各個(gè)區(qū)的容量、使用容量、gc 時(shí)間等信息),打印 10 次。jstat 還可以以其他角度監(jiān)視各區(qū)內(nèi)存大小、監(jiān)視類裝載信息等,具體可以 google jstat 的詳細(xì)用法。 jmap用 jmap(Memory Map for Java)查看堆內(nèi)存信息 執(zhí)行 jmap -histo pid 可以打印出當(dāng)前堆中所有每個(gè)類的實(shí)例數(shù)量和內(nèi)存占用,如下,class name 是每個(gè)類的類名([B 是 byte 類型,[C是 char 類型,[I 是 int 類型),bytes 是這個(gè)類的所有示例占用內(nèi)存大小,instances 是這個(gè)類的實(shí)例數(shù)量。 執(zhí)行 jmap -dump 可以轉(zhuǎn)儲(chǔ)堆內(nèi)存快照到指定文件,比如執(zhí)行: jmap -dump:format=b,file=/data/jvm/dumpfile_jmap.hprof 3361 可以把當(dāng)前堆內(nèi)存的快照轉(zhuǎn)儲(chǔ)到 dumpfile_jmap.hprof 文件中,然后可以對(duì)內(nèi)存快照進(jìn)行分析。 jconsole、jvisualvm利用 jconsole、jvisualvm 分析內(nèi)存信息(各個(gè)區(qū)如 Eden、Survivor、Old 等內(nèi)存變化情況),如果查看的是遠(yuǎn)程服務(wù)器的 JVM,程序啟動(dòng)需要加上如下參數(shù): "-Dcom.sun.management.jmxremote=true" 下圖是 jconsole 界面,概覽選項(xiàng)可以觀測(cè)堆內(nèi)存使用量、線程數(shù)、類加載數(shù)和 CPU 占用率;內(nèi)存選項(xiàng)可以查看堆中各個(gè)區(qū)域的內(nèi)存使用量和左下角的詳細(xì)描述(內(nèi)存大小、GC 情況等);線程選項(xiàng)可以查看當(dāng)前 JVM 加載的線程,查看每個(gè)線程的堆棧信息,還可以檢測(cè)死鎖;VM 概要描述了虛擬機(jī)的各種詳細(xì)參數(shù)。 第三方工具MAT、GChisto、GCViewer、JProfiler、arthas、async-profile。 JVM 調(diào)優(yōu)經(jīng)驗(yàn)總結(jié)JVM 配置方面,一般情況可以先用默認(rèn)配置(基本的一些初始參數(shù)可以保證一般的應(yīng)用跑的比較穩(wěn)定了),在測(cè)試中根據(jù)系統(tǒng)運(yùn)行狀況(會(huì)話并發(fā)情況、會(huì)話時(shí)間等),結(jié)合 gc 日志、內(nèi)存監(jiān)控、使用的垃圾收集器等進(jìn)行合理的調(diào)整,當(dāng)老年代內(nèi)存過小時(shí)可能引起頻繁 Full GC,當(dāng)內(nèi)存過大時(shí) Full GC 時(shí)間會(huì)特別長(zhǎng)。 那么 JVM 的配置比如新生代、老年代應(yīng)該配置多大最合適呢?答案是不一定,調(diào)優(yōu)就是找答案的過程,物理內(nèi)存一定的情況下,新生代設(shè)置越大,老年代就越小,F(xiàn)ull GC 頻率就越高,但 Full GC 時(shí)間越短;相反新生代設(shè)置越小,老年代就越大,F(xiàn)ull GC 頻率就越低,但每次 Full GC 消耗的時(shí)間越大。 建議如下: -Xms 和 -Xmx 的值設(shè)置成相等,堆大小默認(rèn)為 -Xms 指定的大小,默認(rèn)空閑堆內(nèi)存小于 40% 時(shí),JVM 會(huì)擴(kuò)大堆到 -Xmx 指定的大??;空閑堆內(nèi)存大于 70% 時(shí),JVM 會(huì)減小堆到 -Xms 指定的大小。如果在 Full GC 后滿足不了內(nèi)存需求會(huì)動(dòng)態(tài)調(diào)整,這個(gè)階段比較耗費(fèi)資源。 代碼實(shí)現(xiàn)方面,性能出現(xiàn)問題比如程序等待、內(nèi)存泄漏除了 JVM 配置可能存在問題,代碼實(shí)現(xiàn)上也有很大關(guān)系: 避免產(chǎn)生死循環(huán),產(chǎn)生死循環(huán)后,循環(huán)體內(nèi)可能重復(fù)產(chǎn)生大量實(shí)例,導(dǎo)致內(nèi)存空間被迅速占滿。 總結(jié)本文從認(rèn)識(shí)JDK、JRE、JVM,到編譯,類加載,初始化,垃圾回收,性能調(diào)優(yōu)。可以算的是把JVM的整個(gè)流程給過了一遍。希望對(duì)你有所幫助。 我是老田,專門給大家分享技術(shù)知識(shí),歡迎加入我的學(xué)習(xí)小組。 最后,碼字不易,望大家給個(gè)贊、在看,謝啦老鐵! |
|