天天看點

JVM:Java記憶體區域Java記憶體區域

上周開始看周志明老師的《深入了解Java虛拟機》,目前看完第三章了,今天複習了一下前面看的内容,正好做一下總結

Java記憶體區域

1、運作時資料區域

運作時資料區域分為線程獨占區和線程共享區,線程獨占區包括虛拟機棧、本地方法棧、程式計數器,線程共享區包括堆和方法區

JVM:Java記憶體區域Java記憶體區域

1)、程式計數器

程式計數器是一塊較小的記憶體空間,它可以看作是目前線程所執行的位元組碼的行号訓示器

如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛拟機位元組碼指令的位址;如果正在執行的是Native方法,這個計數器值則為空

此記憶體區域是唯一一個在Java虛拟機規範中沒有規定任何OutOfMemoryError情況的區域

2)、Java虛拟機棧

虛拟機棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時建立一個棧幀用于存儲局部變量表、操作數棧、動态連結、方法出口等資訊

每一個方法從調用直至執行完成的過程就對應着一個棧幀在虛拟機棧中入棧到出棧的過程

局部變量表存放了編譯器可知的各種基本資料類型、對象引用和returnAddress類型

64長度的long和double類型的資料會占用2個局部變量空間,其餘的資料類型隻占用1個

局部變量表所需的記憶體空間在編譯期完成配置設定,當進入一個方法時,這個方法需要在幀中配置設定多大的局部變量空間是完全确定的,在方法運作期間不會改變局部變量表的大小

3)、本地方法棧

虛拟機棧為虛拟機執行Java方法服務,而本地方法棧則為虛拟機使用到的Native方法服務

Sun HotSpot把本地方法棧和虛拟機棧合二為一

4)、Java堆

Java堆是被所有線程共享的一塊記憶體區域,在虛拟機啟動時建立。此記憶體區域的唯一目的就是存放對象執行個體,幾乎所有的對象執行個體都在這裡配置設定記憶體

Java堆可以處于實體上不連續的記憶體空間中,隻要邏輯上是連續的即可

5)、方法區

方法區是各個線程共享的記憶體區域,它用于存儲已被虛拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料

6)、運作時常量池

運作時常量池是方法區的一部分,用于存放編譯期生成的各種字面量和符号引用,這部分内容将在類加載後進入方法區的運作時常量池中存放

7)、直接記憶體

NIO是一種基于通道與緩沖區的I/O方式,它可以使用Native函數庫直接配置設定堆外記憶體,然後通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊記憶體的引用進行操作。在一些場景中顯著提高性能,因為避免了在Java堆和Native堆中來回複制資料

2、HotSpot虛拟機對象

1)、對象的建立

虛拟機遇到一條new指令時,首先将去檢查這個指令的參數是否能在常量池中定位到一個類的符号引用,并且檢查這個符号引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程

在類加載檢查通過後,虛拟機将為新生對象配置設定記憶體。對象所需記憶體的大小在類加載完成後便可完全确定,為對象配置設定空間的任務等同于把一塊确定大小的記憶體從Java堆中劃分出來

配置設定記憶體有兩種方式:

  • 指針碰撞:假設Java堆中記憶體是絕對規整的,所有用過的記憶體都放在一邊,空間的記憶體放在另一邊,中間放着一個指針作為分界點的訓示器,那所配置設定記憶體就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離
  • 空閑清單:如果Java堆中的記憶體并不是規整的,已使用的記憶體和空閑的記憶體互相交錯,虛拟機維護一個清單,記錄哪些記憶體是可用的,在配置設定的時候從清單中找到一塊足夠大的空間劃分給對象執行個體,并更新清單上的記錄

