【學習筆記】學習JVM,肝完這篇1w+字的文章收穫滿滿

2020-10-13 15:00:17

大家好,我是oldou,這次文章介紹的是關於JVM的相關知識,學習來源還是觀看B站狂神的視訊學習、同時在網上查詢了很多資料進行整理【文末有參考地址】,畢竟網上的學習資料很多很多,當然要好好的利用起來進行學習,文章內容可能整理得不是特別好的那種,但是看完絕對是有收穫的,如果文中有不對的地方還請各位指正,在此感激不盡,如果本文對你有所幫助,希望點贊支援一下哈,謝謝各位!【關於JVM的整體圖我整理好之後會發出一個連結】

前言

  • 請你談談對JVM的理解?Java8虛擬機器器和之前的變化更新有什麼不一樣?
  • 什麼是OOM?什麼是棧溢位StackOverFlowError?怎麼分析?
  • JVM的常用調優引數有哪些?
  • 記憶體快站如何抓取?怎麼分析Dump檔案?
  • 談談你對JVM中的類載入器的認識

一問到這些問題,說實話沒學過JVM的同學一般都會大皺眉頭,然後默默地…
在這裡插入圖片描述
不過也別灰心,遇見不會的就說明我們還有進步的空間,畢竟現在不會不代表我們以後不會,不會的我們可以去學,所以我們要努力不斷的學習新的技術、新的知識,因為人生本來就需要不斷的學習,下面我們開始進入正文吧。

JVM的初識(瞭解即可)

定義

JVM就是java虛擬機器器,它是一個虛構出來的計算機,可在實際的計算機上模擬各種計算機的功能。JVM有自己完善的硬體結構,例如處理器、堆疊和暫存器等,還具有相應的指令系統。

作用

  • JVM是java位元組碼執行的引擎,還能優化java位元組碼,使之轉化成效率更高的機器指令。
  • JVM中類的裝載是由類載入器和它的子類來實現的,類載入是java執行時一個重要的系統元件,負責在執行時查詢和裝入類檔案的類。
  • 不同的平臺對應著不同的JVM,在執行位元組碼(class檔案)時,JVM負責將每一條要執行的位元組碼送給直譯器,直譯器再將其翻譯成特定平臺換將的機器指令並執行,這樣就實現了跨平臺執行。

工作原理

JVM在整個JDK中處於最底層,負責與作業系統的互動。作業系統裝入jvm是通過JDK中的java.exe來實現的,具體步驟如下:

  • a、建立JVM裝載環境和設定;
  • b、裝載jvm.dll;
  • c、初始化jvm.dll;
  • d、呼叫JNIEnv範例裝載並處理class類;
  • e、執行java程式

JVM的體系結構(掌握)

完整圖
在這裡插入圖片描述

簡略圖
在這裡插入圖片描述

當我們執行一個Java程式碼的時候,會按照上圖步驟依次進行,下面簡單解釋上圖中的JVM執行時資料區域:

  • 1、程式計數器:指向當前執行緒正在執行的位元組碼的地址,行號。執行緒私有,無GC

  • 2、Java棧:儲存當前執行緒執行方法所需要的資料,指令,返回地址。執行緒私有,無GC

  • 3、本地方法棧:與Java棧相同,不同的是它存的是本地方法的資料。

  • 4、方法區:儲存類資訊(欄位方法的位元組碼,部分方法的構造器),常數、靜態變數,JIT(即時編譯的資訊)。執行緒共用,無GC,非堆區;(java.lang.OutOfMemoryError:PermGen space)。

  • 5、堆-heap:儲存類範例,一個JVM範例只有一個堆記憶體,執行緒共用,需要GC。

  • 6、JNI【Java Native Interface】(Java本地方法介面)
    凡是帶了native關鍵字的方法就會進入到本地方法棧,其他的就是進入Java棧;

  • 7、 Navite Interface 本地介面
    本地介面的作用就是融合不同的程式語言為Java所用,它的初衷就是融合C/C++程式,Java剛誕生的時候是C/C++橫行的時候,那個時候想要立足就必須由呼叫C/C++的程式,於是就在記憶體中專門開闢了一塊區域處理標記為native的程式碼,它的具體做法就是再Nativa Method Stack中登記native方法,再(Execution Engine)執行引擎執行的時候載入Native Libraies。
    目前該方法的使用越來越少了,除非是與硬體相關的應用,使用Java玩嵌入式等等。由於限制的異構領域間通訊很發達,可以使用Socket通訊等等。

  • 8、Native Method Stack 本地方法棧
    它的具體做法就是Native Method Stack中登記native方法,在(Execution Engine)執行引擎執行的時候載入Native Libraies【本地庫】。

