Java虛擬機器之記憶體區域

碼頭工人發表於2020-12-07

原創文章,轉載請標明出處!https://www.cnblogs.com/boycelee/p/14095080.html

一、背景

相對於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](https://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](https://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、程式碼示例

在安卓開發時,我們需要呼叫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](https://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》

十二、最後

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






原創文章,轉載請標明出處!https://www.cnblogs.com/boycelee/p/14095080.html

相關文章