Java 內(nèi)存區(qū)域

Java 內(nèi)存區(qū)域詳解

如果沒有特殊說明,都是針對的是 HotSpot 虛擬機惑申。

寫在前面 (常見面試題)

基本問題

  • 介紹下 Java 內(nèi)存區(qū)域(運行時數(shù)據(jù)區(qū))
  • Java 對象的創(chuàng)建過程(五步,建議能默寫出來并且要知道每一步虛擬機做了什么)
  • 對象的訪問定位的兩種方式(句柄和直接指針兩種方式)

拓展問題

  • String 類和常量池
  • 8 種基本類型的包裝類和常量池

一 概述

對于 Java 程序員來說人芽,在虛擬機自動內(nèi)存管理機制下萤厅,不再需要像 C/C++程序開發(fā)程序員這樣為每一個 new 操作去寫對應(yīng)的 delete/free 操作祈坠,不容易出現(xiàn)內(nèi)存泄漏和內(nèi)存溢出問題赦拘。正是因為 Java 程序員把內(nèi)存控制權(quán)利交給 Java 虛擬機躺同,一旦出現(xiàn)內(nèi)存泄漏和溢出方面的問題丸逸,如果不了解虛擬機是怎樣使用內(nèi)存的黄刚,那么排查錯誤將會是一個非常艱巨的任務(wù)憔维。

運行時數(shù)據(jù)區(qū)域

Java 虛擬機在執(zhí)行 Java 程序的過程中會把它管理的內(nèi)存劃分成若干個不同的數(shù)據(jù)區(qū)域业扒。JDK. 1.8 和之前的版本略有不同程储,下面會介紹到章鲤。

JDK 1.8 之前:

JDK 1.8 :

線程私有的:

  • 程序計數(shù)器
  • 虛擬機棧
  • 本地方法棧

線程共享的:

  • 方法區(qū)
  • 直接內(nèi)存 (非運行時數(shù)據(jù)區(qū)的一部分)

2.1 程序計數(shù)器

程序計數(shù)器是一塊較小的內(nèi)存空間帚呼,可以看作是當前線程所執(zhí)行的字節(jié)碼的行號指示器萝挤。字節(jié)碼解釋器工作時通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令根欧,分支凤粗、循環(huán)、跳轉(zhuǎn)柔袁、異常處理捶索、線程恢復(fù)等功能都需要依賴這個計數(shù)器來完成腥例。

另外酝润,為了線程切換后能恢復(fù)到正確的執(zhí)行位置要销,每條線程都需要有一個獨立的程序計數(shù)器疏咐,各線程之間計數(shù)器互不影響浑塞,獨立存儲缩举,我們稱這類內(nèi)存區(qū)域為“線程私有”的內(nèi)存仅孩。

從上面的介紹中我們知道程序計數(shù)器主要有兩個作用:

  1. 字節(jié)碼解釋器通過改變程序計數(shù)器來依次讀取指令辽慕,從而實現(xiàn)代碼的流程控制,如:順序執(zhí)行公浪、選擇欠气、循環(huán)预柒、異常處理宜鸯。
  2. 在多線程的情況下淋袖,程序計數(shù)器用于記錄當前線程執(zhí)行的位置即碗,從而當線程被切換回來的時候能夠知道該線程上次運行到哪兒了拜姿。

注意:程序計數(shù)器是唯一一個不會出現(xiàn) OutOfMemoryError 的內(nèi)存區(qū)域蕊肥,它的生命周期隨著線程的創(chuàng)建而創(chuàng)建,隨著線程的結(jié)束而死亡批狱。

2.2 Java 虛擬機棧

與程序計數(shù)器一樣赔硫,Java 虛擬機棧也是線程私有的爪膊,它的生命周期和線程相同推盛,描述的是 Java 方法執(zhí)行的內(nèi)存模型耘成,每次方法調(diào)用的數(shù)據(jù)都是通過棧傳遞的瘪菌。

Java 內(nèi)存可以粗糙的區(qū)分為堆內(nèi)存(Heap)和棧內(nèi)存 (Stack),其中棧就是現(xiàn)在說的虛擬機棧师妙,或者說是虛擬機棧中局部變量表部分。 (實際上曾掂,Java 虛擬機棧是由一個個棧幀組成,而每個棧幀中都擁有:局部變量表若专、操作數(shù)棧蝴猪、動態(tài)鏈接自阱、方法出口信息沛豌。)