各個版本之間的區別

  • JDK1.6以及之前:有永久代,字串常數池和執行時常數池都在方法區;
  • JDK1.7:有永久代,但已經逐步「去永久代」,字串常數池移到堆中,執行時常數池還在方法區中(永久帶);
  • JDK1.8之後:無永久代,字串常數池在堆中,執行時常數池在元空間;

類載入器(Class Loader)

類載入器的作用

類載入器,顧名思義就是用來載入類的,但是它的作用不僅僅用於載入類,因為對於任意一個類都需要載入它的類載入器和這個類本身以此確立它在Java虛擬機器器中的唯一性,而每一個類載入器都擁有一個獨立的類名稱空間。

說直白點,類載入器的作用就是比較兩個類是否「相等」,只有它們是由同一個類載入器載入的時候才有意義,對於同一個類,如果由不同的類載入器載入,那麼它們必然不想等。(相等包括Class物件的equals方法、isAssignableFrom()方法、isInstance()方法返回的結果,也包括用instanceof關鍵詞判斷的情況)。

類載入器的類別

(1)BootstrapClassLoader(啟動類載入器,又名根載入器)
C++編寫,用於載入Java核心庫 java.*(例如:java.lang.*),構造ExtClassLoader(擴充套件類載入器)AppClassLoader(系統類載入器)。由於引導類載入器涉及到虛擬機器器本地實現細節,開發者無法直接獲取到啟動類載入器的參照,所以不允許程式設計師直接通過參照進行操作。

(2)ExtClassLoader(標準擴充套件類載入器)
該類載入器由Java編寫,用於載入擴充套件類庫,主要負責載入【jre/lib/ext】目錄下的一些擴充套件的jar,例如classpath中的jrejavax.*或者java.ext.dir指定位置的類,開發者可以直接使用擴充套件類載入器。

(3)AppClassLoader(系統類載入器)
Java編寫,主要負責載入應用程式的主函數類,載入程式所在的目錄,例如user.dir所在的位置的class

(4)CustomClassLoader(使用者自定義類載入器)
Java編寫,使用者自定義的類載入器,可載入指定路徑的class檔案。

開發者角度的類載入器位置
在這裡插入圖片描述

  • 根類載入器,載入位於/jre/lib目錄中的或者被引數-Xbootclasspath所指定的目錄下的核心Java類庫。此類載入器是Java虛擬機器器的一部分,使用native程式碼(C++)編寫。如圖所示,rt.jar這個jar包就是Bootstrap根類載入器負責載入的,其中包含了java各種核心的類如java.langjava.iojava.utiljava.sql

  • 擴充套件類載入器,載入位於/jre/lib/ext目錄中的或者java.ext.dirs系統變數所指定的目錄下的拓展類庫。此載入器由sun.misc.Launcher$ExtClassLoader實現。

  • 系統類載入器,載入使用者路徑(ClassPath)上所指定的類庫。此載入器由sun.misc.Launcher$AppClassLoader實現。

類載入器之間的關係

