讀書筆記——Java虛擬機自動內存管理機制

本文章已受權微信公衆號郭霖(guolin_blog)轉載。java

本文章講解的內容是Java虛擬機自動內存管理機制git

概述

對於從事CC++程序開發的開發人員來講,在內存管理領域,他們既擁有每個對象的「全部權」又擔負着每個對象生命開始到終結的維護責任程序員

對於Java程序員來講,在Java虛擬機自動內存管理機制的幫助下,再也不須要爲每個new操做去寫配對的delete/free代碼不容易出現內存泄漏和內存溢出問題,這看起來一切美好,不過正是由於Java程序員把內存控制的權力交給Java虛擬機,一旦出現內存泄漏內存溢出的問題的時候,若是不瞭解Java虛擬機是怎樣使用內存的話,那麼排查錯誤將會一項異常艱難的工做。github

運行時數據區域

Java虛擬機在執行Java程序的過程當中會把它所管理的內存劃分爲若干個不一樣的數據區域。這些區域有各自的用途,以及建立銷燬的時間,有的區域隨着Java虛擬機進程啓動存在,有的區域則依賴用戶線程啓動結束創建銷燬。根據**《Java虛擬機規範(Java SE 7版)》的規定,Java虛擬機所管理的內存將會包括如下幾個運行時數據區域**,以下圖所示:算法

JavaVirtualMachineRuntimeDataArea.png

由全部線程共享的數據區編程

  • 方法區
  • 直接內存

線程隔離的數據區數組

  • 程序計數器
  • 虛擬機棧
  • 本地方法棧

程序計數器

程序計數器(Program Counter Register)是一塊較小內存空間,它能夠看做是當前線程所執行的字節碼行號指示器。在虛擬機概念模型(僅是概念模型,各類虛擬機可能會經過一些更高效的方式去實現)裏,字節碼解釋器工做時就是經過改變這個計數器來選取下一條須要執行的字節碼指令分支循環跳轉異常處理線程恢復基礎功能都須要依賴這個計數器來完成。緩存

因爲Java虛擬機多線程是經過線程輪流切換分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)都只會執行一條線程中的指令。所以,爲了線程切換後能恢復正確的執行位置每條線程都須要有一個獨立的程序計數器各條線程之間計數器互不影響獨立存儲,咱們稱這類內存區域爲**「線程私有」內存**。安全

  • 若是線程正在執行的是一個Java方法,這個計數器記錄的是正在執行虛擬機字節碼指令的地址
  • 若是線程正在執行的是一個Native方法,這個計數器值則爲空(Undefined)

此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域服務器

Java虛擬機棧

程序計數器同樣,Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同虛擬機棧描述的是Java方法執行的內存模型,每一個方法在執行的同時都會建立一個棧幀(Stack Frame)用於存儲局部變量表操做數棧動態連接方法出口等消息。每個方法從調用直至執行完成的過程,就對應着一個棧幀虛擬機棧入棧出棧的過程。

常常有人把Java內存區分爲堆內存(Heap)棧內存(Stack),這種分法比較粗糙Java內存區域劃分實際上遠比這複雜。這種劃分方式的流行只能說明大多數程序員最關注的與對象內存分配關係最密切的內存區域這兩塊。其中所指的**「堆」後面會講到,而所指的就是如今講的Java虛擬機棧中的局部變量表部分**。

局部變量表存放了編譯器可知的各類基本數據類型(boolean、byte、char、short、int、float、long、double)對象引用(reference類型,它不等同於對象自己,多是一個指向對象起始地址的引用指針,也多是指向一個表明對象的句柄或其餘與此對象相關的位置)returnAddress類型(指向了一條字節碼指令的地址)

其中64位長度longdouble類型的數據會佔用兩個局部變量空間(Slot)其他的數據類型只佔用一個局部變量表所需的內存空間編譯期間完成分配,當進入一個方法時,這個方法須要在中分配多大的局部變量空間徹底肯定的,在方法運行期間不會改變局部變量表大小