局部變量表主要存放了編譯期可知的各種數(shù)據(jù)類型(boolean加派、byte芍锦、char娄琉、short孽水、int匈棘、float、long鹃愤、double)软吐、對象引用(reference 類型凹耙,它不同于對象本身肖抱,可能是一個指向?qū)ο笃鹗嫉刂返囊弥羔樢馐觯部赡苁侵赶蛞粋€代表對象的句柄或其他與此對象相關(guān)的位置)荤崇。

Java 虛擬機棧會出現(xiàn)兩種錯誤:StackOverFlowErrorOutOfMemoryError潮针。

  • StackOverFlowError 若 Java 虛擬機棧的內(nèi)存大小不允許動態(tài)擴展每篷,那么當線程請求棧的深度超過當前 Java 虛擬機棧的最大深度的時候雳攘,就拋出 StackOverFlowError 錯誤刚照。
  • OutOfMemoryError 若 Java 虛擬機堆中沒有空閑內(nèi)存无畔,并且垃圾回收器也無法提供更多內(nèi)存的話浑彰。就會拋出 OutOfMemoryError 錯誤郭变。

Java 虛擬機棧也是線程私有的,每個線程都有各自的 Java 虛擬機棧周伦,而且隨著線程的創(chuàng)建而創(chuàng)建专挪,隨著線程的死亡而死亡寨腔。

擴展:那么方法/函數(shù)如何調(diào)用?

Java 椨铝樱可用類比數(shù)據(jù)結(jié)構(gòu)中棧比默,Java 棧中保存的主要內(nèi)容是棧幀命咐,每一次函數(shù)調(diào)用都會有一個對應(yīng)的棧幀被壓入 Java 棧醋奠,每一個函數(shù)調(diào)用結(jié)束后窜司,都會有一個棧幀被彈出塞祈。

Java 方法有兩種返回方式:

  1. return 語句议薪。
  2. 拋出異常斯议。

不管哪種返回方式都會導致棧幀被彈出哼御。

2.3 本地方法棧

和虛擬機棧所發(fā)揮的作用非常相似尿扯,區(qū)別是: 虛擬機棧為虛擬機執(zhí)行 Java 方法 (也就是字節(jié)碼)服務(wù)衷笋,而本地方法棧則為虛擬機使用到的 Native 方法服務(wù)。 在 HotSpot 虛擬機中和 Java 虛擬機棧合二為一泊脐。

本地方法被執(zhí)行的時候容客,在本地方法棧也會創(chuàng)建一個棧幀缩挑,用于存放該本地方法的局部變量表供置、操作數(shù)棧芥丧、動態(tài)鏈接、出口信息坊罢。

方法執(zhí)行完畢后相應(yīng)的棧幀也會出棧并釋放內(nèi)存空間续担,也會出現(xiàn) StackOverFlowError 和 OutOfMemoryError 兩種錯誤。

2.4 堆

Java 虛擬機所管理的內(nèi)存中最大的一塊活孩,Java 堆是所有線程共享的一塊內(nèi)存區(qū)域物遇,在虛擬機啟動時創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對象實例诱鞠,幾乎所有的對象實例以及數(shù)組都在這里分配內(nèi)存蕉朵。

Java世界中“幾乎”所有的對象都在堆中分配冷蚂,但是,隨著JIT編譯期的發(fā)展與逃逸分析技術(shù)逐漸成熟,棧上分配蹄衷、標量替換優(yōu)化技術(shù)將會導致一些微妙的變化,所有的對象都分配到堆上也漸漸變得不那么“絕對”了。從jdk 1.7開始已經(jīng)默認開啟逃逸分析厚骗,如果某些方法中的對象引用沒有被返回或者未被外面使用(也就是未逃逸出去)厦瓢,那么對象可以直接在棧上分配內(nèi)存谎仲。

Java 堆是垃圾收集器管理的主要區(qū)域夹姥,因此也被稱作GC 堆(Garbage Collected Heap).從垃圾回收的角度飞涂,由于現(xiàn)在收集器基本都采用分代垃圾收集算法容燕,所以 Java 堆還可以細分為:新生代和老年代:再細致一點有:Eden 空間、From Survivor、To Survivor 空間等灰粮。進一步劃分的目的是更好地回收內(nèi)存,或者更快地分配內(nèi)存。