在這裡插入圖片描述
圖中的層次關係,稱為類載入器的雙親委派模型。雙親委派模型要求除了頂層的根類載入器以外,其餘的類載入器都應該有自己的父類別載入器(一般不是以繼承實現,而是使用組合關係來複用父載入器的程式碼)。
如果一個類收到類載入請求,它首先請求父類別載入器去載入這個類,只有當父類別載入器無法完成載入時(其目錄搜尋範圍內沒找到需要的類),子類載入器才會自己去載入

類載入的過程圖

在這裡插入圖片描述

雙親委派機制

什麼是雙親委派機制?

當某個類載入器需要載入某個.class檔案的時候,這個類載入器會首先將這個任務委託給它的上級類載入器(父級載入器),遞迴這個操作,如果上級的類載入器沒有進行載入,那麼這個類載入器才會自己去載入這個.class檔案。

原始碼分析

【我們在java.lang包中首先找到ClassLoader,然後開啟ClassLoader類,Ctrl+F搜尋loadClass方法,下面為該方法的原始碼】

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 首先檢查這個class是否已經被載入過了
                Class<?> c = findLoadedClass(name);
        //如果 c==null就表示該class沒有被載入
                if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 如果有父類別的載入器就將該class委託父類別載入器進行載入
                             if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    //如果父類別的載入器為空,則說明遞迴到bootStrapClassloader根載入器了
                                     //bootStrapClassloader比較特殊,無法通過get獲取
                                     c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {}
            //如果 c==null就表示該class在父載入器那邊沒有被載入
                      if (c == null) {
                //如果bootstrapClassLoader 仍然沒有載入過,則會一層一層的遞迴回來,並且嘗試自己去載入這個class
                long t1 = System.nanoTime();
                c = findClass(name);
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

這裡需要注意的一點就是long t0 = System.nanoTime();這個的原始碼public static native long nanoTime();中的native使用這個關鍵字宣告的方法表示告知JVM呼叫,該方法在外部定義,可能使用C/C++去實現了,這個關鍵字詳細的解釋可以百度去查一下,這裡不做過多的介紹。下面使用流程圖來解釋一下原始碼中的流程。

委派機制的流程圖

【這裡我畫的那張圖太大了不好截圖,參照了別人的圖,圖地址我放在文末了。後面我會將我自己畫的整個JVM的圖分享出來。】
在這裡插入圖片描述
從上圖中我們就更容易理解了,當一個.class這樣的檔案要被載入時。不考慮我們自定義類載入器的話,首先就會在AppClassLoader中檢查是否載入過,如果有那就無需再載入了。如果沒有,那麼會拿到父載入器,然後呼叫父載入器的loadClass方法。父類別中同理會先檢查自己是否已經載入過,如果沒有再往上。注意這個過程,知道到達Bootstrap classLoader之前,都是沒有哪個載入器自己選擇載入的。如果父載入器無法載入,會下沉到子載入器去載入,一直到最底層,如果沒有任何載入器能載入,就會丟擲ClassNotFoundException

雙親委派機制的作用

  • 保證資料安全,能夠防止重複載入同一個.class檔案。通過往父類別載入器去委託,如果已經載入過了那麼就不用再載入一遍;
  • 保證核心 .class不能被篡改。通過委託方式,不會去篡改核心 .class,即使篡改了也不會去載入,即使載入也不會是同一個 .class物件了。不同的載入器載入同一個 .class也不是同一個 Class物件。這樣保證了 Class執行安全。

沙箱安全機制

什麼是沙箱?

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

Java中的安全模型

在Java中將執行程式分成原生程式碼和遠端程式碼兩種,原生程式碼預設視為可信任的,而遠端程式碼則被看作是不受信的。對於授信的原生程式碼,可以存取一切本地資源。而對於非授信的遠端程式碼在早期的Java實現中,安全依賴於沙箱 (Sandbox) 機制。如下圖所示
在這裡插入圖片描述
但如此嚴格的安全機制也給程式的功能擴充套件帶來障礙,比如當使用者希望遠端程式碼存取本地系統的檔案時候,就無法實現。因此在後續的Java1.1版本中,針對安全機制做了改進,增加了安全策略,允許使用者指定程式碼對本地資源的存取許可權。如下圖所示
在這裡插入圖片描述
在 Java1.2 版本中,再次改進了安全機制,增加了程式碼簽名。不論原生程式碼或是遠端程式碼,都會按照使用者的安全策略設定,由類載入器載入到虛擬機器器中許可權不同的執行空間,來實現差異化的程式碼執行許可權控制。如下圖所示
在這裡插入圖片描述
當前最新的安全機制實現,則引入了域 (Domain) 的概念。虛擬機器器會把所有程式碼載入到不同的系統域和應用域,系統域部分專門負責與關鍵資源進行互動,而各個應用域部分則通過系統域的部分代理來對各種需要的資源進行存取。虛擬機器器中不同的受保護域 (Protected Domain),對應不一樣的許可權 (Permission)。存在於不同域中的類檔案就具有了當前域的全部許可權,如下圖所示
在這裡插入圖片描述
以上提到的都是基本的 Java 安全模型概念,在應用開發中還有一些關於安全的複雜用法,其中最常用到的 API 就是doPrivilegeddoPrivileged方法能夠使一段受信任程式碼獲得更大的許可權,甚至比呼叫它的應用程式還要多,可做到臨時存取更多的資源。有時候這是非常必要的,可以應付一些特殊的應用場景。例如,應用程式可能無法直接存取某些系統資源,但這樣的應用程式必須得到這些資源才能夠完成功能。

組成沙箱的基本元件

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

(2)類載入器(Class Loader:其中類載入器在以下三個方面對Java沙箱起作用

  • 它防止惡意程式碼去幹涉善意程式碼;(使用了雙親委派機制)
  • 它守護了被信任的類庫邊界;
  • 它將程式碼歸入到保護域,確定了程式碼可以進行哪些操作。
    虛擬機器器為不同的類載入器載入的類提供不同的名稱空間,名稱空間由一系列唯一的名稱組成,每一個被裝載的類將有一個名字,這個名稱空間是由Java虛擬機器器為每一個類裝載器維護的,它們互相之間甚至不可見。

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

  • 1、從最內層JVM自帶類載入器開始載入,外層惡意同名類得不到載入從而無法使用;
  • 2、由於嚴格通過包來區分了存取域,外層惡意的類通過內建程式碼也無法獲得許可權存取到內層類,破壞程式碼就自然無法生效。

(3)存取控制器(access controller):存取控制器可以控制和訊API對作業系統的存取許可權,而這個控制的策略設定可以由用於指定。
(4)安全管理器(security manager):是核心API和作業系統之間的主要介面。實現許可權控制,比存取控制器優先順序高。
(5)軟體安全包(security package):Java.security下的類和擴充套件包下的類,執行使用者為自己的應用增加新的安全也行,包括以下:

  • 安全提供者
  • 訊息摘要
  • 數位簽章
  • 加密
  • 鑑別

Native關鍵字

我們在原始碼中經常看見Native這個關鍵字,例如我們最常用的執行緒方法,

public static void main(String[] args) {

    new Thread(()->{

    },"myThread").start();
}

我們點進去這個start()方法,在該原始碼中我們發現這個start0()方法

public synchronized void start() {

    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {

        }
    }
}

我們找到start0()的定義發現竟然是private native void start0();這樣的,很驚奇,這個native幹了啥呢?僅僅就是這樣宣告了一下就完事了,它到底是弄了什麼操作呢?是不是有很多的問號,沒關係我們來學習一下。、

首先來看一下這個圖【注意標紅的地方】:
在這裡插入圖片描述

  • 凡是使用了native關鍵字修飾的,就說明Java的作用範圍已經達不到了,這個時候就會去呼叫底層的C語言庫。

  • 凡是帶了native關鍵字的會進入本地方法棧:當類載入進來的時候,將堆、棧記憶體分配好之後就會進入到本地方法棧呼叫start0(),而本地方法棧裡的東西Java範圍是作用不到的,那麼本地方法棧就會呼叫本地方法介面【JNIJava Native Interface,作用寫在下面】,通過JNI載入本地方法庫中的方法去執行操作。

  • JNI的作用】:擴充套件Java的使用,它可以融合不同的程式語言為Java所用,最初是CC++,後續新增了其他語言。原因就是:Java剛誕生的時候C語言和C++那個時候超級流行,而想要立足的話就必須要有呼叫CC++的程式,於是就會在記憶體區域中專門開闢一個標記區域【Navicat Method Stack】用於登記native方法【只是登記而不用執行】,並且在最終執行的時候通過JNI載入本地方法庫中的方法。

注意:本地方法棧、本地方法介面還有本地方法庫的介紹在上面JVM體系結構部分已經解釋,請往上檢視。

方法區【Method Area】

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

靜態變數【static】、常數【final】、類資訊(構造方法、介面定義)【Class】、執行時的常數池存在方法區中【常數池】,但是範例變數存在堆記憶體中,和方法區無關 。

舉例子,根據程式碼簡單畫一下記憶體圖【物件剛載入的時候是什麼樣子的】:

public class Test {
    private int a;
    private String name="oldou";

    public static void main(String[] args) {
        Test test = new Test();
        test.a=1;
        System.out.println(test.a+"\t"+test.name);
    }
}

在這裡插入圖片描述

理解一下棧【Stack】

注意:概念也許對你來說有些枯燥,但是當你真正沉下心看並且弄懂的時候,真的很有趣。下面的每句話基本上都需要理解。

stack)又名堆疊,它是一種資料結構【運算受限的線性表】。限定僅在表尾進行插入和刪除操作的線性表。這一端被稱為棧頂,相對地,把另一端稱為棧底。向一個棧插入新元素又稱作進棧、入棧或壓棧,它是把新元素放到棧頂元素的上面,使之成為新的棧頂元素;從一個棧刪除元素又稱作出棧或退棧,它是把棧頂元素刪除掉,使其相鄰的元素成為新的棧頂元素。如下圖所示:
在這裡插入圖片描述

  • 首先得搞清楚,的意思相當於儲存貨物或者供旅客住宿的地方,就是指資料暫時儲存的地方,因此才會由出棧、進棧的說法。

  • 棧作為一種資料結構,是一種只能在一端進行插入【push】和刪除操作【pop】的特殊線性表。它按照先進後出的原則儲存資料,先進入的資料被壓入棧底,最後的資料在棧頂,需要讀資料的時候從棧頂開始彈出資料(最後一個資料被第一個讀出來)。棧具有記憶作用,對棧的插入與刪除操作中,不需要改變棧底指標。

  • 每一個執行的方法都會產生一個棧幀【以上就有兩個棧幀:方法A、方法B】,程式正在執行的方法一定在棧的頂部,方法執行完之後就會被彈出棧,直到全部執行完畢。棧的執行原理本來就是先進後出,後進先出,先進入的方法就會被後進的方法壓住【又名:壓棧】。棧就像一個桶一樣,如果棧堆滿了就會丟擲錯誤 【StackOverflowError】。

  • 棧是允許在同一端進行插入和刪除操作的特殊線性表。允許進行插入和刪除操作的一端稱為棧頂(top),另一端為棧底(bottom);棧底固定,而棧頂浮動;棧中元素個數為零時稱為空棧。插入一般稱為進棧(PUSH),刪除則稱為退棧(POP)。棧也稱為先進後出表。與棧類似的佇列遵循FIFO原則【First Input First Output,先進先出】。

  • 這就是為什麼我們程式中的main()方法先執行,結果最後結束的原因,因為main()方法入棧後被壓在棧低,上面的方法棧幀執行一個就POP一個最後main()出棧後才結束main()方法。

  • 棧記憶體主管著程式的執行,生命週期以及執行緒同步,執行緒結束後,棧記憶體就會釋放,所以對於棧來說,不存在垃圾回收問題,因為一旦執行緒結束了,棧就Over了。

