1. 程式人生 > 其它 >狂神說筆記——JVM入門07

狂神說筆記——JVM入門07

JVM入門

參考視訊:B站狂神,寫這個只是方便個人複習,怎麼寫是我自己的事,我能看懂就行,沒要求非要讓你看!白嫖還挑刺,是很沒有風度的事情。希望做個有風度的“五好青年”!


面試常見

  1. 請你談談你對JVM的理解?
  2. java8虛擬機器和之前的變化更新?
  3. 什麼是OOM,什麼是棧溢位StackOverFlowError? 怎麼分析?
  4. JVM的常用調優引數有哪些?
  5. 記憶體快照如何抓取?怎麼分析Dump檔案?
  6. 談談JVM中,類載入器你的認識?

1.JVM的位置

三種JVM:

  • Sun公司:HotSpot 用的最多
  • BEA:JRockit
  • IBM:J9VM

我們學習都是:HotSpot

2.JVM的體系結構

  • jvm調優:99%都是在方法區和堆,大部分時間調堆。 JNI(java native interface)本地方法介面。

3.類載入器

  • 作用:載入Class檔案——如果new Student();(具體例項在堆裡,引用變數名放棧裡) 。
  • 先來看看一個類載入到 JVM 的一個基本結構:
  • 類是模板,物件是具體的,通過new來例項化物件。car1,car2,car3,名字在棧裡面,真正的例項,具體的資料在堆裡面,棧只是引用地址。
  1. 虛擬機器自帶的載入器
  2. 啟動類(根)載入器
  3. 擴充套件類載入器
  4. 應用程式載入器
package github.JVM.Demo01;

/**
 * @author subeiLY
 * @create 2021-06-08 07:42
 */
public class Test01 {
    public static void main(String[] args) {
        Test01 test01 = new Test01();
        Test01 test02 = new Test01();
        Test01 test03 = new Test01();

        System.out.println(test01.hashCode());
        System.out.println(test02.hashCode());
        System.out.println(test03.hashCode());
/*
1836019240
325040804
1173230247
 */

        Class<? extends Test01> aClass1 = test01.getClass();

        ClassLoader classLoader = aClass1.getClassLoader();
        System.out.println(classLoader);
        System.out.println(classLoader.getParent());
        System.out.println(classLoader.getParent().getParent());
/*
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@330bedb4
null
 */

        Class<? extends Test01> aClass2 = test02.getClass();
        Class<? extends Test01> aClass3 = test03.getClass();

        System.out.println(aClass1.hashCode());
        System.out.println(aClass2.hashCode());
        System.out.println(aClass3.hashCode());
        /*
        2133927002
        2133927002
        2133927002
         */
    }
}

類載入器的分類

  • Bootstrap ClassLoader 啟動類載入器
  • Extention ClassLoader 標準擴充套件類載入器
  • Application ClassLoader 應用類載入器
  • User ClassLoader 使用者自定義類載入器

4.雙親委派機制

package java.lang;

/**
 * @author subeiLY
 * @create 2021-06-08 08:06
 */
public class String {
    /*
    雙親委派機制:安全
    1.APP-->EXC-->BOOT(最終執行)
    BOOT
    EXC
    APP
     */
    public String toString() {
        return "Hello";
    }

    public static void main(String[] args) {
        String s = new String();
        System.out.println(s.getClass());
        s.toString();
    }
    /*
    1.類載入器收到類載入的請求
    2.將這個請求向上委託給父類載入器去完成,一直向上委託,知道啟動類載入
    3.啟動載入器檢查是否能夠載入當前這個類,能載入就結束,使用當前的載入器,否則,丟擲異常,適知子載入器進行載入
    4.重複步驟3
     */
}
  • idea報了一個錯誤:

這是因為,在執行一個類之前,首先會在應用程式載入器(APP)中找,如果APP中有這個類,繼續向上在擴充套件類載入器EXC中找,然後再向上,在啟動類( 根 )載入器BOOT中找。如果在BOOT中有這個類的話,最終執行的就是根載入器中的。如果BOOT中沒有的話,就會倒找往回找。

過程總結

  • 1.類載入器收到類載入的請求

  • 2.將這個請求向上委託給父類載入器去完成,一直向上委託,直到啟動類載入器

  • 3.啟動類載入器檢查是否能夠載入當前這個類,能載入就結束,使用當前的載入器,否則,丟擲異常,一層一層向下,通知子載入器進行載入

  • 4.重複步驟3

  • 關於雙親委派機制的部落格:

    你確定你真的理解“雙親委派“了嗎?!


