本文章已受權微信公衆號郭霖(guolin_blog)轉載。java
本文章講解的內容是Java虛擬機自動內存管理機制。git
對於從事C、C++程序開發的開發人員來講,在內存管理領域,他們既擁有每個對象的「全部權」,又擔負着每個對象生命開始到終結的維護責任。程序員
對於Java程序員來講,在Java虛擬機自動內存管理機制的幫助下,再也不須要爲每個new操做去寫配對的delete/free代碼,不容易出現內存泄漏和內存溢出問題,這看起來一切美好,不過正是由於Java程序員把內存控制的權力交給Java虛擬機,一旦出現內存泄漏和內存溢出的問題的時候,若是不瞭解Java虛擬機是怎樣使用內存的話,那麼排查錯誤將會一項異常艱難的工做。github
Java虛擬機在執行Java程序的過程當中會把它所管理的內存劃分爲若干個不一樣的數據區域。這些區域有各自的用途,以及建立和銷燬的時間,有的區域隨着Java虛擬機進程的啓動而存在,有的區域則依賴用戶線程的啓動和結束而創建和銷燬。根據**《Java虛擬機規範(Java SE 7版)》的規定,Java虛擬機所管理的內存將會包括如下幾個運行時數據區域**,以下圖所示:算法
由全部線程共享的數據區:編程
線程隔離的數據區:數組
程序計數器(Program Counter Register)是一塊較小的內存空間,它能夠看做是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型(僅是概念模型,各類虛擬機可能會經過一些更高效的方式去實現)裏,字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。緩存
因爲Java虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)都只會執行一條線程中的指令。所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,咱們稱這類內存區域爲**「線程私有」的內存**。安全
此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。服務器
與程序計數器同樣,Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型,每一個方法在執行的同時都會建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等消息。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。
常常有人把Java內存區分爲堆內存(Heap)和棧內存(Stack),這種分法比較粗糙,Java內存區域的劃分實際上遠比這複雜。這種劃分方式的流行只能說明大多數程序員最關注的、與對象內存分配關係最密切的內存區域是這兩塊。其中所指的**「堆」後面會講到,而所指的棧就是如今講的Java虛擬機棧中的局部變量表部分**。
局部變量表存放了編譯器可知的各類基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它不等同於對象自己,多是一個指向對象起始地址的引用指針,也多是指向一個表明對象的句柄或其餘與此對象相關的位置)和returnAddress類型(指向了一條字節碼指令的地址)。
其中64位長度的long和double類型的數據會佔用兩個局部變量空間(Slot),其他的數據類型只佔用一個。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法須要在幀中分配多大的局部變量空間是徹底肯定的,在方法運行期間不會改變局部變量表的大小。
在Java虛擬機規範中,對這個區域規定了兩種異常狀態:
本地方法棧(Native Method Stack)與虛擬機棧所發揮的做用是很是類似的,它們之間的區別不過是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的Native方法服務。在虛擬機規範中對本地方法棧中方法使用的語言、使用方式與數據結構並沒有強制規定,所以具體的虛擬機能夠自由實現它。甚至有的虛擬機(例如:Sun HotSpot虛擬機)直接就把虛擬機棧和本地方法棧合二爲一。與虛擬機棧同樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。
對於大多數應用來講,Java堆(Java Heap)是Java虛擬機所管理的的內存中最大的一塊。Java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存。這一點在Java虛擬機規範中描述是:全部的對象實例以及數組都要在堆上分配,可是隨着JIT編譯器的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會致使一些微妙的變化發生,全部的對象都分配在堆上也漸漸變得不是那麼**」絕對「**了。
Java堆是垃圾收集器管理的主要區域,所以不少時候也被稱作**」GC堆(Grabage Collected Heap)。從內存回收的角度來看,因爲如今收集器基本採用分代收集算法**,因此Java堆中還能夠細分爲:新生代和老年代;再細緻一點的有Eden空間、From Survivor空間、To Survivor空間等。從內存分配的角度來看,線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,TLAB)。不過不管如何劃分,都與存放內容無關,不管哪一個區域,存儲的仍然是對象實例,進一步劃分的目的是爲了更好地回收內存,或者更快地分配內存。
根據Java虛擬機規範的規定,Java堆能夠處於物理上不連續的內存空間中,只要邏輯上是連續便可,就像咱們的磁盤同樣。在實現時,既能夠實現成固定大小的,也能夠是可擴展的,不過當前主流的虛擬機都是按照可擴展來實現的**(經過-Xmx和-Xms控制)。若是在堆中沒有內存完成實例分配**,而且堆也沒法擴展時,將會拋出OutOfMemoryError異常。
方法區(Method Area)與Java堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然Java虛擬機規範把方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫作非堆(Non-Heap),目的應該是與Java堆區分開來。
對於習慣在HotSpot虛擬機上開發、部署程序的開發者來講,不少人都更願意把方法區稱爲**「永久代」(Permanent Generation),本質上二者並不等價**,僅僅是由於HotSpot虛擬機的設計團隊選擇把GC分代收集擴展到方法區,或者說使用永久代來實現方法區而已,這樣HotSpot的垃圾收集器能夠像管理Java堆同樣管理這部分內存,可以省去專門爲方法區編寫內存管理代碼的工做。對於其餘虛擬機(例如:BEA JRockit、IBM J9等等)來講是不存在永久代的概念的。原則上,如何實現方法區屬於虛擬機實現細節,不受虛擬機規範約束,可是使用永久代來實現方法區,如今看來並非一個好主意,由於這樣更容易遇到內存溢出問題(永久代有-XX:MaxPermSize的上限,J9和JRockit只要沒有觸碰到進程可用內存的上限,例如:32系統中的4GB,就不會出現問題),並且有極少數方法(例如:String.intern())會由於這個緣由致使不一樣虛擬機下有不一樣的表現。所以,對HotSpot虛擬機,根據官方發佈的路線圖信息,如今也有放棄永久代並逐步改成採用Native Memory來實現方法區的規劃了,在目前已經發布的JDK 1.7的HotSpot中,已經把本來放在永久代的字符串常量池移出。
Java虛擬機規範對方法區的限制很是寬鬆,除了和Java堆同樣不須要連續的內存和能夠選擇固定大小或者可擴展外,還能夠選擇不實現垃圾收集。相對而言,垃圾收集行爲在這個區域是比較少出現的,可是並不是數據進入了方法區就如永久代的名字同樣**「永久」存在了。這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載**,通常來講,這個區域的回收「成績」比較難使人滿意,尤爲是類型的加載,條件至關苛刻,可是這部分區域的回收確實是必要的。在Sun公司的BUG列表中,曾出現過的若干個嚴重的BUG就是因爲低版本的HotSpot虛擬機對此區域未徹底回收而致使內存泄漏。
根據Java虛擬機規範的規定,當方法區沒法知足內存分配須要時,將拋出OutOfMemoryError異常。
運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯器生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放。
Java虛擬機對Class文件每一部分(天然也包括常量池)的格式都有嚴格規定,每個字節用於存儲哪一種數據都必須符合規範上的要求才會被虛擬機承認、裝載和執行,可是對於運行時常量池,Java虛擬機規範沒有作任何細節的要求,不一樣的提供商實現的虛擬機能夠按照本身的須要來實現這個內存區域。不過,通常來講,除了保存Class文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中。
運行時常量池相對於Class文件常量池的另一個重要特徵是具有動態性,Java語言並不要求常量必定只有編譯期才能產生,也就是並不是預置入Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的即是String類的**intern()**方法。
既然運行時常量池是方法區的一部分,天然受到方法區內存的限制,當常量池沒法再申請到內存時就會拋出OutOfMemoryError異常。
直接內存(Direct Memory)並非虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域。可是這部分內存也被頻繁地使用,並且也可能致使OutOfMemoryError異常出現,因此咱們放在這裏一塊兒講解。
在JDK1.4中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可使用Native函數庫直接分配堆外內存,而後經過一個存儲在Java堆中的DirectByteBuffer對象做爲這塊內存的引用進行操做。這樣就能在一些場景中顯著提升性能,由於避免了在Java堆和Native堆中來回複製數據。
顯然,本機直接內存的分配不會受到Java堆大小的限制,可是,既然是內存,確定仍是會受到本機總內存(包括RAM以及SWAP區或者分頁文件)大小以及處理器尋址空間的限制。服務器管理員在配置虛擬機參數時,會根據實際內存設置**-Xmx等參數信息,可是常常忽略直接內存**,使得各個內存區域總和大於物理內存限制(包括物理和操做系統級的限制),從而致使動態擴展時出現OutOfMemoryError異常。
介紹完Java虛擬機的運行時數據區以後,咱們大體知道了虛擬機內存的概況,在瞭解內存放了些什麼後,也許就會想更進一步瞭解這些虛擬機內存中的數據的其餘細節,譬如它們是如何建立、如何佈局以及如何訪問的。對於這樣涉及細節的問題,必須把討論範圍限定在具體的虛擬機和集中在某一個內存區域上纔有意義。基於實用優先的原則,我以經常使用的虛擬機HotSpot和經常使用的內存區域Java堆爲例,深刻探討HotSpot虛擬機在Java堆中對象分配、佈局和訪問的全過程。
Java是一門面向對象的編程語言,在Java程序運行過程當中無時無刻都有對象被建立出來。在語言層面上,建立對象(例如:克隆、反序列化)一般僅僅是一個new關鍵字而已,而在虛擬機中,對象(文章中討論的對象僅限於普通的Java對象,不包括數組和Class對象等)的建立又是怎樣一個過程呢?
虛擬機遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已被加載、解析和初始化過。若是沒有,那必須先執行相應的類加載過程。
在類加載檢查經過後,接下來虛擬機將爲新生對象分配內存。對象所需內存的大小在類加載完成後即可徹底肯定,爲對象分配空間的任務等同於把一塊肯定大小的內存從Java堆中劃分出來。假設Java堆中內存時絕對規整的,全部用過的內存都放在一邊,空閒的內存放在另外一邊,中間放着一個指針做爲分界點的指示器,那所分配內存就僅僅是把那個指針向空閒空間那邊挪動一段與對象大小相等的距離,這種分配方式成爲**「指針碰撞」(Bump the Pointer)。若是Java堆中的內存並非規整的**,已使用的內存和空閒的內存相互交錯,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄,這種分配方式成爲**「空閒列表」(Free List)。選擇哪一種分配方式由Java堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。所以,在使用Serial**、ParNew等帶Compact過程的收集器時,系統採用的分配算法是指針碰撞,而使用CMS這種基於Mark-Sweep算法的收集器時,一般採用空閒列表。
除如何劃分可用空間以外,還有另一個須要考慮的問題是對象建立在虛擬機中是很是頻繁的行爲,即便是僅僅修改一個指針所指向的位置,在併發狀況下也並非線程安全的,可能出現正在給對象A分配內存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的狀況。解決這個問題有兩種方案:
內存分配完成後,虛擬機須要將分配到的內存空間都初始化爲零值(不包括對象頭),若是使用TLAB,這一工做過程也能夠提早至TLAB分配時進行。這一步操做保證了對象的實例字段在Java代碼中能夠不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。
接下來,虛擬機要對對象進行必要的設置,譬如這個對象是哪一個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭(Object Header)之中。根據虛擬機當前的運行狀態的不一樣,例如:是否啓用偏向鎖等,對象頭會有不一樣的設置方式。
在上面工做都完成後,從虛擬機的視角來看,一個新的對象已經產生了,可是從Java程序的視角來看,對象建立纔剛剛開始——init方法尚未執行,全部的字段都還爲零。因此,通常來講(由字節碼中是否跟隨invokespecial指令所決定),執行new指令以後會接着執行init方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算徹底生產出來。
總結一下對象的建立過程:
在HotSpot虛擬機中,對象在內存中存儲的佈局能夠分爲三塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。
HotSpot虛擬機的對象頭包括兩部分信息:
第一部分用於存儲對象自身的運行時數據,例如:哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分數據的長度在32位和64位的虛擬機(未開啓壓縮指針)中分別爲32bit和64bit,官方稱它爲**「Mark Word」。對象須要存儲的運行時數據不少**,其實已經超出了32位、64位Bitmap結構所能記錄的限度,可是對象頭信息是與對象自身定義的數據無關的額外存儲成本,考慮到虛擬機的空間效率,Mark Word被設計成一個非固定的數據結構以便在極小的空間內存儲儘可能多的信息,它會根據對象的狀態複用本身的存儲空間,例如:在32位的HotSpot虛擬機中,若是對象處於未被鎖定的狀態下,那麼Mark Word的32bit空間中的25bit用於存儲對象哈希碼,4bit用於存儲對象分代年齡,2bit用於存儲鎖標誌位,1bit固定爲0,而在其餘狀態(輕量級鎖定、重量級鎖定、GC標記、可偏向)下對象的存儲內容以下圖所示:
第二部分是類型指針,即對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。並非全部的虛擬機實現都必須在對象數據上保留類型指針,換句話說,查找對象的元數據信息並不必定要通過對象自己,這點將在下面要講的對象的訪問定位講解。另外,若是對象是一個Java數組,那在對象頭中必須有一塊用於記錄數組長度的數據,由於虛擬機能夠經過普通Java對象的元數據信息肯定Java對象的大小,可是從數組的元數據中卻沒法肯定數組的大小。
實例數據是對象真正存儲的有效信息,也是在程序代碼中所定義的各類類型的字段內容。不管是從父類繼承下來的,仍是在子類中定義的,都須要記錄起來。這部分的存儲順序會受到虛擬機分配策略參數(FieldsAllocationStyle)和字段在Java源碼中定義順序的影響。HotSpot虛擬機默認的分配策略爲longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),從分配策略中能夠看出,相同寬度的字段老是被分配到一塊兒。在知足這個前提條件的狀況下,在父類中定義變量會出如今子類以前。若是CompactFields參數值爲true(默認爲true),那麼子類之中較窄的變量也可能會插入到父類變量的空隙之中。
對齊填充不是必然存在的,也沒有特別的定義,它僅僅起着佔位符的做用。因爲HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說,就是對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(1倍或者2倍),所以,當對象實例數據部分沒有對齊時,就須要經過對齊填充來補全。
創建對象是爲了使用對象,咱們的Java程序須要經過棧上的reference數據來操做堆上的具體對象。因爲reference類型在Java虛擬機規範中只規定一個指向對象的引用,並無定義這個引用應該經過何種方式去定位、訪問堆中對象的具體位置,因此對象訪問方式也是取決於虛擬機實現而定的。目前主流的訪問方式有使用句柄和直接指針兩種:
若是使用句柄訪問的話,那麼Java堆中將會劃分出一塊內存來做爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息,以下圖所示:
若是使用直接指針訪問的話,那麼Java堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,而reference中存儲的直接就是對象地址,以下圖所示:
這兩種對象訪問方式各有優點,使用句柄來訪問的最大好處就是reference中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是很是廣泛的行爲)時只會改變句柄中實例數據指針,而reference自己不須要修改。
使用直接指針訪問方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷,因爲對象的訪問在Java中很是頻繁,所以這裏開銷聚沙成塔後也是一項很是可觀的執行成本。本文章討論的虛擬機Sun HotSpot使用的是第二種方式,也就是使用直接指針進行對象訪問的,可是從整個軟件開發的範圍來看,各類語言和框架使用句柄來進行對象訪問也是十分常見的。
我想聊一下Java基本數據類型包裝類常量池和String類型常量池。
Java基本數據類型中的byte、short、int、long、boolean、char的包裝類使用了常量池,它們只在**[-128, 127]範圍內使用相應類型的緩存數據**,超出這個範圍的就會建立新的對象,而float和double的包裝類沒有使用常量池。
舉個例子,代碼以下所示:
/** * Created by TanJiaJun on 2020/6/26. */
public class ConstantPoolTest {
public static void main(String[] args) {
Integer i1 = 3;
Integer i2 = 4;
Integer i3 = 7;
Integer i4 = 7;
Integer i5 = 777;
Integer i6 = 777;
Integer i7 = new Integer(3);
Integer i8 = new Integer(4);
Integer i9 = new Integer(7);
Double d1 = 7.7;
Double d2 = 7.7;
System.out.println(i3 == i4); // true
System.out.println(i1 + i2 == i3); // true
System.out.println(i5 == i6); // false
System.out.println(i3 == i9); // false
System.out.println(i7 + i8 == i9); // true
System.out.println(i7 + i8 == 7); // true
System.out.println(d1 == d2); // false
}
}
複製代碼
在Java中,==有兩個做用:
解析:
當聲明爲如上述示例代碼中的i1、i2、i3、i4、i5、i6時,編譯器會幫咱們自動裝箱,調用Integer類的valueOf方法,看下相關的源碼,源碼以下所示:
// Integer.java
package java.lang;
import java.lang.annotation.Native;
public final class Integer extends Number implements Comparable<Integer> {
private static class IntegerCache {
// 緩存的最小值是-128
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// 緩存的最大值是127
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// 數組的最大大小爲Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// 若是不能將該屬性解析爲int,就忽略它
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
}
複製代碼
能夠看到Integer類的valueOf方法,若是是大於等於IntegerCache.low的值(-128),同時小於等於IntegerCache.high的值(127),就會使用IntegerCache,也就是使用緩存,不然就建立新的Integer對象。
這裏順便提一下equals方法,它和**==有什麼區別呢?先看下Object類的equals**方法,源碼以下所示:
// Object.java
public class Object {
// 省略部分代碼
public boolean equals(Object obj) {
return (this == obj);
}
// 省略部分代碼
}
複製代碼
能夠看到equals方法的邏輯就是**==,而後看下Integer類的equals**方法,源碼以下所示:
// Integer.java
public final class Integer extends Number implements Comparable<Integer> {
// 省略部分代碼
// Integer的值
private final int value;
// 以int的形式返回該Integer的值
public int intValue() {
return value;
}
public boolean equals(Object obj) {
// 判斷參數obj是否爲Integer類的實例
if (obj instanceof Integer) {
// 若是參數obj是Integer類的實例,就調用它的intValue方法獲得值,而且判斷value是否與該值相等
return value == ((Integer)obj).intValue();
}
// 若是參數obj不是Integer類的實例,就返回false
return false;
}
// 省略部分代碼
}
複製代碼
再看下String類的equals方法,源碼以下所示:
// String.java
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
// 省略部分代碼
public boolean equals(Object anObject) {
// 判斷參數anObject的引用地址是否與該對象相同
if (this == anObject) {
// 若是參數anObject的引用地址與該對象相同,就返回true
return true;
}
// 若是參數anObject的引用地址與該對象不相同,就判斷anObject是否爲String類的實例
if (anObject instanceof String) {
// 強制轉成String對象
String anotherString = (String)anObject;
int n = length();
if (n == anotherString.length()) {
int i = 0;
// 判斷String類型的參數anObject中的每一個字符是否與該對象的每一個字符相等
while (n-- != 0) {
if (charAt(i) != anotherString.charAt(i))
// 若是String類型的參數anObject中的有其中一個字符與該對象的其中一個字符不相等,就返回false
return false;
i++;
}
// 若是String類型的參數anObject中的每一個字符都與該對象的每一個字符相等,就返回true
return true;
}
}
// 若是參數anObject不是String類的實例,就返回false
return false;
}
// 省略部分代碼
}
複製代碼
能夠看到Integer類和String類重寫了Object類的equals方法,邏輯也改爲判斷對應類型的值是否相等。
在JDK 1.7以後(包括JDK 1.7),字符串常量池從方法區移動到堆。
示例代碼以下:
String str = "譚嘉俊";
複製代碼
這種聲明方式叫作字面量聲明,它是把字符串用雙引號包起來,而後賦值給一個變量,這種狀況下,它會把字符串放到字符串常量池,而後返回給變量。
示例代碼以下:
String str = new String("譚嘉俊");
複製代碼
使用new String()方法無論在字符串常量池中有沒有,它都會在堆中建立一個新的對象。
源碼代碼以下:
// String.java
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
// 省略部分代碼
public native String intern();
}
複製代碼
能夠看到intern方法是個native方法。
字符串常量池最初是空的,由String類私有地維護,當intern方法被調用的時候,若是當前字符串存在於字符串常量池,判斷條件是使用equals方法是返回true的話,就會直接返回這個字符串在字符串常量池的引用,若是不存在,它就會在字符串常量池中建立一個引用,而且指向堆中已存在的字符串,而後返回對應的字符串常量池的引用。
舉個例子,代碼以下:
/** * Created by TanJiaJun on 2020/6/27. */
public class StringConstantPoolTest {
public static void main(String[] args) {
String str1 = "譚嘉俊";
String str2 = "譚嘉俊";
String str3 = new String("譚嘉俊");
String str4 = new String("譚嘉俊");
String str5 = "我叫譚嘉俊";
String str6 = "我叫";
String str7 = new String(str6 + "譚嘉俊");
String str8 = new String(str6 + "譚嘉俊");
System.out.println(str1 == str2); // 1.true
System.out.println(str3 == str4); // 2.false
System.out.println(str1 == str3); // 3.false
str7.intern();
System.out.println(str5 == str7); // 4.false
str7 = str7.intern();
System.out.println(str5 == str7); // 5.true
str8 = str8.intern();
System.out.println(str7 == str8); // 6.true
}
}
複製代碼
參考文獻:
[1] 周志明,深刻理解Java虛擬機(第2版)[M],機械工業出版社,2013年9月1日,37頁~49頁
個人GitHub:TanJiaJunBeyond
Android通用框架:Android通用框架
個人掘金:譚嘉俊
個人簡書:譚嘉俊
個人CSDN:譚嘉俊