對象建立在虛拟機中是非常頻繁的行為,即使是僅僅修改一個指針所指向的位置,在并發情況下也并不是線程安全的,可能出現正在給對象A配置設定記憶體,指針還沒來得及修改,對象B又同時使用了原來的指針來配置設定記憶體的情況。解決方案有兩種:

  • 對配置設定記憶體空間的動作進行同步處理——實際上虛拟機采用CAS配上失敗重試的方式保證更新操作的原子性
  • 把記憶體配置設定的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先配置設定一塊記憶體,稱為本地線程配置設定緩沖(TLAB)。哪個線程要配置設定記憶體,就在哪個線程的TLAB上配置設定,隻有TLAB用完并配置設定新的TLAB時,才需要同步鎖定。虛拟機是否使用TLAB,可以通過-XX:+/-UseTLAB參數來設定

記憶體配置設定完成後,虛拟機需要将配置設定到的記憶體空間都初始化為零值(不包括對象頭),如果使用TLAB,這一工作過程也可以提前至TLAB配置設定時進行。這一步保證了對象的執行個體字段在Java代碼中可以不賦初始值就直接使用,程式能通路到這些字段的資料類型所對應的零值

2)、對象的記憶體布局

在HotSpot虛拟機中,對象在記憶體中存儲的布局可以分為3塊區域:對象頭、執行個體資料和對齊填充

A.對象頭包括兩部分資訊,第一部分用于存儲對象自身的運作時資料,如哈希碼、GC分代年齡、鎖狀态标志、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分資料的長度在32位和64位的虛拟機中分别為32bit和63bit,官方稱它為Mark Word

Mark Word被設計成一個非固定的資料結構以便在極小的空間記憶體儲盡量多的資訊,它會根據對象的狀态複用自己的存儲空間

對象頭的另一部分是類型指針,即對象指向它的類中繼資料的指針,虛拟機通過這個指針來确定這個對象是哪個類的執行個體。并不是所有的虛拟機實作都必須在對象資料上保留類型指針,換句話說,查找對象的中繼資料資訊并不一定要經過對象本身,如果對象是一個Java數組,那在對象頭中還必須有一塊用于記錄數組長度的資料

B.執行個體資料部分是對象真正存儲的有效資訊,也是在程式代碼中所定義的各種類型的字段内容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。這部分的存儲順序會受到虛拟機配置設定政策參數和字段在Java源碼中定義順序的影響。HotSpot虛拟機預設的配置設定政策為longs/doubles、ints、shorts/chars、bytes/booleans、oops,相同寬度的字段總是被配置設定到一起。在滿足這個前提條件的情況下,在父類中定義的變量會出現在子類之前。如果CompactFields參數值為true(預設為true),那麼子類之中較窄的變量也可能會插入到父類變量的空隙之中

C.對齊填充并不是必然存在的,它僅僅起着占位符的作用,由于HotSpot VM的自動記憶體管理系統要求對象起始位址必須是8位元組的整數倍,而對象頭部分正好是8位元組的倍數,是以,當對象執行個體資料部分沒有對齊時,就需要通過對齊填充來補全

3)、對象的通路定位

Java程式需要通過棧上的reference資料來操作堆上的具體對象,目前主流的通路方式有使用句柄和直接指針兩種

如果使用句柄通路的話,那麼Java堆中将會劃分出一塊記憶體來作為句柄池,reference中存儲的就是對象的句柄位址,而句柄中包含了對象執行個體資料與類型資料各自的具體位址資訊

JVM:Java記憶體區域Java記憶體區域

如果使用直接指針通路,那麼Java堆對象的布局中就必須考慮如何放置通路類型資料的相關資訊,而reference中存儲的直接就是對象位址

JVM:Java記憶體區域Java記憶體區域

使用句柄來通路的最大好處就是reference中存儲的穩定的句柄位址,在對象被移動時隻會改變句柄中的執行個體資料指針,而reference本身不需要修改

使用直接指針通路方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷

虛拟機Sun HotSpot是使用直接指針就行對象通路的

3、String.intern()方法相關案例