面試官:java雙親委派機制及作用

  • 概念:當某個類載入器需要載入某個.class檔案時,它首先把這個任務委託給他的上級類載入器,遞迴這個操作,如果上級的類載入器沒有載入,自己才會去載入這個類。

  • 例子:當一個Hello.class這樣的檔案要被載入時。不考慮我們自定義類載入器,首先會在AppClassLoader中檢查是否載入過,如果有那就無需再載入了。如果沒有,那麼會拿到父載入器,然後呼叫父載入器的loadClass方法。父類中同理也會先檢查自己是否已經載入過,如果沒有再往上。注意這個類似遞迴的過程,直到到達Bootstrap classLoader之前,都是在檢查是否載入過,並不會選擇自己去載入。直到BootstrapClassLoader,已經沒有父載入器了,這時候開始考慮自己是否能載入了,如果自己無法載入,會下沉到子載入器去載入,一直到最底層,如果沒有任何載入器能載入,就會丟擲ClassNotFoundException。

作用

  1. 防止重複載入同一個.class。通過委託去向上面問一問,載入過了,就不用再載入一遍。保證資料安全。
  2. 保證核心.class不能被篡改。通過委託方式,不會去篡改核心.class,即使篡改也不會去載入,即使載入也不會是同一個.class物件了。不同的載入器載入同一個.class也不是同一個Class物件。這樣保證了Class執行安全。

比如:如果有人想替換系統級別的類:String.java。篡改它的實現,在這種機制下這些系統的類已經被Bootstrap classLoader載入過了(為什麼?因為當一個類需要載入的時候,最先去嘗試載入的就是BootstrapClassLoader),所以其他類載入器並沒有機會再去載入,從一定程度上防止了危險程式碼的植入。

5.沙箱安全機制

​ Java安全模型的核心就是Java沙箱(sandbox),什麼是沙箱?沙箱是一個限制程式執行的環境。沙箱機制就是將Java程式碼限定在虛擬機器(JVM)特定的執行範圍中,並且嚴格限制程式碼對本地系統資源訪問,通過這樣的措施來保證對程式碼的有效隔離,防止對本地系統造成破壞。沙箱主要限制系統資源訪問,那系統資源包括什麼?CPU、記憶體、檔案系統、網路。不同級別的沙箱對這些資源訪問的限制也可以不一樣。

​ 所有的Java程式執行都可以指定沙箱,可以定製安全策略。

​ 在]ava中將執行程式分成原生代碼和遠端程式碼兩種,原生代碼預設視為可信任的,而遠端程式碼則被看作是不受信的。對於授信的原生代碼,可以訪問一切本地資源。而對於非授信的遠端程式碼在早期的ava實現中,安全依賴於沙箱(Sandbox)機制。如下圖所示JDK1.0安全模型。

​ 但如此嚴格的安全機制也給程式的功能擴充套件帶來障礙,比如當用戶希望遠端程式碼訪問本地系統的檔案時候,就無法實現。因此在後續的Java1.1 版本中,針對安全機制做了改進,增加了安全策略,允許使用者指定程式碼對本地資源的訪問許可權。如下圖所示JDK1.1安全模型。

​ 在Java1.2版本中,再次改進了安全機制,增加了程式碼簽名。不論原生代碼或是遠端程式碼,都會按照使用者的安全策略設定,由類載入器載入到虛擬機器中許可權不同的執行空間,來實現差異化的程式碼執行許可權控制。如下圖所示JDK1.2安全模型。

​ 當前最新的安全機制實現,則引入了域(Domain)的概念。虛擬機器會把所有程式碼載入到不同的系統域和應用域,系統域部分專門負責與關鍵資源進行互動,而各個應用域部分則通過系統域的部分代理來對各種需要的資源進行訪問。虛擬機器中不同的受保護域(Protected Domain),對應不一樣的許可權(Permission)。存在於不同域中的類檔案就具有了當前域的全部許可權,如下圖所示最新的安全模型(jdk 1.6)。

組成沙箱的基本元件:

  • 位元組碼校驗器(bytecode verifier)︰確保Java類檔案遵循lava語言規範。這樣可以幫助lava程式實現記憶體保護。但並不是所有的類檔案都會經過位元組碼校驗,比如核心類。

  • 類裝載器(class loader) :其中類裝載器在3個方面對Java沙箱起作用:

    。它防止惡意程式碼去幹涉善意的程式碼;
    。它守護了被信任的類庫邊界;
    。它將程式碼歸入保護域,確定了程式碼可以進行哪些操作。

