從一道面試題深入了解java虛擬機記憶體結構
- 2019 年 10 月 3 日
- 筆記
記得剛大學畢業時,為了應付面試,瘋狂的在網上刷JAVA的面試題,很多都靠死記硬背。其中有道面試題,給我的印象非常之深刻,有個大廠的面試官,順著這道題目,一直往下問,問到java虛擬機的知識,最後把我給問住了。
我當時的表情是這樣的:
後來我有機會面試別人了,也按照他的思路出面試題,很多已經工作了2年的程式設計師,結果也和我當年一樣,都敗在java虛擬機知識上。
我們先看面試題:
String str1 = "hello Alunbar"; String str2 = new String(str1);
會創建幾個對象?
網上給出的解釋是創建2個對象,str1對象在常量池中,str2對象在堆中。
下面是我和面試官的對話。
面試官:上面的程式碼創建了幾個對象?
我:2個。
面試官:為什麼是2個呢?
我:str1對象在常量池中,str2對象在堆中。用「=」等號創建String對象時,會先從字元串常量池中查找是否已經存在字元串對象,存在就直接返回引用地址,否則創建字元串對象並返回引用地址。
面試官:為什麼會在常量池中創建字元串對象?
我:。。。我思考了半分鐘,尷尬的回答不知道。
面試官:說說jvm虛擬機的記憶體結構。
我:。。。我再次面露難色,場面一度非常尷尬。
這次面試結束之後,我就回去瘋狂查找資料,了解jvm虛擬機的相關知識。
這也是我的第一次面試,給我的印象非常之深刻。
下面我們來說說面試官的兩個問題。
1、為什麼會在常量池中創建字元串對象。
2、java虛擬機的記憶體結構。
先來看第一個問題。
為什麼會在常量池中創建字元串對象?
字元串在所有程式語言中都是最常用的類型,其他的數據類型都可以轉換為字元串類型,像int、long等基本數據類型和String都是可以互相轉換的。為了提高字元串的使用效率,jvm虛擬機中特別開闢了一個常量池的記憶體空間,用於存儲基本數據類型的對象,常量池中的對象是可以相互共享的,當然也包括了String。
我們一般將儲存字元串的常量池成為字元串常量池。字元串常量池中會存在很多已經創建好的字元串對象,由於String類是用final修飾的,它的值一經創建就不可改變,因此我們不用擔心String對象共享而帶來程式的混亂。
我們來看一段的程式碼:
String s1 = "Hello"; String s2 = "Hello";
這段程式碼只創建一個對象,s1和s2是同一個對象。根據上面的解讀,java String s1 = "Hello"
這行程式碼會先在字元串常量池查找Hello對象,沒有發現,然後創建Hello對象並將引用返回給s1。java String s2 = "Hello"
這行程式碼,也先去字元串常量池中查找Hello對象,發現已經存在,則直接返回給s2。因此s1和s2是同一個對象。
接著說說使用new創建字元串對象。
通過new創建字元串對象,會在堆中開闢一塊新的記憶體空間,存儲String字元串對象,因此使用new方式都會生成新的字元串對象,不管字元串的內容是否一致,使用new創建字元串時存在堆中,堆中的對象會被回收,而使用「=」創建字元串對象,是存放在常量池中,不會被回收,因此建議使用「=」的方式創建字元串對象,避免不必要的java對象創建和銷毀的開銷。
我們來看下面的創建字元串對象時的記憶體結構圖:
s1和s2是通過「=」創建的字元串對象,它們的記憶體地址都一樣,s3是使用new方式創建的字元串對象,s3和s1、s2的記憶體地址不一樣。
現在接著看第二個問題。
java虛擬機的記憶體結構
虛擬機記憶體結構是一個很複雜的問題,這裡只能講一個大概,主要講各個記憶體區域的作用。
java虛擬機由類載入器、運行時數據區和執行引擎構成。如下圖所示:
平時我們說的java虛擬機記憶體結構,就是講運行時數據區。
java虛擬機在執行java程式時,會將記憶體分為幾個區域:程式計數器、方法區、虛擬機棧、本地方法棧、堆。
其中,方法區和堆是執行緒共享,程式計數器、虛擬機棧、本地方法棧時執行緒不共享。
1、程式計數器
只要學過彙編語言,對這個程式計數器都好理解,就是記錄下一條將要執行的位元組碼指令。
通過作業系統知識我們知道啟動一個程式時,就會創建一個進程,因此在執行java程式時,就會創建一個進程,java虛擬機就是一個進程。
一個進程中由多個執行緒組成,在任何一個時刻,java虛擬機只能執行一條執行緒中的指令。
java虛擬機通過讀取某一個執行緒中的程式計數器決定該執行緒需要執行哪個基礎功能,例如循環、讀取資料庫、跳轉、異常處理、執行緒恢復等。
因此每個執行緒的程式計數器是相互獨立,互不影響的。
2、java虛擬機棧
就是我們常說的java棧,在執行方法時,會在java棧中創建一個棧幀,用於存儲局部變數表、操作數棧、方法出口等資訊。
局部變數表中又會存放執行方法需要的boolean、char等各種基本數據類型,對象引用等。局部變數表大小在程式碼編譯期間就已經確定。java棧也是執行緒私有。
創建執行緒時同步創建java棧,執行緒結束,java棧也同時銷毀,釋放佔用的記憶體。
3、本地方法棧
和java虛擬機棧功能類似,有的虛擬機會將java虛擬機棧和本地方法棧合併。本地方法棧主要為虛擬機執行Native方法提供服務。
4、java堆
虛擬機中最大的一塊記憶體區域,虛擬機啟動時創建,主要用於存放對象實例,這塊記憶體區域由所有執行緒共享。這個區域內的對象,可以被所有的執行緒訪問。
這個區域也是java虛擬機重點管理的對象,當這塊區域中的對象沒有被引用,達到回收標準時,就會被java垃圾收集器回收,釋放佔用的內容空間。
java堆分為新生代和老年代,新生代又分為Eden空間、From Survivor空間和To Survivor空間。
使用new操作創建對象時,就會在這個區域開闢一塊記憶體用於存儲對象。
上面提到的java String str1 = new String("Hello")
創建字元串,就會在java堆中開闢一塊記憶體用於存儲str1對象。
5、方法區
方法區主要存儲被虛擬機載入的類資訊、常量、靜態變數等數據,我們也將這個記憶體區域稱為永久代,這個區域不會進行記憶體回收。
方法區和java堆一樣,所有執行緒共享。
方法區中包含一個運行時常量池,上面提到的java String str = "Hello"
創建字元串,就是在運行時常量池中創建「Hello」對象。
小結:
1、兩種創建字元串對象的差異。
2、java虛擬機記憶體區域的作用。