在 JDK 7 版本及JDK 7 版本之前晰骑,堆內(nèi)存被通常被分為下面三部分:

  1. 新生代內(nèi)存(Young Generation)
  2. 老生代(Old Generation)
  3. 永生代(Permanent Generation)

JDK 8 版本之后方法區(qū)(HotSpot 的永久代)被徹底移除了(JDK1.7 就已經(jīng)開始了)抚官,取而代之是元空間,元空間使用的是直接內(nèi)存。


上圖所示的 Eden 區(qū)、兩個 Survivor 區(qū)都屬于新生代(為了區(qū)分酵镜,這兩個 Survivor 區(qū)域按照順序被命名為 from 和 to),中間一層屬于老年代。

大部分情況,對象都會首先在 Eden 區(qū)域分配畔乙,在一次新生代垃圾回收后钥庇,如果對象還存活,則會進入 s0 或者 s1胁后,并且對象的年齡還會加 1(Eden 區(qū)->Survivor 區(qū)后對象的初始年齡變?yōu)?1)净宵,當它的年齡增加到一定程度(默認為 15 歲)敏储,就會被晉升到老年代中。對象晉升到老年代的年齡閾值畦幢,可以通過參數(shù) -XX:MaxTenuringThreshold 來設(shè)置刊头。

修正(issue552):“Hotspot遍歷所有對象時,按照年齡從小到大對其所占用的大小進行累積,當累積的某個年齡大小超過了survivor區(qū)的一半時,取這個年齡和MaxTenuringThreshold中更小的一個值,作為新的晉升年齡閾值”。

動態(tài)年齡計算的代碼如下

uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
  //survivor_capacity是survivor空間的大小
  size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);
  size_t total = 0;
  uint age = 1;
  while (age < table_size) {
    total += sizes[age];//sizes數(shù)組是每個年齡段對象大小
    if (total > desired_survivor_size) break;
    age++;
  }
  uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
  ...
}

堆這里最容易出現(xiàn)的就是 OutOfMemoryError 錯誤,并且出現(xiàn)這種錯誤之后的表現(xiàn)形式還會有幾種汗菜,比如:

  1. OutOfMemoryError: GC Overhead Limit Exceeded : 當JVM花太多時間執(zhí)行垃圾回收并且只能回收很少的堆空間時菌瘪,就會發(fā)生此錯誤。
  2. java.lang.OutOfMemoryError: Java heap space :假如在創(chuàng)建新的對象時, 堆內(nèi)存中的空間不足以存放新創(chuàng)建的對象, 就會引發(fā)java.lang.OutOfMemoryError: Java heap space 錯誤赁咙。(和本機物理內(nèi)存無關(guān)凤覆,和你配置的內(nèi)存大小有關(guān)拥峦!)
  3. ......

2.5 方法區(qū)

方法區(qū)與 Java 堆一樣玄柠,是各個線程共享的內(nèi)存區(qū)域这弧,它用于存儲已被虛擬機加載的類信息户矢、常量挂洛、靜態(tài)變量柒巫、即時編譯器編譯后的代碼等數(shù)據(jù)。雖然 Java 虛擬機規(guī)范把方法區(qū)描述為堆的一個邏輯部分,但是它卻有一個別名叫做 Non-Heap(非堆),目的應(yīng)該是與 Java 堆區(qū)分開來。

方法區(qū)也被稱為永久代宝踪。很多人都會分不清方法區(qū)和永久代的關(guān)系秕重,為此我也查閱了文獻厉膀。

2.5.1 方法區(qū)和永久代的關(guān)系

《Java 虛擬機規(guī)范》只是規(guī)定了有方法區(qū)這么個概念和它的作用,并沒有規(guī)定如何去實現(xiàn)它。那么富纸,在不同的 JVM 上方法區(qū)的實現(xiàn)肯定是不同的了晓褪。 方法區(qū)和永久代的關(guān)系很像 Java 中接口和類的關(guān)系,類實現(xiàn)了接口类嗤,而永久代就是 HotSpot 虛擬機對虛擬機規(guī)范中方法區(qū)的一種實現(xiàn)方式笔咽。 也就是說船庇,永久代是 HotSpot 的概念,方法區(qū)是 Java 虛擬機規(guī)范中的定義遮咖,是一種規(guī)范,而永久代是一種實現(xiàn)暑脆,一個是標準一個是實現(xiàn)玄帕,其他的虛擬機實現(xiàn)并沒有永久代這一說法锡移。