​ 虛擬機器為不同的類載入器載入的類提供不同的名稱空間,名稱空間由一系列唯一的名稱組成,每一個被裝載的類將有一個名字,這個名稱空間是由Java虛擬機器為每一個類裝載器維護的,它們互相之間甚至不可見。

類裝載器採用的機制是雙親委派模式。

1.從最內層VM自帶類載入器開始載入,外層惡意同名類得不到載入從而無法使用;

2.由於嚴格通過包來區分了訪問域,外層惡意的類通過內建程式碼也無法獲得許可權訪問到內層類,破壞程式碼就自然無法生效。

  • 存取控制器(access controller)︰存取控制器可以控制核心API對作業系統的存取許可權,而這個控制的策略設定,可以由使用者指定。
  • 安全管理器(security manager)︰是核心API和作業系統之間的主要介面。實現許可權控制,比存取控制器優先順序高。
  • 安全軟體包(security package) : java.security下的類和擴充套件包下的類,允許使用者為自己的應用增加新的安全特性,包括:
    • 安全提供者
    • 訊息摘要
    • 數字簽名
    • 加密
    • 鑑別

6.Native

  • 編寫一個多執行緒類啟動。
 public static void main(String[] args) { 
            new Thread(()->{ },"your thread name").start(); 
 }
  • 點進去看start方法的原始碼:
public synchronized void start() {
      
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        group.add(this);

        boolean started = false;
        try {
            start0();	// 呼叫了一個start0方法
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
              
            }
        }
    }
	// 這個Thread是一個類,這個方法定義在這裡是不是很詭異!看這個關鍵字native;
    private native void start0();
  • 凡是帶了native關鍵字的,說明 java的作用範圍達不到,去呼叫底層C語言的庫!

  • JNI:Java Native Interface(Java本地方法介面)

  • 凡是帶了native關鍵字的方法就會進入本地方法棧;

  • Native Method Stack 本地方法棧

  • 本地介面的作用是融合不同的程式語言為Java所用,它的初衷是融合C/C++程式,Java在誕生的時候是C/C++橫行的時候,想要立足,必須有呼叫C、C++的程式,於是就在記憶體中專門開闢了一塊區域處理標記為native的程式碼,它的具體做法是 在 Native Method Stack 中登記native方法,在 ( ExecutionEngine ) 執行引擎執行的時候載入Native Libraies。

  • 目前該方法使用的越來越少了,除非是與硬體有關的應用,比如通過Java程式驅動印表機或者Java系統管理生產裝置,在企業級應用中已經比較少見。因為現在的異構領域間通訊很發達,比如可以使用Socket通訊,也可以使用Web Service等等,不多做介紹!

7.PC暫存器

程式計數器:Program Counter Register

  • 每個執行緒都有一個程式計數器,是執行緒私有的,就是一個指標,指向方法區中的方法位元組碼(用來儲存指向像一條指令的地址,也即將要執行的指令程式碼),在執行引擎讀取下一條指令,是一個非常小的記憶體空間,幾乎可以忽略不計。

8.方法區

Method Area 方法區

  • 方法區是被所有執行緒共享,所有欄位和方法位元組碼,以及一些特殊方法,如建構函式,介面程式碼也在此定義,簡單說,所有定義的方法的資訊都儲存在該區域,此區域屬於共享區間;

  • 靜態變數、常量、類資訊(構造方法、介面定義)、執行時的常量池存在方法區中,但是例項變數存在堆記憶體中,和方法區無關

  • static ,final ,Class ,常量池~

9.棧

  • 在計算機流傳有一句廢話: 程式 = 演算法 + 資料結構

  • 但是對於大部分同學都是: 程式 = 框架 + 業務邏輯

  • 棧:後進先出 / 先進後出

  • 佇列:先進先出(FIFO : First Input First Output)

棧管理程式執行

  • 儲存一些基本型別的值、物件的引用、方法等。

  • 棧的優勢是,存取速度比堆要快,僅次於暫存器,棧資料可以共享。

思考:為什麼main方法最後執行!為什麼一個test() 方法執行完了,才會繼續走main方法!

喝多了吐就是棧,吃多了拉就是佇列

