前言
在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));
}
}
輸出
先說兩個比較結果:
- str1的地址值等於str2的地址值
- str1的地址值等於CONST_STRING的地址值
- str3的地址值不等於str1地址值,即也不等於str2值
圖解
有些同學有可能會有疑問,明明四個字串的值都是“Hello World”,地址值卻有相等 ,也有不相等的,這裡就會引入JVM的堆記憶體、棧記憶體,常量池的一些基本概念,我們直接上圖再講解說明:
- 定義靜態常量CONST_STRING放入方法區;賦值時,JVM會在字串常量池中放入"Hello World"字串供共享使用,並將記憶體地址賦給靜態常量。
- 定義str1變數並給該字串賦值時,JVM首先會在字串常量池中尋找字串值相同的記憶體地址,並將該記憶體地址賦值str1變數。所以在程式中列印輸出str1變數的地址值是否等 於靜態常量CONST_STRING的地址值時,得到的結果是True.
- 定義str2變數時按以上2過程同樣處理,也即str2的地址值等於CONST_STRING和str1的地址值。
- 在定義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的架構設計是非常精妙的,需要深入底層進行剖析;在對系統架構的學習過程中,結合原理動手寫樣例程式碼,畫原型圖是我們學習路上必不可少的一步。