舉一個有趣的例子,讓你輕鬆搞懂JVM記憶體管理

智慧zhuhuix發表於2020-06-07

前言

在JAVA虛擬機器記憶體管理中,堆、棧、方法區、常量池等概念經常被提到,對理論知識的理解也常常停留在字面意思上,比如說堆記憶體中存放物件,棧記憶體中存放區域性變數,常量池中存放字串常量表等,本篇文章通過一個有趣的例子,儘量將這些理論概念通過程式樣例及圖解的方式表達清楚,讓我們更能深入底層知識。

例子

原始碼
  • StringJvm類中定義了一個CONST_STRING字串常量,並賦值"Hello World";
  • main方法定義String型別的str1,str2變數,兩個變數都賦值"Hello World";
  • 比較一下str1和str2的地址值是否相等 ;
  • new 一個String型別的str3,並通過建構函式初始化值為"Hello World";
  • 比較一下str3和str3的地址值是否相等。
/**
 * 理解JVM--堆記憶體中的物件
 * @author zhuhuix
 * @date 2020-05-19
 *
 */
public class StringJvm {
    public  static final String  CONST_STRING="Hello World";

    public static void main(String[] args) {

        String str1 =CONST_STRING;
        String str2 =CONST_STRING;
        System.out.println("str1==str2: "+ (str1 == str2));
		System.out.println("str1==CONST_STRING: "+ (str1 == CONST_STRING));
		 
        String str3 =new String(CONST_STRING);
        System.out.println("str3==str1: "+(str3 == str1));
    }
}

輸出

先說兩個比較結果:

  1. str1的地址值等於str2的地址值
  2. str1的地址值等於CONST_STRING的地址值
  3. str3的地址值不等於str1地址值,即也不等於str2值
    在這裡插入圖片描述
圖解

有些同學有可能會有疑問,明明四個字串的值都是“Hello World”,地址值卻有相等 ,也有不相等的,這裡就會引入JVM的堆記憶體、棧記憶體,常量池的一些基本概念,我們直接上圖再講解說明:
在這裡插入圖片描述

  1. 定義靜態常量CONST_STRING放入方法區;賦值時,JVM會在字串常量池中放入"Hello World"字串供共享使用,並將記憶體地址賦給靜態常量。
  2. 定義str1變數並給該字串賦值時,JVM首先會在字串常量池中尋找字串值相同的記憶體地址,並將該記憶體地址賦值str1變數。所以在程式中列印輸出str1變數的地址值是否等 於靜態常量CONST_STRING的地址值時,得到的結果是True.
  3. 定義str2變數時按以上2過程同樣處理,也即str2的地址值等於CONST_STRING和str1的地址值。
  4. 在定義str3變數時,採用了強制new一個物件及構造方法傳值方式處理,我們知道new一個物件,一定會在堆中分配記憶體給該物件,也即str3的地址值引用在堆記憶體中,所以str3的地址值肯定不等於str1的地址值。
深入分析

以上的圖解只是JVM的理論上解釋了為什麼有些地址值相等,有些地址值不等,接下來我們建立一個獲取地址值的工具類,來驗證以上的理論知識。

首先我們利用Java中的Unsafe類建立一個工具類及靜態方法,模仿C++手動管理記憶體的能力來獲取物件的實際地址值。


/**
 * 獲取JVM object的記憶體地址
 * @author zhuhuix
 * @date 2020-05-19
 *
 */
public class ObjectAddress {
    private static Unsafe unsafe;

    static
    {
        try
        {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe)field.get(null);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }

    public static long addressOf(Object o) {
        Object[] array = new Object[] {o};

        long baseOffset = unsafe.arrayBaseOffset(Object[].class);
        int addressSize = unsafe.addressSize();
        long objectAddress;
        switch (addressSize)
        {
            case 4:
                objectAddress = unsafe.getInt(array, baseOffset);
                break;
            case 8:
                objectAddress = unsafe.getLong(array, baseOffset);
                break;
            default:
                throw new Error("unsupported address size: " + addressSize);
        }

        return(objectAddress);
    }

接下來改造一下原來的程式:

/**
 * 理解JVM--堆記憶體中的物件
 * @author zhuhuix
 * @date 2020-05-19
 *
 */
public class StringJvm {
    public  static final String  CONST_STRING="Hello World";

    public static void main(String[] args) {

       System.out.println( "CONST_STRING的地址值:"+ObjectAddress.addressOf(CONST_STRING));

        String str1 =CONST_STRING;
        String str2 =CONST_STRING;
        System.out.println( "str1的地址值 :"+ObjectAddress.addressOf(str1));
        System.out.println("str1==str2: "+ (str1 == str2));
        System.out.println("str1==CONST_STRING: "+ (str1 == CONST_STRING));

        String str3 =new String(CONST_STRING);
        System.out.println("str3的地址值 :"+ ObjectAddress.addressOf(str3));
        System.out.println("str3==str1: "+(str3 == str1));

    }
}

輸出結果如下:
這裡我們實際看到了物件的實際地址值,靜態常量CONST_STRING的地址值等於str1的地址值;str3的地址值不等於str1的地址值。
在這裡插入圖片描述

學以致用

執行時常量池相對於Class檔案常量池的另外一個重要特徵是具備動態性,Java語言並不要求常量一定只有編譯期才能產生,也就是說,並非預置入Class檔案中常量池的內容才能進入方法區執行時常量池,執行期間也可以將新的常量放入池中,這種特性被開發人員利用得比較多的便是String類的intern()方法。

我們看下上面這段話,JVM告訴我們常量池有動態的特性,利用String類的intern()方法能夠將當前String物件與常量池動態繫結起來。為了更好理解,我們再改造一下例子:

/**
 * 理解JVM--堆記憶體中的物件
 * @author zhuhuix
 * @date 2020-05-19
 *
 */
public class StringJvm {
    public  static final String  CONST_STRING="Hello World";

    public static void main(String[] args) {

       System.out.println( "CONST_STRING的地址值:"+ObjectAddress.addressOf(CONST_STRING));

        String str1 =CONST_STRING;
        String str2 =CONST_STRING;
        System.out.println( "str1的地址值 :"+ObjectAddress.addressOf(str1));
        System.out.println("str1==str2: "+ (str1 == str2));
        System.out.println("str1==CONST_STRING: "+ (str1 == CONST_STRING));

        String str3 =new String(CONST_STRING);
        System.out.println("str3 new String的地址值 :"+ ObjectAddress.addressOf(str3));
        System.out.println("str3==str1: "+(str3 == str1));
		//通過intern方法在程式執行運程中與常量池進行動態繫結
        str3=str3.intern();
        System.out.println("str3.intern()的地址值 :"+ ObjectAddress.addressOf(str3));

        System.out.println("str3.intern()後==str1: "+(str3 == str1));
    }
}

看下輸出結果:我們發現利用此方法str3的地址值與str1地址一致了,也就是說str3也指向了常量池中初始的記憶體分配地址。
在這裡插入圖片描述
把以上改造後的程式碼,再用這張圖說明就一清二楚了。
在這裡插入圖片描述

寫在最後

JVM的架構設計是非常精妙的,需要深入底層進行剖析;在對系統架構的學習過程中,結合原理動手寫樣例程式碼,畫原型圖是我們學習路上必不可少的一步。

相關文章