2.5.2 常用參數(shù)

JDK 1.8 之前永久代還沒被徹底移除的時候通常通過下面這些參數(shù)來調(diào)節(jié)方法區(qū)大小

-XX:PermSize=N //方法區(qū) (永久代) 初始大小
-XX:MaxPermSize=N //方法區(qū) (永久代) 最大大小,超過這個值將會拋出 OutOfMemoryError 異常:java.lang.OutOfMemoryError: PermGen

相對而言,垃圾收集行為在這個區(qū)域是比較少出現(xiàn)的礁遣,但并非數(shù)據(jù)進入方法區(qū)后就“永久存在”了醇王。

JDK 1.8 的時候,方法區(qū)(HotSpot 的永久代)被徹底移除了(JDK1.7 就已經(jīng)開始了)饭冬,取而代之是元空間,元空間使用的是直接內(nèi)存。

下面是一些常用參數(shù):

-XX:MetaspaceSize=N //設(shè)置 Metaspace 的初始(和最小大信枪颉)
-XX:MaxMetaspaceSize=N //設(shè)置 Metaspace 的最大大小

與永久代很大的不同就是甘邀,如果不指定大小的話,隨著更多類的創(chuàng)建垮庐,虛擬機會耗盡所有可用的系統(tǒng)內(nèi)存松邪。

2.5.3 為什么要將永久代 (PermGen) 替換為元空間 (MetaSpace) 呢?

  1. 整個永久代有一個 JVM 本身設(shè)置固定大小上限,無法進行調(diào)整哨查,而元空間使用的是直接內(nèi)存逗抑,受本機可用內(nèi)存的限制,雖然元空間仍舊可能溢出寒亥,但是比原來出現(xiàn)的幾率會更小邮府。

當你元空間溢出時會得到如下錯誤: java.lang.OutOfMemoryError: MetaSpace

你可以使用 -XX:MaxMetaspaceSize 標志設(shè)置最大元空間大小,默認值為 unlimited溉奕,這意味著它只受系統(tǒng)內(nèi)存的限制褂傀。-XX:MetaspaceSize 調(diào)整標志定義元空間的初始大小如果未指定此標志,則 Metaspace 將根據(jù)運行時的應(yīng)用程序需求動態(tài)地重新調(diào)整大小腐宋。

  1. 元空間里面存放的是類的元數(shù)據(jù)紊服,這樣加載多少類的元數(shù)據(jù)就不由 MaxPermSize 控制了, 而由系統(tǒng)的實際可用空間來控制檀轨,這樣能加載的類就更多了。

  2. 在 JDK8欺嗤,合并 HotSpot 和 JRockit 的代碼時, JRockit 從來沒有一個叫永久代的東西, 合并之后就沒有必要額外的設(shè)置這么一個永久代的地方了参萄。

2.6 運行時常量池

運行時常量池是方法區(qū)的一部分。Class 文件中除了有類的版本煎饼、字段讹挎、方法、接口等描述信息外吆玖,還有常量池表(用于存放編譯期生成的各種字面量和符號引用)

既然運行時常量池是方法區(qū)的一部分筒溃,自然受到方法區(qū)內(nèi)存的限制,當常量池無法再申請到內(nèi)存時會拋出 OutOfMemoryError 錯誤沾乘。

JDK1.7 及之后版本的 JVM 已經(jīng)將運行時常量池從方法區(qū)中移了出來怜奖,在 Java 堆(Heap)中開辟了一塊區(qū)域存放運行時常量池。

修正(issue747翅阵,reference):

  1. JDK1.7之前運行時常量池邏輯包含字符串常量池存放在方法區(qū), 此時hotspot虛擬機對方法區(qū)的實現(xiàn)為永久代
  2. JDK1.7 字符串常量池被從方法區(qū)拿到了堆中, 這里沒有提到運行時常量池,也就是說字符串常量池被單獨拿到堆,運行時常量池剩下的東西還在方法區(qū), 也就是hotspot中的永久代 歪玲。
  3. JDK1.8 hotspot移除了永久代用元空間(Metaspace)取而代之, 這時候字符串常量池還在堆, 運行時常量池還在方法區(qū), 只不過方法區(qū)的實現(xiàn)從永久代變成了元空間(Metaspace)

