一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

周末請(qǐng)看:2萬字!JVM核心知識(shí)總結(jié),贈(zèng)送18連環(huán)炮

 田維常 2021-03-28

回復(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ì)你有所幫助。

趁年輕,加油!不吃學(xué)習(xí)的苦,必吃生活的苦!

認(rèn)識(shí)JDK、JVM、JRE

什么是JVM

JVM 全稱 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)。

什么是JRE

JRE全程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文件有哪些方式

.class文件也是需要查找的,以下是查找.class文件的常用方式:

  1. 從本地文件系統(tǒng)中加載.class文件
  2. 從jar包中或者war包中加載.class文件
  3. 通過網(wǎng)絡(luò)或者從數(shù)據(jù)庫(kù)中加載.class文件
  4. 把一個(gè)Java源文件動(dòng)態(tài)編譯,并加載

加載進(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ī)制分為三步:

[Loading]裝載:其實(shí)就是我們上面查找class文件并導(dǎo)入到JVM中。

[Linking] 連接:就是對(duì)整個(gè)class內(nèi)容進(jìn)行一系列的校驗(yàn)、為一些變量進(jìn)行數(shù)據(jù)準(zhǔn)備、把字節(jié)碼中符號(hào)進(jìn)行解析等操作。

[Initializing]初始化:創(chuàng)建我們使用的對(duì)象;User user=new User();

其中連接又分三個(gè)步驟:驗(yàn)證、準(zhǔn)備、解析。

連接

驗(yàn)證

首先肯定是要保證被加載類的正確性,也就是做一些.class文件人的校驗(yàn)罷了;

  • 文件格式驗(yàn)證

  • 元數(shù)據(jù)驗(yàn)證

  • 字節(jié)碼驗(yàn)證

  • 符號(hào)引用驗(yàn)證

準(zhǔn)備

為類的靜態(tài)變量分配內(nèi)存空間,并將其初始化為默認(rèn)值。

比如說User.java中有個(gè)變量int a;

 public class InitialDemo {
        static int a = 10;
    
        public static void main(String[] args) {
            System.out.println(a);
        }
 }

在這個(gè)階段,會(huì)對(duì)這些static修飾的變量進(jìn)行賦值,附一個(gè)初始值,這里就是給

int a =0;

因?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命令能查看,它以

CONSTANT_Class_info

CONSTANT_Fieldref_info、

CONSTANT_Methodref_info等類型的常量出現(xiàn)。

符號(hào)引用與虛擬機(jī)的內(nèi)存布局無關(guān),引用的目標(biāo)并不一定加載到內(nèi)存中。在Java中,一個(gè)java類將會(huì)編譯成一個(gè)class文件。

在編譯時(shí),java類并不知道所引用的類的實(shí)際地址,因此只能使用符號(hào)引用來代替。

比如:org.simple.People類引用了org.simple.Language類,在編譯時(shí)People類并不知道Language類的實(shí)際內(nèi)存地址,因此只能使用符號(hào)org.simple.Language(假設(shè)是這個(gè),當(dāng)然實(shí)際中是由類似于CONSTANT_Class_info的常量來表示的)來表示Language類的地址。

