主頁 > 後端開發 > 我把面試問爛了的?JVM?總結了一下(帶答案,萬字總結,精心打磨,建議收藏)

我把面試問爛了的?JVM?總結了一下(帶答案,萬字總結,精心打磨,建議收藏)

2021-09-25 16:45:46 後端開發

💂 個人主頁: Java程式魚

💬 如果文章對你有幫助,歡迎關注、點贊、收藏(一鍵三連)和訂閱專欄

👤 微信號:hzy1014211086,想加入技術交流群的小伙伴可以加我好友,群里會分享學習資料、學習方法


序號內容鏈接地址
1Java基礎知識面試題https://blog.csdn.net/qq_35620342/article/details/119636436
2Java集合容器面試題https://blog.csdn.net/qq_35620342/article/details/119947254
3Java并發編程面試題https://blog.csdn.net/qq_35620342/article/details/119977224
4Java例外面試題https://blog.csdn.net/qq_35620342/article/details/119977051
5JVM面試題https://blog.csdn.net/qq_35620342/article/details/119948989
6Java Web面試題https://blog.csdn.net/qq_35620342/article/details/119642114
7Spring面試題https://blog.csdn.net/qq_35620342/article/details/119956512
8Spring MVC面試題https://blog.csdn.net/qq_35620342/article/details/119965560
9Spring Boot面試題https://blog.csdn.net/qq_35620342/article/details/120333717
10MyBatis面試題https://blog.csdn.net/qq_35620342/article/details/119956541
11Spring Cloud面試題待分享
12Redis面試題https://blog.csdn.net/qq_35620342/article/details/119575020
13MySQL資料庫面試題https://blog.csdn.net/qq_35620342/article/details/119930887
14RabbitMQ面試題待分享
15Dubbo面試題待分享
16Linux面試題待分享
17Tomcat面試題待分享
18ZooKeeper面試題待分享
19Netty面試題待分享
20資料結構與演算法面試題待分享

在這里插入圖片描述
作者金華,上海張江資訊技術專修學院副院長,上海師范大學兼職教授,軟體與資訊技術講師,長期從事軟體與資訊技術技能培訓與職業規劃作業,本書將相關知識的系統整合,符合現在Java的主流應用,拒絕全面不實用;本書知識點主要圍繞技術升級和面試技巧展開,讓你在升級專業知識的同時更能順利通過面試,

京東自營購買鏈接:
《Java核心技術及面試指南》- 京東圖書

當當自營購買鏈接:
《Java核心技術及面試指南》- 當當圖書

截止到9月24日14:00,留言獲贊最高的兩位同學,將獲得《Java核心技術及面試指南》圖書一本

文章目錄

  • 前言
  • 一、虛擬機類加載機制
    • 1.虛擬機類加載程序
      • 加載階段
      • 連接階段
      • 初始化階段
    • 2.類加載器(ClassLoader)
      • 啟動類加載器(Bootstrap)
      • 擴展類加載器(Extension)
      • 應用程式類加載器(APP)
      • 自定義類加載器
    • 3.用戶自定義加載器
    • 4.雙親委派機制
  • 二、Java運行時資料區
    • 1.Program Counter Register(程式計數器)
    • 2.Java虛擬機堆疊
      • 區域變數表(Local Variables)
      • 運算元堆疊(Operand Stack)
      • 動態連接(Dynamic Linking)
      • 方法回傳地址(Return Address)
      • 一些附加資訊
    • 3.本地方法堆疊(執行緒私有)
    • 4.Java堆
    • 5.方法區
    • 6.運行時常量池
    • 7.直接記憶體
  • 三、物件記憶體布局
    • 1.物件頭
    • 2.資料實體
    • 3.對齊填充
  • 四、物件訪問定位
  • 五、如何判定物件為垃圾物件?
    • 1.參考計數演算法
    • 2.可達性分析演算法
  • 六、參考
  • 七、如何回收垃圾物件?
    • 1.垃圾收集演算法
      • 標記-清除演算法
      • 復制演算法
      • 標記-整理演算法
    • 2.垃圾收集演算法
      • Serial收集器
      • ParNew收集器
      • Parallel Scavenge收集器
      • Serial Old收集器
      • Parallel Old收集器
      • CMS收集器
      • G1收集器


前言

目前記憶體的動態分配與記憶體回收技術已經相當成熟,一切看起來都進入了"自動化"時代,那么為什么我們還要去了解GC和記憶體分配呢?
當需要排查各種記憶體溢位、記憶體泄露問題時,當垃圾收集成為系統達到更高并發量的瓶頸時,我們就需要對這些"自動化"的技術進行必要的監控和調節, 要想實作性能調優,得具備相關工具監控程式性能,有了監控資訊,才能進行調優,

一、虛擬機類加載機制

Class檔案中描述的各種資訊,最終都是要加載到虛擬機中之后才能運行和使用,

JVM把Class檔案加載到記憶體,并對資料進行校驗、轉換決議和初始化,最終形成可以被JVM直接使用的Java型別,這個程序被稱作虛擬機的類加載機制,與那些在編譯時需要進行連接的語言不同,在Java語言里面,型別的加載、連接和初始化程序都是在程式運行期間完成的,這種策略讓Java語言進行提前編譯會面臨額外的困難,也會讓類加載時稍微增加一些性能開銷,但是卻為Java應用提供了極高的擴展性和靈活性,Java天生可以動態擴展的語言特性就是依賴運行期動態加載和動態連接這個特點實作的,(例如Java多型、動態代理)

Java虛擬機中的類加載(JVM把class檔案加載到記憶體),按先后順序需要經過加載、鏈接、初始化三個步驟,其中,鏈接程序中同樣需要驗證;而記憶體中的類沒有經過初始化,同樣不能使用,

ClassLoader只負責class檔案的加載,至于它是否可以運行,則由ExecutionEngine決定,

1.虛擬機類加載程序

在這里插入圖片描述

在這里插入圖片描述

加載階段

什么情況下需要開始類加載程序的加載階段?這個「 Java虛擬機規范」中沒有強制約束,這點可以交給虛擬機的具體實作來自由把握,但是對于初始化階段,「 Java虛擬機規范」則是嚴格規定了有且只有六種情況必須對類進行“初始化”(而加載、驗證、準備自然需要在此之前開始)

  • 遇到new、getstatic、putstatic或invokestatic這四條位元組碼指令時,如果型別沒有進行過初始化,則需要先觸發其初始化階段,能夠生成這四條指令的典型Java代碼場景有:

    (1)使用new關鍵字實體化物件的時候,(new)
    (2)讀取或設定一個型別的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候,
    (3)呼叫一個型別的靜態方法的時候,(invokestatic)

  • 使用java.lang.reflect包的方法對型別進行反射呼叫的時候,如果型別沒有進行過初始化,則需要先觸發其初始化,

  • 當初始化類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化,

  • 當虛擬機啟動時,被標明為啟動類的類(用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類),

  • 當使用JDK 7新加入的動態語言支持時,如果一個java.lang.invoke.MethodHandle實體最后的決議結果為REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四種型別的方法句柄,并且這個方法句柄對應的類沒有進行過初始化,則需要先觸發其初始化,

  • 當一個介面中定義了JDK 8新加入的默認方法(被default關鍵字修飾的介面方法)時,如果有這個介面的實作類發生了初始化,那該介面要在其之前被初始化,

Java程式對類的使用方式分為:主動使用和被動使用,
除了上述6種方式,其他使用Java類的方式都被看作為是對類的被動使用,都不會導致類的初始化,

加載、驗證、準備、初始化、卸載這5個階段順序是確定的,而決議階段則不一定,它在某些情況下可以在初始化階段之后再開始,這是為了支持Java語言的運行時系結特性(也稱為動態系結或晚期系結),

加載階段,Java虛擬機需要完成三件事:

  • 通過類的全限定名(例如:org.apache.commons.lang3.StringUtils)來獲取定義此類的二進制位元組流(Class檔案位元組流)
  • 將這個位元組流所代表的靜態存盤結構轉化為方法區的運行時資料結構
  • 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料訪問入口

連接階段

(1)驗證
驗證是連接階段的第一步,這一階段的目的是確保Class檔案的位元組流中包含的資訊符合《Java虛擬機規范》的全部約束要求,保證這些資訊被當作代碼運行后不會危害虛擬機自身的安全,

主要包括四種驗證:檔案格式驗證、元資料驗證、位元組碼驗證、符合參考驗證,

(2)準備
準備階段是正式為類變數(即靜態變數,被static修飾的變數)分配記憶體并設定類變數初始值(即0)的階段,從概念上講,這些變數所使用的記憶體都應當在方法區中進行分配,但必須注意到方法區本身是一個邏輯上的區域,在JDK 7及之前,HotSpot使用永久代來實作方法區時,實作是完全符合這種邏輯概念的;而在JDK 8及之后,類變數則會隨著Class物件一起存放在Java堆中,這時候“類變數在方法區”就完全是一種對邏輯概念的表述了,

public static int value = 123,那么value在準備階段過后的初始值為0,而不是123,因為這時尚未開始執行任何Java方法,而把value賦值為123的putstatic指令是程式被編譯后,存放在類構造器<clinit>()方法之中,所以把value賦值為123的動作要到類的初始化階段才會被執行,

public static final int value = 123456,final在編譯的時候就會分配了,準備階段會顯示初始化,編譯時javac將會為value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設定將value賦值為123456,

(3)決議

決議階段是Java虛擬機將常量池內的符號參考替換為直接參考的程序,

初始化階段

進行準備階段時,變數已經賦過一次系統要求的初始零值,而在初始化階段,則會根據程式員通程序式編碼制定的主觀計劃去初始化類變數和其他資源,我們也可以從另外一種更直接的形式來表達:初始化階段就是執行類構造器<clinit>()方法的程序,<clinit>()并不是程式員在Java代碼中直接撰寫的方法,它是Javac編譯器的自動生成物,但我們非常有必要了解這個方法具體是如何產生的,以及<clinit>()方法執行程序中各種可能會影響程式運行行為的細節,這部分比起其他類加載程序更貼近于普通的程式開發人員的實際作業

(1)<clinit>()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態陳述句塊(static{}塊)中的陳述句合并產生的,編譯器收集的順序是由陳述句在源檔案中出現的順序決定的,靜態陳述句塊中只能訪問到定義在靜態陳述句塊之前的變數,定義在它之后的變數,在前面的靜態陳述句塊可以賦值,但是不能訪問,如下所示,

public class StaticTest {
    static {
        i = 0; //  給變數復制可以正常編譯通過
        System.out.println(i); // 這句編譯器會報錯“非法向前參考” 
    }
    static int i = 1;
}

public class StaticTest {
    static {
        i = 2; //  給變數復制可以正常編譯通過
    }
    static int i = 1;

    public static void main(String[] args) {
        System.out.println(StaticTest.i);
    }
}

答案:1,為什么可以呢?在linking階段的準備階段,已經把i加載到記憶體,并且賦初始值(零值)了,

如果沒有靜態變數賦值動作和靜態陳述句塊,就不會生成

(2)<clinit>()方法與類的建構式(即在虛擬機視角中的實體構造器()方法)不同,它不需要顯式地呼叫父類構造器,Java虛擬機會保證在子類的<clinit>()方法執行前,父類的<clinit>()方法已經執行完畢,因此在Java虛擬機中第一個被執行的<clinit>()方法的型別肯定是java.lang.Object,