棧中主要存放一些基本型別的變數(byte、short、int、long、float、double、boolean、char)、物件參照、範例的方法。

【注】關於棧的程式碼實現之類的我就不放了,網上很多都有,後面再進行整理。

堆【Heap】

什麼是堆記憶體?

堆記憶體是是Java記憶體中的一種,它的作用是用於儲存參照型別,當new範例化得到一個參照變數【物件或者陣列】的時候,java虛擬機器器會在堆記憶體中開闢一個不一定是連續的空間分配給該範例,根據零散的記憶體地址,實則是根據雜湊演演算法生成一長串數位指向該範例的實體地址,相當於門牌號起到標識作用。當參照丟失了,會被垃圾回收機制回收,但不是立馬釋放堆記憶體。

堆是一種資料結構,它是儲存的單位,一個JVM只有一個堆記憶體,並且堆記憶體的大小是可以調節的。

  • 堆中儲存的全部是物件範例,每個物件都包含一個與之對應的class的資訊(class資訊存放在方法區)。
  • jvm只有一個堆區(heap)被所有執行緒共用,堆中不存放基本型別和物件參照,只存放物件本身,幾乎所有的物件範例和陣列都在堆中分配。

堆記憶體的特點是什麼?

堆記憶體的特點就是:

  • 堆記憶體可以看作是一個管道,FIFO【先進先出,後進後出】。
  • 堆可以動態地分配記憶體大小,生存期不需要事先告訴編譯器,缺點是由於要在執行時動態的分配記憶體,所以存取的速度慢。

