Java虛擬機之記憶體區域

原創文章,轉載請標明出處!

一、背景

相對於C/C++C程式設計師,Java程式設計師會相對輕鬆一些,因為Java虛擬機的記憶體管理機制會管理記憶體,不需要開發人員手動進行記憶體管理,也不容易出現記憶體泄露和記憶體溢出的。但如果不了解虛擬機如何管理記憶體,在記憶體出現問題時就會束手無策,所以學習虛擬機如何管理記憶體也是一件必要的事情。

二、運行時記憶體區域概述

1、官方描述

The Java Virtual Machine defines various run-time data areas that are used during execution of a program.

2、中文翻譯

Java虛擬機定義了在程式運行期間的各種運行時數據區域。

3、記憶體區域簡述

在《Java虛擬機規範》中運行時數據區域會包括PC暫存器、Java虛擬機棧、堆、方法區、運行常量池、本地方法棧。因為運行時常量池是方法區的一部分,所以本篇文章將常量池放在方法區章節中的子節來講解。

4、運行時數據區簡圖

JVM-運行時數據區簡圖

5、運行時數據區詳圖

JVM-運行時數據區詳圖

三、JVM執行緒

JVM數據區域與執行緒關係

1、官方描述

Some of these data areas are created on Java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits. Other data areas are per thread. Per-thread data areas are created when a thread is created and destroyed when the thread exits.

2、中文解釋

一部分數據區域與虛擬機進程同生共死,另一部分數據區域與執行緒同生共死。

3、關係圖

JVM-數據區域與執行緒關係

四、PC暫存器

1、官方解釋