Java虛擬機規範中,對這個區域規定了兩種異常狀態

  • 若是線程請求的棧深度大於虛擬機所容許的的深度,將拋出StackOverflowError異常。
  • 若是虛擬機棧能夠動態擴展(當前大部分的Java虛擬機均可動態擴展,只不過Java虛擬機規範中也容許固定長度的虛擬機棧),若是擴展時沒法申請到足夠的內存,就會拋出OutOfMemoryError異常。

本地方法棧

本地方法棧(Native Method Stack)虛擬機棧所發揮的做用是很是類似的,它們之間的區別不過是虛擬機棧虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的Native方法服務。在虛擬機規範中對本地方法棧方法使用的語言使用方式數據結構沒有強制規定,所以具體的虛擬機能夠自由實現它。甚至有的虛擬機(例如:Sun HotSpot虛擬機)直接就把虛擬機棧本地方法棧合二爲一。與虛擬機棧同樣,本地方法棧區域也會拋出StackOverflowErrorOutOfMemoryError異常。

Java堆

對於大多數應用來講,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.7HotSpot中,已經把本來放在永久代字符串常量池移出。

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異常。

HotSpot虛擬機對象探祕

介紹完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又同時使用了原來的指針分配內存的狀況。解決這個問題有兩種方案

  • 分配內存空間的動做進行同步處理——實際上虛擬機採用CAS配上失敗重試的方式保證更新操做的原子性
  • 內存分配的動做按照線程劃分到不一樣的空間之中進行,即每一個線程Java堆預先分配一小塊內存,稱爲本地線程分配緩衝(Thread Local Allocation Buffer,TLAB)。哪一個線程分配內存,就在哪一個線程TLAB上分配,只有TLAB用完並分配新的TLAB時,才須要同步鎖定虛擬機是否使用TLAB,能夠經過**-XX:+/-UseTLAB**參數來設定。

內存分配完成後,虛擬機須要將分配到的內存空間初始化零值(不包括對象頭),若是使用TLAB,這一工做過程也能夠提早至TLAB分配時進行。這一步操做保證了對象實例字段Java代碼中能夠不賦初始值直接使用,程序能訪問到這些字段數據類型所對應的零值

接下來,虛擬機要對對象進行必要的設置,譬如這個對象是哪一個類的實例、如何才能找到元數據信息對象的哈希碼對象的GC分代年齡等信息。這些信息存放在對象對象頭(Object Header)之中。根據虛擬機當前的運行狀態的不一樣,例如:是否啓用偏向鎖等,對象頭會有不一樣的設置方式

在上面工做都完成後,從虛擬機的視角來看,一個新的對象已經產生了,可是從Java程序的視角來看,對象建立纔剛剛開始——init方法尚未執行,全部的字段都還爲零。因此,通常來講(由字節碼中是否跟隨invokespecial指令所決定),執行new指令以後會接着執行init方法,把對象按照程序員的意願進行初始化,這樣一個真正可用對象纔算徹底生產出來

總結一下對象的建立過程

  1. 類加載檢查
  2. 分配內存
  3. 初始化零值
  4. 設置對象頭
  5. 執行init方法

對象的內存佈局

HotSpot虛擬機中,對象內存存儲的佈局能夠分爲三塊區域對象頭(Header)實例數據(Instance Data)對齊填充(Padding)

對象頭