New物件在堆中如何分配?

由Java虛擬機器器的自動垃圾回收器來進行管理

堆和棧的區別

  • 存放的東西不同:堆記憶體用於存放由new建立的物件或者陣列,棧記憶體用於物件參照和基本資料型別等等;

  • 儲存資料的原則不一樣:堆遵循FIFO原則【先進先出,後進後出】,而棧是先進後出,後進先出;

  • 當在一段程式碼塊定義一個變數時,Java就在棧中為這個變數分配記憶體空間,當超過變數的作用域後,Java會自動釋放掉為該變數所分配的記憶體空間,該記憶體空間可以立即被另作他用。

  • 在堆中分配的記憶體,由Java虛擬機器器的自動垃圾回收器來管理。

堆記憶體的模型圖

在這裡插入圖片描述
Java堆主要用於存放各種類的範例物件和陣列,它是垃圾收集器管理的主要區域,因此很多時候被稱為「GC堆」,在Java中堆被分為兩個區域:新生代和老年代,【注意這裡不包括元空間(方法區),元空間原來叫永久代,JDK1.8之後改名為元空間】

下面就開始逐一的介紹一下新生代、老年代以及元空間。

新生代

為什麼堆要分代呢?

為什麼要給堆分代呢?當然咯,不分代也是可以的,只是分代的話可以優化GC效能,假如不分代的話,那我們建立的所有物件都放在一塊,當需要垃圾回收的時候我們需要去找哪些物件沒用,這樣就會對整個堆區域進行全面掃描,這樣耗效能啊,如果分帶的話,將新建立的物件放在某一個區域,當需要GC的時候就去掃描回收,多方便是不是。