由于父類的<clinit>()方法先執行,也就意味著父類中定義的靜態陳述句塊要優先于子類的變數賦值操作,如下所示,欄位B的值將會是2而不是0,

public class TestDemo {

    static class Parent{
        public static int A = 1;

        static {
            A = 2;
        }
    }

    static class Sub extends Parent{
        public static int B  = A;
    }

    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
}

(3)Java虛擬機必須保證一個類的<clinit>()方法在多執行緒環境中被正確地加鎖同步,如果多個執行緒同時去初始化一個類,那么只會有其中一個執行緒去執行這個類的<clinit>()方法,其他執行緒都需要阻塞等待,直到活動執行緒執行完畢<clinit>()方法,如果在一個類的<clinit>()方法中有耗時很長的操作,那就可能造成多個行程阻塞,在實際應用中這種阻塞往往是很隱蔽的,代下所示:

public class DeadThreadTest {
    public static void main(String[] args) {
        Runnable r = () -> {
            System.out.println(Thread.currentThread().getName()+"開始");
            DeadThread deadThread = new DeadThread();
            System.out.println(Thread.currentThread().getName()+"結束");
        };

        Thread t1 = new Thread(r,"執行緒1");
        Thread t2 = new Thread(r,"執行緒2");

        t1.start();
        t2.start();
    }
}

class DeadThread{
    static{
        if(true){
            System.out.println(Thread.currentThread().getName()+"初始化當前類");
            while(true){
                
            }
        }
    }
}

結果:
執行緒2開始
執行緒1開始
執行緒2初始化當前類

執行緒2在初始化當前類時死回圈了,會造成后面所有的執行緒全部阻塞,

2.類加載器(ClassLoader)

Java虛擬機設計團隊有意把類加載階段中的“通過一個類的全限定名來獲取描述該類的二進制位元組流”這個動作放到Java虛擬機外部去實作,以便讓應用程式自己決定如何去獲取所需的類,實作這個動作的代碼被稱為“類加載器”(Class Loader)

目前類加載器在類層次劃分、OSGi、程式熱部署、代碼加密等領域大放異彩,

類加載器:把我們硬碟上編譯好的.Class檔案,通過類裝載器將位元組碼檔案加載到記憶體中,生成一個Class物件,
在這里插入圖片描述
在這里插入圖片描述

這里的四者是包含關系,不是上下層,也不是子父類的繼承關系

ClassLoader:是一個抽象類,我們可以繼承它實作自定義加載器,

啟動類加載器(Bootstrap)

啟動類加載器(Bootstrap):主要加載jre/lib/rt.jar(Java核心API ),getClassLoader為null,(C++實作的)

出于安全考慮,Bootstrap啟動類加載器只加載包名為java、javax、sun等開頭的類

Object object = new Object();
object.getClass().getClassLoader();//null

String string = new String();
string.getClass().getClassLoader();//null

并不繼承自java.lang.ClassLoader,沒有父加載器

擴展類加載器(Extension)

擴展類加載器(Extension):通過反射創建Class實體,而這個類在jre/lib/ext的jar包中,這時加載器就是Extension ClassLoader,加載jre/lib/ext里的類,

getClassLoader:sun.misc.Launcher$ExtensionLoader@HashCode

直接繼承自URLClassLoader,間接繼承ClassLoader

應用程式類加載器(APP)

應用程式類加載器(APP):它負責加載用戶類路徑(ClassPath)上所有的類別庫,

getClassLoader:sun.misc.Launcher$AppLoader@HashCode

直接繼承自URLClassLoader,間接繼承ClassLoader

對于用戶自定義的類,如果沒有自定義過自己的類加載器,默認使用應用程式類加載器加載

可以通過ClassLoader.getSystemClassLoader();獲取應用程式類加載器

自定義類加載器

自定義類加載器的父類是應用程式類加載器

sun.misc.Launcher:它是一個Java虛擬機的入口應用

獲取父類加載器:classLoader.getParent()

擴展類加載器和應用程式類加載器都繼承了ClassLoader.

獲取ClassLoader方法:
方式一:獲取當前類的ClassLoader
clazz.getClassLoader();
方式二:獲取當前執行緒背景關系的ClassLoader
Thread.currentThread().getContextClassLoader()
方式三:獲取系統的ClassLoader
ClassLoader.getSystemClassLoader()
方式四:獲取呼叫者的ClassLoader
DriverManager.getCallerClassLoader()

3.用戶自定義加載器

為什么要自定義類加載器?

  • 隔離加載類(通過類加載器實作類的隔離、多載等功能)
  • 修改類加載的方式
  • 擴展加載源(增加除了磁盤位置之外的Class檔案來源)
  • 防止原始碼泄露

用戶自定義類加載器實作步驟:
(1)開發人員通過繼承抽象類java.lang.ClassLoader類的方式,實作自己的類加載器,以滿足一些特殊的需求,
(2)在JDK1.2之前,在自定義類加載器時,總會去繼承ClassLoader類并重寫loadClass()方法,從而實作自定義的類加載,但是在JDK1.2之后,已不再建議用戶去覆寫loadClass()方法,而是建議把自定義類的加載邏輯寫在findClass()中,
(3)在撰寫自定義類加載器時,如果沒有太過于復雜的需求,可以直接繼承URLClassLoader類,這樣就可以避免自己去撰寫findClass()發放及其獲取位元組碼流的方式,使自定義類加載器撰寫更加簡潔,

舉例:
防止原始碼泄露實作步驟:
(1)繼承ClassLoader,重寫findClass()
(2)在findClass()中,傳入的name是加密的,先寫解密邏輯,然后在獲取位元組碼二進制流

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

(3)呼叫defineClass()把二進制流位元組轉化為Class

protected final Class<?> defineClass(String name, byte[] b, int off, int len)
    throws ClassFormatError
{
    return defineClass(name, b, off, len, null);
}

比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個Class檔案,被同一個Java虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等, 這里所指的“相等”,包括代表類的Class物件的equals()方法、isAssignableFrom()方法、isInstance()方法的回傳結果,也包括了使用instanceof關鍵字做物件所屬關系判定等各種情況,

4.雙親委派機制

雙親委派模型的作業原理:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞回,因此所有的加載請求最終都應該傳送到最頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去完成加載,

為什么根類加載器為NULL?
根類加載器并不是Java實作的,而且由于程式通常須訪問根加載器,因此訪問擴展類加載器的父類加載器時回傳null,

出于安全考慮,Bootstrap啟動類加載器只加載包名為java、javax、sun等開頭的類

自定義一個包java.lang,自定義Java核心類別庫沒有的類,運行時報錯,
java.lang.SecurityException:prohibited package name:java.lang

舉例:自定義一個包java.lang,自定義一個類String,然后里面宣告main方法,運行時報錯,(沙箱機制)

package java.lang;

public class String {
	public static void main(String[] args) {
		System.out.println(1);
	}
}
錯誤: 在類 java.lang.String 中找不到 main 方法, 請將 main 方法定義為:
   public static void main(String[] args)
否則 JavaFX 應用程式類必須擴展javafx.application.Application

加載String時,使用的是BootstrapClassLoader,加載的是Java核心類別庫的String,并非我們自定義的String,核心類別庫的String類,沒有main方法,因此報錯,

沙箱機制:是由基于雙親委派機制上采取的一種JVM的自我保護機制,假設你要寫一個java.lang.String 的類,由于雙親委派機制的原理,此請求會先交給Bootstrap試圖進行加載,但是Bootstrap在加載類時首先通過包和類名查找rt.jar中有沒有該類,有則優先加載rt.jar包中的類,因此就保證了java的運行機制不會被破壞.(安全特性,防止惡意代碼對Java的破壞)

雙親委派優勢:

  • 避免類的重復加載
  • 保護程式安全,防止核心API被篡改

二、Java運行時資料區

JVM記憶體布局規定了Java在運行程序中記憶體申請、分配、管理的策略,保證了JVM的高效穩定運行,不同的JVM對于記憶體的劃分方式和管理機制存在著部分差異,

Java虛擬機在執行Java程式的程序中會把它所管理的記憶體劃分為若干個不同的資料區域,這些區域有各自的用途,以及創建和銷毀的時間,有的區域隨著虛擬機行程的啟動而一直存在,有些區域則是依賴用戶執行緒的啟動和結束而建立和銷毀,

JVM運行時資料區:Java代碼運行的時候每個資料區的區塊存的是什么用來干什么怎么存的

需提前理解的概念:
一個類可以看成三類,資料(int i = 0等…)、指令(int c = i…代碼)、控制(if else switch…)

Java 虛擬機運行時資料區:Java 虛擬機在執行 Java 程式的程序中會把它所管理的記憶體劃分為若干個不同的資料區域,這些區域有各自的用途,以及創建和消耗的時間,有的區域隨著虛擬機行程的啟動而一直存在,有些區域則是依賴用戶執行緒的啟動和結束而建立和消耗,
在這里插入圖片描述

1.Program Counter Register(程式計數器)

Program Counter Register:程式計數器

作用:程式計數器用來存盤指向下一條指令的地址,也就是將要執行的指令代碼,由執行引擎讀取下一條指令,

它是一塊較小的記憶體空間,它可以看作是當前執行緒所執行的位元組碼的行號指示器,位元組碼解釋器作業時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,它是程式控制流的指示器,分支、回圈、跳轉、例外處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成,

由于Java虛擬機的多執行緒是通過執行緒輪流切換、分配處理器執行時間的方式來實作的,在任何一個確定的時刻,一個處理器(對于多核處理器來說是一個內核)都只會執行一條執行緒中的指令,因此,為了執行緒切換后能恢復到正確的執行位置,每個執行緒都需要有一個獨立的程式計數器,各個執行緒之間計數器互不影響,獨立存盤,

比如我A()方法呼叫了B()方法,執行完B之后怎么恢復,這時就需要程式計數器,位元組碼解釋器就是通過改變計數器的值來選取下一條執行的位元組碼指令,

如果執行緒執行的是Java方法,這個計數器記錄的是正在執行的虛擬機位元組碼指令的地址,如果執行的是native方法,這個計數器的值為undefined,

2.Java虛擬機堆疊

堆疊是運行時的單位,而堆是存盤的單位,

即:堆疊解決程式的運行問題,即程式如何執行,或者說如何處理資料,堆解決的是資料存盤的問題,即資料怎么放、放在哪兒,

作用:主管Java程式的運行,它保存方法的區域變數、部分結果,并參與方法的呼叫和回傳,

它描述的是Java方法執行的執行緒記憶體模型,每個方法在執行的同時都會創建一個堆疊幀用于存盤區域變數表、運算元堆疊、動態連接、方法出口等資訊,每一個方法從呼叫直至執行完成的程序,就對應著一個堆疊幀在虛擬機堆疊中入堆疊到出堆疊的程序,

在執行緒上執行的每一個方法都對應著一個堆疊楨

假如執行A方法創建一個A堆疊幀,A堆疊幀入堆疊,A方法呼叫B方法,需要為B方法創建一個B堆疊幀,然后入堆疊,B方法執行到方法出口之后,B堆疊幀出堆疊,然后A方法執行到方法出口之后,A堆疊幀出堆疊,這就是方法執行程序,

堆疊幀伴隨著方法從創建到執行完成,