The Java Virtual Machine can support many threads of execution at once (JLS §17). Each Java Virtual Machine thread has its own pc (program counter) register. At any point, each Java Virtual Machine thread is executing the code of a single method, namely the current method ([§2.6](//docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6)) for that thread. If that method is not native , the pc register contains the address of the Java Virtual Machine instruction currently being executed. If the method currently being executed by the thread is native, the value of the Java Virtual Machine's pc register is undefined. The Java Virtual Machine's pc register is wide enough to hold a returnAddress or a native pointer on the specific platform.

2、中文翻譯

Java虛擬機支援同時執行多個執行緒。每一個Java虛擬機執行緒都有自己的PC暫存器。在任何時刻,Java虛擬機一個執行緒都只在執行某一個單一函數程式碼。如果函數不是native函數,PC暫存器中就包含當前正在被執行的Java虛擬機指令的地址。反之當前函數是native函數,pc暫存器中的值是undefined。pc暫存器的大小足夠存儲返回地址或native指針。

3、概述

(1)PC暫存器並非真正意義上的物理暫存器,pc暫存器是對物理暫存器的一種模擬;

(2)PC暫存器是一塊較小的記憶體空間;

(3)可以將其看做當前執行緒執行的位元組碼指令的「行號指示器」;

(4)位元組碼解釋器的工作就是改變pc暫存器的值來選取下一條需要執行的位元組碼指令;

(5)PC暫存器的作用是存儲下一條指令地址,也就是即將要執行的指令程式碼,然後由執行引擎讀取下一條指令;

(6)在Java虛擬機規範中,PC暫存器是執行緒私有的,其生命周期與執行緒生命周期保持一致;

(7)PC暫存器是Java虛擬機規範中沒有規定任何OutOtMemoryError的區域。

3、什麼是上下文切換?

當單核處理器執行多執行緒程式碼時,會為每個執行緒分配CPU時間片,CPU通過時間片分配演算法來循環執行任務,當前任務執行一個時間片後會切換到下一個任務。但是在切換前會保存上一個任務的狀態,以便於下次切換回任務時,可以再次載入這個任務之前的狀態。所以任務從保存到再一次載入的過程就是一次「上下文切換」。CPU通過不停進行上下文切換,讓我們覺得多個執行緒是同時執行的。

4、什麼是CPU時間片?

CPU時間片就是CPU分配給每個執行緒的時間段。由於CPU只有一個核數有限,只能同時處理程式的一部分任務,不能同時滿足所有要求,為了公平處理多執行緒問題,就引入的時間片的概念,為每個執行緒分配時間片,輪流執行。

5、為什麼PC暫存器是「執行緒私有」的?

由於Java虛擬機多執行緒是通過執行緒上下文切換的方式來實現的。在任何時刻,一個處理器只會執行一條程式中的指令,因此在上下文切換後為了能夠恢復到正確的執行位置,每個執行緒都需要有一個獨立的PC暫存器,執行緒之間獨立存儲,互不影響。

五、虛擬機棧

1、官方解釋

Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread. A Java Virtual Machine stack stores frames ([§2.6](//docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6)). A Java Virtual Machine stack is analogous to the stack of a conventional language such as C: it holds local variables and partial results, and plays a part in method invocation and return. Because the Java Virtual Machine stack is never manipulated directly except to push and pop frames, frames may be heap allocated. The memory for a Java Virtual Machine stack does not need to be contiguous.

2、中文解釋

Java虛擬機棧是執行緒私有的,與執行緒同生共死。Java虛擬機中有一個個存儲棧幀。Java的棧能夠存儲局部變數與部分返回結果,並參與函數的調用與返回。因為Java虛擬機棧只push或pop棧幀,不直接操作Java虛擬機棧,棧幀可能被堆分配。Java虛擬機棧的記憶體不需要連續。

3、概述

Java虛擬機棧描述的是Java函數執行的執行緒記憶體模型。每個函數被執行時,Java虛擬機會同步創建一個棧幀用於存儲局部變數標配、操作數棧、動態鏈接和函數返回地址等資訊。函數被調用直至執行完畢的過程,就對應著一個棧幀在虛擬機中入棧和出棧的過程。

4、棧的結構

JVM-Java虛擬機棧結構

5、棧的存儲

(1)棧是執行緒私有,棧中的數據都已棧幀形式存在。棧幀是棧的基本單位;

(2)執行緒中正在執行的函數都有其對應的棧幀;

(3)棧幀是一個記憶體區塊,是一個數據集合,其中存儲著函數執行過程中的數據資訊;

6、棧的運行原理

(1)JVM直接對Java棧的操作只有兩個,就是對棧幀的壓棧和出棧。

(2)在同一執行緒同一時間下,只會有一個活動的棧幀,且該棧幀是當前正在執行的函數對應的棧幀,即棧頂棧幀也稱為「當前棧幀」。

(3)執行引擎運行的所有位元組碼指令只針對當前棧幀進行操作。

(4)若方法中調用了其他方法,對應的新的棧幀就會被創建,放在棧頂,成為新的當前棧幀。

JVM-Java虛擬機棧運行原理

7、局部變數表

(1)概述

​ 1)局部變數表定義為一個數組,主要用於存儲方法參數和定義在方法體內的局部變數。包括基本數據類型和對象引用以及returnAddress類型(指向特定指令地址的指針,例如pc暫存器中的值就是returnAddress類型)。

​ 2)局部變數表所需容量在編譯期間已經確定下下來,並保存在方法的Code屬性的maximun local variables數據項中。在函數運行期局部變數表大小不會改變。

(2)舉例程式碼

public class Car {

    public static void main(String[] args) {
        Car car = new Car();
        String name = "Boyce Car";
    }
}

(3)位元組碼文件中的局部變數表

JVM-位元組碼文件局部變數表

​ 1)start pc表示位元組碼指令的行號;

​ 2) length是pc指針起始位置到結束位置的距離;

​ 3) start pc與length共同描述變數的作用域範圍。

(4)Slot

​ 1)局部變數表的最基本存儲單元Slot(變數槽)

​ 2)在局部變數表中,32位以內的類型只佔一個Slot(包括returnAddress類型),64位類型(long和duble) 佔兩個Slot。

​ 3)局部變數表中,每一個Slot都會分配到一個訪問索引,通過這個索引可以訪問到局部變數表中對應的局部變 量值。

​ 4)當一個實例方法被調用時,它的方法參數和方法體內定義的局部變數將會按照順序被複制到局部變數表中 的Slot上。

​ 5)當需要訪問局部變數表中的64位的局部變數值時,只需要使用2個Slot索引中的前一個索引即可。

​ 6)如果當前幀由構造函數或實例方法創建的,那麼該對象引用this將存放在index為0的Slot處。

JVM-slot

8、操作數棧

(1)每個棧幀都包含一個先進後出的操作數棧。

(2)操作數棧在方法執行過程中,根據位元組碼指令,往棧中寫入或讀取數據,即入棧或出棧。