新生代的介紹

新生代主要用於儲存新生的物件,一般需要佔據堆的1/3的空間,由於頻繁建立物件,所以新生代會頻繁的觸發Minor GC進行垃圾回收。
新生代有分為Eden區【伊甸園區】、SurvivorFromSurvivorTo三個區域,下面依次介紹一下:

  • Enden區:是Java新物件的出生地(如果新建立的物件佔用記憶體很大,則會被直接分配到老年代),當Eden區記憶體不夠的時候就會出發Minor GC,對新生代區進行一次垃圾回收。
  • SurvivorTo:保留了一次Minor GC過程中的倖存者;
  • SurvivorFrom:上一次GC的倖存者,作為這一次GC的被掃描者;

新生代中的GC

HotSpot JVM把年輕代分為了三部分:1個Eden區【伊甸園區】和2個Survivor區【倖存區,分別是From、To】,預設的比例為8:1,一般情況下,新建立的物件都會被分配到Eden區,這些物件再經過第一次的Minor GC後,如果還存活就會被轉移到Survivor區。物件在Survivor區中每熬過一次Minor GC,年齡就會被增長一次,當它們的年齡達到一次歲數的時候就會被移到老年代中。

因為新生代中的物件基本上都是朝生夕死(80%以上),所以在新生代中的垃圾回收演演算法使用的是複製演演算法複製演演算法的基本思想就是將記憶體分為兩塊,每次只使用其中的一塊,當這一塊用完之後就將還或者的物件複製到另一塊上面,複製演演算法並不會產生記憶體碎片。

