JVM 運行時數據區域


C語言的陰影

還記得剛進大學的時候,以為這個世界上最難學的不過C語言了。盡管后來陸續學了很多的更難的課程,盡管慢慢掌握了計算機的很多原理之后,回頭來看C語言,似乎沒那么難理解,可當年初學C語言時的“陰影”,這么多年來,一直沒有散去。

我經常還能想到幾年前,懶散的趴在逸夫教學樓F1教室最后一排的座位上,聽蘭書敏老師講着“戲院”(C語言)的場景。蘭老師問到:“你們怎么都不吭聲?到底是哪里聽不懂?”老師,學生當時真是哪哪兒都沒聽懂啊。

 

身在Java,心在C(Java大神勿噴,C對我來說,真是一種情懷)

沒想到,工作一年多的時間里,用的最多的語言不是對我影響最大的C,而是大學畢業之后現學現賣的Java。所以我對C和Java都算有一點了解。

 

一條有意思的Java面試題

前幾天在搜索一個問題的解決方案時,偶然看到一個Java面試題,覺得網上絕大多數解釋,有些浮於表面。而真神們又不屑於解釋這些無聊的問題,所以覺得有必要站在一個“雙修(殘廢)”者的角度,談談這個問題。

 

Java內存分配

在解釋這個問題之前,我想簡單的記錄一下Java虛擬機對內存的分配管理。

網上有很多關於Java內存管理的講解,但不知道為什么,大多數作者並沒有系統的講解,有些過於散碎。

我們先來看看這張圖(我不會畫圖,畫的太丑,各位受累受累了)。簡單的說,Java運行時內存區域,就由上面幾部分構成。青綠色標記的,是每個線程私有的內存區域,其他的為線程共享的內存區域。我們先簡單的依次說明每個部分是用來存什么的,最后再用一個簡單的例子,將各個部分結合起來簡單介紹其內存分配的基本過程。

首先,程序計數器(pc)。這個東西對於很多開發者來說,再熟悉不過了,盡管不同領域的pc,具體用法上存在一些小小的差異,但總的來說,pc是用來記錄程序運行到哪里了,下一步又該執行哪一步操作。pc占據的內存是線程級的,即隨線程的創建而產生,隨線程的銷毀而銷毀(被回收)。

其次JVM棧和本地方法棧。這兩個棧在存儲結構上,基本相同,以至於很多的JVM產商,將二者合而為一。JVM棧,顧名思義,是用來存儲Java方法運行過程中使用的棧數據,本地方法棧就是用來存儲本地方法執行過程中的棧數據。棧中存儲的數據,是一種被稱為“棧幀”的東西。棧幀主要包括:局部變量表和操作數棧。棧幀的入棧和出棧,分別意味着一個方法的執行與結束。

接着,我們來看看方法區。方法區主要是用來存類型數據的,與類型相關的東西,比如常量,靜態變量,編譯后的代碼等,基本都存儲在這一區域。而因為“無用類”的判斷條件非常苛刻(有三點,第一,該類無可達對象,第二,該類的ClassLoader已被回收,第三,該類的Class對象無引用),這個區域存儲的內容很難會被回收,所以你可能會在很多地方看到“永久代”一詞,其實說的主要也就是這個方法區。方法區中,有個特殊的區域,被划分(邏輯划分,不一定為物理划分)出來,即“運行時常量池”。運行時常量池,保存着字面量,符號引用等。方法區是線程共享的,隨JVM啟動而創建,JVM退出而銷毀。

最后,是這個堆。堆,在很多領域也有用到。在Java中,堆,是用來存儲對象的相關內容,包括對象的對象頭和實例數據(數組對象還有一個數組的長度)。不同的JVM實現,對象可能還包括類型指針(指向對象所屬的類型信息,存在方法區中)和占位符(虛擬機實現可能需要內存對齊)等。

 

一個簡單的例子

public void test (int result, int num) {
    TestClassB classB = new TestClassB();
    classB.methodB(); 
}

public class TestClassB {
    public void methodB(result, num) {
        int finalResutl = result + num;
        ......
    }
}

//author: Feng_zhulin
//http://cnblogs.com/zhulin-jun

現在假設線程A在執行test方法,並已經執行到TestClassB classB = new TestClassB()。首先,會去判斷類TestClassB有沒有被加載到方法區中,如果沒有,先加載類(類的加載過程不詳細說明,有空可以寫篇Java類加載過程的博客)入方法區;然后因為執行的是new操作,需要創建一個對象,這時候需要在堆上申請內存(內存分配有很多方案,需要考慮多線程下的線程安全問題等諸多因素,不詳細闡述),用於存放對象的相關數據(對象頭,實例數據,類型指針,占位符等);再然后為TestClassB的成員賦“零值”(不同類型的數據,零值不同,基本數據類型int的零值為0,引用類型的零值為null,等);最后,設置對象頭。這樣對於JVM來說,對象就創建成功了(后面就是執行類的構造方法了,那是屬於Java語言層面的創建對象的過程)。