說明:

  • 1、棧也叫棧記憶體,主管Java程式的執行,是線上程建立時建立,它的生命期是跟隨執行緒的生命期,執行緒結束棧記憶體也就釋放。

  • 2、對於棧來說不存在垃圾回收問題,只要執行緒一旦結束,該棧就Over,生命週期和執行緒一致,是執行緒私有的。

  • 3、方法自己調自己就會導致棧溢位(遞迴死迴圈測試)。

棧裡面會放什麼東西那?

  • 8大基本型別 + 物件的引用 + 例項的方法

棧執行原理

  • Java棧的組成元素——棧幀。

  • 棧幀是一種用於幫助虛擬機器執行方法呼叫與方法執行的資料結構。他是獨立於執行緒的,一個執行緒有自己的一個棧幀。封裝了方法的區域性變量表、動態連結資訊、方法的返回地址以及運算元棧等資訊。

  • 第一個方法從呼叫開始到執行完成,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。

當一個方法A被呼叫時就產生了一個棧幀F1,並被壓入到棧中,A方法又呼叫了B方法,於是產生了棧幀F2也被壓入棧中,B方法又呼叫了C方法,於是產生棧幀F3也被壓入棧中 執行完畢後,先彈出F3, 然後彈出F2,在彈出F1........

  • 遵循 “先進後出” / "後進先出" 的原則。
  • 棧滿了,丟擲異常:stackOverflowError
  • 物件例項化的過程。

10.三種JVM

  • Sun公司HotSpot java Hotspot™64-Bit server vw (build 25.181-b13,mixed mode)
  • BEA JRockit
  • IBM 39 VM
  • 我們學習都是:Hotspot

11.堆

Java7之前

  • Heap 堆,一個JVM例項只存在一個堆記憶體,堆記憶體的大小是可以調節的。

  • 類載入器讀取了類檔案後,需要把類,方法,常變數放到堆記憶體中,儲存所有引用型別的真實資訊,以方便執行器執行。

  • 堆記憶體分為三部分:

    • 新生區 Young Generation Space Young/New

    • 養老區 Tenure generation space Old/Tenure

    • 永久區 Permanent Space Perm

  • 堆記憶體邏輯上分為三部分:新生,養老,永久(元空間 : JDK8 以後名稱)。

誰空誰是to

  • GC垃圾回收主要是在新生區和養老區,又分為輕GC 和 重GC,如果記憶體不夠,或者存在死迴圈,就會導致

  • 在JDK8以後,永久儲存區改了個名字(元空間)。

12.新生區、養老區

  • 新生區是類誕生,成長,消亡的區域,一個類在這裡產生,應用,最後被垃圾回收器收集,結束生命。

  • 新生區又分為兩部分:伊甸區(Eden Space)和倖存者區(Survivor Space),所有的類都是在伊甸區被new出來的,倖存區有兩個:0區 和 1區,當伊甸園的空間用完時,程式又需要建立物件,JVM的垃圾回收器將對伊甸園區進行垃圾回收(Minor GC)。將伊甸園中的剩餘物件移動到倖存0區,若倖存0區也滿了,再對該區進行垃圾回收,然後移動到1區,那如果1區也滿了呢?(這裡倖存0區和1區是一個互相交替的過程)再移動到養老區,若養老區也滿了,那麼這個時候將產生MajorGC(Full GC),進行養老區的記憶體清理,若養老區執行了Full GC後發現依然無法進行物件的儲存,就會產生OOM異常 “OutOfMemoryError ”。如果出現 java.lang.OutOfMemoryError:java heap space異常,說明Java虛擬機器的堆記憶體不夠,原因如下:

    • 1、Java虛擬機器的堆記憶體設定不夠,可以通過引數 -Xms(初始值大小),-Xmx(最大大小)來調整。

    • 2、程式碼中建立了大量大物件,並且長時間不能被垃圾收集器收集(存在被引用)或者死迴圈。

13.永久區(Perm)

  • 永久儲存區是一個常駐記憶體區域,用於存放JDK自身所攜帶的Class,Interface的元資料,也就是說它儲存的是執行環境必須的類資訊,被裝載進此區域的資料是不會被垃圾回收器回收掉的,關閉JVM才會釋放此區域所佔用的記憶體。
  • 如果出現 java.lang.OutOfMemoryError:PermGen space,說明是 Java虛擬機器對永久代Perm記憶體設定不夠。一般出現這種情況,都是程式啟動需要載入大量的第三方jar包,
  • 例如:在一個Tomcat下部署了太多的應用。或者大量動態反射生成的類不斷被載入,最終導致Perm區被佔滿。