相關(guān)問題:JVM 常量池中存儲的是對象還是引用呢?: https://www.zhihu.com/question/57109429/answer/151717241 by RednaxelaFX

2.7 直接內(nèi)存

直接內(nèi)存并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分掷匠,也不是虛擬機規(guī)范中定義的內(nèi)存區(qū)域滥崩,但是這部分內(nèi)存也被頻繁地使用。而且也可能導致 OutOfMemoryError 錯誤出現(xiàn)讹语。

JDK1.4 中新加入的 NIO(New Input/Output) 類钙皮,引入了一種基于通道(Channel)緩存區(qū)(Buffer) 的 I/O 方式,它可以直接使用 Native 函數(shù)庫直接分配堆外內(nèi)存顽决,然后通過一個存儲在 Java 堆中的 DirectByteBuffer 對象作為這塊內(nèi)存的引用進行操作短条。這樣就能在一些場景中顯著提高性能,因為避免了在 Java 堆和 Native 堆之間來回復(fù)制數(shù)據(jù)擎值。

本機直接內(nèi)存的分配不會受到 Java 堆的限制慌烧,但是,既然是內(nèi)存就會受到本機總內(nèi)存大小以及處理器尋址空間的限制鸠儿。

三 HotSpot 虛擬機對象探秘

通過上面的介紹我們大概知道了虛擬機的內(nèi)存情況屹蚊,下面我們來詳細的了解一下 HotSpot 虛擬機在 Java 堆中對象分配、布局和訪問的全過程进每。

3.1 對象的創(chuàng)建

下圖便是 Java 對象的創(chuàng)建過程汹粤,我建議最好是能默寫出來,并且要掌握每一步在做什么田晚。

Step1:類加載檢查

虛擬機遇到一條 new 指令時嘱兼,首先將去檢查這個指令的參數(shù)是否能在常量池中定位到這個類的符號引用,并且檢查這個符號引用代表的類是否已被加載過贤徒、解析和初始化過芹壕。如果沒有汇四,那必須先執(zhí)行相應(yīng)的類加載過程。

Step2:分配內(nèi)存

類加載檢查通過后踢涌,接下來虛擬機將為新生對象分配內(nèi)存通孽。對象所需的內(nèi)存大小在類加載完成后便可確定,為對象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從 Java 堆中劃分出來睁壁。分配方式“指針碰撞”“空閑列表” 兩種背苦,選擇哪種分配方式由 Java 堆是否規(guī)整決定,而 Java 堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定潘明。

內(nèi)存分配的兩種方式:(補充內(nèi)容行剂,需要掌握)

選擇以上兩種方式中的哪一種,取決于 Java 堆內(nèi)存是否規(guī)整钳降。而 Java 堆內(nèi)存是否規(guī)整厚宰,取決于 GC 收集器的算法是"標記-清除",還是"標記-整理"(也稱作"標記-壓縮")遂填,值得注意的是固阁,復(fù)制算法內(nèi)存也是規(guī)整的


內(nèi)存分配并發(fā)問題(補充內(nèi)容,需要掌握)

在創(chuàng)建對象的時候有一個很重要的問題城菊,就是線程安全,因為在實際開發(fā)過程中碉克,創(chuàng)建對象是很頻繁的事情凌唬,作為虛擬機來說,必須要保證線程是安全的漏麦,通常來講客税,虛擬機采用兩種方式來保證線程安全:

  • CAS+失敗重試: CAS 是樂觀鎖的一種實現(xiàn)方式。所謂樂觀鎖就是撕贞,每次不加鎖而是假設(shè)沒有沖突而去完成某項操作更耻,如果因為沖突失敗就重試,直到成功為止捏膨。虛擬機采用 CAS 配上失敗重試的方式保證更新操作的原子性秧均。
  • TLAB: 為每一個線程預(yù)先在 Eden 區(qū)分配一塊兒內(nèi)存,JVM 在給線程中的對象分配內(nèi)存時号涯,首先在 TLAB 分配目胡,當對象大于 TLAB 中的剩余內(nèi)存或 TLAB 的內(nèi)存已用盡時,再采用上述的 CAS 進行內(nèi)存分配