(3)操作數棧的作用主要是用於保存計算過程的中間結果,同時作為計算過程中變數的臨時存儲空間。

(4)操作數棧根據push/pop進行操作,無法使用索引方式訪問。

(5)如果一個函數帶有返回值,其返回值會被壓入當前棧幀的操作數棧中,並更新pc暫存器中的下一條位元組碼指令。

(6)Java虛擬機的指令架構是基於棧式架構,其中的棧指的就是操作數棧。

(7)基於棧式結構計算過程的位元組碼指令:

jclasslib查看位元組碼1

9、棧頂快取

(1)操作數棧存儲於記憶體,頻繁操作進行IO操作影響執行效率。HotSpot虛擬機的設計者提出了棧頂快取技術,將棧頂元素快取在暫存器中,以此減少IO操作,提升運行效率。(處理器訪問任何暫存器和 Cache 等封裝以外的數據資源都可以當成 I/O 操作,包括記憶體,磁碟,顯示卡等外部設備。)

10、動態鏈接

(1)每個棧幀內部都包含一個紙箱運行時常量池中該棧幀所屬方法的引用。包含這個引用的目的就是為了支援當前程式碼能夠實現動態鏈接(Dynamic Linking)。例如:invokedynamic指令。

(2)Java源文件被編譯成位元組碼文件時,字面量與符號引用都被保存至位元組碼文件的常量池中。例如:當一個函數調用另一個函數時,就通過常量池中指向的函數的符號引用來表示,動態鏈接的作用就是將這些符號引用轉換為調用函數的直接引用(函數在實際運行時記憶體中的入口地址)。

11、方法返回地址

(1)存放調用該函數的主函數的pc暫存器的值;

(2)一個函數的結束有兩種方式:1)正常執行完成;2)異常,非正常退出;

(3)無論通過哪種方式退出,在函數退出後都返回到該函數被調用的位置,程式才能繼續執行。正常退出時,調用方的pc暫存器的值作為返回地址,即調用該方法的指令的下一條指令的地址。而如果通過異常退出,返回地址是要通過異常表來確定,棧幀中一般不會保存這部分資訊。

六、本地方法棧

1、官方解釋

An implementation of the Java Virtual Machine may use conventional stacks, colloquially called "C stacks," to support native methods (methods written in a language other than the Java programming language). Native method stacks may also be used by the implementation of an interpreter for the Java Virtual Machine's instruction set in a language such as C. Java Virtual Machine implementations that cannot load native methods and that do not themselves rely on conventional stacks need not supply native method stacks. If supplied, native method stacks are typically allocated per thread when each thread is created.

2、中文翻譯

Java虛擬機的實現可以使用傳統的堆棧,以支援本地方法,本地方法棧也可以用於實現Java虛擬機指令集的解釋器。Java虛擬機不能載入本地方法且不提供本地方法棧。如果提供本地方法棧,則執行緒創建時為每個執行緒分配一個本地方法棧。

3、概述

(1)本地方法棧與Java虛擬機棧相似,Java虛擬機棧用於管理Java函數的執行問題,而本地方法棧則是用於管理本地函數(Native)的執行問題。

(2)《Java虛擬機規範》中對本地方法棧沒有強制規定,因此虛擬機可以根據需求自由實現。如Hot-Spot虛擬機就直接將本地方法棧和虛擬機棧合二為一。

(3)棧深度溢出或棧擴展失敗時會分配拋出StackOverflowError和OutOfMemoryError異常。

(4)當執行緒調用本地方式時,它和虛擬機就有相同的許可權,不再受虛擬機的限制。

4、程式碼示例

在Android開發時,我們需要調用C/C++程式碼,我們就需要用到JNI(Java Native Interface)。

package com.example.nativetest1;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();
}
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_nativetest1_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

七、堆空間

1、官方描述

The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.

The heap is created on virtual machine start-up. Heap storage for objects is reclaimed by an automatic storage management system (known as a garbage collector); objects are never explicitly deallocated. The Java Virtual Machine assumes no particular type of automatic storage management system, and the storage management technique may be chosen according to the implementor's system requirements. The heap may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger heap becomes unnecessary. The memory for the heap does not need to be contiguous.

2、中文翻譯

(1)堆空間是執行緒共享的,類的對象實例與數組分配的記憶體都在堆空間中。