各種虛擬機(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方法

  • findClass方法

  • defineClass方法

loadClass方法
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
        //使用了同步鎖,保證不出現(xiàn)重復(fù)加載
        synchronized (getClassLoadingLock(name)) {
            // 首先檢查自己是否已經(jīng)加載過
            Class<?> c = findLoadedClass(name);
            //沒找到
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //有父類
                    if (parent != null) {
                        //讓父類去加載
                        c = parent.loadClass(name, false);
                    } else {
                        //如果沒有父類,則委托給啟動(dòng)加載器去加載
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    // 如果都沒有找到,則通過自定義實(shí)現(xiàn)的findClass去查找并加載
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            //是否需要在加載時(shí)進(jìn)行解析
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

正如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 {
            throw new ClassNotFoundException(name);
}

在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,
                                ProtectionDomain protectionDomain) throws ClassFormatError{
            protectionDomain = preDefineClass(name, protectionDomain);
            String source = defineClassSourceLocation(protectionDomain);
            Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
            postDefineClass(c, protectionDomain);
            return c;
        }

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類
  • 覆蓋findClass(String className)方法

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類。

打破雙親委派模型的案例

tomcat

tomcat 通過 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ì)于頂層來說,還是一樣的。

OSGi

OSGi 曾經(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)、停止、卸載,就可以了。

SPI

Java 中有一個(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;

/**
 * @author 田維常
 * @version 1.0
 * @date 2020/11/7 21:18
 */

public class Integer {
    public Integer(int value) {
        System.exit(0);
    }

    public static void main(String[] args) {
        Integer i = new Integer(1);
        System.err.println(i);
    }
}

初始化這個(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è)模塊

[1. The pc Register]程序計(jì)數(shù)器/寄存器

[2. Java Virtual Machine Stacks]Java虛擬機(jī)棧

[3. Heap]

[4. Method Area] 方法區(qū)

[5. Native Method Stacks]本地方法棧

什么是方法區(qū)

方法區(qū)是用于存儲(chǔ)類結(jié)構(gòu)信息的地方,線程共享,包括常量池、靜態(tài)變量、構(gòu)造函數(shù)等類型信息,類型信息是由類加載器在類加載時(shí)從類.class文件中提取出來的。

官網(wǎng)的介紹;

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.1

從上面的介紹中,我們大致可以得出以下結(jié)論:

  1. 方法區(qū)是各個(gè)線程共享的內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建,生命周期和JVM生命周期一樣。
  2. 用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
  3. 雖然Java虛擬機(jī)規(guī)范把方法區(qū)描述為堆的一個(gè)邏輯部分,但是它卻又一個(gè)別名叫做Non-Heap(非堆),目 的是與Java堆區(qū)分開來。
  4. 當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí),將拋出OOM=OutOfMemoryError異常。

用一段代碼來加深印象:

/**
 * @author 老田
 * @version 1.0
 * @date 2020/11/5 12:55
 */

public class User {
    private static String a = "";
    private static final int b = 10;
    
}

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). 
Java虛擬機(jī)支持多線程并發(fā)
Each Java Virtual Machine thread has its own pc (program counter) register. 
每個(gè)Java虛擬機(jī)線程都擁有一個(gè)寄存器
At any point, each Java Virtual Machine thread is executing the code of a single method, namely the current method (§2.6) for that thread. 
在任何時(shí)候,每個(gè)Java虛擬機(jī)線程都在執(zhí)行單個(gè)方法的代碼,即該線程的當(dāng)前方法
If that method is not native, the pc register contains the address of the Java Virtual Machine instruction currently being executed. 
如果線程正在執(zhí)行Java方法,則計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;
If the method currently being executed by the thread is native, the value of the Java Virtual Machine's pc register is undefined. 
如果正在執(zhí)行的是Native方法,則這個(gè)計(jì)數(shù)器為空。
The Java Virtual Machine'
s pc register is wide enough to hold a returnAddress or a native pointer on the specific platform.
Java虛擬機(jī)的pc寄存器足夠?qū)?,可以容納特定平臺(tái)上的返回地址或本機(jī)指針。