Java虛擬機規范允許Java堆疊的大小是動態或者是固定不變的,

  • 如果采用固定大小的Java虛擬機堆疊,那每一個執行緒的Java虛擬機堆疊容量可以在執行緒創建的時候獨立選定,如果執行緒請求分配的堆疊容量超過Java虛擬機堆疊允許的最大容量,Java虛擬機將拋出一個StackOverflowError例外,
  • 如果Java虛擬機堆疊可以動態擴展,并且在嘗試擴展的時候無法申請到足夠的記憶體,或者在創建新的執行緒時沒有足夠的記憶體去創建對應的虛擬機堆疊,那Java虛擬機將會拋出一個OutOfMemoryError例外,

堆疊沒有GC

設定堆疊記憶體大小
我們可以使用引數-Xss(stack size)選項來設定執行緒的最大堆疊空間,堆疊的大小直接決定了函式呼叫的最大可達深度,
默認值:
Linux/x64(64-bit):1024KB
macOs(64-bit):1024KB

JVM直接對Java堆疊的操作只有兩個,就是對堆疊楨的壓堆疊和出堆疊,遵循先進后出原則,

在一潭訓動執行緒中,一個時間點上,只會有一個活動的堆疊楨,即只有當前正在執行的方法的堆疊楨(堆疊頂堆疊楨)是有效的,這個堆疊楨被稱為當前堆疊楨,與當前堆疊楨對應的方法就是當前方法,定義這個方法的類就是當前類

Java方法有兩種回傳函式的方式,一種是正常的函式回傳,使用return指令,另一種是拋出例外,不管使用哪種方式,都會導致堆疊楨被彈出.

堆疊楨內部結構:

區域變數表(Local Variables)

區域變數表存放了編譯期可知的各種基本資料型別(boolean、byte、char、short、int、float、long、double)、物件參考(reference型別,他不等同于物件本身,可能是一個指向物件起始地址的參考指標,也可能是指向一個代表物件的句柄或其他與此物件相關的位置)和returnAddress型別(指向了一條位元組碼指令的地址),

由于區域變數表是建立在執行緒的堆疊上,是執行緒的私有資料,因此不存在資料安全問題,

區域變數所需的容量大小是在編譯期確定下來的,并保存在方法的Code屬性的maximum local variables資料項中,在方法運行期間是不會改變區域變數表的大小的,

其中64位長度的long和double型別的資料會占用2個Slot,其余的資料型別只占用1個Slot,區域變數所需的記憶體空間在編譯期間分配完成,當進入一個方法時,這個方法需要在幀中分配多大的區域變數空間是完全確定的,在方法運行期間不會改變區域變數表的大小,
注意:虛擬機堆疊的大小會變,因為不停的創建和銷毀堆疊幀,還有別的操作都會改變虛擬機堆疊大小,

區域變數表最基本的存盤單元是Slot(變數槽),32位一個變數槽

JVM會為區域變數表中的每一個Slot都分配一個訪問索引,通過這個索引即可成功訪問到區域變數表中的指定區域變數值,(如果占兩個槽,使用起始索引)

補充:0槽位放this,所以從1開始

注意:區域變數必須顯式賦值

1)boolean——1byte 0為false 非0為true
2)byte——1 byte
3)short——2 bytes
4)int——4 bytes
5)long——8 bytes
6)float——4 bytes
7)double——8 bytes
8)char——2 bytes

運算元堆疊(Operand Stack)

在方法執行程序中,根據位元組碼指令,往堆疊中寫入資料(ipush)或提取資料(iload),即入堆疊/出堆疊,

注意:這里堆疊不是指堆疊楨,指的是運算元堆疊

作用:用于保存計算程序的中間結果,同時作為計算程序中變數臨時的存盤空間,

某些位元組碼指令值壓入運算元堆疊,其余的位元組碼指令將運算元取出堆疊,使用它們后再把結果壓入堆疊,

動態連接(Dynamic Linking)

方法回傳地址(Return Address)

存盤呼叫該方法的程式計數器的值
無論通過哪種方式退出,在方法退出后都回傳到該方法被呼叫的位置,方法正常退出時,呼叫者的程式計數器的值作為回傳地址,而通過例外退出的,回傳地址是要通過例外表來確定,堆疊楨中一般不會保存這部分資訊,

一些附加資訊

在Java虛擬機規范中,對這個區域規定了兩種例外狀況:如果執行緒請求的堆疊深度大于虛擬機所允許的深度,將拋出StackOverflowError例外(死回圈遞回);如果虛擬機堆疊可以動態擴展(當前大部分的Java虛擬機都可以動態擴展,只不過Java虛擬機規范中也允許固定長度的虛擬機堆疊),如果擴展時無法申請到足夠的記憶體,就會拋出OutOfMemoryError例外,

遞回死回圈呼叫,會一直創建堆疊幀,從而導致堆疊記憶體溢位,假如我們不限制堆疊深度,無法申請到足夠的記憶體就會拋出記憶體溢位

方法中定義區域變數是否執行緒安全?

  • 內部定義內部消亡,是執行緒安全的,
  • 內部產生,但是沒有在內部消亡,回傳到方法外,這是執行緒不安全的,(逃逸)

3.本地方法堆疊(執行緒私有)

本地方法堆疊與虛擬機堆疊所發揮的作用是非常相似的,它們之間的區別不過是虛擬機堆疊為虛擬機執行Java方法服務,而本地方法堆疊則為虛擬機使用到的Native方法服務,在虛擬機規范中對本地方法堆疊中方法使用的語言、使用方式與資料結構沒有強制規定,因此具體的虛擬機可以自由實作它,甚至有的虛擬機(Sun公司的HotSpot虛擬機)直接把本地方法堆疊和虛擬機堆疊合二為一,與虛擬機堆疊一樣,本地方法堆疊也會拋出StackOverflowError和OutOfMemoryError例外

4.Java堆

在虛擬機啟動時創建,其空間大小也就確定了,是JVM管理的最大一塊記憶體空間(堆記憶體的大小可調節),此記憶體區域的唯一目的就是存放物件實體,幾乎所有的物件實體都在這里分配記憶體,
注意:這里不是所有的物件實體都分配在堆記憶體,

Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC堆”(Garbage Collected Heap),從記憶體回收的角度來看,由于現在收集器基本都采用分代收集演算法,所以Java堆中還可以細分為:新生代和老年代,再細致一點的有Eden空間(伊甸園)、From Survivor(幸存者0區)空間、To Survivor(幸存者1區)空間等,從記憶體分配角度來看,執行緒共享的Java堆中可能劃分多個執行緒私有的分配緩沖區(Thread Local Allocation Buffer,TLAB),不過無論如何劃分,都不會改變Java堆中存盤內容的共性,無論是哪個區域,存盤的都仍然是物件實體,進一步劃分目的是為了更好地回收記憶體,或者更快地分配記憶體,

根據Java虛擬機規范的規定,Java堆可以處于物理上不連續的記憶體空間,只要邏輯上連續即可,就像我們的磁盤空間一樣,在實作時,既可以實作成固定大小的,也可以是可擴展的,不過目前主流的虛擬機都是按照可擴展的來實作的,通過-Xmx和-Xms控制,如果在堆中沒有記憶體完成實體分配,并且堆也無法再擴展時,將會拋出OutOfMemoryError例外(OutOfMemoryError: Java Heap space),

-XX:SurvivorRatio,設定新生代中Eden和S0/S1空間的比例,默認-XX:SurvivorRatio=8,Eden:S0:S1=8:1:1,假設設定成-XX:SurvivorRatio=4,Eden:S0:S1=4:1:1
總結:SurvivorRatio值就是設定Eden區的比例占多少,S0/S1相同

-XX:NewRatio,配置年輕代與老年代堆結構的占比,默認-XX:NewRatio=2新生代占1,老年代占2,年輕代占整個堆的1/3,假如-XX:NewRatio=4新生代占1,老年代占4,年輕代占整個堆的1/5
總結:NewRatio值就是設定老年代的占比,剩下的1給新生代

(1)new的物件先放伊甸園區,此區有大小限制
(2)當伊甸園的空間填滿時,程式又需要創建物件,JVM的垃圾回收器將對伊甸園區進行垃
圾回收(Minor GC), 將伊甸園區中的不再被其他物件所參考的物件進行銷毀,再加載新的物件放到伊甸園區
在這里插入圖片描述
(3)然后將伊甸園中的剩余物件移動到幸存者0區
在這里插入圖片描述
(4)如果再次觸發垃圾回收,【本次存活物件】 和 【上次幸存下來的放到幸存者0區的且本次沒有被回收的物件】,都會被放到幸存者1區,

(5)如果再次經歷垃圾回收,此時會重新放回幸存者0區,接著再去幸存者1區(form/to)
在這里插入圖片描述

(6)啥時候能去養老區呢?可以設定次數,默認是15次,
補充:可以設定引數: -XX:MaxTenuringThreshold=進行設定,

針對幸存者 S0,S1區總結:復制之后有交換,誰空誰是 to
關于垃圾回收:頻繁在新生區收集,很少在養老區收集,幾乎不在永久區/元空間收集

注意:Eden 區滿時,會觸發 Minor GC,此時會回收 Eden 區和幸存者區,但是幸存者區滿了不會觸發Minor GC,那怎么辦?

當Survivor空間不足以容納一次 Minor GC之后存活的物件時,就需要依賴其他記憶體區域(實際上大多就是老年代) 進行分配擔保(Handle Promotion), (Serial、ParNew等新生代收集器均采用這種策略來設計新生代的記憶體布局)(來源JVM深入理解虛擬機)

記憶體的分配擔保就好比我們去銀行借款,如果我們信譽很好,在98%的情況下都能按時償還,于是銀行可能會默認我們下一次也能按時按量地償還貸款,只需要一個擔保人能保證如果我不能還款時,可以從他的賬戶扣錢,那銀行就認為沒有風險了,記憶體的分配擔保也一樣,如果另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活物件時,這些物件將直接通過分配擔保機制進入老年代,這對虛擬機來說就是安全的,
在這里插入圖片描述

如果Survivor 區中相同年齡的存活物件大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的物件可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡,
理由:相同年齡物件占Survivor空間的一半,每次復制演算法都要從form到to,非常耗時

堆區是執行緒共享區域,任何執行緒都可以訪問到堆區中的共享資料

為什么有TLAB(Thread Local Allocation Buffer)?

  • 由于物件實體的創建在JVM中非常頻繁,因此在并發環境下從堆區中劃分記憶體空間是執行緒不安全的
  • 為避免多個執行緒操作同一地址,需要使用加鎖等機制,進而影響分配速度,

什么是TLAB(Thread Local Allocation Buffer)?

  • 從記憶體模型而不是垃圾收集的角度,對Eden區域繼續進行劃分,JVM為每個執行緒分配了一個私有快取區域,它包含在Eden空間內,
  • 多執行緒同時分配記憶體時,使用TLAB可以避免一系列的非執行緒安全問題,同時還能夠提升記憶體分配的吞吐量,因此我們可以將這種記憶體分配方式稱之為快速分配策略,
  • 據我所知所有OpenJDK衍生出來的JVM都提供了TLAB的設計,

①盡管不是所有的物件實體都能夠在TLAB中成功分配記憶體,但JVM確實是將TLAB作為記憶體分配的首選,
②在程式中,開發人員可以通過選項“-Xx :UseTLAB” 設定是否開啟TLAB空間,
③默認情況下,TLAB空間的記憶體非常小,僅占有整個Eden空間的1%,當然我們可以通
過選項“-XX:TLABWasteTargetPercent”設定TLAB空間所占用Eden空間的百分比大小,
④一旦物件在TLAB空間分配記憶體失敗時,JVM就會嘗試著通過使用加鎖機制確保資料操
作的原子性,從而直接在Eden空間中分配記憶體,