在GC開始的時候,物件只會存在於Eden區和名為「From」的Survivor區,Survivor「To」是空的。緊接著進行GC,Eden區中所有存活的物件都會被複制到「To」,而在「From」區中,仍存活的物件會根據他們的年齡值來決定去向。年齡達到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設定)的物件會被移動到年老代中,沒有達到閾值的物件會被複制到「To」區域。經過這次GC後,Eden區和From區已經被清空。這個時候,「From」「To」會交換他們的角色,也就是新的「To」就是上次GC前的「From」,新的「From」就是上次GC前的「To」。不管怎樣,都會保證名為ToSurvivor區域是空的。Minor GC會一直重複這樣的過程,直到「To」區被填滿,「To」區被填滿之後,會將所有物件移動到年老代中。
在這裡插入圖片描述

一個物件的一輩子

我是一個普通的java物件,我出生在Eden區,在Eden區我還看到和我長的很像的小兄弟,我們在Eden區中玩了挺長時間。有一天Eden區中的人實在是太多了,我就被迫去了Survivor區的「From」區,自從去了Survivor區,我就開始漂了,有時候在Survivor的「From」區,有時候在Survivor的「To」區,居無定所。直到我18歲的時候,爸爸說我成人了,該去社會上闖闖了。於是我就去了老年代那邊,年老代裡,人很多,並且年齡都挺大的,我在這裡也認識了很多人。在老年代裡,我生活了20年(每次GC加一歲),然後被回收。

【文章地址附在文末,搜了很多篇進行學習,感覺這個看著懂一些,描述得感覺蠻到位的…】

有關年輕代的JVM引數

  • (1)-XX:NewSize和-XX:MaxNewSize:用於設定年輕代的大小,建議設為整個堆大小的1/3或者1/4,兩個值設為一樣大。

  • (2)-XX:SurvivorRatio:用於設定Eden和其中一個Survivor的比值,這個值也比較重要。

  • (3)-XX:+PrintTenuringDistribution:這個引數用於顯示每次Minor GC時Survivor區中各個年齡段的物件的大小。

  • (4)-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold:用於設定晉升到老年代的物件年齡的最小值和最大值,每個物件在堅持過一次Minor GC之後,年齡就加1。

老年代

老年代用於存放新生代中經過多次垃圾回收仍然存活的物件。在老年代的物件都比較穩定,因此MajorGC不會頻繁執行,而在進行MajorGC之前一般都會進行一次MinorGC,使得新生代的物件晉入老年代,一般是空間不夠用時才觸發,當無法找到足夠大的連續空間分配給新建立的大物件的時候也會觸發一次MajorGC進行垃圾回收騰出空間。

  • MajorGC採用標記-清除演演算法:首先掃描一次所有老年代,標記出存活的物件,然後回收沒有標記的物件。
  • MajorGC的耗時比較長,因為要先掃描然後再回收。
  • MajorGC會產生記憶體碎片,為了減少記憶體損耗,我們一般需要進行合併或者標記出來方便下次直接分配。
  • 當老年代也滿了裝不下的時候,就會丟擲OOM(Out of Memory)異常。

下面來測試一下,模擬一下這個OOM錯誤:
測試程式碼:

