JVM--解析Java內存區域及數據的內存分配與線程安全之間的一些聯繫

最近一直在看《Java多線程編程核心技術》的第二章,主要講的是線程共享變量與線程私有變量以及如何寫出線程安全的代碼。看這部分一開始沒太注意,只是記住了一條規則,“類中的成員變量,也叫實例變量,也叫全局變量,它是非線程安全,是所有線程共享的變量,定義在方法中的私有變量是線程安全的,是每個線程私有的”。很好理解不是嗎,然後一帆風順的看到了關於volatile這部分的知識,看過之後我陷入了凌亂。。。關於這部分我之後進行總結,而現在我覺得你如果真的想寫出線程安全的代碼,那麼Java的內存分配以及佈局就是我們需要掌握的基礎。爲此,我粗略的看了一下《深入理解Java虛擬機》這本書的第二章,並且查閱了一些資料,現在彙總整理如下。

注:學習這部分內容之前如果你對進程的內存映像或數據在內存中的分配有大概的瞭解,建議你先忘記它們,因爲這是講Java虛擬機運行時的數據區,和之前的知識並不相同,所以學習的時候不要拿自己以前所瞭解的知識進行比較與衡量。


Java虛擬機運行時的數據區

先來看一張圖片:
Java虛擬機運行時的數據區

在這裏我們只需要關注線程共享區中的堆,以及線程獨佔區中的虛擬機棧,就是我們平時說的棧,只是這種說法在JVM中並不嚴謹,正確的應該說是虛擬機棧中局部變量表部分。(被static關鍵字聲明的東西就是存儲在方法區中)

其次,我在查閱資料的時候,看到網上很多資料都說棧中的數據共享而堆中的數據不共享,起初我還以爲是博主寫錯了,瞭解之後發現我們考慮問題的立場不同,我們今天要討論的是堆或棧內的數據對多線程是否共享,而他們所說的棧內數據共享則是在創建新變量時爲節省內存空間而採取的一種措施,被稱爲Slot複用,兩個完全是不同的東西。具體的區別在後面給大家說明。


虛擬機棧與堆

看了上面的圖片,對JVM的數據區也有了個大概的認識,我們來詳細說一下虛擬機棧和堆中到底都存儲的是哪些數據。

下面這段話摘自網上他人博客:

基本類型(primitive types), 共有8種,即int, short, long, byte, float, double, boolean, char(注意,並沒有string的基本類型)。這種類型的定義是通過諸如int a = 3; long b = 255L;的形式來定義的,稱爲自動變量。值得注意的是,自動變量存的是字面值,不是類的實例,即不是類的引用,這裏並沒有類的存在。如int a = 3; 這裏的a是一個指向int類型的引用,指向3這個字面值。這些字面值的數據,由於大小可知,生存期可知(這些字面值固定定義在某個程序塊裏面,程序塊退出後,字段值就消失了),出於追求速度的原因,就存在於棧中。

最後需要補充的是所有對象的引用也都存在於棧中,而實際的對象本身是存儲在堆中的,我們這時候倒可以將引用理解爲一個指針,它指向了我們在堆中創建的對象。

再來說明前面說的棧內數據共享是什麼東西,還是摘自他人博客:

另外,棧有一個很重要的特殊性,就是存在棧中的數據可以共享。假設我們同時定義int a = 3; int b = 3; 編譯器先處理int a = 3;首先它會在棧中創建一個變量爲a的引用,然後查找有沒有字面值爲3的地址,沒找到,就開闢一個存放3這個字面值的地址,然後將a指向3的地址。接着處理int b = 3;在創建完b的引用變量後,由於在棧中已經有3這個字面值,便將b直接指向3的地址。這樣,就出現了a與b同時均指向3的情況。 特別注意的是,這種字面值的引用與類對象的引用不同。假定兩個類對象的引用同時指向一個對象,如果一個對象引用變量修改了這個對象的內部狀態,那麼另一個對象引用變量也即刻反映出這個變化。相反,通過字面值的引用來修改其值,不會導致另一個指向此字面值的引用的值也跟着改變的情況。

至於堆中存儲的數據,只要記住一句話,所有new出來的變量都存儲在堆中。但是在這裏我們還要考慮String類型的特殊性和自動拆箱與裝箱的相關概念,好吧,概念實在太多了。我們之後再說這些問題。我們先來看兩個問題:(下面講述的問題只針對同一個對象,在多個不同的對象中,堆中的數據也是不共享的)

1.基本數據類型的成員變量放在jvm的哪塊內存區域裏?看了上面的概念之後,我們知道基本數據類型應該是存放在虛擬機棧中的。如果你也認爲是虛擬機棧中,那麼根據上面的圖片,它應該屬於線程獨佔區啊,怎麼會屬於共享變量呢?

2.與上面的問題對應,方法中新建的非基本數據類型放在jvm的哪塊內存區域裏?如果我們在方法中new了一個對象,按道理來說,new 出來的對象都是存放在堆上的,而根據上圖我們又發現… …Java堆是屬於線程共享區的。這是怎麼一回事呢?

首先回答第一個問題:

class {
    private int i;
}