一個JVM實體只存在一個堆記憶體, 堆記憶體的大小是可以調節的, 類加載器讀取了類檔案后,需要把類、方法、常變數放到堆記憶體中,保存所有參考型別的真實資訊,以方便執行器執行,
堆記憶體邏輯上分為三部分:新生代+老年代+元資料(JDK8)
新生代包含:伊甸園區、幸存0區、幸存1區

堆:
優點:運行時的資料區,可以動態的分配記憶體大小,生存期也不必事先告訴編譯器,因為它是運行時動態分配記憶體空間,垃圾收集器會自動收走不再使用的資料
缺點:運行時動態時分配記憶體空間,因此存取速度慢些

堆疊:(執行緒私有)
優點:存取速度比堆快,僅次于計算機里的暫存器,堆疊的資料可以共享,
缺點:大小和生存期是確定的,缺乏靈活性,

物件的參考存放在堆疊中,物件本身存放在堆中,

5.方法區

方法區(Method Area)是各個執行緒共享的記憶體區域,它用于存盤已被虛擬機加載的型別資訊(類的版本、欄位、方法、介面)、常量、靜態變數、即時編譯器編譯后的代碼快取等資料,雖然《Java虛擬機規范》中把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫作“非堆”(Non Heap),目的是與Java堆區分開來,

例如java核心java,會加載到方法區

(1)堆疊、堆、方法區關系
在這里插入圖片描述
雖然《Java虛擬機規范》中把方法區描述為堆的一個邏輯部分,但一些簡單的實作可能不會選擇去進行垃圾收集或者進行壓縮,”但對于HotSpot JVM而言,方法區還有一個別名叫做Non-Heap (非堆),目的就是要和堆分開,

(2)方法區基本理解:

  • 方法區(Method Area) 與Java堆一樣,是各個執行緒共享的記憶體區域,
  • 方法區在JVM啟動的時候被創建,并且它的實際的物理記憶體空間中和Java堆區一樣都可以是不連續的,
  • 方法區的大小,跟堆空間一樣,可以選擇固定大小或者可擴展
  • 方法區的大小決定了系統可以保存多少個類,如果系統定義了太多的類,導致方法區
    溢位,虛擬機同樣會拋出記憶體溢位錯誤: java.lang .OutOfMemoryError: PermGen space 或者java.lang.OutOfMemoryError: Metaspace,
  • 關閉JVM就會釋放這個區域的記憶體,

(3)Hotspot中方法區的演進
在JDK7及之前,習慣上把方法區稱為永久代,JDK8開始,使用元空間取代了永久代
補充:可以把方法區理解為Java介面,永久代是Java介面實作類

本質上,方法區和永久代并不等價,僅是對HotSpot而言,《Java虛擬機規范》對如何實作方法區,不做統一要求,例如:BEA JRockit / IBM J9 中不存在永久代的概念,

現在看來,當年使用永久代,不是好的idea,導致Java程式更容易OOM(超過-XX:MaxPermSize上限)

到了JDK8時,HotSpot終于完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地記憶體中實作的元空間(Metaspace)來代替

元空間的本質和永久代類似,都是對JVM規范中方法區的實作,不過元空間與永久代最大的區別在于:元空間不在虛擬機設定的記憶體中,而是使用本地記憶體,

根據《Java虛擬機規范》的規定,如果方法區無法滿足新的記憶體分配需求時,將拋出OOM例外(OutOfMemoryError: Metaspace)

當Oracle收購BEA獲得了JRockit的所有權后,準備把JRockit中的優秀功能,譬如Java Mission Control管理工具,移植到HotSpot虛擬機時,但因為兩者對方法區實作的差異而面臨諸多困難,考慮到HotSpot未來的發展,在JDK 6的時候HotSpot開發團隊就有放棄永久代,逐步改為采用本地記憶體(Native Memory)來實作方法區的計劃了,到了JDK 7的HotSpot,已經把原本放在永久代的字串常量池、靜態變數等移出,而到了JDK8,終于完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地記憶體中實作的元空間(Metaspace) 來代替,把JDK 7中永久代還剩余的內容(主要是型別資訊)全部移到元空間中,

(4)設定方法區大小
方法區的大小不必是固定的,JVM可以根據應用的需要動態調整
①JDK7及之前:
通過-XX:PermSize來設定永久代初始分配空間,默認值是20.75M
-XX:MaxPermSize來設定永久代最大可分配空間,32位機器默認是64M,64機器默認是82M
當JVM加載的類資訊容量超過了這個值,會報例外OutOfMemoryError: PermGenspace
②JDK8及以后:
元資料區大小可以使用引數-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定,替代上述原有的兩個引數

默認值依賴于平臺,Windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即沒有限制
補充:-XXMaxMetaspaceSize一般不會改

與永久代不同,如果不指定大小,默認情況下,虛擬機會耗盡所有的可用系統記憶體,如果元資料區發生溢位,虛擬機一樣會拋出例外OutOfMemoryError: Metaspace

-XX:MetaspaceSize: 設定初始的元空間大小,對于一個64位的服務器端JVM來說,其默認的-XX:MetaspaceSize值為21MB,這就是初始的高水位線,一旦觸及這個水位線,Full GC將會被觸發并卸載沒用的類( 即這些類對應的類加載器不再存活) ,然后這個高水位線將會重置,新的高水位線的值取決于GC后釋放了多少元空間,如果釋放的空間不足,那么在不超過MaxMetaspaceSize時,適當提高該值,如果釋放空間過多,則適當降低該值,

如果初始化的高水位線設定過低,上述高水位線調整情況會發生很多次,通過垃圾回收器的日志可以觀察到Full GC多次呼叫,為了避免頻繁地GC,建議將-XX :MetaspaceSize設定為一個相對較高的值,

JDK1.6及之前:有永久代,靜態變數存放在永久代上
JDK1.7:有永久代,但已經逐步“去永久代”,字串常量池、靜態變數移除,保存在堆中
JDK1.8及之后:無永久代,型別資訊、欄位、方法、常量保存在本地記憶體的元空間,但字串常量池、靜態變數仍在堆

6.運行時常量池

運行時常量池(Runtime Constant Pool)是方法區的一部分,Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池,用于存放編譯期生成的各種字面量和符號參考,這部分內容將在類加載后進入方法區的運行時常量池中存放,

Java虛擬機對Class檔案每一部分(自然也包括常量池)的格式都有嚴格的規定,每一個位元組用于存盤哪種資料都必須符合規范上的需求才會被虛擬機認可、裝載和執行,但對于運行時常量池,Java虛擬機規范沒有做任何細節的要求,不同的提供商實作的虛擬機可以按照自己的需要來實作這個記憶體區域,不過,一般來說,除了保持Class檔案中描述的符號參考外,還會把翻譯出來的直接參考也存盤在運行時常量池中,

運行時常量池相對于Class檔案常量池的另外一個重要特征是具備動態性,Java語言并不要求常量一定只有編譯期才能產生,也就是并非預置入Class檔案中常量池的內容才能進入方法區運行時常量池,運行期間也能將新的常量放入池中,這種特性被開發人員利用得比較多的便是String類的intern()方法,
補充:String.intern():用來回傳常量池中的某字串,如果常量池中已經存在該字串,則直接回傳常量池中該物件的參考,否則,在常量池中加入該物件,然后回傳參考,
案例:String a = “abc”; String b = new String(‘abc’), a==b.intern();//true

既然運行時常量池也是方法區的一部分,自然受到方法區記憶體的限制,當常量池無法再申請到記憶體時會拋出OutOfMemoryError例外,

7.直接記憶體

直接記憶體(Direct Memory)并不是虛擬機運行時資料區的一部分,也不是Java虛擬機規范中定義的記憶體區域,但是這部分記憶體也被頻繁地使用,而且也可能導致OutOfMemoryError例外出現,

JDK1.4中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(Buffer)的I/O方式,它可以使用Native函式庫直接分配堆外記憶體,然后通過一個存盤在Java堆中的DirectByteBuffer物件作為這塊記憶體的參考進行操作,這樣能在一些場景中顯著提高性能,因為避免了在Java堆和Native堆中來回復制資料,

顯然,本機直接記憶體的分配不會受到Java堆大小的限制,但是,既然是記憶體,肯定還是會受到本機總記憶體(包括RAM以及SWAP區或者分頁檔案)大小以及處理器尋址空間的限制,服務器管理員在配置虛擬機引數時,會根據實際記憶體設定-Xmx等引數資訊,但經常忽略直接記憶體,使得各個記憶體區域總和大于物理記憶體限制(包括物理的和作業系統級的限制),從而導致動態擴展時出現OutOfMemoryError例外

三、物件記憶體布局

在HotSpot虛擬機中,物件在記憶體中存盤的布局可以分為3塊區域:物件頭(Header)、實體資料(Instance Data)和對齊填充(Padding)

在這里插入圖片描述

1.物件頭

物件頭,HotSpot虛擬機的物件頭包括兩部分資訊,

  • 第一部分用于存盤物件自身的運行時資料,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等,
  • 另一部分是型別指標,即物件指向它的類元資料的指標,虛擬機通過這個指標來確定這個物件是哪個類的實體,并不是所有的虛擬機實作都必須在物件資料上保留型別指標,換句話說,查找物件的元資料資訊并不一定要經過物件本身,

另外,如果物件是一個Java陣列,那么物件頭中還必須有一塊用于記錄陣列長度的資料,因為虛擬機可以通過普通Java物件的元資料資訊確定Java物件的大小,但是從陣列的元資料中卻無法確定陣列的大小,

在這里插入圖片描述

2.資料實體

資料實體:是物件真正存盤的有效資訊,也是在程式代碼中所定義的各種型別的欄位內容,無論是從父類繼承下來的,還是在子類定義的,都需要記錄,這部分的存盤順序會受到虛擬機分配策略引數和欄位在Java原始碼中的定義順序的影響,HotSpot虛擬機默認的分配策略為longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),從策略分配中可以看出,相同寬度的欄位總是被分配到一起,在滿足這個前提條件的情況下,在父類中定義的變數會出現在子類之前,如果CompactFields引數值為true(默認為true),那么子類之中較窄的變數可能會插入到父類的空隙之中,

3.對齊填充

對齊填充:對齊填充并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用,由于HotSpot VM的自動記憶體管理系統要求物件起始地址是8位元組的整數倍,換句話說,就是物件的大小必須是8位元組的整數倍,而物件頭部分正好是8位元組的倍數,因此,當物件實體資料部分沒有對齊時,就需要通過對齊填充來補全,

四、物件訪問定位

JVM是如何通過堆疊楨中的物件參考訪問到其內部的物件實體的呢?
在這里插入圖片描述

建立物件是為了使用物件,我們的Java程式需要通過堆疊的reference資料來操作堆上的具體物件,由于reference型別在Java虛擬機規范中只規定了一個指向物件的參考,并沒有定義這個參考通過何種方式去定位、訪問堆中的物件的具體位置,所以物件訪問方式也是取決于虛擬機實作而定的,目前主流的訪問方式有使用句柄和直接指標兩種

