C語言的陰影面試
還記得剛進大學的時候,覺得這個世界上最難學的不過C語言了。儘管後來陸續學了不少的更難的課程,儘管慢慢掌握了計算機的不少原理以後,回頭來看C語言,彷佛沒那麼難理解,可當年初學C語言時的「陰影」,這麼多年來,一直沒有散去。數組
我常常還能想到幾年前,懶散的趴在逸夫教學樓F1教室最後一排的座位上,聽蘭書敏老師講着「戲院」(C語言)的場景。蘭老師問到:「大家怎麼都不吭聲?究竟是哪裏聽不懂?」老師,學生當時真是哪哪兒都沒聽懂啊。安全
身在Java,心在C(Java大神勿噴,C對我來講,真是一種情懷)多線程
沒想到,工做一年多的時間裏,用的最多的語言不是對我影響最大的C,而是大學畢業以後現學現賣的Java。因此我對C和Java都算有一點了解。工具
一條有意思的Java面試題this
前幾天在搜索一個問題的解決方案時,偶然看到一個Java面試題,以爲網上絕大多數解釋,有些浮於表面。而真神們又不屑於解釋這些無聊的問題,因此以爲有必要站在一個「雙修(殘廢)」者的角度,談談這個問題。spa
Java內存分配線程
在解釋這個問題以前,我想簡單的記錄一下Java虛擬機對內存的分配管理。3d
網上有不少關於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中內存須要開發者本身管理的問題,也去除了指針的概念,讓程序出錯的機率大幅度下降,卻也由於沒有指針,在我這種裝了兩個半桶漿糊的人眼中,不少地方變的難以想象的臃腫和麻煩。