實(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. 
JVM中的所有線程共享這個(gè)堆。
The heap is the run-time data area from which memory for all class instances and arrays is allocated.
所有的Java對(duì)象實(shí)例以及數(shù)組都在堆上分配。
The heap is created on virtual machine start-up
在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建

在前面類加載階段我們已經(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)、老年代 ( Old)。

新生代 ( Young ) 又被劃分為三個(gè)區(qū)域:Eden區(qū)From Survivor區(qū)、To Survivor區(qū)。

注意:很多的文章或者書籍里也稱From Survivor區(qū)為S0區(qū),To Survivor區(qū)為S1區(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指得是Young區(qū)的GC

經(jīng)過GC之后,有些對(duì)象就會(huì)被清理掉,有些對(duì)象可能還存活著,對(duì)于存活著的對(duì)象需要將其復(fù)制到Survivor

區(qū),然后再清空Eden區(qū)中的這些對(duì)象。

TLAB的全稱是 Thread Local Allocation Buffer,JVM默認(rèn)給每個(gè)線程開辟一個(gè) buffer 區(qū)域,用來加速對(duì)象分配。這個(gè) buffer 就放在 Eden 區(qū)中。

這個(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ù)‐XX:+MaxTenuringThreshold),此時(shí)對(duì)象會(huì)被移動(dòng)到Old區(qū), 如果Eden區(qū)和From區(qū) 沒有達(dá)到閾值的對(duì)象會(huì)被復(fù)制到To區(qū)。

此時(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.
一個(gè)線程的創(chuàng)建也就同事創(chuàng)建一個(gè)java虛擬機(jī)棧
A Java Virtual Machine stack stores frames (§2.6). 
Java虛擬機(jī)堆棧存儲(chǔ)幀
The memory for a Java Virtual Machine stack does not need to be contiguous.
Java虛擬機(jī)堆棧的內(nèi)存不需要是連續(xù)的。

看一段代碼

public class JavaStackDemo {

    private void checkParam(String passWd, String userName) {
        // TODO: 2020/11/6 用戶名和密碼校驗(yàn) 
    }

    private void getUserName(String passWd, String userName) {
        checkParam(passWd, userName);
    }

    private void login(String passWd, String userName) {
        getUserName(passWd, userName);
    }

    public static void main(String[] args) {
        //這里是演示代碼,希望大家能結(jié)合自己平時(shí)寫的代碼理解,那樣會(huì)更爽
        //你就不再死記硬背了
        JavaStackDemo javaStackDemo = new JavaStackDemo();
        javaStackDemo.login("老田""111111");
    }
}

啟動(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í)也是有問題的,比如說看下面這段代碼

/**
 * TODO
 *
 * @author 田維常
 * @version 1.0
 * @date 2020/11/6 9:05
 */

public class JavaStackDemo {

    public static void main(String[] args) {
        JavaStackDemo javaStackDemo = new JavaStackDemo();
        javaStackDemo.test();
    }
    //循環(huán)調(diào)用test方法
    private void test(){
        test();
    }
}

調(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á)深度;-Xss size 設(shè)置線程堆棧大?。ㄒ宰止?jié)為單位)。附加字母k或K表示KB,m或M表示MB,和g或G表示GB。默認(rèn)值取決于平臺(tái):

  • Linux / x64(64位):1024 KB
  • macOS(64位):1024 KB
  • Oracle Solaris / x64(64位):1024 KB
  • Windows:默認(rèn)值取決于虛擬內(nèi)存

下面的示例以不同的單位將線程堆棧大小設(shè)置為1024 KB:

-Xss1m (1mb)
-Xss1024k  (1024kb)
-Xss1048576

回到上面的話題。

什么是棧幀?

上面提到過,調(diào)用方法就生成一個(gè)棧幀,然后入棧。

看一段代碼

public class JavaStackDemo {

    public static void main(String[] args) {
        JavaStackDemo javaStackDemo = new JavaStackDemo();
        javaStackDemo.getUserType(21);
    }

    public String getUserType(int age) {
        int temp = 18;
        if (age < temp) {
            return "未成年人";
        }
        //動(dòng)態(tài)鏈接
        //userService.xx();
        return "成年人";
    } 
}

既然是和方法有關(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ù)

javac -g:vars XXX.class這樣就可以把本地變量表給輸出來了。

指令bipush 18  將18壓入操作數(shù)棧
istore_2 將棧頂int型數(shù)值存入第三個(gè)本地變量
iload_1 將第二個(gè)int型本地變量推送至棧頂
iload_2 將第三個(gè)int型本地變量推送至棧頂
if_icmpge 比較棧頂兩int型數(shù)值大小, 當(dāng)結(jié)果大于等于0時(shí)跳轉(zhuǎn)
ldc 將int,float或String型常量值從常量池中推送至棧頂
areturn 從當(dāng)前方法返回對(duì)象引用

官網(wǎng)

https://docs.oracle.com/javase/specs/jvms/se8/html/

這些都是字節(jié)碼指令。

LocalVariableTable 本地變量表
Start  Length  Slot  Name   Signature
   0      14     0  this   Lcom/tian/demo/test/JavaStackDemo;
   0      14     1   age   I
   3      11     2  temp   I