(2)堆空間在虛擬機啟動時創建,堆空間的記憶體由垃圾回收器進行回收,對象不顯示釋放。Java虛擬機不限定垃圾回收器,垃圾回收器技術可以根據實現者的需求自行選擇。堆空間大小可以是固定,也可以是根據需求進行擴展的。堆的記憶體空間可以物理上不連續。

3、概述

(1)Java虛擬機實例中只有一個堆記憶體,堆是虛擬機管理的最大一塊記憶體,其被所有執行緒共享。

(2)Java堆存在為唯一目的就是存儲對象實例。

(3)《Java虛擬機規範》中規定:「所有對象實例和數組都應該在堆上分配」。隨著Java語言的發展未來可能被打破,但是目前仍然沒有。

(4)《Java虛擬機規範》中規定:「堆可以處於物理上不連續的記憶體空間中,但邏輯上它應該被視為連續的」。

(5)《Java虛擬機規範》中並沒有對堆的劃分有任何要求,「新生代、老年代、Eden、Survivor」等名詞只是一部分垃圾回收器的設計,而非某一虛擬機固有的記憶體布局。

(6)所有執行緒都共享堆空間,但還是可以劃分執行緒私有的緩衝區(Thread Local Allocation Buffer, TLAB)。

(7)數組和對象永遠不會存儲在棧上,因為棧幀中只保存引用,引用指向對象和數組在堆中存放的位置。

4、堆、棧和方法區的關係

棧負責解決執行問題,堆負責解決數據存儲問題。

JVM-對象創建(堆、棧、方法區關係)

5、TLAB

為什麼會出現TLAB?

(1)堆空間是執行緒共享的。

(2)JVM中會頻繁創建對象實例,因此在並發環境下操作堆空間的記憶體區域是執行緒不安全的。

(3)當多執行緒同時操作同一地址時,就需要加上同步機制,這就會影響對象實例創建速度。

(4)如何解決解決這一問題呢?這就出現了TLAB。

什麼是TLAB?

(1) JVM為每個執行緒分配了一個私有的快取區域。

(2)多執行緒同時分配記憶體時,使用TLAB可以避免一系列的非執行緒安全問題,同時還能夠提升記憶體分配的吞吐量,因此我們可以將這種分配方式稱為「快速分配策略」。

(3)基於OpenJDK衍生出來的JVM都提供了TLAB的設計。

(4)TLAB默認棧堆空間(Eden區)的1%。

JVM-TLAB

對象分配(開啟TLAB時)

(1)JVM將TLAB作為記憶體分配的首選。

(2)默認詳情下,TLAB僅占堆空間(Eden區)的1%。

(3)當對象在TLAB空間分配失敗時,JVM就會嘗試通過使用加鎖機制確保數據操作的原子性,從而直接在堆空間(Eden區)分配記憶體。

JVM-對象分配(TLAB)

6、逃逸分析

(1)逃逸分析的本質是分析對象動態作用域。

(2)當一個對象在方法中被定義後,對象只在方法內部使用,則認為沒有發生逃逸。

(3)當一個對象在方法中被定義後,它被外部方法所引用時,則認為發生逃逸。

(4)發生逃逸

public static StringBuffer createStringBuffer(String s1, String s2) {
        StringBuffer stringBuffer = new StringBuffer();
        return stringBuffer;
    }

(5)未發生逃逸

public static String createStringBuffer(String s1, String s2) {
        StringBuffer stringBuffer = new StringBuffer();
        return stringBuffer.toString();
    }

判斷方式是new的對象實例是否能在方法外被調用。

7、逃逸分析-程式碼優化

使用逃逸分析後,編譯器可以對程式碼做如下優化

棧上分配

(1)將堆上分配轉化為棧上分配。對象在程式中被分配,如果要使對象不會發生逃逸,對象可以在棧上分配,而不是堆上分配。

(2)JIT編譯器在編譯期間會藉助逃逸分析來判斷對象是否逃逸出方法,如果沒有逃逸,就可能會被優化為棧上分配,最後執行緒結束後棧空間被回收,局部變數對象也會被回收。

同步省略

(1)如果對象只能被一個執行緒訪問到,那麼這個對象可以不考慮同步。