物件訪問的兩種方式:
(1)句柄訪問
如果使用句柄訪問的話,那么Java堆中將劃分出一塊記憶體來作為句柄池,reference中存盤的就是物件的句柄地址,而句柄中包含了物件實體資料與型別資料各自的具體地址資訊,
在這里插入圖片描述

補充:使用句柄方式需要保存到物件實體資料的指標和到物件型別資料的指標,

(2)直接指標

如果使用直接指標訪問,那么Java堆物件的布局中就必須考慮如何放置訪問型別資料的相關資訊,而reference中存盤的直接就是物件地址,
在這里插入圖片描述

使用直接指標方式,需要保存到物件型別資料的指標就行,

這兩種物件訪問方式各有優勢,使用句柄來訪問的最大好處就是reference中存盤的是穩定的句柄地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時只會改變句柄中的實體資料指標,而reference本身不需要修改,使用直接指標訪問方式的最大好處就是速度更快,它節省了一次指標定位的時間開銷,由于物件的訪問在Java中非常頻繁,因此這類開銷積少成多后也是一項非常可觀的執行成本,HotSpot使用的就是直接指標訪問,

五、如何判定物件為垃圾物件?

1.參考計數演算法

給物件中添加一個參考計數器,每當有一個地方參考它時,計數器值加1,當參考失效時,計數器值減1,任何時刻計數器為0的物件就是不可能再被使用的,
參考失效:把物件參考賦值null

優點: 參考計數演算法的實作簡單,判斷效率也很高,在大部分情況下它都是一個不錯的演算法

缺點:

  • 它需要單獨的欄位存盤計數器,這樣的做法增加了存盤空間的開銷,
  • 每次賦值都需要更新計數器,伴隨著加法和減法操作,這增加了時間開銷,
  • 它很難解決物件之間相互回圈參考的問題,因此主流的Java虛擬機里沒有選用參考計數演算法來管理記憶體,

舉例:假設p.next = A, A.next = B, B.next=C,C.next = A,此時計數A是2,B是1,C是1,然后p.next=null,此時計數A是1,B是1,C是,此時會導致A、B、C不會被回收

在這里插入圖片描述

垃圾回收詳細日志資訊:-XX:+PrintGCDetails

public class ReferenceCoatingGC {
    public Object instance = null;
    private static final int _1MB = 1024 * 1024;
 	
    /**
      * 這個成員屬性的唯一意義就是占記憶體,以便能在GC日志中看清楚是否被回收過
      */
    private byte[] bigSize = new byte[2 * _1MB];
    public static void testGC() {
        ReferenceCoatingGC objA = new ReferenceCoatingGC();
        ReferenceCoatingGC objB = new ReferenceCoatingGC();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
        // 假設在這行發生GC,objA和objB
        System.gc();
    }
 	
    public static void main(String[] args) {
        testGC();
    }
}

從上面運行結果可以看出,虛擬機并沒有因為兩個物件互相參考就不回收它們,這也從側面說明虛擬機并不是通過參考計數演算法來判斷物件是否存活的,

2.可達性分析演算法

根節點列舉:所有收集器在根節點列舉這一步驟時都是必須暫停用戶執行緒的,即便號稱停頓時間可控或幾乎不會發生停頓的CMS、G1等收集器,在這一步驟也會暫停用戶執行緒

執行效率比參考計演算法低一點,但是可以解決回圈參考問題

這個演算法的基本思路就是通過一系列的稱為‘GC Roots’的物件作為起始點,從這些節點開始向下搜索,搜索所走的路徑稱為參考鏈,當一個物件到GC Roots沒有任何參考鏈相連(用圖論的話來說,就是從GC Roots到這個物件不可達)時,則證明此物件是不可用的,如圖,物件object5、object6、object7雖然互相有關聯,但是它們到GC Roots是不可達的,所以它們將會被判定為是可回收的物件,
在這里插入圖片描述

假如給object1賦值null,那么object1、object12、object3、object4全部變為可回收物件

總結:基本思路就是通過一系列名為”GC Roots"的物件作為起始點,從這個被稱為GC Roots的物件開始向下搜索,如果一個物件到GC Roots沒有任何參考鏈相連時,則說明此物件不可用,也即給定一個集合的參考作為根出發,通過參考關系遍歷物件圖,能被
遍歷到的(可到達的)物件就被判定為存活;沒有被遍歷到的就自然被判定為死亡,

Java中,可作為GC Roots的物件包括下面幾種:
1)虛擬機堆疊(堆疊幀中的本地變數表)中參考的物件(t1)
2)方法區中類靜態屬性參考的物件(t2)
3)方法區中常量參考的物件(t3)
4)本地方法堆疊中JNI(即一般說的Native方法)參考的物件

public class GCRootDemo {
    
    private static GCRootDemo t2 = new GCRootDemo();
    
    private static final GCRootDemo t3 = new GCRootDemo();
    
    public static void main(String[] args) {
        m1();
    }

    private static void m1() {
        GCRootDemo t1 = new GCRootDemo();
        System.gc();
        System.out.println("第一次GC完成");
    }
}

六、參考

無論是通過參考計數演算法判斷物件的參考數量,還是通過可達性分析演算法判斷物件的參考鏈是否可達,判斷物件是否存活都與‘參考’有關,在JDK1.2以前,Java 中的參考的定義很傳統,如果reference型別的資料中存盤數值代表的是另外一塊記憶體的起始地址,就稱這塊記憶體代表著一個參考,這種定義很純粹,但是太過狹隘,一個物件在這種定義下就只有被參考或者沒有被參考兩種狀態了,對于如何描述一些‘食之無味,棄之可惜’的物件就顯得無能為力,我們希望能描述這樣一類物件:當存盤空間還足夠時,則能保留在記憶體之中;如果存盤空間在進行垃圾收集后還是非常緊張,則可以拋棄這些物件,很多系統的快取功能都符合這樣的應用場景,

在JDK1.2后,Java對參考的概念進行了擴充,將參考分為強參考、軟參考、弱參考、虛參考4種,這4種參考強度依次逐漸減弱,

  • 強參考:強參考就是指在程式代碼之中普遍存在的,類似Object obj = new Object()這類的參考,只要強參考還存在,垃圾收集器永遠不會回收掉被參考的物件,
  • 軟參考:軟參考是用來描述一些還有用但并非必要的物件,對于軟參考關聯著的物件,在系統將要發生記憶體溢位例外之前,將會把這些物件列進回收范圍之中進行第二次回收,如果這次回識訓沒有足夠的記憶體,才會拋出記憶體溢位例外,在JDK1.2之后,提供了SoftReference類來實作軟參考,
  • 弱參考:弱參考也是用來描述非必需物件的,但是它的強度比軟參考更弱一些,被弱參考關聯的物件只能生存到下一次垃圾收集發生之前,當垃圾收集器作業時,無論當前記憶體是否足夠,都會回收掉只被弱參考關聯的物件,在JDK1.2后,提供了WeakReference類來實作弱參考
  • 虛參考:虛參考也稱為幽靈或者幻影參考,它是最弱的一種參考關系,一個物件是否有虛參考的存在,完全不會對其生存時間構成影響,也無法通過虛參考來取得一個物件實體,為一個物件設定虛參考關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知,在JDK1.2之后,提供PhantomReference類來實作虛參考

生存還是死亡:即使在可達性分析演算法中不可達的物件,也并非是’非死不可’的,這時候它們暫時處于‘緩刑’階段,要真正宣告一個物件死亡,至少要經歷兩次標記程序:如果物件在進行可達性分析后發現沒有GC Roots相連接的參考鏈,那它將會被第一次標記并且進行一次篩選,篩選的條件是此物件是否有必要執行finalize()方法,當物件沒有覆寫finalize()方法,或者finalize()方法已經被虛擬機呼叫過,虛擬機將這兩種情況都視為‘沒有必要執行’,

如果這個物件被判定為有必要執行finalize()方法,那么這個物件將會放置在一個叫做F-Queue的佇列中,并在稍后由一個由虛擬機自動建立、低優先級的Finalizer執行緒去執行它,這里所謂的’執行’是指虛擬機會觸發這個方法,但并不承諾會等待它運行結束,這樣做的原因是,如果一個物件在finalize()方法中執行緩慢,或者發生了死回圈(更極端的情況),將很可能會導致F-Queue佇列中其他物件永久處于等待,甚至導致整個記憶體回收系統崩潰,finalize()方法是物件逃脫死亡命運的最后一次機會,稍后GC將對F-Queue中的物件進行第二次小規模的標記,如果物件在finalize()中成功拯救自己-------只要重新與參考鏈上的任何一個物件建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變數或者物件的成員變數,那么在第二次標記時它將被移除‘即將回收’的集合,如果物件這時候還沒有逃脫,那基本上它就真的被回收了,

總結:物件可以在被GC時自我拯救,但是這種自救的機會只有一次,因為一個物件的finalize()方法最多只會被系統自動呼叫一次,不建議使用這種方法拯救物件,它的運行代價高昂,不確定性大,無法保證各個物件呼叫順序,

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
 	
    public void isAlive() {
 	System.out.println("yes,i am still alive");
    }
 	
    @Override
    protected void finalize() throws Throwable {
 	super.finalize();
 	System.out.println("finalize method executed!");
 	FinalizeEscapeGC.SAVE_HOOK = this;
    }
 	
    public static void main(String[] args) {
 	SAVE_HOOK = new FinalizeEscapeGC();
 		
 	// 物件第一次成功拯救自己
 	SAVE_HOOK = null;
 	System.gc();
 	try {
 	    // 因為finalize方法優先級低,所以暫停0.5秒等待它
 	    Thread.sleep(500);
 	    if(null != SAVE_HOOK) {
 		SAVE_HOOK.isAlive();
 	    }else {
 		System.out.println("no i am dead");
 	    }
 	} catch (InterruptedException e) {
 	    e.printStackTrace();
 	}
 		
    // 物件第二次自救,自救失敗
 	SAVE_HOOK = null;
 	System.gc();
 	try {
 	    // 因為finalize方法優先級低,所以暫停0.5秒等待它
 	    Thread.sleep(500);
 	    if(null != SAVE_HOOK) {
 		SAVE_HOOK.isAlive();
 	    }else {
 		System.out.println("no i am dead");
 	    }
        } catch (InterruptedException e) {
 	    e.printStackTrace();
 	}
    }
}

執行結果:
finalize method executed!
yes,i am still alive
no i am dead

七、如何回收垃圾物件?

1.垃圾收集演算法

記憶體回收的方法論,垃圾收集器是方法論的落地實作

JVM中比較常見的三種垃圾收集演算法:

標記-清除演算法

標記無用物件,然后進行清除回收,

標記-清除演算法(Mark-Sweep)是一種常見的基礎垃圾收集演算法,它將垃圾收集分為兩個階段:

  • 標記階段:標記出可以回收的物件,
  • 清除階段:回收被標記的物件所占用的空間,

標記-清除演算法之所以是基礎的,是因為后面講到的垃圾收集演算法都是在此演算法的基礎上進行改進的,

優點:實作簡單,不需要物件進行移動,

缺點:標記、清除程序效率低,產生大量不連續的記憶體碎片,提高了垃圾回收的頻率,

  • 效率問題,標記和清除兩個程序的執行效率都隨物件數量增長而降低(標記時通過GC Root遞回遍歷可達物件,清除時需要遍歷所有堆空間的物件),
  • 空間問題,標記清除之后會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以后在程式運行程序中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作,
  • 這種方式清理出來的空閑記憶體是不連續的,產生記憶體碎片,需要維護一個空閑串列,