Step3:初始化零值

內(nèi)存分配完成后链快,虛擬機需要將分配到的內(nèi)存空間都初始化為零值(不包括對象頭)誉己,這一步操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數(shù)據(jù)類型所對應(yīng)的零值域蜗。

Step4:設(shè)置對象頭

初始化零值完成之后巨双,虛擬機要對對象進行必要的設(shè)置噪猾,例如這個對象是哪個類的實例、如何才能找到類的元數(shù)據(jù)信息筑累、對象的哈希碼袱蜡、對象的 GC 分代年齡等信息。 這些信息存放在對象頭中疼阔。 另外戒劫,根據(jù)虛擬機當前運行狀態(tài)的不同,如是否啟用偏向鎖等婆廊,對象頭會有不同的設(shè)置方式迅细。

Step5:執(zhí)行 init 方法

在上面工作都完成之后,從虛擬機的視角來看淘邻,一個新的對象已經(jīng)產(chǎn)生了茵典,但從 Java 程序的視角來看,對象創(chuàng)建才剛開始宾舅,<init> 方法還沒有執(zhí)行统阿,所有的字段都還為零。所以一般來說筹我,執(zhí)行 new 指令之后會接著執(zhí)行 <init> 方法扶平,把對象按照程序員的意愿進行初始化,這樣一個真正可用的對象才算完全產(chǎn)生出來蔬蕊。

3.2 對象的內(nèi)存布局

在 Hotspot 虛擬機中结澄,對象在內(nèi)存中的布局可以分為 3 塊區(qū)域:對象頭實例數(shù)據(jù)對齊填充岸夯。

Hotspot 虛擬機的對象頭包括兩部分信息麻献,第一部分用于存儲對象自身的運行時數(shù)據(jù)(哈希碼、GC 分代年齡猜扮、鎖狀態(tài)標志等等)勉吻,另一部分是類型指針,即對象指向它的類元數(shù)據(jù)的指針旅赢,虛擬機通過這個指針來確定這個對象是那個類的實例齿桃。

實例數(shù)據(jù)部分是對象真正存儲的有效信息,也是在程序中所定義的各種類型的字段內(nèi)容鲜漩。

對齊填充部分不是必然存在的源譬,也沒有什么特別的含義,僅僅起占位作用孕似。 因為 Hotspot 虛擬機的自動內(nèi)存管理系統(tǒng)要求對象起始地址必須是 8 字節(jié)的整數(shù)倍踩娘,換句話說就是對象的大小必須是 8 字節(jié)的整數(shù)倍。而對象頭部分正好是 8 字節(jié)的倍數(shù)(1 倍或 2 倍),因此养渴,當對象實例數(shù)據(jù)部分沒有對齊時雷绢,就需要通過對齊填充來補全。

3.3 對象的訪問定位

建立對象就是為了使用對象理卑,我們的 Java 程序通過棧上的 reference 數(shù)據(jù)來操作堆上的具體對象翘紊。對象的訪問方式由虛擬機實現(xiàn)而定,目前主流的訪問方式有①使用句柄②直接指針兩種:

  1. 句柄: 如果使用句柄的話藐唠,那么 Java 堆中將會劃分出一塊內(nèi)存來作為句柄池帆疟,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息宇立;
  1. 直接指針: 如果使用直接指針訪問踪宠,那么 Java 堆對象的布局中就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息,而 reference 中存儲的直接就是對象的地址妈嘹。

這兩種對象訪問方式各有優(yōu)勢柳琢。使用句柄來訪問的最大好處是 reference 中存儲的是穩(wěn)定的句柄地址,在對象被移動時只會改變句柄中的實例數(shù)據(jù)指針润脸,而 reference 本身不需要修改柬脸。使用直接指針訪問方式最大的好處就是速度快,它節(jié)省了一次指針定位的時間開銷毙驯。

四 重點補充內(nèi)容

4.1 String 類和常量池

String 對象的兩種創(chuàng)建方式:

String str1 = "abcd";//先檢查字符串常量池中有沒有"abcd"倒堕,如果字符串常量池中沒有,則創(chuàng)建一個爆价,然后 str1 指向字符串常量池中的對象涩馆,如果有,則直接將 str1 指向"abcd""允坚;
String str2 = new String("abcd");//堆中創(chuàng)建一個新的對象
String str3 = new String("abcd");//堆中創(chuàng)建一個新的對象
System.out.println(str1==str2);//false
System.out.println(str2==str3);//false