注意:

  • JDK1.6之前: 有永久代,常量池1.6在方法區;
  • JDK1.7: 有永久代,但是已經逐步 “去永久代”,常量池1.7在堆;
  • JDK1.8及之後:無永久代,常量池1.8在元空間。

熟悉三區結構後方可學習JVM垃圾回收機制

  • 實際而言,方法區(Method Area)和堆一樣,是各個執行緒共享的記憶體區域,它用於儲存虛擬機器載入的:類資訊+普通常量+靜態常量+編譯器編譯後的程式碼,雖然JVM規範將方法區描述為堆的一個邏輯部分,但它卻還有一個別名,叫做Non-Heap(非堆),目的就是要和堆分開

  • 對於HotSpot虛擬機器,很多開發者習慣將方法區稱之為 “永久代(Parmanent Gen)”,但嚴格本質上說兩者不同,或者說使用永久代實現方法區而已,永久代是方法區(相當於是一個介面interface)的一個實現,Jdk1.7的版本中,已經將原本放在永久代的字串常量池移走。

  • 常量池(Constant Pool)是方法區的一部分,Class檔案除了有類的版本,欄位,方法,介面描述資訊外,還有一項資訊就是常量池,這部分內容將在類載入後進入方法區的執行時常量池中存放!

14.堆記憶體調優

  • -Xms:設定初始分配大小,預設為實體記憶體的 “1/64”。
  • -Xmx:最大分配記憶體,預設為實體記憶體的 “1/4”。
  • -XX:+PrintGCDetails:輸出詳細的GC處理日誌。

測試1

程式碼測試

public class Demo01 {
    public static void main(String[] args) {
        // 返回虛擬機器試圖使用的最大記憶體
        long max = Runtime.getRuntime().maxMemory();    // 位元組:1024*1024
        // 返回jvm的總記憶體
        long total = Runtime.getRuntime().totalMemory();

        System.out.println("max=" + max + "位元組\t" + (max/(double)1024/1024) + "MB");

        System.out.println("total=" + total + "位元組\t" + (total/(double)1024/1024) + "MB");

        // 預設情況下:分配的總記憶體是電腦記憶體的1/4,初始化的記憶體是電腦的1/64

    }
}
  • IDEA中進行VM調優引數設定,然後啟動。
  • 發現,預設的情況下分配的記憶體是總記憶體的 1/4,而初始化的記憶體為 1/64 !
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
  • VM引數調優:把初始記憶體,和總記憶體都調為 1024M,執行,檢視結果!
  • 來大概計算分析一下!
  • 再次證明:元空間並不在虛擬機器中,而是使用本地記憶體。

測試2

程式碼:

package github.JVM.Demo02;

import java.util.Random;

/**
 * @author subeiLY
 * @create 2021-06-08 10:22
 */
public class Demo02 {
    public static void main(String[] args) {
        String str = "suneiLY";
        while (true) {
            str += str + new Random().nextInt(88888888)
                    + new Random().nextInt(999999999);
        }
    }
}
  • vm引數:
-Xms8m -Xmx8m -XX:+PrintGCDetails
  • 測試,檢視結果!
  • 這是一個young 區域撐爆的JAVA 記憶體日誌,其中 PSYoungGen 表示 youngGen分割槽的變化1536k 表示 GC 之前的大小。

  • 488k 表示GC 之後的大小。

  • 整個Young區域的大小從 1536K 到 672K , young代的總大小為 7680K。

  • user – 總計本次 GC 匯流排程所佔用的總 CPU 時間。

  • sys – OS 呼叫 or 等待系統時間。

  • real – 應用暫停時間。

  • 如果GC 執行緒是 Serial Garbage Collector 序列蒐集器的方式的話(只有一條GC執行緒,), real time 等於user 和 system 時間之和。

  • 通過日誌發現Young的區域到最後 GC 之前後都是0,old 區域 無法釋放,最後報堆溢位錯誤。

其他文章連結

15.GC

1.Dump記憶體快照

​ 在執行java程式的時候,有時候想測試執行時佔用記憶體情況,這時候就需要使用測試工具查看了。在eclipse裡面有 Eclipse Memory Analyzer tool(MAT)外掛可以測試,而在idea中也有這麼一個外掛,就是JProfiler,一款效能瓶頸分析工具!