標記-清除演算法的執行程序如圖所示:
在這里插入圖片描述
標記清除之后會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以后在程式運行程序中需要分配較大物件時,在找合適記憶體空間的時候也比較耗時,

需要暫停用戶執行緒,執行標記和清除操作

復制演算法

為了解決標記-清除演算法的效率不高的問題,產生了復制演算法,它把記憶體空間劃為兩個相等的區域,每次只使用其中一個區域,垃圾收集時,遍歷當前使用的區域,把存活物件復制到另外一個區域中,最后將當前使用的區域的可回收的物件進行回收,

復制演算法的執行程序如圖所示:
在這里插入圖片描述
現在的商業虛擬機都采用這種收集演算法來回收新生代,IBM公司的專門研究表明,新生代中的物件98%是’朝生暮死’的,所以不需要按照1:1比例來劃分記憶體空間,回收的具體做法是把新生代分為一塊較大的Eden空間和兩塊較小的Survivor空間, 每次分配記憶體只使用Eden和其中一塊Survivor,發生垃圾收集時,將Eden和Survivor中仍然存活的物件一次性 復制到另外一塊Survivor空間上,然后直接清理掉Eden和已用過的那塊Survivor空間,HotSpot 虛擬機默認Eden和Survivor的大小比例是 8:1,也即每次新生代中可用記憶體空間為 整個新生代容量的90% (Eden的80%加上 一個Survivor的10%),只有一個 Survivor空間,即10%的新生代是會被 “浪費”的,當然,98%的物件可被回收僅僅是“普通場景”下測得的資料,任何人都沒有辦法百分百保證每次回收都只有不多于10%的物件存活,因此這種回收方式還有一個充當罕見情況的“逃生門” 的安全設計,當Survivor空間不足以容納一次 Minor GC之后存活的物件時,就需要依賴其他記憶體區域(實際上大多就是老年代) 進行分配擔保(Handle Promotion), (Serial、ParNew等新生代收集器均采用這種策略來設計新生代的記憶體布局)

記憶體的分配擔保就好比我們去銀行借款,如果我們信譽很好,在98%的情況下都能按時償還,于是銀行可能會默認我們下一次也能按時按量地償還貸款,只需要一個擔保人能保證如果我不能還款時,可以從他的賬戶扣錢,那銀行就認為沒有風險了,記憶體的分配擔保也一樣,如果另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活物件時,這些物件將直接通過分配擔保機制進入老年代,這對虛擬機來說就是安全的,

總結:它將堆分為新生代和老年代,新生代又分為Eden空間和兩塊Survivor空間,它們的比例大概是8: 1: 1, 新生代中的物件大多存活率不高,所以我們一般采用復制演算法,每次使用Eden 空間和其中的一塊Survivor空間,當進行回收時, 將該兩塊空間中還存活的物件復制到另一塊 Survivor空間中,每進行一次Minor GC物件的年齡就會加1, 默認達到15就可以進入老年代 (數值可以自己用調優引數設定),Survivor區存放不下的物件,因為每次Minor GC的時候會將Eden區和一個from區的存存活物件放入to區,所以當to區裝不下的物件時就會進入老年代

優點:按順序分配記憶體即可,實作簡單、運行高效,不用考慮記憶體碎片,

缺點:

  • 在物件存活率較高時,復制的物件很多時,效率大大降低
  • 記憶體縮小了一半,需要額外空間做分配擔保(老年代)

標記-整理演算法

在新生代中可以使用復制演算法,但是在老年代就不能選擇復制演算法了,因為老年代的物件存活率會較高,這樣會有較多的復制操作,導致效率變低,標記-清除演算法可以應用在老年代中,但是它效率不高,在記憶體回收后容易產生大量記憶體碎片,因此就出現了一種標記-整理演算法(Mark-Compact)演算法,與標記-整理演算法不同的是,在標記可回收的物件后將所有存活的物件壓縮到記憶體的一端,使他們緊湊的排列在一起,然后對端邊界以外的記憶體進行回收,回收后,已用和未用的記憶體都各自一邊,

優點:解決了標記-清理演算法存在的記憶體碎片問題,

缺點:仍需要進行區域物件移動,一定程度上降低了效率,

“標記- 整理”演算法的執行程序如下圖所示:
在這里插入圖片描述

2.垃圾收集演算法

Serial收集器

在JDK1.3之前,它是虛擬機新生代收集的唯一選擇,Serial 是一個單執行緒的收集器, 它不但只會使用一個 CPU 或一條執行緒去完成垃圾收集作業,并且在進行垃圾收集的同時,必須暫停其他所有的作業執行緒,直到垃圾收集結束,

Serial 垃圾收集器雖然在收集垃圾程序中需要暫停所有其他的作業執行緒,但是它簡單高效,對于限定單個 CPU 環境來說,沒有執行緒互動的開銷,可以獲得最高的單執行緒垃圾收集效率,因此 Serial垃圾收集器依然是 java 虛擬機運行在 Client 模式下默認的新生代垃圾收集器,
在這里插入圖片描述
-XX:UseSerialGC,開啟后會使用Serial(Young區用)+ Serial Old(Old區用)的收集器組合

ParNew收集器

ParNew 垃圾收集器其實是 Serial 收集器的多執行緒版本,也使用復制演算法,除了使用多執行緒進行垃圾收集之外,其余的行為和 Serial 收集器完全一樣, ParNew 垃圾收集器在垃圾收集程序中同樣也要暫停所有其他的作業執行緒,

ParNew 收集器默認開啟和 CPU 數目相同的執行緒數,可以通過-XX:ParallelGCThreads 引數來限制垃圾收集器的執行緒數,

ParNew 雖然是除了多執行緒外和Serial 收集器幾乎完全一樣,但是ParNew垃圾收集器是很多 Java虛擬機運行在 Server 模式下新生代的默認垃圾收集器,

在這里插入圖片描述

并行和并發都是并發編程中的專業名詞,在談論垃圾收集器的背景關系語境中,它們可
以理解為:
①并行(Parallel) :并行描述的是多條垃圾收集器執行緒之間的關系,說明同一時間有多條這樣的執行緒在協同作業,通常默認此時用戶執行緒是處于等待狀態,(指多條垃圾收集執行緒并行作業,但此時用戶執行緒仍然處于等待狀態)
②并發(Concurrent) :并發描述的是垃圾收集器執行緒與用戶執行緒之間的關系,說明同一時間垃圾收集器執行緒與用戶執行緒都在運行,由于用戶執行緒并未被凍結,所以程式仍然能回應服務請求,但由于垃圾收集器執行緒占用了一部分系統資源,此時應用程式的處理的吞吐量將受到一定影響,(指用戶執行緒與垃圾收集執行緒同時執行,但不一定是并行的,可能會交替執行,用戶程式在繼續運行,而垃圾收集程式運行于另一個CPU上)

Parallel Scavenge收集器

Parallel Scavenge 收集器也是一個新生代垃圾收集器,同樣使用復制演算法,也是一個多執行緒的垃圾收集器, 它重點關注的是程式達到一個可控制的吞吐量(Thoughput, CPU 用于運行用戶代碼的時間/CPU 總消耗時間,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)),高吞吐量可以最高效率地利用 CPU 時間,盡快地完成程式的運算任務,主要適用于在后臺運算而不需要太多互動的任務, 自適應調節策略也是 ParallelScavenge 收集器與 ParNew 收集器的一個重要區別,
在這里插入圖片描述
-XX:ParallelGCThreads:設定年輕代并行收集器的執行緒數,

-XX:UseParallelGC、-XX:UseParallelOldGC可互相激活,不管配置哪個,兩個都會開啟,ParallelGC采用復制演算法,ParallelOldGC采用標記-整理演算法

JDK1.8 默認收集器: Parallel Scavenge (新生代) 和 Parallel Old (老年代)

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同樣是一個單執行緒收集器,使用’標記-整理’演算法,這個收集器的主要意義也是在給Client模式下的虛擬機使用,如果在Server模式下,那么它主要還有兩大用途:一種用途是在JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另一種就是作為CMS收集器發生失敗時的后備預案,在并發收集發生Concurrent Mode Failure時使用

在這里插入圖片描述

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多執行緒并發收集,基于’標記-整理’演算法實作,這個收集器是在JDK1.6中才開始提供,在此之前,新生代的Parallel Scavenge收集器一直處于比較尷尬的狀態,原因是,如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old收集器外別無選擇,由于老年代Serial Old收集器在服務端應用性能上的‘拖累’,使用了Parallel Scavenge收集器也未必在整體應用上獲得吞吐量最大化的效果,由于單執行緒的老年代收集中無法充分利用服務器多CPU的處理能力,在老年代很大而且硬體條件比較高級的環境中,這種組合的吞吐量甚至還不一定有ParNew加CMS組合’給力’,

直到Parallel Old收集器出現后,’吞吐量優先’收集器終于有了比較名副其實的應用組合,在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器,Parallel Old收集器的作業程序如圖所示:
在這里插入圖片描述

CMS收集器