(2)場景:在動態編譯同步塊程式碼時,JIT編譯器可以藉助逃逸分析來判斷同步塊所使用的鎖對象是否只能被一個執行緒訪問。如果確定只有一個執行緒能訪問到,JIT編譯器在編譯這個程式碼塊時就會取消對這部分程式碼的同步。這個過程叫「同步省略」也叫「鎖消除」。

標量替換

(1)有的對象可能不需要一個連續的記憶體結構存在也可以被訪問到,那麼對象的部分(或全部)可以不存儲在記憶體,而是存儲在CPU暫存器中。

(2)變數(Scalar)是指一個無法再分解成為更小的數據的數據。例如:Java中原始數據類型就是標量。

(3)聚合量(Aggregate)是指一個能夠被分解成為更小數據的數據。例如:Java對象。

(4)應用場景是在JIT階段,如果經過逃逸分析,發現一個對象不會被外界訪問,經過JIT優化,就會把這個對象拆解為若干個成員變數來替代(變數)。這個過程就叫標量替換。

小結

(1)目前逃逸分析技術並不成熟。

(2)逃逸分析本身也耗費性能,無法保證逃逸分析消耗的性能小於函數本身。

(3)目前只有變數替換被應用。

(4)目前堆是存儲對象實例的唯一選擇。

八、方法區

1、官方解釋

The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods ([§2.9](//docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.9)) used in class and instance initialization and interface initialization.

The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.

2、中文解釋

(1)方法區是執行緒共享的,類似傳統語言用於存儲編譯程式碼的存儲區。

(2)方法區用於存儲類結構資訊,例如運行時常量池、域資訊、函數數據、函數與構造函數程式碼、類元資訊和實例初始化、介面初始化中使用的特殊函數。

(3)Java虛擬機啟動時就會創建方法區,邏輯上方法區是堆空間的一部分(實際上不是)。方法區可以不實現垃圾收集策略(Hotspot有實現)。方法區大小可以固定,也可以是根據計算需求擴展的。方法區記憶體物理上可以不連續。

3、概述

(1)在《Java虛擬機規範中》中方法區邏輯上是堆空間的一部分,但實際Hot-Spot虛擬機實現時,卻將堆空間與方法區做了區分,方法區還有一個別名叫做Non-Heap(非堆)。所以可以將方法區看做獨立於堆的記憶體空間。

JDK7以前堆空間與方法區

JVM-JDK8以後堆空間與方法區

(2)方法區是執行緒共享的。

(3)方法區的大小和對空間一樣可以通過參數設置,是可以擴展的。

(4)方法區的大小決定能夠保存多少類,如果類太多就會造成方法區記憶體溢出。

(5)關閉JVM方法區記憶體就會釋放。

4、方法區結構

(1)存儲已經被虛擬機載入的類型資訊、常量、靜態變數、域資訊、方法資訊、即時編譯器編譯後的程式碼快取等。(JDK8之前

JVM-方法區存儲數據

(2)類型資訊。對每個載入的類型(類class、介面interface、枚舉enum、註解annotation)。JVM必須在方法區中存儲以下類型資訊:

​ a)該類型的完整名稱(包名.類型)。

​ b)該類型直接父類的完整名稱(介面或Object類都沒有父類)。

​ c)該類型的修飾符(public、abstract、final)。

​ d)該類型直接介面的有序列表。

(3)域資訊

​ a)JVM必須在方法區中保存類型的所有域的相關資訊和域的聲明順序。

​ b)域相關資訊有域名稱、域類型、域修飾符(public、private、protected、static、final、volatile、transient)

(4)方法資訊。JVM必須保存所有方法的以下資訊,同域資訊一樣包括聲明順序:

​ a)方法名稱

​ b)方法的返回類型(或void)

​ c)方法參數的數量和類型(按順序)

​ d)方法的修飾符(public、private、protected、static、final、syschronized、native、abstract)

​ e)方法的位元組碼(bytecodes)、操作數棧、局部變數表以及大小

​ f)異常表

(5)源程式碼

/**
 * @author jianw.li
 * @date 2020/12/2 10:51 下午
 * @Description: 方法區測試
 */
public class MethodAreaTestDemo extends MethodAreaTest implements Serializable {

    public int num = 1;
    private static String str = "測試測試";

    public void sub() {
        int i = 10;
        int j = 1;
        int k = i - j;
        System.out.println(k);
    }

