<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          2w 字長文爆肝 JVM 經(jīng)典面試題!太頂了!

          共 26174字,需瀏覽 53分鐘

           ·

          2021-04-12 22:00

          點擊上方老周聊架構(gòu)關(guān)注我


          1、Java 類加載過程

          Java 類加載需要經(jīng)歷以下 7 個過程:


          1.1 加載

          加載是類加載的第一個過程,在這個階段,將完成以下三件事情:

          • 通過一個類的全限定名獲取該類的二進(jìn)制流。

          • 將該二進(jìn)制流中的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法去運行時數(shù)據(jù)結(jié)構(gòu)。

          • 在內(nèi)存中生成該類的 Class 對象,作為該類的數(shù)據(jù)訪問入口。

          1.2 驗證

          驗證的目的是為了確保 Class 文件的字節(jié)流中的信息不會危害到虛擬機(jī)。

          在該階段主要完成以下四種驗證:

          • 文件格式驗證:驗證字節(jié)流是否符合 Class 文件的規(guī)范,如主次版本號是否在當(dāng)前虛擬機(jī)范圍內(nèi),常量池中的常量是否有不被支持的類型。

          • 元數(shù)據(jù)驗證:對字節(jié)碼描述的信息進(jìn)行語義分析,如這個類是否有父類,是否集成了不被繼承的類等。

          • 字節(jié)碼驗證:是整個驗證過程中最復(fù)雜的一個階段,通過驗證數(shù)據(jù)流和控制流的分析,確定程序語義是否正確,主要針對方法體的驗證。如:方法中的類型轉(zhuǎn)換是否正確,跳轉(zhuǎn)指令是否正確等。

          • 符號引用驗證:這個動作在后面的解析過程中發(fā)生,主要是為了確保解析動作能正確執(zhí)行。

          1.3 準(zhǔn)備

          準(zhǔn)備階段是為類的靜態(tài)變量分配內(nèi)存并將其初始化為默認(rèn)值,這些內(nèi)存都將在方法區(qū)中進(jìn)行分配。準(zhǔn)備階段不分配類中的實例變量的內(nèi)存,實例變量將會在對象實例化時隨著對象一起分配在 Java 堆中。

          public static String value = "公眾號【老周聊架構(gòu)】"// 在準(zhǔn)備階段 value 初始值為 null 。在初始化階段才會變?yōu)?nbsp;"公眾號【老周聊架構(gòu)】" 。

          1.4 解析

          該階段主要完成符號引用到直接引用的轉(zhuǎn)換動作。解析動作并不一定在初始化動作完成之前,也有可能在初始化之后。

          1.5 初始化

          初始化是類加載的最后一步,前面的類加載過程,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器參與之外,其余動作完全由虛擬機(jī)主導(dǎo)和控制。到了初始化階段,才真正開始執(zhí)行類中定義的 Java 程序代碼。

          1.5.1 類構(gòu)造器

          初始化階段是執(zhí)行類構(gòu)造器<client>方法的過程。<client>方法是由編譯器自動收集類中的類變量的賦值操作和靜態(tài)語句塊中的語句合并而成的。虛擬機(jī)會保證子<client>方法執(zhí)行之前,父類的<client>方法已經(jīng)執(zhí)行完畢,如果一個類中沒有對靜態(tài)變量賦值也沒有靜態(tài)語句塊,那么編譯器可以不為這個類生成<client>()方法。

          注意以下幾種情況不會執(zhí)行類初始化:

          • 通過子類引用父類的靜態(tài)字段,只會觸發(fā)父類的初始化,而不會觸發(fā)子類的初始化。

          • 定義對象數(shù)組,不會觸發(fā)該類的初始化。

          • 常量在編譯期間會存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用定義常量的類,不會觸
            發(fā)定義常量所在的類。

          • 通過類名獲取 Class 對象,不會觸發(fā)類的初始化。

          • 通過 Class.forName 加載指定類時,如果指定參數(shù) initialize 為 false 時,也不會觸發(fā)類初
            始化,其實這個參數(shù)是告訴虛擬機(jī),是否要對類進(jìn)行初始化。

          • 通過 ClassLoader 默認(rèn)的 loadClass 方法,也不會觸發(fā)初始化動作。

          1.6 使用

          1.7 卸載

          2、描述一下 JVM 加載 Class 文件的原理機(jī)制

          Java 語言是一種具有動態(tài)性的解釋型語言,類(Class)只有被 載到 JVM 后才能運行。當(dāng)運行指定程序時,JVM 會將編譯生成 的 .class 文件按照需求和一定的規(guī)則加載到內(nèi)存中,并組織成為一個完整的 Java 應(yīng)用程序。這個加載過程是由類加載器完成,具體來說,就是由 ClassLoader 和它的子類來實現(xiàn)的。類加載器本身也是一個類,其實就是把類文件從硬盤讀取到內(nèi)存中。

          類的加載方式分為隱式加載和顯示加載。隱式加載指的是程序在使 用 new 等方式創(chuàng)建對象時,會隱式地調(diào)用類的加載器把對應(yīng)的類加載到 JVM 中。顯示加載指的是通過直接調(diào)用 class.forName() 方法來把所需的類加載到 JVM 中。

          任何一個工程項目都是由許多類組成的,當(dāng)程序啟動時,只把需要的類加載到 JVM 中,其他類只有被使用到的時候才會被加載,采用這種方法一方面可以加快加載速度,另一方面可以節(jié)約程序運行時對內(nèi)存的開銷。此外,在 Java 語言中,每個類或接口都對應(yīng)一個 .class 文件,這些文件可以被看成是一個個可以被動態(tài)加載的單元,因此當(dāng)只有部分類被修改時,只需要重新編譯變化的類即可, 而不需要重新編譯所有文件,因此加快了編譯速度。

          在 Java 語言中,類的加載是動態(tài)的,它并不會一次性將所有類全部加載后再運行,而是保證程序運行的基礎(chǔ)類(例如基類)完全加載到 JVM 中,至于其他類,則在需要的時候才加載。

          類加載的主要步驟:

          • 裝載。根據(jù)查找路徑找到相應(yīng)的 class 文件,然后導(dǎo)入。

          • 鏈接。鏈接又可分為 3 個小步:
            檢查,檢查待加載的 class 文件的正確性。
            準(zhǔn)備,給類中的靜態(tài)變量分配存儲空間。
            解析,將符號引用轉(zhuǎn)換為直接引用(這一步可選)。

          • 初始化。對靜態(tài)變量和靜態(tài)代碼塊執(zhí)行初始化工作。

          3、什么是類加載器,類加載器有哪些?

          實現(xiàn)通過類的全限定名獲取該類的二進(jìn)制字節(jié)流的代碼塊叫做類加載器。

          主要有以下四種類加載器:


          • 啟動類加載器(Bootstrap ClassLoader):用來加載 Java 核心類庫,無法被 Java 程序直接引用。

          • 擴(kuò)展類加載器(extensions class loader):它用來加載 Java 的擴(kuò)展庫。Java 虛擬機(jī)的實現(xiàn)會提供一個擴(kuò)展庫目錄。該類加載器在此目錄里面查找并加載 Java 類。

          • 系統(tǒng)類加載器(system class loader):它根據(jù) Java 應(yīng)用的類路徑(CLASSPATH)來加載 Java 類。一般來說,Java 應(yīng)用的類都是由它來完成加載的。可以通過 ClassLoader.getSystemClassLoader() 來獲取它。

          • 用戶自定義類加載器,通過繼承 java.lang.ClassLoader 類的方式實現(xiàn)。

          3.1 雙親委派機(jī)制

          當(dāng)一個類收到了類加載請求,他首先不會嘗試自己去加載這個類,而是把這個請求委派給父
          類去完成,每一個層次類加載器都是如此,因此所有的加載請求都應(yīng)該傳送到啟動類加載器中,只有當(dāng)父類加載器反饋自己無法完成這個請求的時候(在它的加載路徑下沒有找到所需加載的 Class),子類加載器才會嘗試自己去加載。

          采用雙親委派的一個好處是比如加載位于 rt.jar 包中的類 java.lang.Object,不管是哪個加載
          器加載這個類,最終都是委托給頂層的啟動類加載器進(jìn)行加載,這樣就保證了使用不同的類加載。


          4、談?wù)勀銓VM的理解

          JVM 是可運行 Java 代碼的假想計算機(jī) ,包括一套字節(jié)碼指令集、一組寄存器、一個棧、 一個垃圾回收,堆和一個存儲方法域。JVM 是運行在操作系統(tǒng)之上的,它與硬件沒有直接的交互。


          5、JVM 內(nèi)存模型

          JVM 內(nèi)存區(qū)域主要分為:

          • 線程共享區(qū)域:【方法區(qū)、JAVA 堆】、直接內(nèi)存。

          • 線程私有區(qū)域:【程序計數(shù)器、虛擬機(jī)棧、本地方法區(qū)】

          線程共享區(qū)域隨虛擬機(jī)的啟動/關(guān)閉而創(chuàng)建/銷毀。

          線程私有數(shù)據(jù)區(qū)域生命周期與線程相同,依賴用戶線程的啟動/結(jié)束而創(chuàng)建/銷毀(在 Hotspot VM 內(nèi),每個線程都與操作系統(tǒng)的本地線程直接映射,因此這部分內(nèi)存區(qū)域的存活跟隨本地線程的生死對應(yīng))。

          直接內(nèi)存并不是 JVM 運行時數(shù)據(jù)區(qū)的一部分,但也會被頻繁的使用。在 JDK 1.4 引入的 NIO 提供了基于 Channel 與 Buffer 的 IO 方式,它可以使用 Native 函數(shù)庫直接分配堆外內(nèi)存,然后使用 DirectByteBuffer 對象作為這塊內(nèi)存的引用進(jìn)行操作,這樣就避免了在 Java 堆和 Native 堆中來回復(fù)制數(shù)據(jù),因此在一些場景中可以顯著提高性能。


          5.1 程序計數(shù)器(線程私有)

          一塊較小的內(nèi)存空間,是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器,每條線程都要有一個獨立的程序計數(shù)器,這類內(nèi)存也稱為“線程私有”的內(nèi)存。

          正在執(zhí)行 java 方法的話,計數(shù)器記錄的是虛擬機(jī)字節(jié)碼指令的地址(當(dāng)前指令的地址)。如果還是 Native 方法,則為空。

          這個內(nèi)存區(qū)域是唯一一個在虛擬機(jī)中沒有規(guī)定任何 OutOfMemoryError 情況的區(qū)域。

          5.2 虛擬機(jī)棧(線程私有)

          是描述 java 方法執(zhí)行的內(nèi)存模型,每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。每一個方法從調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機(jī)棧中入棧到出棧的過程。

          棧幀(Frame)是用來存儲數(shù)據(jù)和部分過程結(jié)果的數(shù)據(jù)結(jié)構(gòu),同時也被用來處理動態(tài)鏈接 (Dynamic Linking)、 方法返回值和異常分派(Dispatch Exception)。棧幀隨著方法調(diào)用而創(chuàng)建,隨著方法結(jié)束而銷毀——無論方法是正常完成還是異常完成(拋出了在方法內(nèi)未被捕獲的異常)都算作方法結(jié)束。

          5.3 本地方法區(qū)(線程私有)

          本地方法區(qū)和 Java Stack 作用類似,區(qū)別是虛擬機(jī)棧為執(zhí)行 Java 方法服務(wù),而本地方法棧則為 Native 方法服務(wù),如果一個 VM 實現(xiàn)使用 C-linkage 模型來支持 Native 調(diào)用,那么該棧將會是一個 C 棧,但 HotSpot VM 直接就把本地方法棧和虛擬機(jī)棧合二為一。

          5.4 堆(Heap)-運行時數(shù)據(jù)區(qū)(線程共享)

          是被線程共享的一塊內(nèi)存區(qū)域,創(chuàng)建的對象和數(shù)組都保存在 Java 堆內(nèi)存中,也是垃圾收集器進(jìn)行垃圾收集的最重要的內(nèi)存區(qū)域。由于現(xiàn)代 VM 采用分代收集算法。因此 Java 堆從 GC 的角度還可以細(xì)分為: 新生代(Eden 區(qū)、From Survivor 區(qū)和 To Survivor 區(qū))和老年代。

          5.5 方法區(qū)/永久代(線程共享)

          即我們常說的永久代(Permanent Generation),用于存儲被 JVM 加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。HotSpot VM 把 GC 分代收集擴(kuò)展至方法區(qū),即使用 Java 堆的永久代來實現(xiàn)方法區(qū),這樣 HotSpot 的垃圾收集器就可以像管理 Java 堆一樣管理這部分內(nèi)存,而不必為方法區(qū)開發(fā)專門的內(nèi)存管理器(永久代的內(nèi)存回收的主要目標(biāo)是針對常量池的回收和類型的卸載,因此收益一般很小)。

          運行時常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運行時常量池中。Java 虛擬機(jī)對 Class 文件的每一部分(自然也包括常量池)的格式都有嚴(yán)格的規(guī)定,每一個字節(jié)用于存儲哪種數(shù)據(jù)都必須符合規(guī)范上的要求,這樣才會被虛擬機(jī)認(rèn)可、裝載和執(zhí)行。

          這里提一下 JDK 8 永久代被元空間(Metaspace)替換。

          6、JVM 運行時內(nèi)存

          Java 堆從 GC 的角度還可以細(xì)分為: 新生代(Eden 區(qū)、Survivor From 區(qū)和 Survivor To 區(qū))和老年代。



          6.1 新生代

          是用來存放新生的對象。一般占據(jù)堆的 1/3 空間。由于頻繁創(chuàng)建對象,所以新生代會頻繁觸發(fā) MinorGC 進(jìn)行垃圾回收。新生代又分為 Eden 區(qū)、Survivor From、Survivor To 三個區(qū)。

          6.1.1 Eden 區(qū)

          Java 新對象的出生地(如果新創(chuàng)建的對象占用內(nèi)存很大,則直接分配到老年代)。當(dāng) Eden 區(qū)內(nèi)存不夠的時候就會觸發(fā) MinorGC,對新生代區(qū)進(jìn)行一次垃圾回收。

          6.1.2 Survivor From

          上一次 GC 的幸存者,作為這一次 GC 的被掃描者。

          6.1.3 Survivor To

          保留了一次 MinorGC 過程中的幸存者。

          6.1.4 MinorGC 的過程(復(fù)制->清空->互換)

          • eden、Survivor From 復(fù)制到 Survivor To,年齡+1
            首先,把 Eden 和 SurvivorFrom 區(qū)域中存活的對象復(fù)制到 Survivor To 區(qū)域(如果有對象的年齡以及達(dá)到了老年的標(biāo)準(zhǔn),則賦值到老年代區(qū)),同時把這些對象的年齡+1(如果 Survivor To 不夠位置了就放到老年區(qū));

          • 清空 eden、Survivor From
            然后,清空 Eden 和 Survivor From 中的對象;

          • Survivor To 和 Survivor From 互換
            最后,Survivor To 和 Survivor From 互換,原 Survivor To 成為下一次 GC 時的 Survivor From
            區(qū)。

          6.2 老年代

          主要存放應(yīng)用程序中生命周期長的內(nèi)存對象。

          老年代的對象比較穩(wěn)定,所以 MajorGC 不會頻繁執(zhí)行。在進(jìn)行 MajorGC 前一般都先進(jìn)行了一次 MinorGC,使得有新生代的對象晉身入老年代,導(dǎo)致空間不夠用時才觸發(fā)。當(dāng)無法找到足夠大的連續(xù)空間分配給新創(chuàng)建的較大對象時也會提前觸發(fā)一次 MajorGC 進(jìn)行垃圾回收騰出空間。

          MajorGC 采用標(biāo)記清除算法:首先掃描一次所有老年代,標(biāo)記出存活的對象,然后回收沒有標(biāo)記的對象。MajorGC 的耗時比較長,因為要掃描再回收。MajorGC 會產(chǎn)生內(nèi)存碎片,為了減少內(nèi)存損耗,我們一般需要進(jìn)行合并或者標(biāo)記出來方便下次直接分配。當(dāng)老年代也滿了裝不下的時候,就會拋出 OOM(Out of Memory)異常。

          6.3 永久代

          指內(nèi)存的永久保存區(qū)域,主要存放 Class 和 Meta(元數(shù)據(jù))的信息,Class 在被加載的時候被放入永久區(qū)域,它和存放實例的區(qū)域不同,GC 不會在主程序運行期對永久區(qū)域進(jìn)行清理。所以這也導(dǎo)致了永久代的區(qū)域會隨著加載的 Class 的增多而脹滿,最終拋出 OOM 異常。

          6.3.1 JDK 8 與元數(shù)據(jù)

          在 JDK 8 中,永久代已經(jīng)被移除,被一個稱為“元數(shù)據(jù)區(qū)”(元空間)的區(qū)域所取代。元空間的本質(zhì)和永久代類似,元空間與永久代之間最大的區(qū)別在:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。因此,默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制。類的元數(shù)據(jù)放入 native memory,字符串池和類的靜態(tài)變量放入 java 堆中,這樣可以加載多少類的元數(shù)據(jù)就不再由 MaxPermSize 控制,而由系統(tǒng)的實際可用空間來控制。

          這里老周要提兩點注意的地方:

          • 如果你們的應(yīng)用是 JDK 8 以上的話,PermSize 以及 MaxPermSize 參數(shù)是不生效的,要改成 MetaspaceSize 以及 MaxMetaspaceSize。

          • 應(yīng)用是 JDK 8 以上的話,MetaspaceSize 以及 MaxMetaspaceSize 一定要設(shè)置,因為不設(shè)置的話,32 位的 JVM MetaspaceSize 以及 MaxMetaspaceSize 默認(rèn)是 16M、64M,64 位的 JVM MetaspaceSize 以及 MaxMetaspaceSize 默認(rèn)是 21M、82M。因為老周線上遇到過沒有設(shè)置而 JVM 采用的默認(rèn)值,導(dǎo)致項目部署階段多次 FullGC 的問題。

          7、垃圾回收與算法

          7.1 如何確定垃圾

          7.1.1 引用計數(shù)法

          在 Java 中,引用和對象是有關(guān)聯(lián)的。如果要操作對象則必須用引用進(jìn)行。因此,很顯然一個簡單 的辦法是通過引用計數(shù)來判斷一個對象是否可以回收。簡單說,即一個對象如果沒有任何與之關(guān)聯(lián)的引用,即他們的引用計數(shù)都不為 0,則說明對象不太可能再被用到,那么這個對象就是可回收對象。

          7.1.2 可達(dá)性分析

          為了解決引用計數(shù)法的循環(huán)引用問題,Java 使用了可達(dá)性分析的方法。通過一系列的“GC roots”對象作為起點搜索。如果在“GC roots”和一個對象之間沒有可達(dá)路徑,則稱該對象是不可達(dá)的。要注意的是,不可達(dá)對象不等價于可回收對象,不可達(dá)對象變?yōu)榭苫厥諏ο笾辽僖?jīng)過兩次標(biāo)記過程。兩次標(biāo)記后仍然是可回收對象,則將面臨回收。

          7.2 垃圾回收算法

          7.2.1 標(biāo)記清除算法(Mark-Sweep)

          最基礎(chǔ)的垃圾回收算法,分為兩個階段,標(biāo)記和清除。標(biāo)記階段標(biāo)記出所有需要回收的對象,清除階段回收被標(biāo)記的對象所占用的空間。



          從圖中我們就可以發(fā)現(xiàn),該算法最大的問題是內(nèi)存碎片化嚴(yán)重,后續(xù)可能發(fā)生大對象不能找到可利用空間的問題。

          7.2.2 復(fù)制算法(copying)

          為了解決 Mark-Sweep 算法內(nèi)存碎片化的缺陷而被提出的算法。按內(nèi)存容量將內(nèi)存劃分為等大小的兩塊。每次只使用其中一塊,當(dāng)這一塊內(nèi)存滿后將尚存活的對象復(fù)制到另一塊上去,把已使用的內(nèi)存清掉。


          這種算法雖然實現(xiàn)簡單,內(nèi)存效率高,不易產(chǎn)生碎片,但是最大的問題是可用內(nèi)存被壓縮到了原本的一半。且存活對象增多的話,Copying 算法的效率會大大降低。

          7.2.3 標(biāo)記整理算法(Mark-Compact)

          結(jié)合了以上兩個算法,為了避免缺陷而提出。標(biāo)記階段和 Mark-Sweep 算法相同,標(biāo)記后不是清理對象,而是將存活對象移向內(nèi)存的一端。然后清除端邊界外的對象。



          7.2.4 分代收集算法

          分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根據(jù)對象存活的不同生命周期將內(nèi)存劃分為不同的域,一般情況下將 GC 堆劃分為老年代(Tenured/Old Generation)和新生代(Young Generation)。老年代的特點是每次垃圾回收時只有少量對象需要被回收,新生代的特點是每次垃圾回收時都有大量垃圾需要被回收,因此可以根據(jù)不同區(qū)域選擇不同的算法。

          7.2.4.1 新生代與復(fù)制算法

          目前大部分 JVM 的 GC 對于新生代都采取 Copying 算法,因為新生代中每次垃圾回收都要 回收大部分對象,即要復(fù)制的操作比較少,但通常并不是按照 1:1 來劃分新生代。一般將新生代劃分為一塊較大的 Eden 空間和兩個較小的 Survivor 空間(From Space, To Space),每次使用 Eden 空間和其中的一塊 Survivor 空間,當(dāng)進(jìn)行回收時,將該兩塊空間中還存活的對象復(fù)制到另一塊 Survivor 空間中。


          7.2.4.2 老年代與標(biāo)記整理算法

          而老年代因為每次只回收少量對象,因而采用 Mark-Compact 算法。

          • JAVA 虛擬機(jī)提到過的處于方法區(qū)的永久代(Permanet Generation),它用來存儲 class 類,
            常量,方法描述等。對永久代的回收主要包括廢棄常量和無用的類。

          • 對象的內(nèi)存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目
            前存放對象的那一塊),少數(shù)情況會直接分配到老年代。

          • 當(dāng)新生代的 Eden Space 和 From Space 空間不足時就會發(fā)生一次 GC,進(jìn)行 GC 后,Eden Space 和 From Space 區(qū)的存活對象會被挪到 To Space,然后將 Eden Space 和 From Space 進(jìn)行清理。

          • 如果 To Space 無法足夠存儲某個對象,則將這個對象存儲到老生代。

          • 在進(jìn)行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反復(fù)循環(huán)。

          • 當(dāng)對象在 Survivor 區(qū)躲過一次 GC 后,其年齡就會+1。默認(rèn)情況下年齡到達(dá) 15 的對象會被移到老年代中。

          8、JAVA 四中引用類型

          8.1 強(qiáng)引用

          在 Java 中最常見的就是強(qiáng)引用,把一個對象賦給一個引用變量,這個引用變量就是一個強(qiáng)引用。當(dāng)一個對象被強(qiáng)引用變量引用時,它處于可達(dá)狀態(tài),它是不可能被垃圾回收機(jī)制回收的,即使該對象以后永遠(yuǎn)都不會被用到 JVM 也不會回收。因此強(qiáng)引用是造成 Java 內(nèi)存泄漏的主要原因之一。

          8.2 軟引用

          軟引用需要用 SoftReference 類來實現(xiàn),對于只有軟引用的對象來說,當(dāng)系統(tǒng)內(nèi)存足夠時它
          不會被回收,當(dāng)系統(tǒng)內(nèi)存空間不足時它會被回收。軟引用通常用在對內(nèi)存敏感的程序中。

          8.3 弱引用

          弱引用需要用 WeakReference 類來實現(xiàn),它比軟引用的生存期更短,對于只有弱引用的對象來說,只要垃圾回收機(jī)制一運行,不管 JVM 的內(nèi)存空間是否足夠,總會回收該對象占用的內(nèi)存。

          8.4 虛引用

          虛引用需要 PhantomReference 類來實現(xiàn),它不能單獨使用,必須和引用隊列聯(lián)合使用。虛 引用的主要作用是跟蹤對象被垃圾回收的狀態(tài)。

          9、GC 垃圾收集器

          Java 堆內(nèi)存被劃分為新生代和老年代兩部分,新生代主要使用復(fù)制和標(biāo)記-清除垃圾回收,
          老年代主要使用標(biāo)記-整理垃圾回收算法,因此 Java 虛擬中針對新生代和年老代分別提供了多種不
          同的垃圾收集器,Sun HotSpot 虛擬機(jī)的垃圾收集器如下:


          9.1 Serial 垃圾收集器(單線程、復(fù)制算法)

          Serial(連續(xù))是最基本垃圾收集器,使用復(fù)制算法,曾經(jīng)是 JDK1.3.1 之前新生代唯一的垃圾收集器。Serial 是一個單線程的收集器,它不但只會使用一個 CPU 或一條線程去完成垃圾收集工作,并且在進(jìn)行垃圾收集的同時,必須暫停其他所有的工作線程,直到垃圾收集結(jié)束。

          Serial 垃圾收集器雖然在收集垃圾過程中需要暫停所有其他的工作線程,但是它簡單高效,對于限定單個 CPU 環(huán)境來說,沒有線程交互的開銷,可以獲得最高的單線程垃圾收集效率,因此 Serial 垃圾收集器依然是 Java 虛擬機(jī)運行在 Client 模式下默認(rèn)的新生代垃圾收集器。

          9.2 ParNew 垃圾收集器(Serial+多線程)

          ParNew(平行的) 垃圾收集器其實是 Serial 收集器的多線程版本,也使用復(fù)制算法,除了使用多線程進(jìn)行垃圾收集之外,其余的行為和 Serial 收集器完全一樣,ParNew 垃圾收集器在垃圾收集過程中同樣也要暫停所有其他的工作線程。

          ParNew 收集器默認(rèn)開啟和 CPU 數(shù)目相同的線程數(shù),可以通過 -XX:ParallelGCThreads 參數(shù)來限制垃圾收集器的線程數(shù)。

          ParNew 雖然是除了多線程外和 Serial 收集器幾乎完全一樣,但是 ParNew 垃圾收集器是很多 Java 虛擬機(jī)運行在 Server 模式下新生代的默認(rèn)垃圾收集器。

          9.3 Parallel Scavenge 收集器(多線程復(fù)制算法、高效)

          Parallel Scavenge 收集器也是一個新生代垃圾收集器,同樣使用復(fù)制算法,也是一個多線程的垃圾收集器,它重點關(guān)注的是程序達(dá)到一個可控制的吞吐量(Thoughput,CPU 用于運行用戶代碼的時間/CPU 總消耗時間,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)), 高吞吐量可以最高效率地利用 CPU 時間,盡快地完成程序的運算任務(wù),主要適用于在后臺運算而不需要太多交互的任務(wù)。自適應(yīng)調(diào)節(jié)策略也是 ParallelScavenge 收集器與 ParNew 收集器的一個重要區(qū)別。

          9.4 Serial Old收集器(單線程標(biāo)記整理算法)

          Serial Old 是 Serial 垃圾收集器年老代版本,它同樣是個單線程的收集器,使用標(biāo)記-整理算法, 這個收集器也主要是運行在 Client 默認(rèn)的 Java 虛擬機(jī)默認(rèn)的年老代垃圾收集器。
          在 Server 模式下,主要有兩個用途:

          • 在 JDK1.5 之前版本中與新生代的 Parallel Scavenge 收集器搭配使用。

          • 作為年老代中使用 CMS 收集器的后備垃圾收集方案。

          9.5 Parallel Old收集器(多線程標(biāo)記整理算法)

          Parallel Old 收集器是 Parallel Scavenge 的年老代版本,使用多線程的標(biāo)記-整理算法,在 JDK1.6 才開始提供。

          在 JDK1.6 之前,新生代使用 Parallel Scavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保證新生代的吞吐量優(yōu)先,無法保證整體的吞吐量,Parallel Old 正是為了在年老代同樣提供吞吐量優(yōu)先的垃圾收集器,如果系統(tǒng)對吞吐量要求比較高,可以優(yōu)先考慮新生代 Parallel Scavenge 和年老代 Parallel Old 收集器的搭配策略。

          9.6 CMS收集器(多線程標(biāo)記清除算法)

          Concurrent Mark Sweep(CMS)收集器是一種年老代垃圾收集器,其最主要目標(biāo)是獲取最短垃圾回收停頓時間,和其他年老代使用標(biāo)記-整理算法不同,它使用多線程的標(biāo)記-清除算法。最短的垃圾收集停頓時間可以為交互比較高的程序提高用戶體驗。

          CMS 工作機(jī)制相比其他的垃圾收集器來說更復(fù)雜,整個過程分為以下 4 個階段:

          9.6.1 初始標(biāo)記

          只是標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)的對象,速度很快,仍然需要暫停所有的工作線程。

          9.6.2 并發(fā)標(biāo)記

          進(jìn)行 GC Roots 跟蹤的過程,和用戶線程一起工作,不需要暫停工作線程。

          9.6.3 重新標(biāo)記

          為了修正在并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運行而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分對象的標(biāo)記 記錄,仍然需要暫停所有的工作線程。

          9.6.4 并發(fā)清除

          清除 GC Roots 不可達(dá)對象,和用戶線程一起工作,不需要暫停工作線程。由于耗時最長的并發(fā)標(biāo)記和并發(fā)清除過程中,垃圾收集線程可以和用戶現(xiàn)在一起并發(fā)工作,所以總體上來看 CMS 收集器的內(nèi)存回收和用戶線程是一起并發(fā)地執(zhí)行。

          9.7 G1收集器

          Garbage First 垃圾收集器是目前垃圾收集器理論發(fā)展的最前沿成果,相比與 CMS 收集器,G1 收集器兩個最突出的改進(jìn)是:

          • 基于標(biāo)記-整理算法,不產(chǎn)生內(nèi)存碎片。

          • 可以非常精確控制停頓時間,在不犧牲吞吐量前提下,實現(xiàn)低停頓垃圾回收。

          G1 收集器避免全區(qū)域垃圾收集,它把堆內(nèi)存劃分為大小固定的幾個獨立區(qū)域,并且跟蹤這些區(qū)域的垃圾收集進(jìn)度,同時在后臺維護(hù)一個優(yōu)先級列表,每次根據(jù)所允許的收集時間,優(yōu)先回收垃圾最多的區(qū)域。區(qū)域劃分和優(yōu)先級區(qū)域回收機(jī)制,確保 G1 收集器可以在有限時間獲得最高的垃圾收集效率。

          10、簡述 Java 垃圾回收機(jī)制

          在 Java 中,程序員是不需要顯式的去釋放一個對象的內(nèi)存的,而是由虛擬機(jī)自行執(zhí)行。在 JVM 中,有一個垃圾回收線程,它是低優(yōu)先級的,在正常情況下是不會執(zhí)行的,只有在虛擬機(jī)空閑或者當(dāng)前堆內(nèi)存不足時,才會觸發(fā)執(zhí)行,掃面那些沒有被任何引用的對象,并將它們添加到要回收的集合中,進(jìn)行回收。

          11、如何判斷一個對象是否存活?(或者 GC 對象的判定方法)

          其實第 7 點回答了哈,這里再詳細(xì)說一下。

          判斷一個對象是否存活有兩種方法:

          11.1 引用計數(shù)法

          所謂引用計數(shù)法就是給每一個對象設(shè)置一個引用計數(shù)器,每當(dāng)有一個地方引用這個對象時,就將計數(shù)器加一,引用失效時,計數(shù)器就減一。當(dāng)一個對象的引用計數(shù)器為零時,說明此對象沒有被引用,也就是“死對象”,將會被垃圾回收。

          引用計數(shù)法有一個缺陷就是無法解決循環(huán)引用問題,也就是說當(dāng)對象 A 引用對 象 B,對象 B 又引用者對象 A,那么此時 A、B 對象的引用計數(shù)器都不為零, 也就造成無法完成垃圾回收,所以主流的虛擬機(jī)都沒有采用這種算法。

          11.2 可達(dá)性算法(引用鏈法)

          該算法的思想是:從一個被稱為 GC Roots 的對象開始向下搜索,如果一個對象到 GC Roots 沒有任何引用鏈相連時,則說明此對象不可用。

          在 Java 中可以作為 GC Roots 的對象有以下幾種:

          • 虛擬機(jī)棧中引用的對象

          • 方法區(qū)類靜態(tài)屬性引用的對象

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

          • 本地方法棧 JNI 引用的對象

          雖然這些算法可以判定一個對象是否能被回收,但是當(dāng)滿足上述條件時,一個對象不一定會被回收。當(dāng)一個對象不可達(dá) GC Root 時,這個對象并不會立馬被回收,而是處于一個死緩的階段,若要被真正的回收需要經(jīng)歷兩次標(biāo)記。

          如果對象在可達(dá)性分析中沒有與 GC Root 的引用鏈,那么此時就會被第一次標(biāo)記并且進(jìn)行一次篩選,篩選的條件是是否有必要執(zhí)行 finalize() 方法。當(dāng)對象沒有覆蓋 finalize() 方法或者已被虛擬機(jī)調(diào)用過,那么就認(rèn)為是沒必要的。如果該對象有必要執(zhí)行 finalize() 方法,那么這個對象將會放在一個稱為 F-Queue 的對隊列中,虛擬機(jī)會觸發(fā)一個 Finalize() 線程去執(zhí)行,此線程是低優(yōu)先級的, 并且虛擬機(jī)不會承諾一直等待它運行完,這是因為如果 finalize() 執(zhí)行緩慢或者發(fā)生了死鎖,那么就會造成 F-Queue 隊列一直等待,造成了內(nèi)存回收系統(tǒng)的崩潰。GC 對處于 F-Queue 中的對象進(jìn)行第二次被標(biāo)記,這時,該對象將被移除 ”即將回收” 集合,等待回收。

          12、垃圾回收的優(yōu)點和原理

          Java 語言中一個顯著的特點就是引入了垃圾回收機(jī)制,使 C++ 程序員最頭疼的內(nèi)存管理的問題迎刃而解,它使得 Java 程序員在編寫程序的時候不再需要考慮內(nèi)存管理。由于有個垃圾回收機(jī)制,Java 中的對象不再有“作用域”的概念,只有對象的引用才有"作用域"。垃圾回收可以有效的防止內(nèi)存泄露,有效的使用可以使用的內(nèi)存。垃圾回收器通常是作為一個單獨的低級別的線程運行,不可預(yù)知的情況下對內(nèi)存堆中已經(jīng)死亡的或者長時間沒有使用的對象進(jìn)行清除和回收,程序員不能實時的調(diào)用垃圾回收器對某個對象或所有對象進(jìn)行垃圾回收。

          13、垃圾回收器可以馬上回收內(nèi)存嗎?有什么辦法主動通知虛擬機(jī)進(jìn)行垃圾回收?

          對于 GC 來說,當(dāng)程序員創(chuàng)建對象時,GC 就開始監(jiān)控這個對象的地址、大小以及使用情況。通常,GC 采用有向圖的方式記錄和管理堆(heap)中的所有對象。通過這種方式確定哪些對象是”可達(dá)的”,哪些對象是”不可達(dá)的”。當(dāng) GC 確定一些對象為“不可達(dá)”時,GC 就有責(zé)任回收這些內(nèi)存空間。

          可以。程序員可以手動執(zhí)行 System.gc(),通知 GC 運行,但是 Java 語言規(guī)范并不保證 GC 一定會執(zhí)行。

          14、Java 中會存在內(nèi)存泄漏嗎,請簡單描述。

          所謂內(nèi)存泄露就是指一個不再被程序使用的對象或變量一直被占據(jù)在內(nèi)存中。Java 中有垃圾回收機(jī)制,它可以保證一對象不再被引用的時候,即對象變成了孤兒的時候,對象將自動被垃圾回收器從內(nèi)存中清除掉。由于 Java 使用有向圖的方式進(jìn)行垃圾回收管理,可以消除引用循環(huán)的問題,例如有兩個對象,相互引用,只要它們和根進(jìn)程不可達(dá)的,那么 GC 也是可以回收它們的,例如下面的代碼可以看到這種情況的內(nèi)存回收:

          public class GarbageTest {

              public static void main(String[] args) throws IOException {
                  try {
                      gcTest();
                  } catch (IOException e) {
                  }

                  System.out.println("has exited gcTest!");
                  System.in.read();
                  System.in.read();
                  System.out.println("out begin gc!");

                  for (int i = 0; i < 100; i++) {
                      System.gc();
                      System.in.read();
                      System.in.read();
                  }
              }

              private static void gcTest() throws IOException {
                  System.in.read();
                  System.in.read();

                  Person p1 = new Person();
                  System.in.read();
                  System.in.read();

                  Person p2 = new Person();
                  p1.setMate(p2);
                  p2.setMate(p1);
                  System.out.println("before exit gctest!");
                  System.in.read();
                  System.in.read();
                  System.gc();
                  System.out.println("exit gctest!");
              }

              private static class Person {
                  byte[] data = new byte[20000000];
                  Person mate = null;

                  public void setMate(Person other) {
                      mate = other;
                  }
              }
          }

          Java 中的內(nèi)存泄露的情況:長生命周期的對象持有短生命周期對象的引用就很可能發(fā)生內(nèi)存泄露,盡管短生命周期對象已經(jīng)不再需要,但是因為長生命周期對象持有它的引用而導(dǎo)致不能被回收,這就是 Java 中內(nèi)存泄露的發(fā)生場景,通俗地說,就是程序員可能創(chuàng)建了一個對象,以后一直不再使用這個對象,這個對象卻一直被引用,即這個對象無用但是卻無法被垃圾回收器回收的,這就是 Java 中可能出現(xiàn)內(nèi)存泄露的情況,例如,緩存系統(tǒng),我們加載了一個對象放在緩存中 (例如放在一個全局 map 對象中),然后一直不再使用它,這個對象一直被緩存引用,但卻不再被使用。

          檢查 Java 中的內(nèi)存泄露,一定要讓程序?qū)⒏鞣N分支情況都完整執(zhí)行到程序結(jié)束,然后看某個對象是否被使用過,如果沒有,則才能判定這個對象屬于內(nèi)存泄露。

          如果一個外部類的實例對象的方法返回了一個內(nèi)部類的實例對象,這個內(nèi)部類對象被長期引用了,即使那個外部類實例對象不再被使用,但由于內(nèi)部類持久外部類的實例對象,這個外部類對象將不會被垃圾回收,這也會造成內(nèi)存泄露。

          我們來看個堆棧經(jīng)典的例子,主要特點就是清空堆棧中的某個元素,并不是徹底把它從數(shù)組中拿掉,而是把存儲的總數(shù)減少。

          public class Stack {
              private Object[] elements = new Object[10];
              private int size = 0;

              public void push(Object e) {
                  ensureCapacity();
                  elements[size++] = e;
              }

              public Object pop() {
                  if (size == 0) {
                      throw new EmptyStackException();
                  }
                  return elements[--size];
              }

              private void ensureCapacity() {
                  if (elements.length == size) {
                      Object[] oldElements = elements;
                      elements = new Object[(2 * elements.length) + 1];
                      System.arraycopy(oldElements, 0, elements, 0, size);
                  }
              }
          }

          上面的原理應(yīng)該很簡單,假如堆棧加了 10 個元素,然后全部彈出來,雖然堆棧是空的,沒有我們要的東西,但是這是個對象是無法回收的,這個才符合了內(nèi)存泄露的兩個條件:無用,無法回收。但是就是存在這樣的東西也不一定會導(dǎo)致什么樣的后果,如果這個堆棧用的比較少,也就浪費了幾個 K 內(nèi)存而已,反正我們的內(nèi)存都上 G 了,哪里會有什么影響,再說這個東西很快就會被回收的,有什么關(guān)系。下面再看個例子。

          public class Bad {
              public static Stack s = Stack();

              static {
                  s.push(new Object());
                  s.pop(); //這里有一個對象發(fā)生內(nèi)存泄露
                  s.push(new Object()); //上面的對象可以被回收了,等于是自愈了
              }
          }

          因為是 static,就一直存在到程序退出,但是我們也可以看到它有自愈功能,就是說如果你的 Stack 最多有 100 個對象,那么最多也就只有 100 個對象無法被回收,其實這個應(yīng)該很容易理解,Stack 內(nèi)部持有 100 個引用,最壞的情況就是他們都是無用的,因為我們一旦放新的進(jìn)去,以前的引用自然消失!

          內(nèi)存泄露的另外一種情況:當(dāng)一個對象被存儲進(jìn) HashSet 集合中以后,就不能修改這個對象中的那些參與計算哈希值的字段了,否則,對象修改后的哈希值與最初存儲進(jìn) HashSet 集合中時的哈希值就不同了,在這種情況下,即使在 contains 方法使用該對象的當(dāng)前引用作為的參數(shù)去 HashSet 集合中檢索對 象,也將返回找不到對象的結(jié)果,這也會導(dǎo)致無法從 HashSet 集合中單獨刪除當(dāng)前對象,造成內(nèi)存泄露。

          15、簡述 Java 內(nèi)存分配與回收策略以及 Minor GC 和 Major GC。

          • 對象優(yōu)先在堆的 Eden 區(qū)分配

          • 大對象直接進(jìn)入老年代

          • 長期存活的對象將直接進(jìn)入老年代

          當(dāng) Eden 區(qū)沒有足夠的空間進(jìn)行分配時,虛擬機(jī)會執(zhí)行一次 Minor GC。Minor GC 通常發(fā)生在新生代的 Eden 區(qū),在這個區(qū)的對象生存期短,往往發(fā)生 GC 的 頻率較高,回收速度比較快;Full GC/Major GC 發(fā)生在老年代,一般情況下, 觸發(fā)老年代 GC 的時候不會觸發(fā) Minor GC,但是通過配置,可以在 Full GC 之前進(jìn)行一次 Minor GC 這樣可以加快老年代的回收速度。

          16、JVM 內(nèi)存為什么要分成新生代,老年代,持久代。新生代中為什么要分為 Eden 和 Survivor。

          第一個問題我覺得是通過分新生代,老年代,持久代而更好的利用有限的內(nèi)存空間。

          第二個問題:

          • 如果沒有 Survivor,Eden 區(qū)每進(jìn)行一次 Minor GC,存活的對象就會被送到老年代。老年代很快被填滿,觸發(fā) Major GC。老年代的內(nèi)存空間遠(yuǎn)大于新生代,進(jìn)行一次 Full GC 消耗的時間比 Minor GC 長得多,所以需要分為 Eden 和 Survivor。

          • Survivor 的存在意義,就是減少被送到老年代的對象,進(jìn)而減少 Full GC 的發(fā)生,Survivor 的預(yù)篩選保證,只有經(jīng)歷 16 次 Minor GC 還能在新生代中存活的對象,才會被送到老年代。

          • 設(shè)置兩個 Survivor 區(qū)最大的好處就是解決了碎片化,剛剛新建的對象在 Eden 中,經(jīng)歷一次 Minor GC,Eden 中的存活對象就會被移動到第一塊 survivor space S0,Eden 被清空;等 Eden 區(qū)再滿了,就再觸發(fā)一次 Minor GC,Eden 和 S0 中的存活對象又會被復(fù)制送入第二塊 survivor space S1(這個過程非常重要,因為這種復(fù)制算法保證了 S1 中來自 S0 和 Eden 兩部分的存活對象占用連續(xù)的內(nèi)存空間,避免了碎片化的發(fā)生)。

          17、 Minor GC ,F(xiàn)ull GC 觸發(fā)條件

          Minor GC 觸發(fā)條件:當(dāng) Eden 區(qū)滿時,觸發(fā) Minor GC。

          Full GC 觸發(fā)條件:

          • 調(diào)用 System.gc 時,系統(tǒng)建議執(zhí)行 Full GC,但是不必然執(zhí)行

          • 老年代空間不足

          • 方法區(qū)空間不足

          • 通過 Minor GC 后進(jìn)入老年代的平均大小大于老年代的可用內(nèi)存

          • 由 Eden區(qū)、From Space 區(qū)向 To Space 區(qū)復(fù)制時,對象大小大于 To Space 可用內(nèi)存,則把該對象轉(zhuǎn)存到老年代,且老年代的可用內(nèi)存小于該對象大小。

          18、當(dāng)出現(xiàn)了內(nèi)存溢出,你怎么排錯?

          • 首先控制臺查看錯誤日志

          • 然后使用 jdk 自帶的 jvisualvm工具查看系統(tǒng)的堆棧日志

          • 定位出內(nèi)存溢出的空間:堆,棧還是永久代(jdk8 以后不會出現(xiàn)永久代的內(nèi)存溢出)。

          • 如果是堆內(nèi)存溢出,看是否創(chuàng)建了超大的對象

          • 如果是棧內(nèi)存溢出,看是否創(chuàng)建了超大的對象,或者產(chǎn)生了死循環(huán)。

          19、你們線上應(yīng)用的 JVM 參數(shù)有哪些?

          這里老周給我們服務(wù)的 JVM 參數(shù)給大家參考下哈,按照自己線上應(yīng)用來答就好了。

          • -server

          • -Xms4096M

          • Xmx4096M

          • -Xmn1536M

          • -XX:MetaspaceSize=256M

          • -XX:MaxMetaspaceSize=256M

          • -XX:+UseParNewGC

          • -XX:+UseConcMarkSweepGC

          • -XX:+CMSScavengeBeforeRemark

          • -XX:CMSInitiatingOccupancyFraction=75

          • -XX:CMSInitiatingOccupancyOnly

          20、什么是內(nèi)存泄漏,它與內(nèi)存溢出的關(guān)系?

          20.1 內(nèi)存泄漏 memory leak

          是指程序在申請內(nèi)存后,無法釋放已申請的內(nèi)存空間,一次內(nèi)存泄漏似乎不會有大的影響,但內(nèi)存泄漏堆積后的后果就是內(nèi)存溢出。

          20.2 內(nèi)存溢出 out of memory

          指程序申請內(nèi)存時,沒有足夠的內(nèi)存供申請者使用,或者說,給了你一塊存儲 int 類型數(shù)據(jù)的存儲空間,但是你卻存儲 long 類型的數(shù)據(jù),那么結(jié)果就是內(nèi)存不夠用,此時就會報錯 OOM,即所謂的內(nèi)存溢出。

          20.3 二者的關(guān)系

          • 內(nèi)存泄漏的堆積最終會導(dǎo)致內(nèi)存溢出

          • 內(nèi)存溢出就是你要的內(nèi)存空間超過了系統(tǒng)實際分配給你的空間,此時系統(tǒng)相當(dāng)于沒法滿足你的需求,就會報內(nèi)存溢出的錯誤。

          • 內(nèi)存泄漏是指你向系統(tǒng)申請分配內(nèi)存進(jìn)行使用(new),可是使用完了以后卻不歸還(delete),結(jié)果你申請到的那塊內(nèi)存你自己也不能再訪問(也許你把它的地址給弄丟了),而系統(tǒng)也不能再次將它分配給需要的程序。就相當(dāng)于你租了個帶鑰匙的柜子,你存完東西之后把柜子鎖上之后,把鑰匙丟了或者沒有將鑰匙還回去,那么結(jié)果就是這個柜子將無法供給任何人使用,也無法被垃圾回收器回收,因為找不到他的任何信息。

          • 內(nèi)存溢出:一個盤子用盡各種方法只能裝4個果子,你裝了5個,結(jié)果掉倒地上不能吃了。這就是溢出。比方說棧,棧滿時再做進(jìn)棧必定產(chǎn)生空間溢出,叫上溢,棧空時再做退棧也產(chǎn)生空間溢出,稱為下溢。就是分配的內(nèi)存不足以放下數(shù)據(jù)項序列,稱為內(nèi)存溢出。說白了就是我承受不了那么多,那我就報錯,

          20.4 內(nèi)存泄漏的分類(按發(fā)生方式來分類)

          • 常發(fā)性內(nèi)存泄漏。發(fā)生內(nèi)存泄漏的代碼會被多次執(zhí)行到,每次被執(zhí)行的時候都會導(dǎo)致一塊內(nèi)存泄漏。

          • 偶發(fā)性內(nèi)存泄漏。發(fā)生內(nèi)存泄漏的代碼只有在某些特定環(huán)境或操作過程下才會發(fā)生。常發(fā)性和偶發(fā)性是相對的。對于特定的環(huán)境,偶發(fā)性的也許就變成了常發(fā)性的。所以測試環(huán)境和測試方法對檢測內(nèi)存泄漏至關(guān)重要。

          • 一次性內(nèi)存泄漏。發(fā)生內(nèi)存泄漏的代碼只會被執(zhí)行一次,或者由于算法上的缺陷,導(dǎo)致總會有一塊僅且一塊內(nèi)存發(fā)生泄漏。比如,在類的構(gòu)造函數(shù)中分配內(nèi)存,在析構(gòu)函數(shù)中卻沒有釋放該內(nèi)存,所以內(nèi)存泄漏只會發(fā)生一次。

          • 隱式內(nèi)存泄漏。程序在運行過程中不停的分配內(nèi)存,但是直到結(jié)束的時候才釋放內(nèi)存。嚴(yán)格的說這里并沒有發(fā)生內(nèi)存泄漏,因為最終程序釋放了所有申請的內(nèi)存。但是對于一個服務(wù)器程序,需要運行幾天,幾周甚至幾個月,不及時釋放內(nèi)存也可能導(dǎo)致最終耗盡系統(tǒng)的所有內(nèi)存。所以,我們稱這類內(nèi)存泄漏為隱式內(nèi)存泄漏。

          20.5 內(nèi)存溢出的原因及解決方法

          20.5.1 內(nèi)存溢出原因

          • 內(nèi)存中加載的數(shù)據(jù)量過于龐大,如一次從數(shù)據(jù)庫取出過多數(shù)據(jù);

          • 集合類中有對對象的引用,使用完后未清空,使得 JVM 不能回收;

          • 代碼中存在死循環(huán)或循環(huán)產(chǎn)生過多重復(fù)的對象實體;

          • 使用的第三方軟件中的 BUG;

          • 啟動參數(shù)內(nèi)存值設(shè)定的過小。

          20.5.2 內(nèi)存溢出的解決方案

          • 修改 JVM 啟動參數(shù),直接增加內(nèi)存。(-Xms,-Xmx參數(shù)一定不要忘記加。)

          • 檢查錯誤日志,查看“OutOfMemory”錯誤前是否有其它異常或錯誤。

          • 對代碼進(jìn)行走查和分析,找出可能發(fā)生內(nèi)存溢出的位置。

          20.5.3 重點排查以下幾點

          • 檢查對數(shù)據(jù)庫查詢中,是否有一次獲得全部數(shù)據(jù)的查詢。一般來說,如果一次取十萬條記錄到內(nèi)存,就可能引起內(nèi)存溢出。這個問題比較隱蔽,在上線前,數(shù)據(jù)庫中數(shù)據(jù)較少,不容易出問題,上線后,數(shù)據(jù)庫中數(shù)據(jù)多了,一次查詢就有可能引起內(nèi)存溢出。因此對于數(shù)據(jù)庫查詢盡量采用分頁的方式查詢。

          • 檢查代碼中是否有死循環(huán)或遞歸調(diào)用。

          • 檢查是否有大循環(huán)重復(fù)產(chǎn)生新對象實體。

          • 檢查 List、Map 等集合對象是否有使用完后,未清除的問題。List、Map 等集合對象會始終存有對對象的引用,使得這些對象不能被 GC 回收。

          21、Full GC 問題的排查和解決經(jīng)歷說一下

          我們可以從以下幾個方面來進(jìn)行排查

          21.1 碎片化

          對于 CMS,由于老年代的碎片化問題,在 YGC 時可能碰到晉升失敗(promotion failures,即使老年代還有足夠多有效的空間,但是仍然可能導(dǎo)致分配失敗,因為沒有足夠連續(xù)的空間),從而觸發(fā)Concurrent Mode Failure,發(fā)生會完全 STW 的 Full GC。Full GC 相比 CMS 這種并發(fā)模式的 GC 需要更長的停頓時間才能完成垃圾回收工作,這絕對是 Java 應(yīng)用最大的災(zāi)難之一。

          為什么 CMS 場景下會有碎片化問題?由于 CMS 在老年代回收時,采用的是標(biāo)記清理(Mark-Sweep)算法,它在垃圾回收時并不會壓縮堆,日積月累,導(dǎo)致老年代的碎片化問題會越來越嚴(yán)重,直到發(fā)生單線程的 Mark-Sweep-Compact GC,即FullGC,會完全 STW。如果堆比較大的話,STW 的時間可能需要好幾秒,甚至十多秒,幾十秒都有可能。

          21.2 GC 時操作系統(tǒng)的活動

          當(dāng)發(fā)生 GC 時,一些操作系統(tǒng)的活動,比如 swap,可能導(dǎo)致 GC 停頓時間更長,這些停頓可能是幾秒,甚至幾十秒級別。

          如果你的系統(tǒng)配置了允許使用 swap 空間,操作系統(tǒng)可能把 JVM 進(jìn)程的非活動內(nèi)存頁移到 swap 空間,從而釋放內(nèi)存給當(dāng)前活動進(jìn)程(可能是操作系統(tǒng)上其他進(jìn)程,取決于系統(tǒng)調(diào)度)。Swapping 由于需要訪問磁盤,所以相比物理內(nèi)存,它的速度慢的令人發(fā)指。所以,如果在 GC 的時候,系統(tǒng)正好需要執(zhí)行 Swapping,那么 GC 停頓的時間一定會非常非常恐怖。

          除了swapping 以外,我們也需要監(jiān)控了解長 GC 暫停時的任何 IO 或者網(wǎng)絡(luò)活動情況等, 可以通過 iostat 和 netstat 兩個工具來實現(xiàn)。我們還能通過 mpstat 查看 CPU 統(tǒng)計信息,從而弄清楚在 GC 的時候是否有足夠的 CPU 資源。

          21.3 堆空間不夠

          如果應(yīng)用程序需要的內(nèi)存比我們執(zhí)行的 Xmx 還要大,也會導(dǎo)致頻繁的垃圾回收,甚至 OOM。由于堆空間不足,對象分配失敗,JVM 就需要調(diào)用 GC 嘗試回收已經(jīng)分配的空間,但是 GC 并不能釋放更多的空間,從而又回導(dǎo)致 GC,進(jìn)入惡性循環(huán)。

          同樣的,如果在老年代的空間不夠的話,也會導(dǎo)致頻繁 Full GC,這類問題比較好辦,給足老年代和永久代。

          21.4 JVM Bug

          什么軟件都有 BUG,JVM 也不例外。有時候,GC 的長時間停頓就有可能是 BUG 引起的。例如,下面列舉的這些 JVM 的 BUG,就可能導(dǎo)致 Java 應(yīng)用在 GC 時長時間停頓。

          6459113: CMS+ParNew: wildly different ParNew pause times depending on heap shape caused by allocation spread

          fixed in JDK 6u1 and 7

          6572569: CMS: consistently skewed work distribution indicated in (long) re-mark pauses

          fixed in JDK 6u4 and 7

          6631166: CMS: better heuristics when combatting fragmentation

          fixed in JDK 6u21 and 7

          6999988: CMS: Increased fragmentation leading to promotion failure after CR#6631166 got implemented

          fixed in JDK 6u25 and 7

          6683623: G1: use logarithmic BOT code such as used by other collectors

          fixed in JDK 6u14 and 7

          6976350: G1: deal with fragmentation while copying objects during GC

          fixed in JDK 8

          如果你的 JDK 正好是上面這些版本,強(qiáng)烈建議升級到更新 BUG 已經(jīng)修復(fù)的版本。

          21.5 顯式 System.gc 調(diào)用

          檢查是否有顯示的 System.gc 調(diào)用,應(yīng)用中的一些類里,或者第三方模塊中調(diào)用 System.gc 調(diào)用從而觸發(fā) STW 的 Full GC,也可能會引起非常長時間的停頓。如下 GC 日志所示,F(xiàn)ull GC 后面的(System)表示它是由調(diào)用 System.GC 觸發(fā)的 FullGC,并且耗時 5.75 秒:

          164638.058: [Full GC (System) [PSYoungGen: 22789K->0K(992448K)]

          [PSOldGen: 1645508K->1666990K(2097152K)] 1668298K->1666990K(3089600K)

          [PSPermGen: 164914K->164914K(166720K)], 5.7499132 secs] [Times: user
          =5.69, sys=0.06, real=5.75 secs]

          如果你使用了 RMI,能觀察到固定時間間隔的 Full GC,也是由于 RMI 的實現(xiàn)調(diào)用了 System.gc。這個時間間隔可以通過系統(tǒng)屬性配置:

          -Dsun.rmi.dgc.server.gcInterval=7200000

          -Dsun.rmi.dgc.client.gcInterval=7200000

          JDK 1.4.2和5.0的默認(rèn)值是60000毫秒,即1分鐘;JDK6以及以后的版本,默認(rèn)值是3600000毫秒,即1個小時。

          如果你要關(guān)閉通過調(diào)用 System.gc() 觸發(fā) Full GC,配置JVM參數(shù) -XX:+DisableExplicitGC 即可。

          21.6 那么如何定位并解決這類問題問題呢?

          • 配置 JVM 參數(shù):-XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps and -XX:+PrintGCApplicationStoppedTime. 如果是 CMS,還需要添加-XX:PrintFLSStatistics=2,然后收集 GC 日志。因為 GC 日志能告訴我們 GC 頻率,是否長時間停頓等重要信息。

          • 使用 vmstat, iostat, netstat 和 mpstat 等工具監(jiān)控系統(tǒng)全方位健康狀況。

          • 使用 GCHisto 工具可視化分析 GC 日志,弄明白消耗了很長時間的 GC,以及這些 GC 的出現(xiàn)是否有一定的規(guī)律。

          • 嘗試從 GC 日志中能否找出一下 JVM 堆碎片化的表征。

          • 監(jiān)控指定應(yīng)用的堆大小是否足夠。

          • 檢查你運行的 JVM 版本,是否有與長時間停頓相關(guān)的 BUG,然后升級到修復(fù)問題的最新 JDK。

          22、GC 中的三色標(biāo)記你了解嗎?

          Java 垃圾回收目前采用的算法是可達(dá)性標(biāo)記算法,即基于 GC Roots 進(jìn)行可達(dá)性分析。分析標(biāo)記過程采用三色標(biāo)記法。

          三色標(biāo)記按照垃圾回收器 ”是否訪問過“ 為條件將對象標(biāo)為三種顏色:

          • 白色:表示對象未被垃圾回收器訪問過;

          • 灰色:表示對象本身被垃圾回收器訪問過,但這個對象上至少有一個引用未被訪問掃描過;

          • 黑色:對象完全被掃描,并且其所有引用都已完成掃描。

          其實灰色就是一個過渡狀態(tài),在垃圾回收器標(biāo)記完成結(jié)束后,對象只有白色或者黑色其中一種狀態(tài),當(dāng)為白色時,說明該對象在可達(dá)性分析后沒有引用,也就是之后被銷毀的對象。當(dāng)為黑色時,說明當(dāng)前對象為此次垃圾回收存活對象。

          當(dāng)垃圾回收開始時,GC Roots 對象是黑色對象。沿著他找到的對象 A 首先是灰色對象,當(dāng)對象 A 所有引用都掃描后,對象 A 為黑色對象,以此類推繼續(xù)往下掃描。

          這是垃圾回收標(biāo)記基本操作。

          但目前的垃圾回收是并發(fā)操作的,就是在你進(jìn)行標(biāo)記的時候,程序線程也是繼續(xù)運行的,那原有的對象引用就有可能發(fā)生變化。

          比如已經(jīng)標(biāo)記為黑色(存活對象)對象,程序運行將其所有引用取消,那么這個對象應(yīng)該是白色的(垃圾對象)。這種情況相對好一些,在下一次垃圾回收時候,我們還是可以把他回收,只是讓他多活了一會兒,系統(tǒng)也不會出現(xiàn)什么問題,可以不解決。

          當(dāng)已經(jīng)標(biāo)記為白色對象(垃圾對象)時,此時程序運行又讓他和其他黑色(存活)對象產(chǎn)生引用,那么該對象最終也應(yīng)該是黑色(存活)對象,如果此時垃圾回收器標(biāo)記完回收后,會出現(xiàn)對象丟失,這樣就引起程序問題。

          出現(xiàn)對象丟失的必要條件是(在垃圾回收器標(biāo)記進(jìn)行時出現(xiàn)的改變):

          • 重新建立了一條或多條黑色對象到白色對象的新引用

          • 刪除了灰色對象到白色對象的直接或間接引用

          因為已經(jīng)標(biāo)記黑色的對象說明此輪垃圾回收中垃圾回收器對其的掃描已經(jīng)完成,不會再掃描,如果他又引用了一個白色對象,而且這個白色對象在垃圾掃描完后還是白色,那么這個白色對象最終會被誤回收。

          為了防止這種情況的出現(xiàn),上邊說的必要條件中的一個處理掉即可避免對象誤刪除;

          當(dāng)黑色對象直接引用了一個白色對象后,我們就將這個黑色對象記錄下來,在掃描完成后,重新對這個黑色對象掃描,這個就是增量更新(Incremental Update)。

          當(dāng)刪除了灰色對象到白色對象的直接或間接引用后,就將這個灰色對象記錄下來,再以此灰色對象為根,重新掃描一次。這個就是原始快照(Snapshot At TheBeginning,SATB)。

          自此,對象可達(dá)標(biāo)記完成。



          歡迎大家關(guān)注我的公眾號【老周聊架構(gòu)】,Java后端主流技術(shù)棧的原理、源碼分析、架構(gòu)以及各種互聯(lián)網(wǎng)高并發(fā)、高性能、高可用的解決方案。

          喜歡的話,點贊、再看、分享三連。

          點個在看你最好看



          瀏覽 141
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  91福利视频在线观看 | 超碰欧美在线 | 日本操屄视频 | 一道本二区三区 | 亚洲最大成人网址 |