作用

  • 分析Dump檔案,快速定位記憶體洩漏;

  • 獲得堆中物件的統計資料

  • 獲得物件相互引用的關係

  • 採用樹形展現物件間相互引用的情況

安裝JProfiler

  1. IDEA外掛安裝
  1. 安裝JProfiler監控軟體
  1. 下載完雙擊執行,選擇自定義目錄安裝,點選Next。
  • 注意:安裝路徑,建議選擇一個檔名中沒有中文,沒有空格的路徑 ,否則識別不了。然後一直點Next。
  1. 註冊
// 註冊碼僅供大家參考
[email protected]#23874-hrwpdp1sh1wrn#0620
[email protected]#36573-fdkscp15axjj6#25257
[email protected]#5481-ucjn4a16rvd98#6038
[email protected]#99016-hli5ay1ylizjj#27215
[email protected]#40775-3wle0g1uin5c1#0674
  1. 配置IDEA執行環境
  • Settings–Tools–JProflier–JProflier executable選擇JProfile安裝可執行檔案。(如果系統只裝了一個版本, 啟動IDEA時會預設選擇)儲存。
  • 程式碼測試:
package github.JVM.Demo02;

import java.util.ArrayList;

/**
 * @author subeiLY
 * @create 2021-06-08 11:13
 */
public class Demo03 {
    byte[] byteArray = new byte[1*1024*1024]; // 1M = 1024K


    public static void main(String[] args) {
        ArrayList<Demo03> list = new ArrayList<>();
        int count = 0;
        try {
            while (true) {
                list.add(new Demo03());  // 問題所在
                count = count + 1;
            }
        } catch (Error e) {
            System.out.println("count:" + count);
            e.printStackTrace();
        }
    }
}
  • vm引數 : -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
  • 尋找檔案:

使用 Jprofiler 工具分析檢視

雙擊這個檔案預設使用 Jprofiler 進行 Open大的物件!

  • 從軟體開發的角度上,dump檔案就是當程式產生異常時,用來記錄當時的程式狀態資訊(例如堆疊的狀態),用於程式開發定位問題。

2.GC四大演算法

1.引用計數法

  • 每個物件有一個引用計數器,當物件被引用一次則計數器加1,當物件引用失效一次,則計數器減1,對於計數器為0的物件意味著是垃圾物件,可以被GC回收。

  • 目前虛擬機器基本都是採用可達性演算法,從GC Roots 作為起點開始搜尋,那麼整個連通圖中的物件邊都是活物件,對於GC Roots 無法到達的物件變成了垃圾回收物件,隨時可被GC回收。

2.複製演算法

  • 年輕代中使用的是Minor GC,採用的就是複製演算法(Copying)。

什麼是複製演算法?

  • Minor GC 會把Eden中的所有活的物件都移到Survivor區域中,如果Survivor區中放不下,那麼剩下的活的物件就被移動到Old generation中,也就是說,一旦收集後,Eden就是變成空的了

  • 當物件在Eden(包括一個Survivor區域,這裡假設是From區域)出生後,在經過一次Minor GC後,如果物件還存活,並且能夠被另外一塊Survivor區域所容納 (上面已經假設為from區域,這裡應為to區域,即to區域有足夠的記憶體空間來儲存Eden 和 From 區域中存活的物件),則使用複製演算法將這些仍然還活著的物件複製到另外一塊Survivor區域(即 to 區域)中,然後清理所使用過的Eden 以及Survivor 區域(即form區域),並且將這些物件的年齡設定為1,以後物件在Survivor區,每熬過一次MinorGC,就將這個物件的年齡 + 1,當這個物件的年齡達到某一個值的時候(預設是15歲,通過- XX:MaxTenuringThreshold 設定引數)這些物件就會成為老年代。

  • -XX:MaxTenuringThreshold 任期門檻=>設定物件在新生代中存活的次數

面試題:如何判斷哪個是to區呢?一句話:誰空誰是to