/**
 * 模擬OOM錯誤
 */
public class hello {
    public static void main(String[] args) {
        String str = "hello world";
        while (true){ //死迴圈
            //通過不斷的產生新物件,然後堆記憶體溢位
            str = str + new Random().nextInt(888888888)
                    + new Random().nextInt(999999999);
        }
    }
}

執行報錯:在這裡插入圖片描述
關於OOM的問題,後面再稍微詳細的說明,這裡不做過多的介紹。

永久區【轉】

永久代,指的是記憶體的永久儲存區域,主要用於存放Class和Meta(後設資料)的資訊,Class在被載入的時候放入到永久區域,它和存放範例的區域不一樣,GC不會在主程式執行期間對永久代區域進行清理,所以也導致了永久代的區域會隨著載入的Class的增多而脹滿,最終丟擲OOM異常。

在Java8中,永久代已經被移除,被一個稱為「後設資料區」(元空間)的區域所取代。

元空間【轉】

其實,移除永久代的工作從JDK1.7就開始了。JDK1.7中,儲存在永久代的部分資料就已經轉移到了Java Heap或者是 Native Heap。但永久代仍存在於JDK1.7中,並沒完全移除,譬如符號參照(Symbols)轉移到了native heap;字面量(interned strings)轉移到了java heap;類的靜態變數(class statics)轉移到了java heap
我們還是通過以上模擬OOM錯誤分別在JDK1.6、JDK1.7、JDK1.8中執行:
JDK 1.6 的執行結果:
在這裡插入圖片描述
JDK 1.7的執行結果:
在這裡插入圖片描述
JDK 1.8的執行結果:
在這裡插入圖片描述
從上述結果可以看出,JDK 1.6下,會出現「PermGen Space」的記憶體溢位,而在 JDK 1.7和 JDK 1.8 中,會出現堆記憶體溢位,並且 JDK 1.8中 PermSize 和 MaxPermGen 已經無效。因此,可以大致驗證 JDK 1.7 和 1.8 將字串常數由永久代轉移到堆中,並且 JDK 1.8 中已經不存在永久代的結論。現在我們看看元空間到底是一個什麼東西?

結論:
元空間的本質和永久代類似,都是對JVM規範中方法區的實現,不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機器器中,而是使用本地記憶體。因此,預設情況下,元空間的大小僅受本地記憶體限制。類的後設資料放入 native memory、字串池和類的靜態變數放入java堆中.。這樣可以載入多少類的後設資料就不再由MaxPermSize控制,而由系統的實際可用空間來控制。

元空間的大小可以通過以下引數來指定元空間的大小:

  • -XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行型別解除安裝,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,適當提高該值。
  • -XX:MaxMetaspaceSize,最大空間,預設是沒有限制的。
    除了上面兩個指定大小的選項以外,還有兩個與 GC 相關的屬性:
    -XX:MinMetaspaceFreeRatio,在GC之後,最小的Metaspace剩餘空間容量的百分比,減少為分配空間所導致的垃圾收集
  • -XX:MaxMetaspaceFreeRatio,在GC之後,最大的Metaspace剩餘空間容量的百分比,減少為釋放空間所導致的垃圾收集

【注:以上圖片文字摘自於:https://www.cnblogs.com/paddix/p/5309550.html】

後續更新中…

JVM垃圾回收流程

堆記憶體的調優

OOM原因分析

GC之參照計數法

GC之複製演演算法

GC之標記壓縮清除演演算法

GC總結

類載入參考:https://zhuanlan.zhihu.com/p/44670213
雙親委派機制參考:https://www.jianshu.com/p/1e4011617650
沙箱安全機制參考:https://www.cnblogs.com/MyStringIsNotNull/p/8268351.html
棧參考:https://baike.baidu.com/item/%E6%A0%88/12808149?fr=aladdin
新生代參考:http://ifeve.com/jvm-yong-generation/
永久代參考:https://www.cnblogs.com/paddix/p/5309550.html