基本數據類型放在棧中,這一概念的確沒有錯,但是這個說法又不是很準確,如上面的代碼,基本數據類型的全局變量i,它是存放在java堆中。因爲它不是靜態的變量,不會獨立於類的實例而存在,而該類實例化之後,放在堆中,當然也包含了它的屬性i。因此成員變量就算是基本數據類型也共享。

是不是覺得有點繞?然而事實的確是這樣。

再來看第二個問題,對象的確是存放在堆中,但我們也說了線程不安全只針對單例模式的成員變量,而此時如果對象被定義在了方法之中,那麼當每個線程調用一次方法都會新創建一個對象,這些對象都屬於每個線程所私有,所以雖然對象本身存在於堆中,但也並不共享。至於基本類型由於每個線程執行時將會把局部變量放在各自棧幀的工作內存中,線程間不共享,故不存在線程安全問題。

最後結合Java虛擬機運行時的數據區總結一下,就是對於同一對象(單例模式),成員變量共享,局部變量不共享。


運行時常量池

運行時常量池存在於方法區中,常量池裏面主要存儲字符串常量和基本類型常量(public static final)。

對於字符串:其對象的引用都是存儲在棧中的,如果是編譯期已經創建好(直接用雙引號定義的)的就存儲在常量池中,如果是運行期(new出來的)才能確定的就存儲在堆中。對於equals相等的字符串,在常量池中永遠只有一份,在堆中有多份。

如以下代碼:

String s1 = "china"; 
String s2 = "china";
String s3 = "china"; 
String ss1 = new String("china"); 
String ss2 = new String("china"); 
String ss3 = new String("china");   

這裏解釋一下,對於通過 new 產生一個字符串(假設爲 ”china” )時,會先去常量池中查找是否已經有了 ”china” 對象,如果沒有則在常量池中創建一個此字符串對象,然後堆中再創建一個常量池中此 ”china” 對象的拷貝對象。

也就是有道面試題: String s = new String(“xyz”); 產生幾個對象?

一個或兩個。如果常量池中原來沒有 ”xyz”, 就是兩個。如果原來的常量池中存在“xyz”時,就是一個。


對於基礎類型的變量和常量:變量存儲在棧中,常量存儲在常量池中。

如以下代碼:

int i1 = 9; 
int i2 = 9; 
int i3 = 9;  
public static final int INT1 = 9; 
public static final int INT2 = 9; 
public static final int INT3 = 9;

自動拆箱與自動裝箱

public class JVM {
    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 2;
        Integer c = 3;
        Integer d = 3;
        Integer e = 321;
        Integer f = 321;

        Long g = 3L;

        out.println(c == d);
        out.println(e == f);
        out.println(c == (a+b));
        out.println(c.equals(a+b));
        out.println(g == (a+b));
        out.println(g.equals(a+b));
    }
}

來看一下程序的運行結果是否符合你的預期:

true
false
true
true
true
false

首先我們先來了解一下包裝類數據:

包裝類數據,如Integer, String, Double,Long等將相應的基本數據類型包裝起來的類。這些類數據全部存在於堆中。

然後我們在對自動裝箱與拆箱做個了結:

1.包裝類的“==”運算在不遇到算術運算的情況下不會自動拆箱,equals()方法不處理數據轉型的關係。

2.在自動裝箱時,把int變成Integer的時候,是有規則的,當你的int的值在-128-IntegerCache.high(127) 時,返回的不是一個新new出來的Integer對象,而是一個已經緩存在堆中的Integer對象,(我們可以這樣理解,系統已經把-128到127之 間的Integer緩存到一個Integer數組中去了,如果你要把一個int變成一個Integer對象,首先去緩存中找,找到的話直接返回引用給你就 行了,不必再新new一個),如果不在-128-IntegerCache.high(127) 時會返回一個新new出來的Integer對象。

對於第二點,不僅Integer有這種特性,其它包裝類數據也具有,我們可以看一下Long的源碼:

private static class LongCache {
    private LongCache(){}

    static final Long cache[] = new Long[-(-128) + 127 + 1];

    static {
        for(int i = 0; i < cache.length; i++)
        cache[i] = new Long(i - 128);
    }
}

在瞭解了上面的概念之後,我相信你已經基本能夠正確解釋代碼運行的結果了。

最後我來解釋一下最後兩個的運行結果爲什麼是true與false,先來看一下後兩句話在進行編譯之後在.class文件中的樣子吧:

System.out.println(g.longValue() == (long)(a.intValue() + b.intValue()));
System.out.println(g.equals(Integer.valueOf(a.intValue() + b.intValue())));

我們可以看到編譯器對out.println(g == (a+b));進行編譯的時候,進行了拆箱與向上轉型的操作,所以此時比較的僅僅是兩個變量的字面值,與基本數據類型的比較是一樣的,所以是true,而最後仍然比較的是對象中的數據並且對a沒有進行向上轉型,Long中存在的數據肯定就和Integer中存在的數據不等了,所以爲false。

再說一點,我們能將字面值直接賦給Integer類是因爲Java語法糖的存在,實際上Integer a = 1在經過編譯之後是這樣的:Integer a = new Integer(1),語法糖幫助我們簡化了語法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章