原理解釋:

  • 年輕代中的GC,主要是複製演算法(Copying)

  • HotSpot JVM 把年輕代分為了三部分:一個 Eden 區 和 2 個Survivor區(from區 和 to區)。預設比例為 8:1:1,一般情況下,新建立的物件都會被分配到Eden區(一些大物件特殊處理),這些物件經過第一次Minor GC後,如果仍然存活,將會被移到Survivor區,物件在Survivor中每熬過一次Minor GC , 年齡就會增加1歲,當它的年齡增加到一定程度時,就會被移動到年老代中,因為年輕代中的物件基本上 都是朝生夕死,所以在年輕代的垃圾回收演算法使用的是複製演算法!複製演算法的思想就是將記憶體分為兩塊,每次只用其中一塊,當這一塊記憶體用完,就將還活著的物件複製到另外一塊上面。複製演算法不會產 生記憶體碎片!

  • 在GC開始的時候,物件只會在Eden區和名為 “From” 的Survivor區,Survivor區“TO” 是空的,緊接著進行GC,Eden區中所有存活的物件都會被複制到 “To”,而在 “From” 區中,仍存活的物件會更具他們的年齡值來決定去向。
  • 年齡達到一定值的物件會被移動到老年代中,沒有達到閾值的物件會被複制到 “To 區域”,經過這次GC後,Eden區和From區已經被清空,這個時候, “From” 和 “To” 會交換他們的角色, 也就是新的 “To” 就是GC前的“From” , 新的 “From” 就是上次GC前的 “To”。
  • 不管怎樣,都會保證名為To 的Survicor區域是空的。 Minor GC會一直重複這樣的過程。直到 To 區 被填滿 ,“To” 區被填滿之後,會將所有的物件移動到老年代中。
  • 因為Eden區物件一般存活率較低,一般的,使用兩塊10%的記憶體作為空閒和活動區域,而另外80%的記憶體,則是用來給新建物件分配記憶體的。一旦發生GC,將10%的from活動區間與另外80%中存活的Eden 物件轉移到10%的to空閒區域,接下來,將之前的90%的記憶體,全部釋放,以此類推;

  • 好處:沒有記憶體碎片;壞處:浪費記憶體空間。

劣勢:

  • 複製演算法它的缺點也是相當明顯的。
    • 1、他浪費了一半的記憶體,這太要命了。
    • 2、如果物件的存活率很高,我們可以極端一點,假設是100%存活,那麼我們需要將所有物件都複製一遍,並將所有引用地址重置一遍。複製這一工作所花費的時間,在物件存活率達到一定程度時,將會變的不可忽視,所以從以上描述不難看出。複製演算法要想使用,最起碼物件的存活率要非常低才行,而且 最重要的是,我們必須要克服50%的記憶體浪費。

標記清除(Mark-Sweep)

  • 回收時,對需要存活的物件進行標記;

  • 回收不是綠色的物件。

  • 當堆中的有效記憶體空間被耗盡的時候,就會停止整個程式(也被稱為stop the world),然後進行兩項工作,第一項則是標記,第二項則是清除。

  • 標記:從引用根節點開始標記所有被引用的物件,標記的過程其實就是遍歷所有的GC Roots ,然後將所有GC Roots 可達的物件,標記為存活的物件。

  • 清除: 遍歷整個堆,把未標記的物件清除。

  • 缺點:這個演算法需要暫停整個應用,會產生記憶體碎片。兩次掃描,嚴重浪費時間。

用通俗的話解釋一下 標記/清除演算法,就是當程式執行期間,若可以使用的記憶體被耗盡的時候,GC執行緒就會被觸發並將程式暫停,隨後將依舊存活的物件標記一遍,最終再將堆中所有沒被標記的物件全部清 除掉,接下來便讓程式恢復執行。

劣勢:

  1. 首先、它的缺點就是效率比較低(遞迴與全堆物件遍歷),而且在進行GC的時候,需要停止應用 程式,這會導致使用者體驗非常差勁

  2. 其次、主要的缺點則是這種方式清理出來的空閒記憶體是不連續的,這點不難理解,我們的死亡物件 都是隨機的出現在記憶體的各個角落,現在把他們清除之後,記憶體的佈局自然亂七八糟,而為了應付 這一點,JVM就不得不維持一個記憶體空間的空閒列表,這又是一種開銷。而且在分配陣列物件的時 候,尋找連續的記憶體空間會不太好找。

3.標記壓縮

  • 標記整理說明:老年代一般是由標記清除或者是標記清除與標記整理的混合實現。

什麼是標記壓縮?