    private static int add(int a, int b){
        int c = a + b;
        return c;
    }

    public static void main(String[] args) {
        add(1, 2);
    }
}

(6)位元組碼文件

通過javap -v MethodAreaTestDemo.class查看

Classfile /Users/lijianwei/IdeaProjects/LeeBoyceJVMTest/out/production/LeeBoyceJVMTest/com/ljw/MethodAreaTestDemo.class
  Last modified 2020-12-2; size 985 bytes
  MD5 checksum f3555565267ef4cbb0c07bebb42263a6
  Compiled from "MethodAreaTestDemo.java"
//注釋:存放至方法區的類資訊
public class com.ljw.MethodAreaTestDemo extends com.ljw.MethodAreaTest implements java.io.Serializable
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #9.#38         // com/ljw/MethodAreaTest."<init>":()V
   #2 = Fieldref           #8.#39         // com/ljw/MethodAreaTestDemo.num:I
   #3 = Fieldref           #40.#41        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = Methodref          #42.#43        // java/io/PrintStream.println:(I)V
   #5 = Methodref          #8.#44         // com/ljw/MethodAreaTestDemo.add:(II)I
   #6 = String             #45            // 測試測試
   #7 = Fieldref           #8.#46         // com/ljw/MethodAreaTestDemo.str:Ljava/lang/String;
   #8 = Class              #47            // com/ljw/MethodAreaTestDemo
   #9 = Class              #48            // com/ljw/MethodAreaTest
  #10 = Class              #49            // java/io/Serializable
  #11 = Utf8               num
  #12 = Utf8               I
  #13 = Utf8               str
  #14 = Utf8               Ljava/lang/String;
  #15 = Utf8               <init>
  #16 = Utf8               ()V
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               LocalVariableTable
  #20 = Utf8               this
  #21 = Utf8               Lcom/ljw/MethodAreaTestDemo;
  #22 = Utf8               sub
  #23 = Utf8               i
  #24 = Utf8               j
  #25 = Utf8               k
  #26 = Utf8               add
  #27 = Utf8               (II)I
  #28 = Utf8               a
  #29 = Utf8               b
  #30 = Utf8               c
  #31 = Utf8               main
  #32 = Utf8               ([Ljava/lang/String;)V
  #33 = Utf8               args
  #34 = Utf8               [Ljava/lang/String;
  #35 = Utf8               <clinit>
  #36 = Utf8               SourceFile
  #37 = Utf8               MethodAreaTestDemo.java
  #38 = NameAndType        #15:#16        // "<init>":()V
  #39 = NameAndType        #11:#12        // num:I
  #40 = Class              #50            // java/lang/System
  #41 = NameAndType        #51:#52        // out:Ljava/io/PrintStream;
  #42 = Class              #53            // java/io/PrintStream
  #43 = NameAndType        #54:#55        // println:(I)V
  #44 = NameAndType        #26:#27        // add:(II)I
  #45 = Utf8               測試測試
  #46 = NameAndType        #13:#14        // str:Ljava/lang/String;
  #47 = Utf8               com/ljw/MethodAreaTestDemo
  #48 = Utf8               com/ljw/MethodAreaTest
  #49 = Utf8               java/io/Serializable
  #50 = Utf8               java/lang/System
  #51 = Utf8               out
  #52 = Utf8               Ljava/io/PrintStream;
  #53 = Utf8               java/io/PrintStream
  #54 = Utf8               println
  #55 = Utf8               (I)V
{
  //注釋:存放至方法區的域資訊
  public int num;
    descriptor: I
    flags: ACC_PUBLIC

  public com.ljw.MethodAreaTestDemo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method com/ljw/MethodAreaTest."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field num:I
         9: return
      LineNumberTable:
        line 10: 0
        line 12: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/ljw/MethodAreaTestDemo;
  //注釋:存放至方法區的函數資訊
  public void sub();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: iconst_1
         4: istore_2
         5: iload_1
         6: iload_2
         7: isub
         8: istore_3
         9: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        12: iload_3
        13: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        16: return
      LineNumberTable:
        line 16: 0
        line 17: 3
        line 18: 5
        line 19: 9
        line 20: 16
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      17     0  this   Lcom/ljw/MethodAreaTestDemo;
            3      14     1     i   I
            5      12     2     j   I
            9       8     3     k   I

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: iconst_1
         1: iconst_2
         2: invokestatic  #5                  // Method add:(II)I
         5: pop
         6: return
      LineNumberTable:
        line 28: 0
        line 29: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  args   [Ljava/lang/String;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #6                  // String 測試測試
         2: putstatic     #7                  // Field str:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 13: 0
}
SourceFile: "MethodAreaTestDemo.java"

5、方法區變化

方法區、永久代以及元空間的關係

(1)方法區不等於永久代不等於元空間。

(2)永久代、元空間只是方法區的實現方式。

(3)永久代的使用,容易導致Java程式OOM(超過-XX:MaxPermSize上限)。

(4)JDK8將將方法區的實現方式由永久代改為元空間。

(5)元空間的本質與永久代類似,都是對Java虛擬機規範中方法區的實現。不過元空間與永久代最大的區別在於元空間不在虛擬機設置的記憶體中,而是使用本地記憶體。

方法區變化細節

版本 說明
JDK6 永久代實現方法區。靜態變數、字元串常量池存放在永久代。
JDK7 永久代實現方法區。已經逐漸去除「永久代」,將字元串常量池、靜態變數移至堆中存儲。
JDK8 元空間實現方法區。類資訊、域資訊、函數資訊、運行時常量池存儲至本地記憶體的元空間中。但字元串常量池和靜態變數仍然存放在堆中。

JVM-JDK6方法區

JVM-JDK7方法區

JVM-JDK8方法區

6、常量池與運行時常量池

(1)運行時常量池是方法區的一部分。

(2)常量池是Class文件的一部分。常量池用於存放編譯期間的字面量和符號引用(字面量和符號引用後續文章講),這部分內容將在類載入後存放到方法區運行時常量池中。

(3)JVM為每個已載入的類型(類或介面)都維護一個常量池。池中的數據項像數組一樣是通過索引訪問的。

(4)運行時常量池中包含多種不同的常量,包括編譯期就已經明確的數值字面量,也包括到運行期解析後才能獲取到的方法或欄位引用。

(5)什麼是字面量?a)文本字元串;b)八種基本類型的值;c)被聲明為final的常量等。