自己this算一個(gè)本地變量,入?yún)ge算一個(gè)本地變量,方法中的臨時(shí)變量temp也算一個(gè)本地變量。

方法出口

return。如果方法不需要返回void的時(shí)候,其實(shí)方法里是默認(rèn)會(huì)為其加上一個(gè)return;

另外方法的返回分兩種:

  • 正常代碼執(zhí)行完畢然后return。

  • 遇到異常結(jié)束

棧幀總結(jié)
  • 方法出口:return或者程序異常

  • 局部變量表:保存局部變量

  • 操作數(shù)棧:保存每次賦值、運(yùn)算等信息

  • 動(dòng)態(tài)鏈接:相對(duì)于C/C++的靜態(tài)連接而言,靜態(tài)連接是將所有類加載,不論是否使用到。而動(dòng)態(tài)鏈接是要用到某各類的時(shí)候在加載到內(nèi)存里。靜態(tài)連接速度快,動(dòng)態(tài)鏈接靈活性更高。

什么是本地方法棧?

Native Method Stacks 翻譯過來就是本地方法棧,與Java虛擬機(jī)棧一樣,但這里的棧是針對(duì)native修飾的方法的,比如System、Unsafe、Object類中的相關(guān)native方法。

public class Object {
    //native修飾的方法
    private static native void registerNatives();
    public final native Class<?> getClass();
    public native int hashCode();
    protected native Object clone() throws CloneNotSupportedException;
    public final native void notify();
    //.......
}    
public final class System {
    //native修飾的方法
    private static native void registerNatives();
    static {
        registerNatives();
    }
    public static native long currentTimeMillis();
    private static native void setIn0(InputStream in);
    private static native void setOut0(PrintStream out);
    private static native void setErr0(PrintStream err);
    //.....
}
public final class Unsafe {
    //native修飾的方法
    private static native void registerNatives();
    public native int getInt(Object var1, long var2);
    public native void putInt(Object var1, long var2, int var4);
    public native Object getObject(Object var1, long var2);
    public native void putObject(Object var1, long var2, Object var4);
    public native boolean getBoolean(Object var1, long var2);
    //...
}  

面試常問: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說明不再引用。

  • 優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,判定效率高
  • 缺點(diǎn):無法解決對(duì)象相互循環(huán)引用的問題,對(duì)象A中引用了對(duì)象B,對(duì)象B中引用對(duì)象A。

public class A 
public B b; 
}
public class B {
public C c; 

public class C 
public A a; 
}

public class Test{
  private void test(){
      A a = new A();
      B b = new B();
      C c = new C();
      
      a.b=b;
      b.c=c;
      c.a=a;
  }
}

可達(dá)性分析算法

當(dāng)一個(gè)對(duì)象到GC Roots沒有引用鏈相連,即就是GC Roots到這個(gè)對(duì)象不可達(dá)時(shí),證明對(duì)象不可用。

GC Roots種類

  • Java 線程中,當(dāng)前所有正在被調(diào)用的方法的引用類型參數(shù)、局部變量、臨時(shí)值等。也就是與我們棧幀相關(guān)的各種引用。

  • 所有當(dāng)前被加載的 Java 類。

  • Java 類的引用類型靜態(tài)變量。

  • 運(yùn)行時(shí)常量池里的引用類型常量(String 或 Class 類型)。

  • JVM 內(nèi)部數(shù)據(jù)結(jié)構(gòu)的一些引用,比如 sun.jvm.hotspot.memory.Universe 類。

  • 用于同步的監(jiān)控對(duì)象,比如調(diào)用了對(duì)象的 wait() 方法。

public class Test{
    private void test(C c){
        A a = new A();
        B b = new B();
        a.b=b;
        //這里的a/b/c都是GC Root;
    }
}