這兩種不同的創(chuàng)建方法是有差別的。

  • 第一種方式是在常量池中拿對象蛾号;
  • 第二種方式是直接在堆內(nèi)存空間創(chuàng)建一個新的對象稠项。

記住一點:只要使用 new 方法,便需要創(chuàng)建新的對象鲜结。

再給大家一個圖應(yīng)該更容易理解展运,圖片來源:https://www.journaldev.com/797/what-is-java-string-pool

String 類型的常量池比較特殊。它的主要使用方法有兩種:

  • 直接使用雙引號聲明出來的 String 對象會直接存儲在常量池中精刷。
  • 如果不是用雙引號聲明的 String 對象拗胜,可以使用 String 提供的 intern 方法。String.intern() 是一個 Native 方法怒允,它的作用是:如果運行時常量池中已經(jīng)包含一個等于此 String 對象內(nèi)容的字符串埂软,則返回常量池中該字符串的引用;如果沒有纫事,JDK1.7之前(不包含1.7)的處理方式是在常量池中創(chuàng)建與此 String 內(nèi)容相同的字符串勘畔,并返回常量池中創(chuàng)建的字符串的引用所灸,JDK1.7以及之后的處理方式是在常量池中記錄此字符串的引用,并返回該引用炫七。
          String s1 = new String("計算機");
          String s2 = s1.intern();
          String s3 = "計算機";
          System.out.println(s2);//計算機
          System.out.println(s1 == s2);//false爬立,因為一個是堆內(nèi)存中的 String 對象一個是常量池中的 String 對象,
          System.out.println(s3 == s2);//true万哪,因為兩個都是常量池中的 String 對象

字符串拼接:

          String str1 = "str";
          String str2 = "ing";

          String str3 = "str" + "ing";//常量池中的對象
          String str4 = str1 + str2; //在堆上創(chuàng)建的新的對象     
          String str5 = "string";//常量池中的對象
          System.out.println(str3 == str4);//false
          System.out.println(str3 == str5);//true
          System.out.println(str4 == str5);//false

盡量避免多個字符串拼接侠驯,因為這樣會重新創(chuàng)建對象。如果需要改變字符串的話奕巍,可以使用 StringBuilder 或者 StringBuffer吟策。

4.2 String s1 = new String("abc");這句話創(chuàng)建了幾個字符串對象?

將創(chuàng)建 1 或 2 個字符串伍绳。如果池中已存在字符串常量“abc”踊挠,則只會在堆空間創(chuàng)建一個字符串常量“abc”。如果池中沒有字符串常量“abc”冲杀,那么它將首先在池中創(chuàng)建效床,然后在堆空間中創(chuàng)建,因此將創(chuàng)建總共 2 個字符串對象权谁。

驗證:

        String s1 = new String("abc");// 堆內(nèi)存的地址值
        String s2 = "abc";
        System.out.println(s1 == s2);// 輸出 false,因為一個是堆內(nèi)存剩檀,一個是常量池的內(nèi)存,故兩者是不同的旺芽。
        System.out.println(s1.equals(s2));// 輸出 true

結(jié)果:

false
true

4.3 8 種基本類型的包裝類和常量池

Java 基本類型的包裝類的大部分都實現(xiàn)了常量池技術(shù)沪猴,即 Byte,Short,Integer,Long,Character,Boolean;前面 4 種包裝類默認創(chuàng)建了數(shù)值[-128采章,127] 的相應(yīng)類型的緩存數(shù)據(jù)运嗜,Character創(chuàng)建了數(shù)值在[0,127]范圍的緩存數(shù)據(jù),Boolean 直接返回True Or False悯舟。如果超出對應(yīng)范圍仍然會去創(chuàng)建新的對象担租。 為啥把緩存設(shè)置為[-128,127]區(qū)間抵怎?(參見issue/461)性能和資源之間的權(quán)衡奋救。

public static Boolean valueOf(boolean b) {
    return (b ? TRUE : FALSE);
}
private static class CharacterCache {         
    private CharacterCache(){}

    static final Character cache[] = new Character[127 + 1];          
    static {             
        for (int i = 0; i < cache.length; i++)                 
            cache[i] = new Character((char)i);         
    }   
}

