從一道面試題深入了解java虛擬機內存結構


記得剛大學畢業時,為了應付面試,瘋狂的在網上刷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虛擬機內存區域的作用。


免責聲明!

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



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