對(duì)象的引用類型有哪些?

  • 強(qiáng)引用:User user=new User();我們開發(fā)中使用最多的對(duì)象引用方式。特點(diǎn):我們平常典型編碼Object obj = new Object()中的obj就是強(qiáng)引用。通過關(guān)鍵字new創(chuàng)建的對(duì)象所關(guān)聯(lián)的引用就是強(qiáng)引用。當(dāng)JVM內(nèi)存空間不足,JVM寧愿拋出OutOfMemoryError運(yùn)行時(shí)錯(cuò)誤(OOM),使程序異常終止,也不會(huì)靠隨意回收具有強(qiáng)引用的“存活”對(duì)象來解決內(nèi)存不足的問題。對(duì)于一個(gè)普通的對(duì)象,如果沒有其他的引用關(guān)系,只要超過了引用的作用域或者顯式地將相應(yīng)(強(qiáng))引用賦值為 null,就是可以被垃圾收集的了,具體回收時(shí)機(jī)還是要看垃圾收集策略。
  • 軟引用:SoftReference object=new  SoftReference(new Object()); 特點(diǎn):軟引用通過SoftReference類實(shí)現(xiàn)。軟引用的生命周期比強(qiáng)引用短一些。只有當(dāng) JVM 認(rèn)為內(nèi)存不足時(shí),才會(huì)去試圖回收軟引用指向的對(duì)象:即JVM 會(huì)確保在拋出 OutOfMemoryError 之前,清理軟引用指向的對(duì)象。軟引用可以和一個(gè)引用隊(duì)列(ReferenceQueue)聯(lián)合使用,如果軟引用所引用的對(duì)象被垃圾回收器回收,Java虛擬機(jī)就會(huì)把這個(gè)軟引用加入到與之關(guān)聯(lián)的引用隊(duì)列中。后續(xù),我們可以調(diào)用ReferenceQueue的poll()方法來檢查是否有它所關(guān)心的對(duì)象被回收。如果隊(duì)列為空,將返回一個(gè)null,否則該方法返回隊(duì)列中前面的一個(gè)Reference對(duì)象。應(yīng)用場(chǎng)景:軟引用通常用來實(shí)現(xiàn)內(nèi)存敏感的緩存。如果還有空閑內(nèi)存,就可以暫時(shí)保留緩存,當(dāng)內(nèi)存不足時(shí)清理掉,這樣就保證了使用緩存的同時(shí),不會(huì)耗盡內(nèi)存。
  • 弱引用:WeakReference object=new  WeakReference (new Object();ThreadLocal中有使用. 弱引用通過WeakReference類實(shí)現(xiàn)。弱引用的生命周期比軟引用短。在垃圾回收器線程掃描它所管轄的內(nèi)存區(qū)域的過程中,一旦發(fā)現(xiàn)了具有弱引用的對(duì)象,不管當(dāng)前內(nèi)存空間足夠與否,都會(huì)回收它的內(nèi)存。由于垃圾回收器是一個(gè)優(yōu)先級(jí)很低的線程,因此不一定會(huì)很快回收弱引用的對(duì)象。弱引用可以和一個(gè)引用隊(duì)列(ReferenceQueue)聯(lián)合使用,如果弱引用所引用的對(duì)象被垃圾回收,Java虛擬機(jī)就會(huì)把這個(gè)弱引用加入到與之關(guān)聯(lián)的引用隊(duì)列中。應(yīng)用場(chǎng)景:弱應(yīng)用同樣可用于內(nèi)存敏感的緩存。
  • 虛引用:幾乎沒見過使用, ReferenceQueue 、PhantomReference
  • 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ì)象以外,其余的都清楚掉。

    • 缺點(diǎn):標(biāo)記和清除效率不高,標(biāo)記和清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片

    復(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%是空著的。

    • 缺點(diǎn):對(duì)象存活率高時(shí),復(fù)制效率會(huì)較低,浪費(fèi)內(nèi)存。

    標(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)目前有哪些垃圾收集器。

    新生代有哪些垃圾收集器

    serial

    Serial收集器是最基本、發(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 Scanvenge

    Parallel 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í)間,
    -XX:GC Time Ratio直接設(shè)置吞吐量的大小。

    老年代有哪些垃圾收集器

    CMS=Concurrent Mark Sweep

    特點(diǎn):最短回收停頓時(shí)間,

    回收算法:標(biāo)記-清除

    回收步驟:

    1. 初始標(biāo)記:標(biāo)記GC Roots直接關(guān)聯(lián)的對(duì)象,速度快
    2. 并發(fā)標(biāo)記:GC Roots Tracing過程,耗時(shí)長(zhǎng),與用戶進(jìn)程并發(fā)工作
    3. 重新標(biāo)記:修正并發(fā)標(biāo)記期間用戶進(jìn)程運(yùn)行而產(chǎn)生變化的標(biāo)記,好事比初始標(biāo)記長(zhǎng),但是遠(yuǎn)遠(yuǎn)小于并發(fā)標(biāo)記
    4. 表發(fā)清除:清除標(biāo)記的對(duì)象

    缺點(diǎn):對(duì)CPU資源非常敏感,CPU少于4個(gè)時(shí),CMS歲用戶程序的影響可能變得很大,有此虛擬機(jī)提供了“增量式并發(fā)收集器”;無法回收浮動(dòng)垃圾;采用標(biāo)記清除算法會(huì)產(chǎn)生內(nèi)存碎片,不過可以通過參數(shù)開啟內(nèi)存碎片的合并整理。

    收集過程:

    serial old

    Serial Old收集器是Serial收集器的老年代版本,也是一個(gè)單線程收集器,不同的是采用"標(biāo)記-整理算

    法",運(yùn)行過程和Serial收集器一樣。

    適用場(chǎng)景:JDK1.5前與Parallel Scanvenge配合使用,作為CMS的后備預(yù)案;

    收集過程:

    Parallel old

    Parallel 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)。

    • 第一個(gè):ZGC 使用了著色指針技術(shù),我們知道 64 位平臺(tái)上,一個(gè)指針的可用位是 64 位,ZGC 限制最大支持 4TB 的堆,這樣尋址只需要使用 42 位,那么剩下 22 位就可以用來保存額外的信息,著色指針技術(shù)就是利用指針的額外信息位,在指針上對(duì)對(duì)象做著色標(biāo)記。
    • 第二個(gè):使用讀屏障,ZGC 使用讀屏障來解決 GC 線程和應(yīng)用線程可能并發(fā)修改對(duì)象狀態(tài)的問題,而不是簡(jiǎn)單粗暴的通過 STW 來進(jìn)行全局的鎖定。使用讀屏障只會(huì)在單個(gè)對(duì)象的處理上有概率被減速。
    • 第三個(gè):由于讀屏障的作用,進(jìn)行垃圾回收的大部分時(shí)候都是不需要 STW 的,因此 ZGC 的大部分時(shí)間都是并發(fā)處理,也就是 ZGC 的第三個(gè)特點(diǎn)。
    • 第四個(gè):基于 Region,這與 G1 算法一樣,不過雖然也分了 Region,但是并沒有進(jìn)行分代。ZGC 的 Region 不像 G1 那樣是固定大小,而是動(dòng)態(tài)地決定 Region 的大小,Region 可以動(dòng)態(tài)創(chuàng)建和銷毀。這樣可以更好的對(duì)大對(duì)象進(jìn)行分配管理。
    • 第五個(gè):壓縮整理。CMS 算法清理對(duì)象時(shí)原地回收,會(huì)存在內(nèi)存碎片問題。ZGC 和 G1 一樣,也會(huì)在回收后對(duì) Region 中的對(duì)象進(jìn)行移動(dòng)合并,解決了碎片問題。

    雖然 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屬性。
    例子:-XX:+UseG1GC(表示啟用G1垃圾收集器)

    堆設(shè)置

    • -Xms 初始堆大小,ms是memory start的簡(jiǎn)稱 ,等價(jià)于-XX:InitialHeapSize

    • -Xmx 最大堆大小,mx是memory max的簡(jiǎn)稱 ,等價(jià)于參數(shù)-XX:MaxHeapSize

    注意:在通常情況下,服務(wù)器項(xiàng)目在運(yùn)行過程中,堆空間會(huì)不斷的收縮與擴(kuò)張,勢(shì)必會(huì)造成不必要的系統(tǒng)壓力。所以在生產(chǎn)環(huán)境中,JVM的Xms和Xmx要設(shè)置成一樣的,能夠避免GC在調(diào)整堆大小帶來的不必要的壓力。

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

    • -XX:NewRatio=n 設(shè)置年輕代和年老代的比值。如:-XX:NewRatio=3,表示年輕代與年老代比值為1:3,年輕代占整個(gè)年輕代年老代和的1/4,默認(rèn)新生代和老年代的比例=1:2。

    • -XX:SurvivorRatio=n 年輕代中Eden區(qū)與兩個(gè)Survivor區(qū)的比值。注意Survivor區(qū)有兩個(gè),默認(rèn)是8,表示

    Eden:S0:S1=8:1:1

    如:-XX:SurvivorRatio=3,表示Eden:Survivor=3:2,一個(gè)Survivor區(qū)占整個(gè)年輕代的1/5。

    • -XX:MaxPermSize=n 設(shè)置持久代大小

    • -XX:MetaspaceSize  設(shè)置元空間大小

    收集器設(shè)置

    • -XX:+UseSerialGC 設(shè)置串行收集器

    • -XX:+UseParallelGC 設(shè)置并行收集器

    • -XX:+UseParalledlOldGC 設(shè)置并行年老代收集器

    • -XX:+UseConcMarkSweepGC 設(shè)置并發(fā)收集器

    垃圾回收統(tǒng)計(jì)信息

    • -XX:+PrintGC

    • -XX:+PrintGCDetails

    • -XX:+PrintGCTimeStamps

    • -Xloggc:filenameGC日志輸出到文件里filename,比如:-Xloggc:/gc.log

    并行收集器設(shè)置

    • -XX:ParallelGCThreads=n 設(shè)置并行收集器收集時(shí)使用的CPU數(shù)。并行收集線程數(shù)。

    • -XX:MaxGCPauseMillis=n 設(shè)置并行收集最大暫停時(shí)間

    • -XX:GCTimeRatio=n 設(shè)置垃圾回收時(shí)間占程序運(yùn)行時(shí)間的百分比。公式為1/(1+n)

    • -XX:MaxGCPauseMillis=n設(shè)置并行收集最大暫停時(shí)間

    并發(fā)收集器設(shè)置

    • -XX:+CMSIncrementalMode 設(shè)置為增量模式。適用于單CPU情況。

    • -XX:ParallelGCThreads=n 設(shè)置并發(fā)收集器年輕代收集方式為并行收集時(shí),使用的CPU數(shù)。并行收集線程數(shù)。

    其他

    • -XX:+PrintCommandLineFlags查看當(dāng)前JVM設(shè)置過的相關(guān)參數(shù)

    Dump異??煺?br>

    • -XX:+HeapDumpOnOutOfMemoryError

    • -XX:HeapDumpPath

    堆內(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
    -XX:HeapDumpPath=D:\study\log_hprof\gc.hprof
    • -XX:OnOutOfMemoryError

    表示發(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):

    • 內(nèi)存占用:程序正常運(yùn)行需要的內(nèi)存大小。
    • 延遲:由于垃圾收集而引起的程序停頓時(shí)間。
    • 吞吐量:用戶程序運(yùn)行時(shí)間占用戶程序和垃圾收集占用總時(shí)間的比值。

    當(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"
    "-Djava.rmi.server.hostname=12.34.56.78"
    "-Dcom.sun.management.jmxremote.port=18181"
    "-Dcom.sun.management.jmxremote.authenticate=false"
    "-Dcom.sun.management.jmxremote.ssl=false"

    下圖是 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è)置大一些,讓對(duì)象在新生代多存活一段時(shí)間,每次 Minor GC 都要盡可能多的收集垃圾對(duì)象,防止或延遲對(duì)象進(jìn)入老年代的機(jī)會(huì),以減少應(yīng)用程序發(fā)生 Full GC 的頻率。
    • 老年代如果使用 CMS 收集器,新生代可以不用太大,因?yàn)?CMS 的并行收集速度也很快,收集過程比較耗時(shí)的并發(fā)標(biāo)記和并發(fā)清除階段都可以與用戶線程并發(fā)執(zhí)行。
    • 方法區(qū)大小的設(shè)置,1.6 之前的需要考慮系統(tǒng)運(yùn)行時(shí)動(dòng)態(tài)增加的常量、靜態(tài)變量等,1.7 只要差不多能裝下啟動(dòng)時(shí)和后期動(dòng)態(tài)加載的類信息就行。

    代碼實(shí)現(xiàn)方面,性能出現(xiàn)問題比如程序等待、內(nèi)存泄漏除了 JVM 配置可能存在問題,代碼實(shí)現(xiàn)上也有很大關(guān)系:

    • 避免創(chuàng)建過大的對(duì)象及數(shù)組:過大的對(duì)象或數(shù)組在新生代沒有足夠空間容納時(shí)會(huì)直接進(jìn)入老年代,如果是短命的大對(duì)象,會(huì)提前出發(fā) Full GC。
    • 避免同時(shí)加載大量數(shù)據(jù),如一次從數(shù)據(jù)庫(kù)中取出大量數(shù)據(jù),或者一次從 Excel 中讀取大量記錄,可以分批讀取,用完盡快清空引用。
    • 當(dāng)集合中有對(duì)象的引用,這些對(duì)象使用完之后要盡快把集合中的引用清空,這些無用對(duì)象盡快回收避免進(jìn)入老年代。
    • 可以在合適的場(chǎng)景(如實(shí)現(xiàn)緩存)采用軟引用、弱引用,比如用軟引用來為 ObjectA 分配實(shí)例:SoftReferenceobjectA=new SoftReference(); 在發(fā)生內(nèi)存溢出前,會(huì)將 objectA 列入回收范圍進(jìn)行二次回收,如果這次回收還沒有足夠內(nèi)存,才會(huì)拋出內(nèi)存溢出的異常。

    避免產(chǎn)生死循環(huán),產(chǎn)生死循環(huán)后,循環(huán)體內(nèi)可能重復(fù)產(chǎn)生大量實(shí)例,導(dǎo)致內(nèi)存空間被迅速占滿。

    • 盡量避免長(zhǎng)時(shí)間等待外部資源(數(shù)據(jù)庫(kù)、網(wǎng)絡(luò)、設(shè)備資源等)的情況,縮小對(duì)象的生命周期,避免進(jìn)入老年代,如果不能及時(shí)返回結(jié)果可以適當(dāng)采用異步處理的方式等。

    總結(jié)

    本文從認(rèn)識(shí)JDK、JRE、JVM,到編譯,類加載,初始化,垃圾回收,性能調(diào)優(yōu)。可以算的是把JVM的整個(gè)流程給過了一遍。希望對(duì)你有所幫助。

    我是老田,專門給大家分享技術(shù)知識(shí),歡迎加入我的學(xué)習(xí)小組。