(6)什麼是符號引用?a)類和方法的全限定名;b)欄位名稱和描述符;c)方法名稱和描述符。

(7)為什麼需要運行時常量池?Java的位元組碼需要使用數據支援,這些數據不能夠直接存儲在位元組碼文件中。為了位元組碼文件中可以使用到數據,可以將數據存放在常量池中,再由位元組碼文件中存放的「指向常量池的引用」指向常量池中的數據。

7、為什麼使用元空間替換永久代?

(1)官方解釋

JVM-永久代移除原因

​ 由於JRockit虛擬機與Hotspot虛擬機融合,JRockit虛擬機虛擬機的用戶不需要也不習慣去設置永久代。所以融合之後索性就去掉了永久代。

JVM-永久代移除元空間替換

​ 一部分類元數據存放在本地記憶體中,而字元串常量池與靜態變數則存放置堆空間中。類元數據僅受限於可使用的本地記憶體大小,而不是-XX:MaxPermSize

(2)永久代的空間大小難以設置

​ 如果動態載入的類過多,容易造成記憶體溢出(OOM),元空間相對於永久代的好處是使用本地記憶體而非虛擬機記憶體,默認情況下元空間大小僅受本地記憶體限制。

(3)永久代調優困難

​ 對方法區的垃圾回收困難。對於類資訊的回收需要同時滿足3個條件:1)該類的所有勢力都已經被回收,堆中不在存在任何該類和其派生子類的實例;2)該類的類載入器已經被回收;3)該類的對象不再被引用,且無法通過反射訪問該類的函數。需要同時滿足以上條件類才能夠「允許」被回收。

8、為什麼將字元串常量池移至堆空間?

​ JDK7中將StringTable放在堆空間中。因為永久代的回收效率很低,只有在full gc時才會觸發。而full gc只有在老年代、永久代空間不足時才會觸發。實際開發過程中,會創建大量字元串,放在堆空間相對於方法區回收效率更高。

9、方法區垃圾回收

(1)《Java虛擬機規範》中提到可以不要求虛擬機在方法區中實現垃圾收集。