HotSpot虛擬機對象頭包括兩部分信息

  • 第一部分用於存儲對象自身的運行時數據,例如:哈希碼(HashCode)GC分代年齡鎖狀態標誌線程持有的鎖偏向線程ID偏向時間戳等,這部分數據的長度在32位64位虛擬機(未開啓壓縮指針)中分別爲32bit64bit,官方稱它爲**「Mark Word」對象須要存儲的運行時數據不少**,其實已經超出了32位64位Bitmap結構所能記錄的限度,可是對象頭信息是與對象自身定義的數據無關額外存儲成本,考慮到虛擬機空間效率Mark Word被設計成一個非固定數據結構以便在極小的空間內存儲儘可能多的信息,它會根據對象的狀態複用本身的存儲空間,例如:在32位HotSpot虛擬機中,若是對象處於未被鎖定的狀態下,那麼Mark Word32bit空間中的25bit用於存儲對象哈希碼4bit用於存儲對象分代年齡2bit用於存儲鎖標誌位1bit固定爲0,而在其餘狀態(輕量級鎖定、重量級鎖定、GC標記、可偏向)對象存儲內容以下圖所示:

    HotSpotVirtualMachineObjectHeadMarkWord.png

  • 第二部分類型指針,即對象指向它的類元數據的指針虛擬機經過這個指針來肯定這個對象哪一個類實例。並非全部的虛擬機實現都必須在對象數據保留類型指針,換句話說,查找對象元數據信息並不必定要通過對象自己,這點將在下面要講的對象的訪問定位講解。另外,若是對象是一個Java數組,那在對象頭中必須有一塊用於記錄數組長度的數據,由於虛擬機能夠經過普通Java對象元數據信息肯定Java對象大小,可是從數組元數據中卻沒法肯定數組的大小

實例數據

實例數據對象真正存儲的有效信息也是在程序代碼中所定義的各類類型的字段內容。不管是從父類繼承下來的,仍是在子類中定義的,都須要記錄起來。這部分的存儲順序會受到虛擬機分配策略參數(FieldsAllocationStyle)字段在Java源碼中定義順序的影響。HotSpot虛擬機默認的分配策略longs/doublesintsshorts/charsbytes/booleansoops(Ordinary Object Pointers),從分配策略中能夠看出,相同寬度的字段老是被分配到一塊兒。在知足這個前提條件的狀況下,在父類中定義變量會出如今子類以前。若是CompactFields參數值爲true(默認爲true),那麼子類之中較窄變量也可能會插入到父類變量的空隙之中。

對齊填充

對齊填充不是必然存在的,也沒有特別的定義,它僅僅起着佔位符的做用。因爲HotSpot VM自動內存管理系統要求對象起始地址必須是8字節整數倍,換句話說,就是對象大小必須是8字節整數倍。而對象頭部分正好是8字節倍數(1倍或者2倍),所以,當對象實例數據部分沒有對齊時,就須要經過對齊填充補全

對象的訪問定位

創建對象是爲了使用對象,咱們的Java程序須要經過上的reference數據來操做上的具體對象。因爲reference類型在Java虛擬機規範只規定一個指向對象的引用並無定義這個引用應該經過何種方式去定位、訪問堆中對象的具體位置,因此對象訪問方式也是取決於虛擬機實現而定的。目前主流訪問方式有使用句柄直接指針兩種:

  • 若是使用句柄訪問的話,那麼Java堆中將會劃分出一塊內存來做爲句柄池reference中存儲的就是對象句柄地址,而句柄中包含了對象實例數據類型數據各自的具體地址信息,以下圖所示:

    AccessTheObjectThroughTheHandle.png

  • 若是使用直接指針訪問的話,那麼Java堆對象的佈局中就必須考慮如何放置訪問類型數據相關信息,而reference中存儲的直接就是對象地址,以下圖所示:

    AccessingAnObjectThroughADirectPointer.png

兩種對象訪問方式各有優點,使用句柄來訪問的最大好處就是reference中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是很是廣泛的行爲)只會改變句柄中實例數據指針,而reference自己不須要修改

使用直接指針訪問方式的最大好處就是速度更快它節省了一次指針定位的時間開銷,因爲對象的訪問Java很是頻繁,所以這裏開銷聚沙成塔後也是一項很是可觀執行成本。本文章討論的虛擬機Sun HotSpot使用的是第二種方式,也就是使用直接指針進行對象訪問的,可是從整個軟件開發的範圍來看,各類語言框架使用句柄來進行對象訪問也是十分常見的

題外話

我想聊一下Java基本數據類型包裝類常量池String類型常量池

Java基本數據類型包裝類常量池