CMS (Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器,目前很大一部分的Java應用集中在互聯網網站或者基于瀏覽器的B/S系統的服務端上,這類應用通常都會較為關注服務的回應速度,希望系統停頓時間盡可能短,以給用戶帶來良好的互動體驗,CMS收集器就非常符合這類應用的需求,

從名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于標記-清除演算法實作的,它的運作程序相對于前面幾種收集器來說要更復雜一些,整個程序分為四個步驟,包括:
在這里插入圖片描述①初始標記(CMS initial mark)
②并發標記(CMS concurrent mark)
③重新標記(CMS remark)
④并發清除(CMS concurrent sweep)
其中初始標記、重新標記這兩個步驟仍然需要“Stop The World”,初始標記僅僅只是標記一下GC Roots能直接關聯到的物件,速度很快;并發標記階段就是從GC Roots的直接關聯物件開始遍歷整個物件圖的程序,這個程序耗時較長但是不需要停頓用戶執行緒,可以與垃圾收集執行緒一起并發運行;而重新標記階段則是為了修正并發標記期間,因用戶程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但也遠比并發標記階段的時間短;最后是并發清除階段,清理洗掉掉標記階段判斷的已經死亡的物件,由于不需要移動存活物件,所以這個階段也是可以與用戶執行緒同時并發的,

由于在整個程序中耗時最長的并發標記和并發清除階段中,垃圾收集器執行緒都可以與用戶執行緒一起作業,所以從總體上來說,CMS收集器的記憶體回收程序是與用戶執行緒一起并發執行的,通過圖3-11可以比較清楚地看到CMS收集器的運作步驟中并發和需要停頓的階段,

safepoint是還原點

優點:并發收集低停頓

CMS是一款優秀的收集器,它最主要優點在名字上已經體現出來:并發收集、低停頓,一些官方公開檔案里面也稱之為“并發低停頓收集器”(Concurrent Low Pause Collector) ,CMS收集器是HotSpot虛擬機追求低停頓的第一次成功嘗試,但是它還遠達不到完美的程度,至少有以下三個明顯的缺點:
①首先,CMS收集器對處理器資源非常敏感,事實上,面向并發設計的程式都對處理器資源比較敏感,在并發階段,它雖然不會導致用戶執行緒停頓,但卻會因為占用了一部分執行緒(或者說處理器的計算能力)而導致應用程式變慢,降低總吞吐量,CMS默認啟動的回收執行緒數是(處理器核心數量+3) /4,也就是說,如果處理器核心數在四個或以上,并發回收時垃圾收集執行緒只占用不超過25%的處理器運算資源,并且會隨著處理器核心數量的增加而下降,但是當處理器核心數量不足四個時,CMS對用戶程式的影響就可能變得很大,如果應用本來的處理器負載就很高,還要分出一半的運算能力去執行收集器執行緒,就可能導致用戶程式的執行速度忽然大幅降低,為了緩解這種情況,虛擬機提供了一種稱為“增量式并發收集器’(Incremental Concurrent Mark Sweep / iCMS)的CMS收集器變種,所做的事情和以前單核處理器年代PC機作業系統靠搶占式多任務來模擬多核并行多任務的思想一樣,是在并發標記、清理的時候讓收集器執行緒、用戶執行緒交替運行,盡量減少垃圾收集執行緒的獨占資源的時間,這樣整個垃圾收集的程序會更長,但對用戶程式的影響就會顯得較少一些,直觀感受是速度變慢的時間更多了,但速度下降幅度就沒有那么明顯,實踐證明增量式的CMS收集器效果很一般,從JDK 7開始,iCMS模式已經被宣告為“deprecated”,即已過時不再提倡用戶使用,到JDK 9發布后iCMS模式被完全廢棄,

②然后,由于CMS收集器無法處理“浮動垃圾”(Floating Garbage),有可能出現“Concurrent Mode Failure”失敗進而導致另一次完全 “Stop The World”的Full GC的產生,在CMS的并發標記和并發清理階段,用戶執行緒是還在繼續運行的,程式在運行自然就還會伴隨有新的垃圾物件不斷產生,但這一部分垃圾物件是出現在標記程序結束以后,CMS無法在當次收集中處理掉它們,只好留待下一次垃圾收集時再清理掉,這一部分垃圾就稱為“浮動垃圾”,同樣也是由于在垃圾收集階段用戶執行緒還需要持續運行,那就還需要預留足夠記憶體空間提供給用戶執行緒使用,因此CMS收集器不能像其他收集器那樣等待到老年代幾乎完全被填滿了再進行收集,必須預留一部分空間供并發收集時的程式運作使用,在JDK 5的默認設定下,CMS收集器當老年代使用了68%的空間后就會被激活,這是一個偏保守的設定,如果在實際應用中老年代增長并不是太快,可以適當調高引數-XX: CMSInitiatingOccupancyFraction的值來提高CMS的觸發百分比,降低記憶體回收頻率,獲取更好的性能,到了JDK 6時,CMS收集器的啟動閾值就已經默認提升至92%,但這又會更容易面臨另一種風險:要是CMS運行期間預留的記憶體無法滿足程式分配新物件的需
要,就會出現一次“并發失敗”(Concurrent Mode Failure) ,這時候虛擬機將不得不啟動后備預案:凍結用戶執行緒的執行,臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,但這樣停頓時間就很長了,所以引數-XX: CMSInitiatingOccupancyFraction設定得太高將會很容易導致大量的并發失敗產生,性能反而降低,用戶應在生產環境中根據實際應用情況來權衡設定,

③還有最后一個缺點,在本節的開頭曾提到,CMS是一款基于 “標記-清除”演算法實作的收集器,如果讀者對前面這部分介紹還有印象的話,就可能想到這意味著收集結束時會有大量空間碎片產生,空間碎片過多時,將會給大物件分配帶來很大麻煩,往往會出現老年代還有很多剩余空間,但就是無法找到足夠大的連續空間來分配當前物件,而不得不提前觸發一次Full GC的情況,為了解決這個問題,CMS收集器提供了一個XX: +UseCMSCompactAtFullCollection開關引數(默認是開啟的,此引數從JDK 9開始廢棄),用于在CMS收集器不得不進行Full GC時開啟記憶體碎片的合并整理程序,由于這個記憶體整理必須移動存活物件,(在 Shenandoah和ZGC出現前)是無法并發的,這樣空間碎片問題是解決了,但停頓時間又會變長,因此虛擬機設計者們還提供了另外一個引數- XX: CMSFullGCsBeforeCompaction (此引數從JDK 9開始廢棄),這個引數的作用是要求CMS收集器在執行過若干次(數量由引數值決定)不整理空間的Full GC之后,下一次進入Full GC前會先進行碎片整理(默認值為0,表示每次進入Full GC時都進行碎片整理),

適用場景:CMS非常適合堆記憶體大、CPU核數多的服務器端應用,也是G1出現之前大型應用的首選收集器

開啟該收集器的JVM引數:-XX:+UseConcMarkSweepGC開啟該引數后會自動將 -XX:+UseParNewGC打開

開啟該引數后,使用ParNew(Young區用) + CMS(Old區用) + Serial Old的收集器組合,Serial Old將作為CMS出錯的后備收集器

G1收集器

G1是一款主要面向服務端應用的垃圾收集器,HotSpot開發團隊最初賦予它的期望是(在比較長期的)未來可以替換掉JDK 5中發布的CMS收集器,現在這個期望目標已經實作過半了,JDK 9發布之日,G1宣告取代Parallel Scavenge加Parallel Old組合,成為服務端模式下的默認垃圾收集器,而CMS則淪落至被宣告為不推薦使用(Deprecate) 的收集器,如果對JDK 9及以上版本的HotSpot虛擬機使用引數-XX: +UseConcMarkSweepGC來開啟CMS收集器的話,用戶會收到一個警告資訊,提示CMS未來將會被廢棄,

但作為一款曾被廣泛運用過的收集器,經過多個版本的開發迭代后,CMS (以及之前幾款收集器)的代碼與HotSpot的記憶體管理、執行、編譯、監控等子系統都有千絲萬縷的聯系,這是歷史原因導致的,并不符合職責分離的設計原則,為此,規劃JDK 10功能目標時,HotSpot虛擬機提出了“統一垃圾收集器介面”,將記憶體回收的“行為”與“實作”進行分離,CMS以及其他收集器都重構成基于這套介面的一種實作,以此為基礎,日后要移除或者加入某一款收集器,都會變得容易許多,風險也可以控制,這算是在為CMS退出歷史舞臺鋪下最后的道路了,

作為CMS收集器的替代者和繼承人,設計者們希望做出一款能夠建立起“停頓時間模型”(Pause Prediction Model)的收集器,停頓時間模型的意思是能夠支持指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間大概率不超過N毫秒這樣的目標,這幾乎已經是實時Java (RTSJ) 的中軟實時垃圾收集器特征了,

那具體要怎么做才能實作這個目標呢?首先要有一個思想上的改變,在G1收集器出現之前的所有其他收集器,包括CMS在內,垃圾收集的目標范圍要么是整個新生代(Minor GC),要么就是整個老年代(Major GC),再要么就是整個Java堆(Full GC),而G1跳出了這個樊籠,它可以面向堆記憶體任何部分來組成回收集(Collection Set,一般簡稱CSet) 進行回收,衡量標準不再是它屬于哪個分代,而是哪塊記憶體中存放的垃圾數量最多,回收收益最大,這就是G1收集器的Mixed GC模式,

G1開創的基于Region的堆記憶體布局是它能夠實作這個目標的關鍵,雖然G1也仍是遵循分代收集理論設計的,但其堆記憶體的布局與其他收集器有非常明顯的差異:G1不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的Java堆劃分為多個大小相等的獨立區域(Region) ,每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間, 或者老年代空間,收集器能夠對扮演不同角色的Region采用不同的策略去處理,這樣無論是新創建的物件還是已經存活了一段時間、熬過多次收集的舊物件都能獲取很好的收集效果,

核心思想是將整個堆記憶體區域分成大小相同的子區域(Region),在JVM啟動時會自動設定這些子區域的大小,在堆的使用上,G1并不要求物件的存盤一定是物理上連續的只要邏輯上連續即可,每個磁區也不會固定地為某個代服務,可以按需在年輕代和老年代之間切換,啟動時可以通過引數-XX:G1HeapRegionSize=n可指定磁區大小(1MB~32MB,且必須是2的冪),默認將整堆劃分為2048個磁區,

大小范圍在1MB~32MB,最多能設定2048個區域,也即能夠支持的最大記憶體為: 32MB * 2048 = 65536MB = 64G記憶體

Region中還有一類特殊的Humongous區域,專門用來存盤大物件,G1認為只要大小超過了一個Region容量一半的物件即可判定為大物件,每個Region的大小可以通過引數-XX:G1HeapRegionSize設定,取值范圍為1MB ~ 32MB,且應為2的N次冪,而對于那些超過了整個Region容量的超級大物件,將會被存放在N個連續的Humongous Region之中,G1的大多數行為都把Humongous Region作為老年代的一部分來進行看待,如圖3-12所示,

雖然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它們都是一系列區域(不需要連續)的動態集合,G1收集器之所以能建立可預測的停頓時間模型,是因為它將Region作為單次回收的最小單元,即每次收集到的記憶體空間都是Region大小的整數倍,這樣可以有計劃地避免在整個Java堆中進行全區域的垃圾收集,更具體的處理思路是讓G1收集器去跟蹤各個Region里面的垃圾堆積的“價值”大小,價值即回收所獲得的空間大小以及回收所需時間的經驗值,然后在后臺維護一個優先級串列,每次根據用戶設定允許的收集停頓時間(使用引數-XX:MaxGCPauseMillis指定,默認值是200毫秒),優先處理回收價值收益最大的那些Region,這也就是“Garbage First”名字的由來,這種使用Region劃分記憶體空間,以及具有優先級的區域回收方式,保證了G1收集器在有限的時間內獲取盡可能高的收集效率,
在這里插入圖片描述

針對Eden區進行收集,Eden區耗盡后會被觸發,主要是小區域收集+形成連續的記憶體塊,避免記憶體碎片
*Eden區的資料移動到Survivor區,假如出現Survivor區空間不夠,Eden區資料會部會晉升到Old區
*假如出現Survivor區空間不夠,Survivor區的資料移動到新的Survivor區,部會資料晉升到Old區
*最后Eden區收拾干凈了,GC結束,用戶的應用程式繼續執行,

G1將堆記憶體“化整為零”的“解題思路”,看起來似乎沒有太多令人驚訝之處,也完全不難理解,但其中的實作細節可是遠遠沒有想象中那么簡單,否則就不會從2004年Sun實驗室發表第一篇關于G1的論文后一直拖到2012年4月JDK 7 Update4發布,用將近10年時間才倒騰出能夠商用的G1收集器來,G1收集器至少有(不限于)以下這些關鍵的細節問題需要
妥善解決:
①譬如,將Java堆分成多個獨立Region后,Region 里面存在的跨Region參考物件如何解決?使用記憶集避免全堆作為GC Roots掃描,但在G1收集器上記憶集的應用其實要復雜很多,它的每個Region都維護有自己的記憶集,這些記憶集會記錄下別的Region指向自己的指標,并標記這些指標分別在哪些卡頁的范圍之內,G1的記憶集在存盤結構的本質上是一種哈希表,Key是別的Region的起始地址,Value是一個集合,里面存盤的元素是卡表的索引號,這種“雙向”的卡表結構(卡表是“我指向誰”,這種結構還記錄了“誰指向我”)比原來的卡表實作起來更復雜,同時由于Region數量比傳統收集器的分代數量明顯要多得多,因此G1收集器要比其他的傳統垃圾收集器有著更高的記憶體占用負擔,根據經驗,G1至少要耗費大約相當于Java堆容量10%至20%的額外記憶體來維持收集器作業,

②譬如,在并發標記階段如何保證收集執行緒與用戶執行緒互不干擾地運行?這里首先要解決的是用戶執行緒改變物件參考關系時,必須保證其不能打破原本的物件圖結構,導致標記結果出現錯誤:CMS收集器采用增量更新演算法實作,而G1收集器則是通過原始快照(SATB)演算法來實作的,此外,垃圾收集對用戶執行緒的影響還體現在回收程序中新創建物件的記憶體分配上,程式要繼續運行就肯定會持續有新物件被創建,G1為每一個Region設計了兩個名為TAMS (Top at Mark Start)的指標,把Region中的一部分空間劃分出來用于并發回收程序中的新物件分配,并發回收時新分配的物件地址都必須要在這兩個指標位置以上,G1收集器默認在這個地址以上的物件是被隱式標記過的,即默認它們是存活的,不納入回收范圍,與CMS中“Concurrent Mode Failure” 失敗會導致Full GC類似,如果記憶體回收的速度趕不上記憶體分配的速度,G1收集器也要被迫凍結用戶執行緒執行,導致Full GC而產生長時間“StopThe World”,

③譬如,怎樣建立起可靠的停頓預測模型?用戶通過-XX: MaxGCPauseMillis引數指定的停頓時間只意味著垃圾收集發生之前的期望值,但G1收集器要怎么做才能滿足用戶的期望呢? G1收集器的停頓預測模型是以衰減均值(Decaying Average)為理論基礎來實作的,在垃圾收集程序中,G1收集器會記錄每個Region的回收耗時、每個Region記憶集里的臟卡數量等各個可測量的步驟花費的成本,并分析得出平均值、標準偏差、置信度等統計資訊,這里強調的“衰減平均值”是指它會比普通的平均值更容易受到新資料的影響,平均值代表整體平均狀態,但衰減平均值更準確地代表“最近的”平均狀態,換句話說,Region的統計狀態越新越能決定其回收的價值,然后通過這些資訊預測現在開始回收的話,由哪些Region組成回收集才可以在不超過期望停頓時間的約束下獲得最高的收益,

如果我們不去計算用戶執行緒運行程序中的動作(如使用寫屏障維護記憶集的操作),G1收集器的運作程序大致可劃分為以下四個步驟:
1)初始標記(Initial Marking) :僅僅只是標記一下GC Roots能直接關聯到的物件,并且修改TAMS指標的值,讓下一階段用戶執行緒并發運行時,能正確地在可用的Region中分配新物件,這個階段需要停頓執行緒,但耗時很短,而且是借用進行Minor GC的時候同步完成的,所以G1收集器在這個階段實際并沒有額外的停頓,