(2)方法區垃圾收集主要2部分內容:1)常量池中廢棄的常量;2)不在使用的類。

(3)方法區中的常量池中主要存放兩大類常量:1)字面量;2)符號引用。

(4)常量池中的常量沒有任何地方引用就可以被回收。

(5)類的回收條件非常苛刻,必須同時滿足3個條件。(可以看上文方法區調優困難中對方法區垃圾回收困難的描述)。

九、直接記憶體

1、概述

(1)直接記憶體並不是虛擬機運行時數據區的一部分,也不是《Java虛擬機規範》中定義的記憶體區域。

(2)直接記憶體是在Java堆外記憶體,是直接想系統申請的堆外記憶體空間。

(3)本機直接記憶體不受Java堆大小限制。

(4)訪問直接記憶體的速度要高於Java堆的速度。讀寫性能要求高時可以考慮直接記憶體。

(5)Java的NIO庫允許Java程式使用直接記憶體,用於數據緩衝區。

(6)直接記憶體的缺點是回收成本高,不受JVM回收機制管理。

(7)直接記憶體可以通過參數設置大小,默認與堆的最大參數值一致。

十、對象實例化過程

1、對象實例化

(1)對象創建方式

​ 1)new

​ 2)反射

​ 3)clone()

​ 4)反序列化

(2)創建對象步驟

​ 1)判斷對象對應類是否載入、連接、初始化。

​ 當虛擬機遇到new指令時,首先會去檢查這個指令的參數能夠在Metaspace的常量池中定位到類的符號引用,並檢查這個符號引用代表的類是否已經被載入、解析和初始化(類元資訊是否存在)。

​ 2)對象記憶體分配

​ a.計算對象佔用空間大小,然後在堆中劃分一塊記憶體給新對象。

​ b.指針碰撞。如果能記憶體規整,只需要使用一個指針作為分界點的指示器,分配的記憶體就是將指針移動一段與對象大小相等的距離。如果垃圾收集器基於壓縮演算法,具備整理過程的收集器,虛擬機就會使用這種方式分配記憶體

​ c.空閑列表。如果記憶體不規整,虛擬機需要維護一個列表, 記錄哪些記憶體塊可以使用,哪些記憶體塊已經被使用,在分配時在列表中找到一塊足夠大小的空間分配給對象實例,並更新列表上的內容。

​ d.採用哪種方式分配記憶體,取決於垃圾收集器是否具備整理(compact)功能。

​ 3)並發問題

​ a.採用cas配上失敗重試保證操作的原子性。

​ b.每個執行緒預先分配TLAB。

​ 4)初始化

​ 為所有屬性設置默認值。例如int類型變數設置默認值為0,String變數設置默認值為null等。

​ 5)設置對象頭

​ a.運行時元數據(MarkWord)。哈希值、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時 間戳

​ b.類型指針。指向類元資訊,確定該對象所屬的類型。

​ 6)執行init方法進行初始化

​ a.顯示初始化或程式碼塊中初始化

​ b.構造器中初始化

​ c.初始化成員變數,執行實例化程式碼塊,調用類的構造方法,並把堆內對象的首地址賦值給引用變數。

​ 7)小結

​ 整理流程:載入類元資訊->對象記憶體分配->處理並發問題->屬性默認值初始化->(零值初始化)->設置對象頭資訊->屬性顯性初始化、程式碼塊初始化、構造器初始化->實例化完成。

2、記憶體布局

(1)示例程式碼

public class Car {

    private int price = 300000;

    private String brand = "BMW";

    private Plant plant;

    public Car() {
        this.plant = new Plant();
    }
  
    public static void main(String[] args) {
        Car car = new Car();
    }
}
public class Plant {
}

(2)記憶體布局圖

JVM-對象創建記憶體布局

3、對象訪問

(1)句柄訪問

JVM-對象訪問句柄訪問

(2)直接指針訪問

JVM-對象訪問直接指針訪問

十一、參考

[1]《The Java Virtual Machine Specification》Java SE 8 Edition

[2]《深入理解Java虛擬機》第二版、第三版

[3]《宋紅康JVM教程》

[4]《Java並發編程藝術》

[5] 《JEP 122: Remove the Permanent Generation》

最後

懂得不多,做得很少。如果文章有不足之處,歡迎留言討論。

原創文章,轉載請標明出處!