Java基本數據類型中的byteshortintlongbooleanchar包裝類使用了常量池,它們只在**[-128, 127]範圍內使用相應類型緩存數據**,超出這個範圍的就會建立新的對象,而floatdouble包裝類沒有使用常量池

舉個例子,代碼以下所示:

/** * 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中,==兩個做用:

  • 比較的是Java基本數據類型的話,就是比較它們的值是否相等。
  • 比較的是引用類型的話,就是比較它們的引用地址是否相同。

解析:

  • i3 == i4,返回truei3i4都是Integer類型,它們的相等,並且都在**[-128, 127]範圍內,因此它們同時使用着常量池中的對象,也就是它們是同一個對象**,所以返回true
  • i1 + i2 == i3,返回truei1i2i3都是Integer類型,加號不適用於Integer對象編譯器會對i1i2進行自動拆箱,進行數值相加,而後變成7 == i3,由於i3也是Integer類型,它沒法和數值進行直接比較,因此編譯器也會對i3進行自動拆箱,最後就變成數值的比較7 == 7,因此返回true
  • i5 == i6,返回falsei5i6都是Integer類型,它們的相等,可是不在**[-128, 127]範圍內,因此它們都各自建立新的對象,也就是它們不是同一個對象**,所以返回false
  • i3 == i9,返回falsei3i9都是Integer類型,i3會使用Integer常量池的對象,而i9會建立新的Integer對象,因此它們不是同一個對象,所以返回false
  • i7 + i8 == i9,返回truei7i8i9都是Integer類型,加號不適用於Integer對象編譯器會對i7i8進行自動拆箱,進行數值相加,而後變成7 == i9,由於i9也是Integer類型,它沒法和數值進行直接比較,因此編譯器也會對i9進行自動拆箱,最後就變成數值的比較7 == 7,因此返回true
  • i7 + i8 == 7,返回truei7i8都是Integer類型,加號不適用於Integer對象編譯器會對i7i8進行自動拆箱,進行數值相加,最後變成數值的比較7 == 7,因此返回true
  • d1 == d2,返回falsed1d2都是Double類型,它們的相等,可是沒有使用常量池,因此它們都各自建立新的對象,也就是它們不是同一個對象,所以返回false

當聲明爲如上述示例代碼中的i1i2i3i4i5i6時,編譯器會幫咱們自動裝箱,調用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 = "譚嘉俊";
複製代碼

這種聲明方式叫作字面量聲明,它是把字符串雙引號包起來,而後賦值給一個變量,這種狀況下,它會把字符串放到字符串常量池,而後返回給變量

new String()

示例代碼以下:

String str = new String("譚嘉俊");
複製代碼

使用new String()方法無論在字符串常量池中有沒有,它都會在建立一個新的對象

intern()

源碼代碼以下:

// 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. str1 == str2,返回truestr1str2都是字面量聲明,並且相等,因此它們都指向字符串常量池中同一個對象,所以返回true
  2. str3 == str4,返回falsestr3str4各自都在建立對象,因此它們不是同一個對象,所以返回false
  3. str1 == str3,返回falsestr1字面量聲明,它是從字符串常量池取出來的,str3是在建立對象,因此它們不是同一個對象,所以返回false
  4. str5 == str7,返回false:雖然str7調用了intern方法,可是沒有返回,因此它們仍是不是同一個對象,所以返回false
  5. str5 == str7,返回truestr7調用了intern方法,而且返回給str7,因此它們都指向字符串常量池中同一個對象,所以返回true
  6. str7 == str8,返回true:前面的邏輯,str7調用了intern方法,str8也調用了intern方法,因此它們都指向字符串常量池中同一個對象,所以返回true

參考文獻:

[1] 周志明,深刻理解Java虛擬機(第2版)[M],機械工業出版社,2013年9月1日,37頁~49頁

個人GitHub:TanJiaJunBeyond

Android通用框架:Android通用框架

個人掘金:譚嘉俊

個人簡書:譚嘉俊

個人CSDN:譚嘉俊

相關文章
相關標籤/搜索