兩種浮點數(shù)類型的包裝類 Float,Double 并沒有實現(xiàn)常量池技術(shù)。**

        Integer i1 = 33;
        Integer i2 = 33;
        System.out.println(i1 == i2);// 輸出 true
        Integer i11 = 333;
        Integer i22 = 333;
        System.out.println(i11 == i22);// 輸出 false
        Double i3 = 1.2;
        Double i4 = 1.2;
        System.out.println(i3 == i4);// 輸出 false

Integer 緩存源代碼:

/**
*此方法將始終緩存-128 到 127(包括端點)范圍內(nèi)的值反惕,并可以緩存此范圍之外的其他值尝艘。
*/
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

應(yīng)用場景:

  1. Integer i1=40;Java 在編譯的時候會直接將代碼封裝成 Integer i1=Integer.valueOf(40);姿染,從而使用常量池中的對象背亥。
  2. Integer i1 = new Integer(40);這種情況下會創(chuàng)建新的對象。
  Integer i1 = 40;
  Integer i2 = new Integer(40);
  System.out.println(i1==i2);//輸出 false

Integer 比較更豐富的一個例子:

  Integer i1 = 40;
  Integer i2 = 40;
  Integer i3 = 0;
  Integer i4 = new Integer(40);
  Integer i5 = new Integer(40);
  Integer i6 = new Integer(0);

  System.out.println("i1=i2   " + (i1 == i2));
  System.out.println("i1=i2+i3   " + (i1 == i2 + i3));
  System.out.println("i1=i4   " + (i1 == i4));
  System.out.println("i4=i5   " + (i4 == i5));
  System.out.println("i4=i5+i6   " + (i4 == i5 + i6));   
  System.out.println("40=i5+i6   " + (40 == i5 + i6));     

結(jié)果:

i1=i2   true
i1=i2+i3   true
i1=i4   false
i4=i5   false
i4=i5+i6   true
40=i5+i6   true

解釋:

語句 i4 == i5 + i6,因為+這個操作符不適用于 Integer 對象隘梨,首先 i5 和 i6 進行自動拆箱操作程癌,進行數(shù)值相加,即 i4 == 40轴猎。然后 Integer 對象無法與數(shù)值進行直接比較嵌莉,所以 i4 自動拆箱轉(zhuǎn)為 int 值 40,最終這條語句轉(zhuǎn)為 40 == 40 進行數(shù)值比較捻脖。

參考

作者:Snailclimb
鏈接:Java 內(nèi)存區(qū)域
來源:github

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锐峭,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子可婶,更是在濱河造成了極大的恐慌沿癞,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件矛渴,死亡現(xiàn)場離奇詭異椎扬,居然都是意外死亡,警方通過查閱死者的電腦和手機具温,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門蚕涤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人铣猩,你說我怎么就攤上這事揖铜。” “怎么了达皿?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵天吓,是天一觀的道長。 經(jīng)常有香客問我峦椰,道長龄寞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任汤功,我火速辦了婚禮萄焦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘冤竹。我一直安慰自己,他們只是感情好茬射,可當我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布鹦蠕。 她就那樣靜靜地躺著,像睡著了一般在抛。 火紅的嫁衣襯著肌膚如雪钟病。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天,我揣著相機與錄音肠阱,去河邊找鬼票唆。 笑死,一個胖子當著我的面吹牛屹徘,可吹牛的內(nèi)容都是我干的走趋。 我是一名探鬼主播,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼噪伊,長吁一口氣:“原來是場噩夢啊……” “哼簿煌!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鉴吹,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤姨伟,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡奠滑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年喷斋,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片铸本。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出淮摔,到底是詐尸還是另有隱情,我是刑警寧澤始赎,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布和橙,位于F島的核電站,受9級特大地震影響造垛,放射性物質(zhì)發(fā)生泄漏魔招。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一五辽、第九天 我趴在偏房一處隱蔽的房頂上張望办斑。 院中可真熱鬧,春花似錦杆逗、人聲如沸乡翅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蠕蚜。三九已至,卻和暖如春悔橄,著一層夾襖步出監(jiān)牢的瞬間靶累,已是汗流浹背腺毫。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留挣柬,地道東北人潮酒。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像邪蛔,于是被迫代替她去往敵國和親急黎。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,691評論 2 361