最后,碼字不易,望大家給個(gè)贊、在看,謝啦老鐵!

    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多

    99国产成人免费一区二区| 国产精品美女午夜视频| 亚洲专区中文字幕视频| 东京热男人的天堂一二三区 | 国产亚洲精品一二三区| 国产主播精品福利午夜二区| 精品视频一区二区不卡| 内射精子视频欧美一区二区| 精品一区二区三区中文字幕| 日本和亚洲的香蕉视频| av在线免费播放一区二区| 日韩精品一区二区三区射精| 国产主播精品福利午夜二区| 欧美国产日本免费不卡| 成年人黄片大全在线观看| 国产不卡最新在线视频| 97人妻精品免费一区二区| 欧美日韩少妇精品专区性色| 国产精品午夜一区二区三区| 欧美日韩亚洲精品内裤| 精品久久久一区二区三| 欧美日韩少妇精品专区性色| 亚洲妇女黄色三级视频| 人妻精品一区二区三区视频免精| 色哟哟精品一区二区三区| 国产视频在线一区二区| 91亚洲国产—区=区a| 日本加勒比在线播放一区| 久久亚洲成熟女人毛片| 香港国产三级久久精品三级| 亚洲欧美黑人一区二区| 粉嫩国产一区二区三区在线| 成人欧美精品一区二区三区| 亚洲国产成人爱av在线播放下载| 偷自拍亚洲欧美一区二页| 人妻少妇av中文字幕乱码高清| 久久机热频这里只精品| 欧美精品亚洲精品日韩专区| 日韩女优精品一区二区三区| 精品人妻一区二区三区免费看| 色婷婷视频国产一区视频|