2)并發標記(Concurrent Marking) :從GC Root開始對堆中物件進行可達性分析,遞回掃描整個堆里的物件圖,找出要回收的物件,這階段耗時較長,但可與用戶程式并發執行,當物件圖掃描完成以后,還要重新處理SATB記錄下的在并發時有參考變動的物件,

3)最終標記(Final Marking) :對用戶執行緒做另一個短暫的暫停,用于處理并發階段結束后仍遺留下來的最后那少量的SATB記錄,

4)篩選回收(Live Data Counting and Evacuation) :負責更新Region的統計資料,對各個Region的回收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構成回收集,然后把決定回收的那一部分Region的存活物件復制到空的Region中,再清理掉整個舊Region的全部空間,這里的操作涉及存活物件的移動,是必須暫停用戶執行緒,由多條收集器執行緒并行完成的,

從上述階段的描述可以看出,G1收集器除了并發標記外,其余階段也是要完全暫停用戶執行緒的,換言之,它并非純粹地追求低延遲,官方給它設定的目標是在延遲可控的情況下獲得盡可能高的吞吐量,所以才能擔當起“全功能收集器”的重任與期望,

從Oracle官方透露出來的資訊可獲知,回收階段(Evacuation) 其實本也有想過設計成與用戶程式一起并發執行,但這件事情做起來比較復雜,考慮到G1只是回收一部分Region, 停頓時間是用戶可控制的,所以并不迫切去實作,而選擇把這個特性放到了G1之后出現的低延遲垃圾收集器(即ZGC) 中,另外,還考慮到G1不是僅僅面向低延遲,停頓用戶執行緒能夠最大幅度提高垃圾收集效率,為了保證吞吐量所以才選擇了完全暫停用戶執行緒的實作方案,通過圖3-13可以比較清楚地看到G1收集器的運作步驟中并發和需要停頓的階段,
在這里插入圖片描述
毫無疑問,可以由用戶指定期望的停頓時間是G1收集器很強大的一個功能,設定不同的期望停頓時間,可使得G1在不同應用場景中取得關注吞吐量和關注延遲之間的最佳平衡,不過,這里設定的“期望值”必須是符合實際的,不能異想天開,畢竟G1是要凍結用戶執行緒來復制物件的,這個停頓時間再怎么低也得有個限度,它默認的停頓目標為兩百毫秒,一般來說,回收階段占到幾十到一百甚至接近兩百毫秒都很正常,但如果我們把停頓時間調得:
非常低,譬如設定為二十毫秒,很可能出現的結果就是由于停頓目標時間太短,導致每次選出來的回收集只占堆記憶體很小的一部分,收集器收集的速度逐漸跟不上分配器分配的速度,導致垃圾慢慢堆積,很可能一開始收集器還能從空閑的堆記憶體中獲得一些喘息的時間,但應用運行時間一長就不行了,最終占滿堆引發Full GC反而降低性能,所以通常把期望停頓時間設定為一兩百毫秒或者兩三百毫秒會是比較合理的,

從G1開始,最先進的垃圾收集器的設計導向都不約而同地變為追求能夠應付應用的記憶體分配速率(Allocation Rate),而不追求一次把整個Java堆全部清理干凈,這樣,應用在分配,同時收集器在收集,只要收集的速度能跟得上物件分配的速度,那一切就能運作得很完美,這種新的收集器設計思路從工程實作上看是從G1開始興起的,所以說G1是收集器技術發展的一個里程碑,

G1收集器常會被拿來與CMS收集器互相比較,畢竟它們都非常關注停頓時間的控制,官方資料中將它們兩個并稱為“The Mostly Concurrent Collectors”在未來,G1收集器最侄訓
是要取代CMS的,而當下它們兩者并存的時間里,分個高低優劣就無可避免,

相比CMS,G1的優點有很多,暫且不論可以指定最大停頓時間、分Region的記憶體布局、按收益動態確定回收集這些創新性設計帶來的紅利,單從最傳統的演算法理論上看,G1也更有發展潛力,與CMS的“標記-清除”演算法不同,G1從整體來看是基于“標記整理”演算法實作的收集器,但從區域(兩個Region之間)上看又是基于“標記-復制”演算法實作,無論如何,這兩
種演算法都意味著G1運作期間不會產生記憶體空間碎片,垃圾收集完成之后能提供規整的可用記憶體,這種特性有利于程式長時間運行,在程式為大物件分配記憶體時不容易因無法找到連續記憶體空間而提前觸發下一次收集,

不過,G1相對 于CMS仍然不是占全方位、壓倒性優勢的,從它出現幾年仍不能在所有應用場景中代替CMS就可以得知這個結論,比起CMS,G1的弱項也可以列舉出不少,如在用戶程式運行程序中,G1無論是為了垃圾收集產生的記憶體占用(Footprint)還是程式運行時的額外執行負載(Overload) 都要比CMS要高,就記憶體占用來說,雖然G1和CMS都使用卡表來處理跨代指標,但G1的卡表實作更為復雜,而且堆中每個Region,無論扮演的是新生代還是老年代角色,都必須有一份卡表,這導致G1的記憶集(和其他記憶體消耗)可能會占整個堆容量的20%乃至更多的記憶體空間;相比起來CMS的卡表就相當簡單,只有唯一一份,而且只需要處理老年代到新生代的參考,反過來則不需要,由于新生代的物件具有朝生夕滅的不穩定性,參考變化頻繁,能省下這個區域的維護開銷是很劃算的,

在執行負載的角度上,同樣由于兩個收集器各自的細節實作特點導致了用戶程式運行時的負載會有不同,譬如它們都使用到寫屏障,CMS用寫后屏障來更新維護卡表;而G1除了使用寫后屏障來進行同樣的(由于G1的卡表結構復雜,其實是更煩瑣的)卡表維護操作外,為了實作原始快照搜索(SATB) 演算法,還需要使用寫前屏障來跟蹤并發時的指標變化情況,相比起增量更新演算法,原始快照搜索能夠減少并發標記和重新標記階段的消耗,避免CMS那樣在最終標記階段停頓時間過長的缺點,但是在用戶程式運行程序中確實會產生由跟蹤參考變化帶來的額外負擔,由于G1對寫屏障的復雜操作要比CMS消耗更多的運算資源,所以CMS的寫屏障實作是直接的同步操作,而G1就不得不將其實作為類似于訊息佇列的結構,把寫前屏障和寫后屏障中要做的事情都放到佇列里,然后再異步處理,

以上的優缺點對比僅僅是針對G1和CMS兩款垃圾收集器單獨某方面的實作細節的定性分析,通常我們說哪款收集器要更好、要好上多少,往往是針對具體場景才能做的定量比較,按照筆者的實踐經驗,目前在小記憶體應用上CMS的表現大概率仍然要會優于G1,而在大記憶體應用上G1則大多能發揮其優勢,這個優劣勢的Java堆容量平衡點通常在6GB至8GB之間,當然,以上這些也僅是經驗之談,不同應用需要量體裁衣地實際測驗才能得出最合適的結論,隨著HotSpot的開發者對G1的不斷優化,也會讓對比結果繼續向G1傾斜,

-XX:+UseG1GC

-XX:G1HeapRegionSize=n,設定的G1區域的大小,值是2的冪,范圍是1MB到32MB,目標是根據最小的Java堆大小劃分出約2048個區域
-XX:MaxGCPauseMillis=n,最大Gc停頓時間,這是個軟目標,JVM將盡可能(但不保證)停頓小于這個時間
XX:InitatingHeapOccupancyPercent=n,堆占用了多少的時候就觸發GC,默認為45
-XX:ConcGCThreads=n,并發Gc使用的執行緒數
-XX:G1ReservePercent=n,設定作為空閑空間的預留記憶體百分比,以降低目標空間溢位的風險,默認值是10%

jdk1.7 默認垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默認垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默認垃圾收集器G1

參考文獻:
《Oracle HotSpot》
《深入理解Java虛擬機》
在這里插入圖片描述
作者金華,上海張江資訊技術專修學院副院長,上海師范大學兼職教授,軟體與資訊技術講師,長期從事軟體與資訊技術技能培訓與職業規劃作業,本書將相關知識的系統整合,符合現在Java的主流應用,拒絕全面不實用;本書知識點主要圍繞技術升級和面試技巧展開,讓你在升級專業知識的同時更能順利通過面試,

京東自營購買鏈接:
《Java核心技術及面試指南》- 京東圖書

當當自營購買鏈接:
《Java核心技術及面試指南》- 當當圖書

截止到9月24日14:00,留言獲贊最高的兩位同學,將獲得《Java核心技術及面試指南》圖書一本

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/302774.html

標籤:java

上一篇:自動化測驗方案

下一篇:【演算法】圖解八大排序

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more