原理:

  • 在整理壓縮階段,不再對標記的物件作回收,而是通過所有存活物件都像一端移動,然後直接清除邊界以外的記憶體。可以看到,標記的存活物件將會被整理,按照記憶體地址依次排列,而未被標記的記憶體會被 清理掉,如此一來,當我們需要給新物件分配記憶體時,JVM只需要持有一個記憶體的起始地址即可,這比維護一個空閒列表顯然少了許多開銷。

  • 標記、整理演算法 不僅可以彌補 標記、清除演算法當中,記憶體區域分散的缺點,也消除了複製演算法當中,記憶體減半的高額代價;

4.標記清除壓縮

  • 先標記清除幾次,再壓縮。

3.總結

  • 記憶體效率:複製演算法 > 標記清除演算法 > 標記壓縮演算法 (時間複雜度);

  • 記憶體整齊度:複製演算法 = 標記壓縮演算法 > 標記清除演算法;

  • 記憶體利用率:標記壓縮演算法 = 標記清除演算法 > 複製演算法;

​ 可以看出,效率上來說,複製演算法是當之無愧的老大,但是卻浪費了太多記憶體,而為了儘量兼顧上面所 提到的三個指標,標記壓縮演算法相對來說更平滑一些 , 但是效率上依然不盡如人意,它比複製演算法多了一個標記的階段,又比標記清除多了一個整理記憶體的過程。

難道就沒有一種最優演算法嗎?

答案: 無,沒有最好的演算法,只有最合適的演算法 。 -----------> 分代收集演算法

年輕代:(Young Gen)

  • 年輕代特點是區域相對老年代較小,物件存活低。
  • 這種情況複製演算法的回收整理,速度是最快的。複製演算法的效率只和當前存活物件大小有關,因而很適 用於年輕代的回收。而複製演算法記憶體利用率不高的問題,通過hotspot中的兩個survivor的設計得到緩解。

老年代:(Tenure Gen)

  • 老年代的特點是區域較大,物件存活率高!
  • 這種情況,存在大量存活率高的物件,複製演算法明顯變得不合適。一般是由標記清除或者是標記清除與標記整理的混合實現。Mark階段的開銷與存活物件的數量成正比,這點來說,對於老年代,標記清除或 者標記整理有一些不符,但可以通過多核多執行緒利用,對併發,並行的形式提標記效率。Sweep階段的 開銷與所管理裡區域的大小相關,但Sweep “就地處決” 的 特點,回收的過程沒有物件的移動。使其相對其他有物件移動步驟的回收演算法,仍然是是效率最好的,但是需要解決記憶體碎片的問題。

16.JMM

  1. 什麼是JMM?

    • JMM:(java Memory Model的縮寫)
  2. 他幹嘛的?官方,其他人的部落格,對應的視訊!

    • 作用:快取一致性協議,用於定義資料讀寫的規則(遵守,找到這個規則)。

    • JMM定義了執行緒工作記憶體和主記憶體之間的抽象關係∶執行緒之間的共享變數儲存在主記憶體(Main Memory)中,每個執行緒都有一個私有的本地記憶體(Local Memory)。

  • 解決共享物件可見性這個問題:volilate
  1. 它該如何學習?

    • JMM:抽象的概念,理論。
  • JMM對這八種指令的使用,制定瞭如下規則:
    • 不允許read和load、store和write操作之一單獨出現。即使用了read必須load,使用了store必須write。
    • 不允許執行緒丟棄他最近的assign操作,即工作變數的資料改變了之後,必須告知主存。
    • 不允許一個執行緒將沒有assign的資料從工作記憶體同步回主記憶體。
    • 一個新的變數必須在主記憶體中誕生,不允許工作記憶體直接使用一個未被初始化的變數。就是懟變數實施use、store操作之前,必須經過assign和load操作。
    • 一個變數同一時間只有一個執行緒能對其進行lock。多次lock後,必須執行相同次數的unlock才能解鎖。
    • 如果對一個變數進行lock操作,會清空所有工作記憶體中此變數的值,在執行引擎使用這個變數前,必須重新load或assign操作初始化變數的值。
    • 如果一個變數沒有被lock,就不能對其進行unlock操作。也不能unlock一個被其他執行緒鎖住的變數。
    • 對一個變數進行unlock操作之前,必須把此變數同步回主記憶體。

  JMM對這八種操作規則和對volatile的一些特殊規則就能確定哪裡操作是執行緒安全,哪些操作是執行緒不安全的了。但是這些規則實在複雜,很難在實踐中直接分析。所以一般我們也不會通過上述規則進行分析。更多的時候,使用java的happen-before規則來進行分析。