String.intern()是一個Native方法,它的作用是:如果字元串常量池中已經包含一個等于此String對象的字元串,則傳回常量池中這個字元串的String對象;否則,将此String對象包含的字元串添加到常量池中,并且傳回此String對象的引用

在JDK1.6及之前的版本中,由于常量池配置設定在永久代内,可以通過-XX:PermSize和-XX:MaxPermSize限制方法區大小,進而間接限制其中常量池的容量

public class RuntimeConstantPoolOOM {
	public static void main(String[] args) {
		String str1 = new StringBuilder("計算機").append("軟體").toString();
		System.out.println(str1.intern() == str1);// true
		String str2 = new StringBuilder("ja").append("va").toString();
		System.out.println(str2.intern() == str2);// false
		String str3 = new StringBuilder("zhangsan").toString();
		System.out.println(str3 == str3.intern());// false
	}
}
           

這段代碼在JDK1.6中運作,第一個結果為false,internet()方法會把首次遇到的字元串執行個體複制到永久代中,傳回的也是永久代中這個字元串執行個體的引用,而由StringBuilder建立的字元串執行個體在Java堆上,是以必然不是同一個引用,将傳回false

JDK1.7開始逐漸去永久代,internet()實作不會再複制執行個體,隻是在常量池中記錄首次出現的執行個體引用,是以internet()傳回的引用和由StringBuilder建立的那個字元串執行個體是同一個

對str2比較傳回false是因為“java”這個字元串在執行StringBuilder.toString()之前已經出現過,字元串常量池中已經有它的引用了

補充:

1)、“java”這個字元串在哪裡出現過?

檢視System類的源碼,根據注釋可以看出,System是由虛拟機自動調用的

public final class System {
    
        /* register the natives via the static initializer.
         *
         * VM will invoke the initializeSystemClass method to complete
         * the initialization for this class separated from clinit.
         * Note that to use properties set by the VM, see the constraints
         * described in the initializeSystemClass method.
         */
        private static native void registerNatives();
           

在initializeSystemClass方法中調用了Version對象的init靜态方法

public final class System {
        private static void initializeSystemClass() {
            props = new Properties();
            initProperties(props);  // initialized by the VM
    
            sun.misc.VM.saveAndRemoveProperties(props);
    
            lineSeparator = props.getProperty("line.separator");
            // 調用了Version對象的init靜态方法
            sun.misc.Version.init();
    
            FileInputStream fdIn = new FileInputStream(FileDescriptor.in);
            FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);
            FileOutputStream fdErr = new FileOutputStream(FileDescriptor.err);
            setIn0(new BufferedInputStream(fdIn));
            setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));
            setErr0(newPrintStream(fdErr, props.getProperty("sun.stderr.encoding")));
    
            loadLibrary("zip");
    
            Terminator.setup();
    
            sun.misc.VM.initializeOSEnvironment();
            Thread current = Thread.currentThread();
            current.getThreadGroup().add(current);
            setJavaLangAccess();
            sun.misc.VM.booted();
        }
           

而Version類裡laucher_name是私有靜态字元串常量

public class Version {
        private static final String launcher_name = "java";
        private static final String java_version = "1.8.0_162";
        private static final String java_runtime_name = "Java(TM) SE Runtime Environment";
        private static final String java_profile_name = "";
        private static final String java_runtime_version = "1.8.0_162-b12";
           

是以sun.misc.Version類會在JDK類庫的初始化過程中被加載并初始化,而在初始化時它需要對靜态常量字段根據指定的常量值(ConstantValue)做預設初始化,此時被sun.misc.Version.launcher靜态常量字段所引用的"java"字元串字面量就被intern到HotSpotVM的字元串常量池

2)、str3的比較為什麼為false?

new StringBuilder(“zhangsan”)這條代碼時,"zhangsan"就已經建立了一個引用,而str3.intern()指向的是"zhangsan"的引用而不是StringBuilder.toString()的引用