上面總提及一個叫做“對象頭”的東西,這個東西跟對象本身沒有什么關系,存儲的是對象的運行時數據,包括對象的hashcode,對象的鎖狀態,對象持有的鎖等等。比如對象的hashcode,用於指定對象的唯一性,在GC和對象定位等過程中都會用到。

 

接着,pc加一(此處加一,表示的是加上一個JVM指令的位數,表示的是下一個指令的內存地址),執行下一步:classB.methodB();這是一個方法調用。正如上面所說,方法的執行和結束,意味着方法棧中,棧幀的進棧和出棧。

 

好滴好滴,又到看圖的時候了(捂臉,我不僅不會畫圖,還沒有好用的畫圖工具,求推薦mac的良心畫圖工具,如果不是免費的,我只接受有破解版的)。對象在堆中存放,然而,對象的操作,方法的執行,就進入了“棧”。調用methodB()時,methodB()棧幀進棧,棧幀包含局部變量表和操作數棧。因為這個地方的methodB()不是類方法,所以,局部變量表的第一個變量為調用該方法的類,即classB(this)。操作數棧用於進行當前數據操作,操作結果出操作數棧,並保存進局部變量表。

例子就這樣簡單的結束了,總的來說,就是類進入方法區,創建的對象在堆中,方法執行的時候,在方法棧中。

 

下面,我們來看這個有意思的Java面試題。

當一個對象被當作參數傳遞到一個方法后,此方法可改變這個對象的屬性,並可返回變化后的結果,那么這里到底是值傳遞還是引用傳遞?

網上的標配答案:是值傳遞。Java語言的方法調用只支持參數的值傳遞。當一個對象實例作為一個參數被傳遞到方法中時,參數的值就是對該對象的引用。對象的屬性可以在被調用過程中被改變,但對對象引用的改變是不會影響到調用者的。

其實這確實是一個很無聊的問題,本來也沒有太當回事,但是一來,這個問題下面的追問者很多,我查了下知乎,對這個問題的提問者和回答人也很多;二來,答案不夠准確,或者說是,沒講到點子上,有人甚至拿《Java核心卷》里的三句話作為答案。

《Java核心卷》對這種問題有如下三句話的描述:

1.一個方法不能修改一個基本數據類型的參數
2.一個方法可以改變一個對象參數的狀態
3.一個方法不能讓對象參數引用一個新的對象

無可厚非,這三句話總結的很經典,但是這只是簡單的說出了結論,原因呢?就用這三句話解釋這個問題,給初學者帶來的感覺,只是,哦,原來Java還有這么一個定理(限制)。那么一個個由JVM規范導致的結果,都成了需要死記硬背的“定理”。

public class Program {
        public static void swap(String x, String y) {
            String temp = x;
            x = y;
            y = temp;
        }

        public static void main (String[] args) {
            String a = "testa";
            String b = "testb";
            swap (a, b);
        }
}
//author: Feng_zhulin
//http://cnblogs.com/zhulin-jun

我們接着看這段代碼,將它還原到內存中。

圖中“0x”開頭的是十六進制的內存地址,隨便舉的例子。在main()方法調用swap()方法的時候,只是將main的局部變量表中的a和b的值(指向運行時常量池的地址)拷貝到swap的局部變量表中的x和y,在swap的局部變量表中進行的換值操作,並未對main局部變量表起作用,所以,在swap退出前,x的值是“testb”, y的值是“testa”,x與y的值互換了,但a與b的值並沒有因此而改變。當然,swap退出之后,相應的局部變量表會被回收,也就沒有所謂的x和y了。

這是這個問題所真正涉及的知識點,我很認同知乎上那位朋友的話,沒有必要非得分出個所謂的“值傳遞”和“引用傳遞”。

這邊我順便提一點在C中,是怎么做到交換上面例子中a和b這兩個值的。

在C中有一個很神奇的東西,名字叫“指針”。可以很簡單的認為,它就是地址。那么“指針的指針”,就是“地址的地址”。上面以“0x”開頭的數據,就是內存地址,如果將這個地址賦值給一個C中的變量,那么這個變量就稱為指針變量。那么我們完全可以通過指針,透過中間變量,直接操作a和b中存儲的內容(此處說的是地址),甚至是直接操作到“testa”和“testb”。

C語言因指針而美麗,卻也因指針而復雜。Java解決了C中內存需要開發者自己管理的問題,也去除了指針的概念,讓程序出錯的概率大幅度降低,卻也因為沒有指針,在我這種裝了兩個半桶漿糊的人眼中,很多地方變的不可思議的臃腫和麻煩。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM