久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

2萬(wàn)字長(zhǎng)文包教包會(huì) JVM 內(nèi)存結(jié)構(gòu) 保姆級(jí)學(xué)習(xí)筆記

 印度阿三17 2020-07-20

直擊面試

反正我是帶著這些問(wèn)題往下讀的

  • 說(shuō)一下 JVM 運(yùn)行時(shí)數(shù)據(jù)區(qū)吧,,都有哪些區(qū)?分別是干什么的,?

  • Java 8 的內(nèi)存分代改進(jìn)

  • 舉例棧溢出的情況,?

  • 調(diào)整棧大小,就能保存不出現(xiàn)溢出嗎,?

  • 分配的棧內(nèi)存越大越好嗎,?

  • 垃圾回收是否會(huì)涉及到虛擬機(jī)棧?

  • 方法中定義的局部變量是否線(xiàn)程安全,?


運(yùn)行時(shí)數(shù)據(jù)區(qū)

內(nèi)存是非常重要的系統(tǒng)資源,,是硬盤(pán)和 CPU 的中間倉(cāng)庫(kù)及橋梁,承載著操作系統(tǒng)和應(yīng)用程序的實(shí)時(shí)運(yùn)行,。JVM 內(nèi)存布局規(guī)定了 Java 在運(yùn)行過(guò)程中內(nèi)存申請(qǐng),、分配,、管理的策略,保證了 JVM 的高效穩(wěn)定運(yùn)行,。不同的 JVM 對(duì)于內(nèi)存的劃分方式和管理機(jī)制存在著部分差異,。

下圖是 JVM 整體架構(gòu),中間部分就是 Java 虛擬機(jī)定義的各種運(yùn)行時(shí)數(shù)據(jù)區(qū)域,。

jvm-framework

Java 虛擬機(jī)定義了若干種程序運(yùn)行期間會(huì)使用到的運(yùn)行時(shí)數(shù)據(jù)區(qū),,其中有一些會(huì)隨著虛擬機(jī)啟動(dòng)而創(chuàng)建,隨著虛擬機(jī)退出而銷(xiāo)毀,。另外一些則是與線(xiàn)程一一對(duì)應(yīng)的,,這些與線(xiàn)程一一對(duì)應(yīng)的數(shù)據(jù)區(qū)域會(huì)隨著線(xiàn)程開(kāi)始和結(jié)束而創(chuàng)建和銷(xiāo)毀。

  • 線(xiàn)程私有:程序計(jì)數(shù)器,、棧,、本地棧

  • 線(xiàn)程共享:堆、堆外內(nèi)存(永久代或元空間,、代碼緩存)

下面我們就來(lái)一一解毒下這些內(nèi)存區(qū)域,,先從最簡(jiǎn)單的入手

一、程序計(jì)數(shù)器

程序計(jì)數(shù)寄存器(Program Counter Register),,Register 的命名源于 CPU 的寄存器,,寄存器存儲(chǔ)指令相關(guān)的線(xiàn)程信息,CPU 只有把數(shù)據(jù)裝載到寄存器才能夠運(yùn)行,。

這里,,并非是廣義上所指的物理寄存器,叫程序計(jì)數(shù)器(或PC計(jì)數(shù)器或指令計(jì)數(shù)器)會(huì)更加貼切,,并且也不容易引起一些不必要的誤會(huì),。JVM 中的 PC 寄存器是對(duì)物理 PC 寄存器的一種抽象模擬

程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,,可以看作是當(dāng)前線(xiàn)程所執(zhí)行的字節(jié)碼的行號(hào)指示器,。

1.1 作用

PC 寄存器用來(lái)存儲(chǔ)指向下一條指令的地址,即將要執(zhí)行的指令代碼,。由執(zhí)行引擎讀取下一條指令,。

jvm-pc-counter

(分析:進(jìn)入class文件所在目錄,執(zhí)行 javap -v xx.class 反解析(或者通過(guò) IDEA 插件 Jclasslib 直接查看,,上圖),,可以看到當(dāng)前類(lèi)對(duì)應(yīng)的Code區(qū)(匯編指令)、本地變量表,、異常表和代碼行偏移量映射表,、常量池等信息。)

1.2 概述

  • 它是一塊很小的內(nèi)存空間,,幾乎可以忽略不計(jì),。也是運(yùn)行速度最快的存儲(chǔ)區(qū)域

  • 在 JVM 規(guī)范中,,每個(gè)線(xiàn)程都有它自己的程序計(jì)數(shù)器,是線(xiàn)程私有的,,生命周期與線(xiàn)程的生命周期一致

  • 任何時(shí)間一個(gè)線(xiàn)程都只有一個(gè)方法在執(zhí)行,,也就是所謂的當(dāng)前方法。如果當(dāng)前線(xiàn)程正在執(zhí)行的是 Java 方法,,程序計(jì)數(shù)器記錄的是 JVM 字節(jié)碼指令地址,,如果是執(zhí)行 natice 方法,則是未指定值(undefined)

  • 它是程序控制流的指示器,,分支,、循環(huán)、跳轉(zhuǎn),、異常處理,、線(xiàn)程恢復(fù)等基礎(chǔ)功能都需要依賴(lài)這個(gè)計(jì)數(shù)器來(lái)完成

  • 字節(jié)碼解釋器工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼指令

  • 它是唯一一個(gè)在 JVM 規(guī)范中沒(méi)有規(guī)定任何 OutOfMemoryError 情況的區(qū)域

?????:使用PC寄存器存儲(chǔ)字節(jié)碼指令地址有什么用呢?為什么使用PC寄存器記錄當(dāng)前線(xiàn)程的執(zhí)行地址呢,?

???♂?:因?yàn)镃PU需要不停的切換各個(gè)線(xiàn)程,這時(shí)候切換回來(lái)以后,,就得知道接著從哪開(kāi)始繼續(xù)執(zhí)行,。JVM的字節(jié)碼解釋器就需要通過(guò)改變PC寄存器的值來(lái)明確下一條應(yīng)該執(zhí)行什么樣的字節(jié)碼指令。

?????:PC寄存器為什么會(huì)被設(shè)定為線(xiàn)程私有的,?

???♂?:多線(xiàn)程在一個(gè)特定的時(shí)間段內(nèi)只會(huì)執(zhí)行其中某一個(gè)線(xiàn)程方法,,CPU會(huì)不停的做任務(wù)切換,這樣必然會(huì)導(dǎo)致經(jīng)常中斷或恢復(fù),。為了能夠準(zhǔn)確的記錄各個(gè)線(xiàn)程正在執(zhí)行的當(dāng)前字節(jié)碼指令地址,,所以為每個(gè)線(xiàn)程都分配了一個(gè)PC寄存器,每個(gè)線(xiàn)程都獨(dú)立計(jì)算,,不會(huì)互相影響,。


二、虛擬機(jī)棧

2.1 概述

Java 虛擬機(jī)棧(Java Virtual Machine Stacks),,早期也叫 Java 棧,。每個(gè)線(xiàn)程在創(chuàng)建的時(shí)候都會(huì)創(chuàng)建一個(gè)虛擬機(jī)棧,其內(nèi)部保存一個(gè)個(gè)的棧幀(Stack Frame),,對(duì)應(yīng)著一次次 Java 方法調(diào)用,,是線(xiàn)程私有的,生命周期和線(xiàn)程一致,。

作用:主管 Java 程序的運(yùn)行,,它保存方法的局部變量、部分結(jié)果,,并參與方法的調(diào)用和返回,。

特點(diǎn):

  • 棧是一種快速有效的分配存儲(chǔ)方式,,訪(fǎng)問(wèn)速度僅次于程序計(jì)數(shù)器

  • JVM 直接對(duì)虛擬機(jī)棧的操作只有兩個(gè):每個(gè)方法執(zhí)行,伴隨著入棧(進(jìn)棧/壓棧),,方法執(zhí)行結(jié)束出棧

  • 棧不存在垃圾回收問(wèn)題

棧中可能出現(xiàn)的異常:

Java 虛擬機(jī)規(guī)范允許 Java虛擬機(jī)棧的大小是動(dòng)態(tài)的或者是固定不變的

  • 如果采用固定大小的 Java 虛擬機(jī)棧,,那每個(gè)線(xiàn)程的 Java 虛擬機(jī)棧容量可以在線(xiàn)程創(chuàng)建的時(shí)候獨(dú)立選定。如果線(xiàn)程請(qǐng)求分配的棧容量超過(guò) Java 虛擬機(jī)棧允許的最大容量,,Java 虛擬機(jī)將會(huì)拋出一個(gè) StackOverflowError 異常

  • 如果 Java 虛擬機(jī)??梢詣?dòng)態(tài)擴(kuò)展,并且在嘗試擴(kuò)展的時(shí)候無(wú)法申請(qǐng)到足夠的內(nèi)存,,或者在創(chuàng)建新的線(xiàn)程時(shí)沒(méi)有足夠的內(nèi)存去創(chuàng)建對(duì)應(yīng)的虛擬機(jī)棧,,那 Java 虛擬機(jī)將會(huì)拋出一個(gè)OutOfMemoryError異常

可以通過(guò)參數(shù)-Xss來(lái)設(shè)置線(xiàn)程的最大棧空間,,棧的大小直接決定了函數(shù)調(diào)用的最大可達(dá)深度,。

官方提供的參考工具,可查一些參數(shù)和操作:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html#BGBCIEFC

2.2 棧的存儲(chǔ)單位

棧中存儲(chǔ)什么,?

  • 每個(gè)線(xiàn)程都有自己的棧,,棧中的數(shù)據(jù)都是以棧幀(Stack Frame)的格式存在

  • 在這個(gè)線(xiàn)程上正在執(zhí)行的每個(gè)方法都各自有對(duì)應(yīng)的一個(gè)棧幀

  • 棧幀是一個(gè)內(nèi)存區(qū)塊,是一個(gè)數(shù)據(jù)集,,維系著方法執(zhí)行過(guò)程中的各種數(shù)據(jù)信息

2.3 棧運(yùn)行原理

  • JVM 直接對(duì) Java 棧的操作只有兩個(gè),,對(duì)棧幀的壓棧出棧,遵循“先進(jìn)后出/后進(jìn)先出”原則

  • 在一條活動(dòng)線(xiàn)程中,,一個(gè)時(shí)間點(diǎn)上,,只會(huì)有一個(gè)活動(dòng)的棧幀。即只有當(dāng)前正在執(zhí)行的方法的棧幀(棧頂棧幀)是有效的,,這個(gè)棧幀被稱(chēng)為當(dāng)前棧幀(Current Frame),,與當(dāng)前棧幀對(duì)應(yīng)的方法就是當(dāng)前方法(Current Method),定義這個(gè)方法的類(lèi)就是當(dāng)前類(lèi)(Current Class)

  • 執(zhí)行引擎運(yùn)行的所有字節(jié)碼指令只針對(duì)當(dāng)前棧幀進(jìn)行操作

  • 如果在該方法中調(diào)用了其他方法,,對(duì)應(yīng)的新的棧幀會(huì)被創(chuàng)建出來(lái),,放在棧的頂端,稱(chēng)為新的當(dāng)前棧幀

  • 不同線(xiàn)程中所包含的棧幀是不允許存在相互引用的,,即不可能在一個(gè)棧幀中引用另外一個(gè)線(xiàn)程的棧幀

  • 如果當(dāng)前方法調(diào)用了其他方法,,方法返回之際,當(dāng)前棧幀會(huì)傳回此方法的執(zhí)行結(jié)果給前一個(gè)棧幀,,接著,,虛擬機(jī)會(huì)丟棄當(dāng)前棧幀,使得前一個(gè)棧幀重新成為當(dāng)前棧幀

  • Java 方法有兩種返回函數(shù)的方式,,一種是正常的函數(shù)返回,,使用 return 指令,另一種是拋出異常,不管用哪種方式,,都會(huì)導(dǎo)致棧幀被彈出

IDEA 在 debug 時(shí)候,,可以在 debug 窗口看到 Frames 中各種方法的壓棧和出棧情況

2.4 棧幀的內(nèi)部結(jié)構(gòu)

每個(gè)棧幀(Stack Frame)中存儲(chǔ)著:

  • 局部變量表(Local Variables)

  • 操作數(shù)棧(Operand Stack)(或稱(chēng)為表達(dá)式棧)

  • 動(dòng)態(tài)鏈接(Dynamic Linking):指向運(yùn)行時(shí)常量池的方法引用

  • 方法返回地址(Return Address):方法正常退出或異常退出的地址

  • 一些附加信息

jvm-stack-frame

繼續(xù)深拋棧幀中的五部分~~

2.4.1. 局部變量表

  • 局部變量表也被稱(chēng)為局部變量數(shù)組或者本地變量表

  • 是一組變量值存儲(chǔ)空間,主要用于存儲(chǔ)方法參數(shù)和定義在方法體內(nèi)的局部變量,,包括編譯器可知的各種 Java 虛擬機(jī)基本數(shù)據(jù)類(lèi)型(boolean,、byte、char,、short,、int、float,、long,、double)、對(duì)象引用(reference類(lèi)型,,它并不等同于對(duì)象本身,,可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔槪部赡苁侵赶蛞粋€(gè)代表對(duì)象的句柄或其他與此相關(guān)的位置)和 returnAddress 類(lèi)型(指向了一條字節(jié)碼指令的地址,,已被異常表取代)

  • 由于局部變量表是建立在線(xiàn)程的棧上,,是線(xiàn)程的私有數(shù)據(jù),因此不存在數(shù)據(jù)安全問(wèn)題

  • 局部變量表所需要的容量大小是編譯期確定下來(lái)的,,并保存在方法的 Code 屬性的 maximum local variables 數(shù)據(jù)項(xiàng)中,。在方法運(yùn)行期間是不會(huì)改變局部變量表的大小的

  • 方法嵌套調(diào)用的次數(shù)由棧的大小決定。一般來(lái)說(shuō),,棧越大,方法嵌套調(diào)用次數(shù)越多,。對(duì)一個(gè)函數(shù)而言,,它的參數(shù)和局部變量越多,使得局部變量表膨脹,,它的棧幀就越大,,以滿(mǎn)足方法調(diào)用所需傳遞的信息增大的需求。進(jìn)而函數(shù)調(diào)用就會(huì)占用更多的??臻g,,導(dǎo)致其嵌套調(diào)用次數(shù)就會(huì)減少。

  • 局部變量表中的變量只在當(dāng)前方法調(diào)用中有效,。在方法執(zhí)行時(shí),,虛擬機(jī)通過(guò)使用局部變量表完成參數(shù)值到參數(shù)變量列表的傳遞過(guò)程。當(dāng)方法調(diào)用結(jié)束后,,隨著方法棧幀的銷(xiāo)毀,,局部變量表也會(huì)隨之銷(xiāo)毀。

  • 參數(shù)值的存放總是在局部變量數(shù)組的 index0 開(kāi)始,,到數(shù)組長(zhǎng)度 -1 的索引結(jié)束

槽 Slot
  • 局部變量表最基本的存儲(chǔ)單元是 Slot(變量槽)

  • 在局部變量表中,,32 位以?xún)?nèi)的類(lèi)型只占用一個(gè) Slot(包括returnAddress類(lèi)型),,64 位的類(lèi)型(long和double)占用兩個(gè)連續(xù)的 Slot

    • byte、short,、char 在存儲(chǔ)前被轉(zhuǎn)換為int,,boolean也被轉(zhuǎn)換為int,0 表示 false,,非 0 表示 true

    • long 和 double 則占據(jù)兩個(gè) Slot

  • JVM 會(huì)為局部變量表中的每一個(gè) Slot 都分配一個(gè)訪(fǎng)問(wèn)索引,,通過(guò)這個(gè)索引即可成功訪(fǎng)問(wèn)到局部變量表中指定的局部變量值,索引值的范圍從 0 開(kāi)始到局部變量表最大的 Slot 數(shù)量

  • 當(dāng)一個(gè)實(shí)例方法被調(diào)用的時(shí)候,,它的方法參數(shù)和方法體內(nèi)部定義的局部變量將會(huì)按照順序被復(fù)制到局部變量表中的每一個(gè) Slot 上

  • 如果需要訪(fǎng)問(wèn)局部變量表中一個(gè) 64bit 的局部變量值時(shí),,只需要使用前一個(gè)索引即可。(比如:訪(fǎng)問(wèn) long 或double 類(lèi)型變量,,不允許采用任何方式單獨(dú)訪(fǎng)問(wèn)其中的某一個(gè) Slot)

  • 如果當(dāng)前幀是由構(gòu)造方法或?qū)嵗椒▌?chuàng)建的,,那么該對(duì)象引用 this 將會(huì)存放在 index 為 0 的 Slot 處,其余的參數(shù)按照參數(shù)表順序繼續(xù)排列(這里就引出一個(gè)問(wèn)題:靜態(tài)方法中為什么不可以引用 this,,就是因?yàn)閠his 變量不存在于當(dāng)前方法的局部變量表中)

  • 棧幀中的局部變量表中的槽位是可以重用的,,如果一個(gè)局部變量過(guò)了其作用域,那么在其作用域之后申明的新的局部變量就很有可能會(huì)復(fù)用過(guò)期局部變量的槽位,,從而達(dá)到節(jié)省資源的目的,。(下圖中,this,、a,、b、c 理論上應(yīng)該有 4 個(gè)變量,,c 復(fù)用了 b 的槽)

  • 在棧幀中,,與性能調(diào)優(yōu)關(guān)系最為密切的就是局部變量表。在方法執(zhí)行時(shí),,虛擬機(jī)使用局部變量表完成方法的傳遞

  • 局部變量表中的變量也是重要的垃圾回收根節(jié)點(diǎn),,只要被局部變量表中直接或間接引用的對(duì)象都不會(huì)被回收

2.4.2. 操作數(shù)棧

  • 每個(gè)獨(dú)立的棧幀中除了包含局部變量表之外,還包含一個(gè)后進(jìn)先出(Last-In-First-Out)的操作數(shù)棧,,也可以稱(chēng)為表達(dá)式棧(Expression Stack)

  • 操作數(shù)棧,,在方法執(zhí)行過(guò)程中,根據(jù)字節(jié)碼指令,,往操作數(shù)棧中寫(xiě)入數(shù)據(jù)或提取數(shù)據(jù),,即入棧(push)、出棧(pop)

  • 某些字節(jié)碼指令將值壓入操作數(shù)棧,,其余的字節(jié)碼指令將操作數(shù)取出棧,。使用它們后再把結(jié)果壓入棧。比如,執(zhí)行復(fù)制,、交換,、求和等操作

概述
  • 操作數(shù)棧,主要用于保存計(jì)算過(guò)程的中間結(jié)果,,同時(shí)作為計(jì)算過(guò)程中變量臨時(shí)的存儲(chǔ)空間
  • 操作數(shù)棧就是 JVM 執(zhí)行引擎的一個(gè)工作區(qū),,當(dāng)一個(gè)方法剛開(kāi)始執(zhí)行的時(shí)候,一個(gè)新的棧幀也會(huì)隨之被創(chuàng)建出來(lái),,此時(shí)這個(gè)方法的操作數(shù)棧是空的

  • 每一個(gè)操作數(shù)棧都會(huì)擁有一個(gè)明確的棧深度用于存儲(chǔ)數(shù)值,,其所需的最大深度在編譯期就定義好了,保存在方法的 Code 屬性的 max_stack 數(shù)據(jù)項(xiàng)中

  • 棧中的任何一個(gè)元素都可以是任意的 Java 數(shù)據(jù)類(lèi)型

    • 32bit 的類(lèi)型占用一個(gè)棧單位深度

    • 64bit 的類(lèi)型占用兩個(gè)棧單位深度

  • 操作數(shù)棧并非采用訪(fǎng)問(wèn)索引的方式來(lái)進(jìn)行數(shù)據(jù)訪(fǎng)問(wèn)的,,而是只能通過(guò)標(biāo)準(zhǔn)的入棧和出棧操作來(lái)完成一次數(shù)據(jù)訪(fǎng)問(wèn)

  • 如果被調(diào)用的方法帶有返回值的話(huà),,其返回值將會(huì)被壓入當(dāng)前棧幀的操作數(shù)棧中,并更新PC寄存器中下一條需要執(zhí)行的字節(jié)碼指令

  • 操作數(shù)棧中元素的數(shù)據(jù)類(lèi)型必須與字節(jié)碼指令的序列嚴(yán)格匹配,,這由編譯器在編譯期間進(jìn)行驗(yàn)證,,同時(shí)在類(lèi)加載過(guò)程中的類(lèi)檢驗(yàn)階段的數(shù)據(jù)流分析階段要再次驗(yàn)證

  • 另外,我們說(shuō)Java虛擬機(jī)的解釋引擎是基于棧的執(zhí)行引擎,,其中的棧指的就是操作數(shù)棧

棧頂緩存(Top-of-stack-Cashing)

HotSpot 的執(zhí)行引擎采用的并非是基于寄存器的架構(gòu),,但這并不代表 HotSpot VM 的實(shí)現(xiàn)并沒(méi)有間接利用到寄存器資源。寄存器是物理 CPU 中的組成部分之一,,它同時(shí)也是 CPU 中非常重要的高速存儲(chǔ)資源,。一般來(lái)說(shuō),寄存器的讀/寫(xiě)速度非常迅速,,甚至可以比內(nèi)存的讀/寫(xiě)速度快上幾十倍不止,,不過(guò)寄存器資源卻非常有限,不同平臺(tái)下的CPU 寄存器數(shù)量是不同和不規(guī)律的,。寄存器主要用于緩存本地機(jī)器指令,、數(shù)值和下一條需要被執(zhí)行的指令地址等數(shù)據(jù)。

基于棧式架構(gòu)的虛擬機(jī)所使用的零地址指令更加緊湊,,但完成一項(xiàng)操作的時(shí)候必然需要使用更多的入棧和出棧指令,這同時(shí)也就意味著將需要更多的指令分派(instruction dispatch)次數(shù)和內(nèi)存讀/寫(xiě)次數(shù),。由于操作數(shù)是存儲(chǔ)在內(nèi)存中的,,因此頻繁的執(zhí)行內(nèi)存讀/寫(xiě)操作必然會(huì)影響執(zhí)行速度。為了解決這個(gè)問(wèn)題,,HotSpot JVM 設(shè)計(jì)者們提出了棧頂緩存技術(shù),,將棧頂元素全部緩存在物理 CPU 的寄存器中,以此降低對(duì)內(nèi)存的讀/寫(xiě)次數(shù),,提升執(zhí)行引擎的執(zhí)行效率

2.4.3. 動(dòng)態(tài)鏈接(指向運(yùn)行時(shí)常量池的方法引用)

  • 每一個(gè)棧幀內(nèi)部都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用,。包含這個(gè)引用的目的就是為了支持當(dāng)前方法的代碼能夠?qū)崿F(xiàn)動(dòng)態(tài)鏈接(Dynamic Linking)。

  • 在 Java 源文件被編譯到字節(jié)碼文件中時(shí),所有的變量和方法引用都作為

    符號(hào)引用

    (Symbolic Reference)保存在 Class 文件的常量池中,。比如:描述一個(gè)方法調(diào)用了另外的其他方法時(shí),,就是通過(guò)常量池中指向方法的符號(hào)引用來(lái)表示的,那么動(dòng)態(tài)鏈接的作用就是為了將這些符號(hào)引用轉(zhuǎn)換為調(diào)用方法的直接引用

jvm-dynamic-linking

JVM 是如何執(zhí)行方法調(diào)用的

方法調(diào)用不同于方法執(zhí)行,,方法調(diào)用階段的唯一任務(wù)就是確定被調(diào)用方法的版本(即調(diào)用哪一個(gè)方法),,暫時(shí)還不涉及方法內(nèi)部的具體運(yùn)行過(guò)程。Class 文件的編譯過(guò)程中不包括傳統(tǒng)編譯器中的連接步驟,,一切方法調(diào)用在 Class文件里面存儲(chǔ)的都是符號(hào)引用,,而不是方法在實(shí)際運(yùn)行時(shí)內(nèi)存布局中的入口地址(直接引用)。也就是需要在類(lèi)加載階段,,甚至到運(yùn)行期才能確定目標(biāo)方法的直接引用,。

【這一塊內(nèi)容,除了方法調(diào)用,,還包括解析,、分派(靜態(tài)分派、動(dòng)態(tài)分派,、單分派與多分派),,這里先不介紹,后續(xù)再挖】

在 JVM 中,,將符號(hào)引用轉(zhuǎn)換為調(diào)用方法的直接引用與方法的綁定機(jī)制有關(guān)

  • 靜態(tài)鏈接:當(dāng)一個(gè)字節(jié)碼文件被裝載進(jìn) JVM 內(nèi)部時(shí),,如果被調(diào)用的目標(biāo)方法在編譯期可知,且運(yùn)行期保持不變時(shí),。這種情況下將調(diào)用方法的符號(hào)引用轉(zhuǎn)換為直接引用的過(guò)程稱(chēng)之為靜態(tài)鏈接

  • 動(dòng)態(tài)鏈接:如果被調(diào)用的方法在編譯期無(wú)法被確定下來(lái),,也就是說(shuō),只能在程序運(yùn)行期將調(diào)用方法的符號(hào)引用轉(zhuǎn)換為直接引用,,由于這種引用轉(zhuǎn)換過(guò)程具備動(dòng)態(tài)性,,因此也就被稱(chēng)之為動(dòng)態(tài)鏈接

對(duì)應(yīng)的方法的綁定機(jī)制為:早期綁定(Early Binding)和晚期綁定(Late Binding)。綁定是一個(gè)字段,、方法或者類(lèi)在符號(hào)引用被替換為直接引用的過(guò)程,,這僅僅發(fā)生一次

  • 早期綁定:早期綁定就是指被調(diào)用的目標(biāo)方法如果在編譯期可知,,且運(yùn)行期保持不變時(shí),,即可將這個(gè)方法與所屬的類(lèi)型進(jìn)行綁定,這樣一來(lái),,由于明確了被調(diào)用的目標(biāo)方法究竟是哪一個(gè),,因此也就可以使用靜態(tài)鏈接的方式將符號(hào)引用轉(zhuǎn)換為直接引用。

  • 晚期綁定:如果被調(diào)用的方法在編譯器無(wú)法被確定下來(lái),,只能夠在程序運(yùn)行期根據(jù)實(shí)際的類(lèi)型綁定相關(guān)的方法,,這種綁定方式就被稱(chēng)為晚期綁定,。

虛方法和非虛方法
  • 如果方法在編譯器就確定了具體的調(diào)用版本,這個(gè)版本在運(yùn)行時(shí)是不可變的,。這樣的方法稱(chēng)為非虛方法,,比如靜態(tài)方法、私有方法,、final 方法,、實(shí)例構(gòu)造器、父類(lèi)方法都是非虛方法

  • 其他方法稱(chēng)為虛方法

虛方法表

在面向?qū)ο缶幊讨?,?huì)頻繁的使用到動(dòng)態(tài)分派,,如果每次動(dòng)態(tài)分派都要重新在類(lèi)的方法元數(shù)據(jù)中搜索合適的目標(biāo)有可能會(huì)影響到執(zhí)行效率。為了提高性能,,JVM 采用在類(lèi)的方法區(qū)建立一個(gè)虛方法表(virtual method table),,使用索引表來(lái)代替查找。非虛方法不會(huì)出現(xiàn)在表中,。

每個(gè)類(lèi)中都有一個(gè)虛方法表,,表中存放著各個(gè)方法的實(shí)際入口。

虛方法表會(huì)在類(lèi)加載的連接階段被創(chuàng)建并開(kāi)始初始化,,類(lèi)的變量初始值準(zhǔn)備完成之后,,JVM 會(huì)把該類(lèi)的方法表也初始化完畢。

2.4.4. 方法返回地址(return address)

用來(lái)存放調(diào)用該方法的 PC 寄存器的值,。

一個(gè)方法的結(jié)束,,有兩種方式

  • 正常執(zhí)行完成

  • 出現(xiàn)未處理的異常,非正常退出

無(wú)論通過(guò)哪種方式退出,,在方法退出后都返回到該方法被調(diào)用的位置,。方法正常退出時(shí),調(diào)用者的 PC 計(jì)數(shù)器的值作為返回地址,,即調(diào)用該方法的指令的下一條指令的地址,。而通過(guò)異常退出的,返回地址是要通過(guò)異常表來(lái)確定的,,棧幀中一般不會(huì)保存這部分信息,。

當(dāng)一個(gè)方法開(kāi)始執(zhí)行后,只有兩種方式可以退出這個(gè)方法:

  1. 執(zhí)行引擎遇到任意一個(gè)方法返回的字節(jié)碼指令,,會(huì)有返回值傳遞給上層的方法調(diào)用者,,簡(jiǎn)稱(chēng)正常完成出口

    一個(gè)方法的正常調(diào)用完成之后究竟需要使用哪一個(gè)返回指令還需要根據(jù)方法返回值的實(shí)際數(shù)據(jù)類(lèi)型而定

    在字節(jié)碼指令中,返回指令包含 ireturn(當(dāng)返回值是 boolean,、byte,、char,、short 和 int 類(lèi)型時(shí)使用),、lreturn,、freturn、dreturn 以及 areturn,,另外還有一個(gè) return 指令供聲明為 void 的方法,、實(shí)例初始化方法、類(lèi)和接口的初始化方法使用,。

  2. 在方法執(zhí)行的過(guò)程中遇到了異常,,并且這個(gè)異常沒(méi)有在方法內(nèi)進(jìn)行處理,也就是只要在本方法的異常表中沒(méi)有搜索到匹配的異常處理器,,就會(huì)導(dǎo)致方法退出,。簡(jiǎn)稱(chēng)異常完成出口

    方法執(zhí)行過(guò)程中拋出異常時(shí)的異常處理,存儲(chǔ)在一個(gè)異常處理表,,方便在發(fā)生異常的時(shí)候找到處理異常的代碼,。

本質(zhì)上,方法的退出就是當(dāng)前棧幀出棧的過(guò)程,。此時(shí),,需要恢復(fù)上層方法的局部變量表、操作數(shù)棧,、將返回值壓入調(diào)用者棧幀的操作數(shù)棧,、設(shè)置PC寄存器值等,讓調(diào)用者方法繼續(xù)執(zhí)行下去,。

正常完成出口和異常完成出口的區(qū)別在于:通過(guò)異常完成出口退出的不會(huì)給他的上層調(diào)用者產(chǎn)生任何的返回值

2.4.5. 附加信息

棧幀中還允許攜帶與 Java 虛擬機(jī)實(shí)現(xiàn)相關(guān)的一些附加信息,。例如,對(duì)程序調(diào)試提供支持的信息,,但這些信息取決于具體的虛擬機(jī)實(shí)現(xiàn),。


三、本地方法棧

3.1 本地方法接口

簡(jiǎn)單的講,,一個(gè) Native Method 就是一個(gè) Java 調(diào)用非 Java 代碼的接口,。我們知道的 Unsafe 類(lèi)就有很多本地方法。

為什么要使用本地方法(Native Method)?

Java 使用起來(lái)非常方便,,然而有些層次的任務(wù)用 Java 實(shí)現(xiàn)起來(lái)也不容易,,或者我們對(duì)程序的效率很在意時(shí),問(wèn)題就來(lái)了

  • 與 Java 環(huán)境外交互:有時(shí) Java 應(yīng)用需要與 Java 外面的環(huán)境交互,,這就是本地方法存在的原因,。

  • 與操作系統(tǒng)交互:JVM 支持 Java 語(yǔ)言本身和運(yùn)行時(shí)庫(kù),但是有時(shí)仍需要依賴(lài)一些底層系統(tǒng)的支持,。通過(guò)本地方法,,我們可以實(shí)現(xiàn)用 Java 與實(shí)現(xiàn)了 jre 的底層系統(tǒng)交互, JVM 的一些部分就是 C 語(yǔ)言寫(xiě)的,。

  • Sun's Java:Sun的解釋器就是C實(shí)現(xiàn)的,,這使得它能像一些普通的C一樣與外部交互,。jre大部分都是用 Java 實(shí)現(xiàn)的,它也通過(guò)一些本地方法與外界交互,。比如,,類(lèi) java.lang.ThreadsetPriority() 的方法是用Java 實(shí)現(xiàn)的,但它實(shí)現(xiàn)調(diào)用的是該類(lèi)的本地方法 setPrioruty(),,該方法是C實(shí)現(xiàn)的,,并被植入 JVM 內(nèi)部。

3.2 本地方法棧(Native Method Stack)

  • Java 虛擬機(jī)棧用于管理 Java 方法的調(diào)用,,而本地方法棧用于管理本地方法的調(diào)用

  • 本地方法棧也是線(xiàn)程私有的

  • 允許線(xiàn)程固定或者可動(dòng)態(tài)擴(kuò)展的內(nèi)存大小

    • 如果線(xiàn)程請(qǐng)求分配的棧容量超過(guò)本地方法棧允許的最大容量,,Java 虛擬機(jī)將會(huì)拋出一個(gè) StackOverflowError 異常

    • 如果本地方法棧可以動(dòng)態(tài)擴(kuò)展,,并且在嘗試擴(kuò)展的時(shí)候無(wú)法申請(qǐng)到足夠的內(nèi)存,,或者在創(chuàng)建新的線(xiàn)程時(shí)沒(méi)有足夠的內(nèi)存去創(chuàng)建對(duì)應(yīng)的本地方法棧,那么 Java虛擬機(jī)將會(huì)拋出一個(gè)OutofMemoryError異常

  • 本地方法是使用 C 語(yǔ)言實(shí)現(xiàn)的

  • 它的具體做法是 Mative Method Stack 中登記 native 方法,,在 Execution Engine 執(zhí)行時(shí)加載本地方法庫(kù)當(dāng)某個(gè)線(xiàn)程調(diào)用一個(gè)本地方法時(shí),,它就進(jìn)入了一個(gè)全新的并且不再受虛擬機(jī)限制的世界。它和虛擬機(jī)擁有同樣的權(quán)限,。

  • 本地方法可以通過(guò)本地方法接口來(lái)訪(fǎng)問(wèn)虛擬機(jī)內(nèi)部的運(yùn)行時(shí)數(shù)據(jù)區(qū),,它甚至可以直接使用本地處理器中的寄存器,直接從本地內(nèi)存的堆中分配任意數(shù)量的內(nèi)存

  • 并不是所有 JVM 都支持本地方法,。因?yàn)?Java 虛擬機(jī)規(guī)范并沒(méi)有明確要求本地方法棧的使用語(yǔ)言,、具體實(shí)現(xiàn)方式、數(shù)據(jù)結(jié)構(gòu)等,。如果 JVM 產(chǎn)品不打算支持 native 方法,,也可以無(wú)需實(shí)現(xiàn)本地方法棧

  • 在 Hotspot JVM 中,直接將本地方棧和虛擬機(jī)棧合二為一


棧是運(yùn)行時(shí)的單位,,而堆是存儲(chǔ)的單位,。

棧解決程序的運(yùn)行問(wèn)題,即程序如何執(zhí)行,,或者說(shuō)如何處理數(shù)據(jù),。堆解決的是數(shù)據(jù)存儲(chǔ)的問(wèn)題,即數(shù)據(jù)怎么放,、放在哪,。

四、堆內(nèi)存

4.1 內(nèi)存劃分

對(duì)于大多數(shù)應(yīng)用,,Java 堆是 Java 虛擬機(jī)管理的內(nèi)存中最大的一塊,,被所有線(xiàn)程共享。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,,幾乎所有的對(duì)象實(shí)例以及數(shù)據(jù)都在這里分配內(nèi)存,。

為了進(jìn)行高效的垃圾回收,,虛擬機(jī)把堆內(nèi)存邏輯上劃分成三塊區(qū)域(分代的唯一理由就是優(yōu)化 GC 性能):

  • 新生帶(年輕代):新對(duì)象和沒(méi)達(dá)到一定年齡的對(duì)象都在新生代

  • 老年代(養(yǎng)老區(qū)):被長(zhǎng)時(shí)間使用的對(duì)象,老年代的內(nèi)存空間應(yīng)該要比年輕代更大

  • 元空間(JDK1.8 之前叫永久代):像一些方法中的操作臨時(shí)對(duì)象等,,JDK1.8 之前是占用 JVM 內(nèi)存,JDK1.8 之后直接使用物理內(nèi)存

JDK7

Java 虛擬機(jī)規(guī)范規(guī)定,,Java 堆可以是處于物理上不連續(xù)的內(nèi)存空間中,,只要邏輯上是連續(xù)的即可,像磁盤(pán)空間一樣,。實(shí)現(xiàn)時(shí),,既可以是固定大小,也可以是可擴(kuò)展的,,主流虛擬機(jī)都是可擴(kuò)展的(通過(guò) -Xmx-Xms 控制),,如果堆中沒(méi)有完成實(shí)例分配,并且堆無(wú)法再擴(kuò)展時(shí),,就會(huì)拋出 OutOfMemoryError 異常,。

年輕代 (Young Generation)

年輕代是所有新對(duì)象創(chuàng)建的地方。當(dāng)填充年輕代時(shí),,執(zhí)行垃圾收集,。這種垃圾收集稱(chēng)為 Minor GC。年輕一代被分為三個(gè)部分——伊甸園(Eden Memory)和兩個(gè)幸存區(qū)(Survivor Memory,,被稱(chēng)為from/to或s0/s1),,默認(rèn)比例是8:1:1

  • 大多數(shù)新創(chuàng)建的對(duì)象都位于 Eden 內(nèi)存空間中

  • 當(dāng) Eden 空間被對(duì)象填充時(shí),執(zhí)行Minor GC,,并將所有幸存者對(duì)象移動(dòng)到一個(gè)幸存者空間中

  • Minor GC 檢查幸存者對(duì)象,,并將它們移動(dòng)到另一個(gè)幸存者空間。所以每次,,一個(gè)幸存者空間總是空的

  • 經(jīng)過(guò)多次 GC 循環(huán)后存活下來(lái)的對(duì)象被移動(dòng)到老年代,。通常,這是通過(guò)設(shè)置年輕一代對(duì)象的年齡閾值來(lái)實(shí)現(xiàn)的,,然后他們才有資格提升到老一代

老年代(Old Generation)

舊的一代內(nèi)存包含那些經(jīng)過(guò)許多輪小型 GC 后仍然存活的對(duì)象,。通常,垃圾收集是在老年代內(nèi)存滿(mǎn)時(shí)執(zhí)行的,。老年代垃圾收集稱(chēng)為 主GC(Major GC),,通常需要更長(zhǎng)的時(shí)間。

大對(duì)象直接進(jìn)入老年代(大對(duì)象是指需要大量連續(xù)內(nèi)存空間的對(duì)象),。這樣做的目的是避免在 Eden 區(qū)和兩個(gè)Survivor 區(qū)之間發(fā)生大量的內(nèi)存拷貝

元空間

不管是 JDK8 之前的永久代,,還是 JDK8 及以后的元空間,都可以看作是 Java 虛擬機(jī)規(guī)范中方法區(qū)的實(shí)現(xiàn),。

雖然 Java 虛擬機(jī)規(guī)范把方法區(qū)描述為堆的一個(gè)邏輯部分,,但是它卻有一個(gè)別名叫 Non-Heap(非堆),,目的應(yīng)該是與 Java 堆區(qū)分開(kāi)。

所以元空間放在后邊的方法區(qū)再說(shuō),。

4.2 設(shè)置堆內(nèi)存大小和 OOM

Java 堆用于存儲(chǔ) Java 對(duì)象實(shí)例,,那么堆的大小在 JVM 啟動(dòng)的時(shí)候就確定了,我們可以通過(guò) -Xmx-Xms 來(lái)設(shè)定

  • -Xmx 用來(lái)表示堆的起始內(nèi)存,,等價(jià)于 -XX:InitialHeapSize

  • -Xms 用來(lái)表示堆的最大內(nèi)存,,等價(jià)于 -XX:MaxHeapSize

如果堆的內(nèi)存大小超過(guò) -Xms 設(shè)定的最大內(nèi)存, 就會(huì)拋出 OutOfMemoryError 異常,。

我們通常會(huì)將 -Xmx-Xms 兩個(gè)參數(shù)配置為相同的值,,其目的是為了能夠在垃圾回收機(jī)制清理完堆區(qū)后不再需要重新分隔計(jì)算堆的大小,從而提高性能

  • 默認(rèn)情況下,,初始堆內(nèi)存大小為:電腦內(nèi)存大小/64

  • 默認(rèn)情況下,,最大堆內(nèi)存大小為:電腦內(nèi)存大小/4

可以通過(guò)代碼獲取到我們的設(shè)置值,當(dāng)然也可以模擬 OOM:

public static void main(String[] args) {

  //返回 JVM 堆大小
  long initalMemory = Runtime.getRuntime().totalMemory() / 1024 /1024;
  //返回 JVM 堆的最大內(nèi)存
  long maxMemory = Runtime.getRuntime().maxMemory() / 1024 /1024;

  System.out.println("-Xms : " initalMemory   "M");
  System.out.println("-Xmx : " maxMemory   "M");

  System.out.println("系統(tǒng)內(nèi)存大?。?quot;   initalMemory * 64 / 1024   "G");
  System.out.println("系統(tǒng)內(nèi)存大?。?quot;   maxMemory * 4 / 1024   "G");
}

查看 JVM 堆內(nèi)存分配

  1. 在默認(rèn)不配置 JVM 堆內(nèi)存大小的情況下,JVM 根據(jù)默認(rèn)值來(lái)配置當(dāng)前內(nèi)存大小

  2. 默認(rèn)情況下新生代和老年代的比例是 1:2,,可以通過(guò) –XX:NewRatio 來(lái)配置

    • 新生代中的 Eden:From Survivor:To Survivor 的比例是 8:1:1,,可以通過(guò) -XX:SurvivorRatio 來(lái)配置

  3. 若在 JDK 7 中開(kāi)啟了 -XX: UseAdaptiveSizePolicy,JVM 會(huì)動(dòng)態(tài)調(diào)整 JVM 堆中各個(gè)區(qū)域的大小以及進(jìn)入老年代的年齡

    此時(shí) –XX:NewRatio-XX:SurvivorRatio 將會(huì)失效,,而 JDK 8 是默認(rèn)開(kāi)啟-XX: UseAdaptiveSizePolicy

    在 JDK 8中,,不要隨意關(guān)閉-XX: UseAdaptiveSizePolicy,除非對(duì)堆內(nèi)存的劃分有明確的規(guī)劃

每次 GC 后都會(huì)重新計(jì)算 Eden,、From Survivor,、To Survivor 的大小

計(jì)算依據(jù)是GC過(guò)程中統(tǒng)計(jì)的GC時(shí)間吞吐量,、內(nèi)存占用量

java -XX: PrintFlagsFinal -version | grep HeapSize
    uintx ErgoHeapSizeLimit                         = 0                                   {product}
    uintx HeapSizePerGCThread                       = 87241520                            {product}
    uintx InitialHeapSize                          := 134217728                           {product}
    uintx LargePageHeapSizeThreshold                = 134217728                           {product}
    uintx MaxHeapSize                              := 2147483648                          {product}
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)
$ jmap -heap 進(jìn)程號(hào)

4.3 對(duì)象在堆中的生命周期

  1. 在 JVM 內(nèi)存模型的堆中,,堆被劃分為新生代和老年代

    • 新生代又被進(jìn)一步劃分為 Eden區(qū)Survivor區(qū),Survivor 區(qū)由 From SurvivorTo Survivor 組成

  2. 當(dāng)創(chuàng)建一個(gè)對(duì)象時(shí),,對(duì)象會(huì)被優(yōu)先分配到新生代的 Eden 區(qū)

    • 此時(shí) JVM 會(huì)給對(duì)象定義一個(gè)對(duì)象年輕計(jì)數(shù)器-XX:MaxTenuringThreshold

  3. 當(dāng) Eden 空間不足時(shí),,JVM 將執(zhí)行新生代的垃圾回收(Minor GC)

    • JVM 會(huì)把存活的對(duì)象轉(zhuǎn)移到 Survivor 中,并且對(duì)象年齡 1

    • 對(duì)象在 Survivor 中同樣也會(huì)經(jīng)歷 Minor GC,,每經(jīng)歷一次 Minor GC,,對(duì)象年齡都會(huì) 1

  4. 如果分配的對(duì)象超過(guò)了-XX:PetenureSizeThreshold,對(duì)象會(huì)直接被分配到老年代

4.4 對(duì)象的分配過(guò)程

為對(duì)象分配內(nèi)存是一件非常嚴(yán)謹(jǐn)和復(fù)雜的任務(wù),,JVM 的設(shè)計(jì)者們不僅需要考慮內(nèi)存如何分配,、在哪里分配等問(wèn)題,并且由于內(nèi)存分配算法和內(nèi)存回收算法密切相關(guān),所以還需要考慮 GC 執(zhí)行完內(nèi)存回收后是否會(huì)在內(nèi)存空間中產(chǎn)生內(nèi)存碎片,。

  1. new 的對(duì)象先放在伊甸園區(qū),,此區(qū)有大小限制

  2. 當(dāng)伊甸園的空間填滿(mǎn)時(shí),程序又需要?jiǎng)?chuàng)建對(duì)象,,JVM 的垃圾回收器將對(duì)伊甸園區(qū)進(jìn)行垃圾回收(Minor GC),,將伊甸園區(qū)中的不再被其他對(duì)象所引用的對(duì)象進(jìn)行銷(xiāo)毀。再加載新的對(duì)象放到伊甸園區(qū)

  3. 然后將伊甸園中的剩余對(duì)象移動(dòng)到幸存者 0 區(qū)

  4. 如果再次觸發(fā)垃圾回收,,此時(shí)上次幸存下來(lái)的放到幸存者 0 區(qū),,如果沒(méi)有回收,就會(huì)放到幸存者 1 區(qū)

  5. 如果再次經(jīng)歷垃圾回收,,此時(shí)會(huì)重新放回幸存者 0 區(qū),接著再去幸存者 1 區(qū)

  6. 什么時(shí)候才會(huì)去養(yǎng)老區(qū)呢,? 默認(rèn)是 15 次回收標(biāo)記

  7. 在養(yǎng)老區(qū),,相對(duì)悠閑。當(dāng)養(yǎng)老區(qū)內(nèi)存不足時(shí),,再次觸發(fā) Major GC,,進(jìn)行養(yǎng)老區(qū)的內(nèi)存清理

  8. 若養(yǎng)老區(qū)執(zhí)行了 Major GC 之后發(fā)現(xiàn)依然無(wú)法進(jìn)行對(duì)象的保存,就會(huì)產(chǎn)生 OOM 異常

4.5 GC 垃圾回收簡(jiǎn)介

Minor GC,、Major GC,、Full GC

JVM 在進(jìn)行 GC 時(shí),并非每次都對(duì)堆內(nèi)存(新生代,、老年代,;方法區(qū))區(qū)域一起回收的,大部分時(shí)候回收的都是指新生代,。

針對(duì) HotSpot VM 的實(shí)現(xiàn),,它里面的 GC 按照回收區(qū)域又分為兩大類(lèi):部分收集(Partial GC),整堆收集(Full GC)

  • 部分收集:不是完整收集整個(gè) Java 堆的垃圾收集,。其中又分為:

    • 目前只有 G1 GC 會(huì)有這種行為

    • 目前,,只有 CMS GC 會(huì)有單獨(dú)收集老年代的行為

    • 很多時(shí)候 Major GC 會(huì)和 Full GC 混合使用,需要具體分辨是老年代回收還是整堆回收

    • 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集

    • 老年代收集(Major GC/Old GC):只是老年代的垃圾收集

    • 混合收集(Mixed GC):收集整個(gè)新生代以及部分老年代的垃圾收集

  • 整堆收集(Full GC):收集整個(gè) Java 堆和方法區(qū)的垃圾

4.6 TLAB

什么是 TLAB (Thread Local Allocation Buffer)?

  • 從內(nèi)存模型而不是垃圾回收的角度,,對(duì) Eden 區(qū)域繼續(xù)進(jìn)行劃分,,JVM 為每個(gè)線(xiàn)程分配了一個(gè)私有緩存區(qū)域,它包含在 Eden 空間內(nèi)

  • 多線(xiàn)程同時(shí)分配內(nèi)存時(shí),,使用 TLAB 可以避免一系列的非線(xiàn)程安全問(wèn)題,,同時(shí)還能提升內(nèi)存分配的吞吐量,因此我們可以將這種內(nèi)存分配方式稱(chēng)為快速分配策略

  • OpenJDK 衍生出來(lái)的 JVM 大都提供了 TLAB 設(shè)計(jì)

為什么要有 TLAB ?

  • 堆區(qū)是線(xiàn)程共享的,,任何線(xiàn)程都可以訪(fǎng)問(wèn)到堆區(qū)中的共享數(shù)據(jù)

  • 由于對(duì)象實(shí)例的創(chuàng)建在 JVM 中非常頻繁,,因此在并發(fā)環(huán)境下從堆區(qū)中劃分內(nèi)存空間是線(xiàn)程不安全的

  • 為避免多個(gè)線(xiàn)程操作同一地址,需要使用加鎖等機(jī)制,進(jìn)而影響分配速度

盡管不是所有的對(duì)象實(shí)例都能夠在 TLAB 中成功分配內(nèi)存,,但 JVM 確實(shí)是將 TLAB 作為內(nèi)存分配的首選,。

在程序中,可以通過(guò) -XX:UseTLAB 設(shè)置是否開(kāi)啟 TLAB 空間,。

默認(rèn)情況下,,TLAB 空間的內(nèi)存非常小,僅占有整個(gè) Eden 空間的 1%,,我們可以通過(guò) -XX:TLABWasteTargetPercent 設(shè)置 TLAB 空間所占用 Eden 空間的百分比大小,。

一旦對(duì)象在 TLAB 空間分配內(nèi)存失敗時(shí),JVM 就會(huì)嘗試著通過(guò)使用加鎖機(jī)制確保數(shù)據(jù)操作的原子性,,從而直接在 Eden 空間中分配內(nèi)存,。

4.7 堆是分配對(duì)象存儲(chǔ)的唯一選擇嗎

隨著 JIT 編譯期的發(fā)展和逃逸分析技術(shù)的逐漸成熟,棧上分配,、標(biāo)量替換優(yōu)化技術(shù)將會(huì)導(dǎo)致一些微妙的變化,,所有的對(duì)象都分配到堆上也漸漸變得不那么“絕對(duì)”了。 ——《深入理解 Java 虛擬機(jī)》

逃逸分析

逃逸分析(Escape Analysis)是目前 Java 虛擬機(jī)中比較前沿的優(yōu)化技術(shù),。這是一種可以有效減少 Java 程序中同步負(fù)載和內(nèi)存堆分配壓力的跨函數(shù)全局?jǐn)?shù)據(jù)流分析算法,。通過(guò)逃逸分析,Java Hotspot 編譯器能夠分析出一個(gè)新的對(duì)象的引用的使用范圍從而決定是否要將這個(gè)對(duì)象分配到堆上,。

逃逸分析的基本行為就是分析對(duì)象動(dòng)態(tài)作用域:

  • 當(dāng)一個(gè)對(duì)象在方法中被定義后,,對(duì)象只在方法內(nèi)部使用,則認(rèn)為沒(méi)有發(fā)生逃逸,。

  • 當(dāng)一個(gè)對(duì)象在方法中被定義后,,它被外部方法所引用,則認(rèn)為發(fā)生逃逸,。例如作為調(diào)用參數(shù)傳遞到其他地方中,,稱(chēng)為方法逃逸。

例如:

public static StringBuffer craeteStringBuffer(String s1, String s2) {
   StringBuffer sb = new StringBuffer();
   sb.append(s1);
   sb.append(s2);
   return sb;
}

StringBuffer sb是一個(gè)方法內(nèi)部變量,,上述代碼中直接將sb返回,,這樣這個(gè) StringBuffer 有可能被其他方法所改變,這樣它的作用域就不只是在方法內(nèi)部,,雖然它是一個(gè)局部變量,,稱(chēng)其逃逸到了方法外部。甚至還有可能被外部線(xiàn)程訪(fǎng)問(wèn)到,,譬如賦值給類(lèi)變量或可以在其他線(xiàn)程中訪(fǎng)問(wèn)的實(shí)例變量,,稱(chēng)為線(xiàn)程逃逸。

上述代碼如果想要 StringBuffer sb不逃出方法,,可以這樣寫(xiě):

public static String createStringBuffer(String s1, String s2) {
   StringBuffer sb = new StringBuffer();
   sb.append(s1);
   sb.append(s2);
   return sb.toString();
}

不直接返回 StringBuffer,,那么 StringBuffer 將不會(huì)逃逸出方法。

參數(shù)設(shè)置:

  • 在 JDK 6u23 版本之后,HotSpot 中默認(rèn)就已經(jīng)開(kāi)啟了逃逸分析

  • 如果使用較早版本,,可以通過(guò)-XX" DoEscapeAnalysis顯式開(kāi)啟

開(kāi)發(fā)中使用局部變量,,就不要在方法外定義。

使用逃逸分析,,編譯器可以對(duì)代碼做優(yōu)化:

  • 棧上分配:將堆分配轉(zhuǎn)化為棧分配,。如果一個(gè)對(duì)象在子程序中被分配,要使指向該對(duì)象的指針永遠(yuǎn)不會(huì)逃逸,,對(duì)象可能是棧分配的候選,,而不是堆分配

  • 同步省略:如果一個(gè)對(duì)象被發(fā)現(xiàn)只能從一個(gè)線(xiàn)程被訪(fǎng)問(wèn)到,那么對(duì)于這個(gè)對(duì)象的操作可以不考慮同步

  • 分離對(duì)象或標(biāo)量替換:有的對(duì)象可能不需要作為一個(gè)連續(xù)的內(nèi)存結(jié)構(gòu)存在也可以被訪(fǎng)問(wèn)到,,那么對(duì)象的部分(或全部)可以不存儲(chǔ)在內(nèi)存,,而存儲(chǔ)在 CPU 寄存器

JIT 編譯器在編譯期間根據(jù)逃逸分析的結(jié)果,發(fā)現(xiàn)如果一個(gè)對(duì)象并沒(méi)有逃逸出方法的話(huà),,就可能被優(yōu)化成棧上分配,。分配完成后,繼續(xù)在調(diào)用棧內(nèi)執(zhí)行,,最后線(xiàn)程結(jié)束,,??臻g被回收,,局部變量對(duì)象也被回收。這樣就無(wú)需進(jìn)行垃圾回收了,。

常見(jiàn)棧上分配的場(chǎng)景:成員變量賦值,、方法返回值、實(shí)例引用傳遞

代碼優(yōu)化之同步省略(消除)
  • 線(xiàn)程同步的代價(jià)是相當(dāng)高的,,同步的后果是降低并發(fā)性和性能

  • 在動(dòng)態(tài)編譯同步塊的時(shí)候,,JIT 編譯器可以借助逃逸分析來(lái)判斷同步塊所使用的鎖對(duì)象是否能夠被一個(gè)線(xiàn)程訪(fǎng)問(wèn)而沒(méi)有被發(fā)布到其他線(xiàn)程。如果沒(méi)有,,那么 JIT 編譯器在編譯這個(gè)同步塊的時(shí)候就會(huì)取消對(duì)這個(gè)代碼的同步,。這樣就能大大提高并發(fā)性和性能。這個(gè)取消同步的過(guò)程就叫做同步省略,,也叫鎖消除,。

public void keep() {
  Object keeper = new Object();
  synchronized(keeper) {
    System.out.println(keeper);
  }
}

如上代碼,代碼中對(duì) keeper 這個(gè)對(duì)象進(jìn)行加鎖,,但是 keeper 對(duì)象的生命周期只在 keep()方法中,,并不會(huì)被其他線(xiàn)程所訪(fǎng)問(wèn)到,所以在 JIT編譯階段就會(huì)被優(yōu)化掉,。優(yōu)化成:

public void keep() {
  Object keeper = new Object();
  System.out.println(keeper);
}
代碼優(yōu)化之標(biāo)量替換

標(biāo)量(Scalar)是指一個(gè)無(wú)法再分解成更小的數(shù)據(jù)的數(shù)據(jù),。Java 中的原始數(shù)據(jù)類(lèi)型就是標(biāo)量。

相對(duì)的,那些的還可以分解的數(shù)據(jù)叫做聚合量(Aggregate),,Java 中的對(duì)象就是聚合量,,因?yàn)槠溥€可以分解成其他聚合量和標(biāo)量。

在 JIT 階段,,通過(guò)逃逸分析確定該對(duì)象不會(huì)被外部訪(fǎng)問(wèn),,并且對(duì)象可以被進(jìn)一步分解時(shí),JVM 不會(huì)創(chuàng)建該對(duì)象,,而會(huì)將該對(duì)象成員變量分解若干個(gè)被這個(gè)方法使用的成員變量所代替,。這些代替的成員變量在棧幀或寄存器上分配空間。這個(gè)過(guò)程就是標(biāo)量替換,。

通過(guò) -XX: EliminateAllocations 可以開(kāi)啟標(biāo)量替換,,-XX: PrintEliminateAllocations 查看標(biāo)量替換情況。

public static void main(String[] args) {
   alloc();
}

private static void alloc() {
   Point point = new Point(1,2);
   System.out.println("point.x=" point.x "; point.y=" point.y);
}
class Point{
    private int x;
    private int y;
}

以上代碼中,,point 對(duì)象并沒(méi)有逃逸出 alloc() 方法,,并且 point 對(duì)象是可以拆解成標(biāo)量的。那么,,JIT 就不會(huì)直接創(chuàng)建 Point 對(duì)象,,而是直接使用兩個(gè)標(biāo)量 int x ,int y 來(lái)替代 Point 對(duì)象,。

private static void alloc() {
   int x = 1;
   int y = 2;
   System.out.println("point.x=" x "; point.y=" y);
}
代碼優(yōu)化之棧上分配

我們通過(guò) JVM 內(nèi)存分配可以知道 JAVA 中的對(duì)象都是在堆上進(jìn)行分配,,當(dāng)對(duì)象沒(méi)有被引用的時(shí)候,需要依靠 GC 進(jìn)行回收內(nèi)存,,如果對(duì)象數(shù)量較多的時(shí)候,,會(huì)給 GC 帶來(lái)較大壓力,也間接影響了應(yīng)用的性能,。為了減少臨時(shí)對(duì)象在堆內(nèi)分配的數(shù)量,,JVM 通過(guò)逃逸分析確定該對(duì)象不會(huì)被外部訪(fǎng)問(wèn)。那就通過(guò)標(biāo)量替換將該對(duì)象分解在棧上分配內(nèi)存,,這樣該對(duì)象所占用的內(nèi)存空間就可以隨棧幀出棧而銷(xiāo)毀,,就減輕了垃圾回收的壓力。

總結(jié):

關(guān)于逃逸分析的論文在1999年就已經(jīng)發(fā)表了,,但直到JDK 1.6才有實(shí)現(xiàn),,而且這項(xiàng)技術(shù)到如今也并不是十分成熟的。

其根本原因就是無(wú)法保證逃逸分析的性能消耗一定能高于他的消耗,。雖然經(jīng)過(guò)逃逸分析可以做標(biāo)量替換,、棧上分配、和鎖消除,。但是逃逸分析自身也是需要進(jìn)行一系列復(fù)雜的分析的,,這其實(shí)也是一個(gè)相對(duì)耗時(shí)的過(guò)程,。

一個(gè)極端的例子,就是經(jīng)過(guò)逃逸分析之后,,發(fā)現(xiàn)沒(méi)有一個(gè)對(duì)象是不逃逸的,。那這個(gè)逃逸分析的過(guò)程就白白浪費(fèi)掉了。

雖然這項(xiàng)技術(shù)并不十分成熟,,但是他也是即時(shí)編譯器優(yōu)化技術(shù)中一個(gè)十分重要的手段,。


五、方法區(qū)

  • 方法區(qū)(Method Area)與 Java 堆一樣,,是所有線(xiàn)程共享的內(nèi)存區(qū)域,。

  • 雖然 Java 虛擬機(jī)規(guī)范把方法區(qū)描述為堆的一個(gè)邏輯部分,但是它卻有一個(gè)別名叫 Non-Heap(非堆),,目的應(yīng)該是與 Java 堆區(qū)分開(kāi),。

  • 運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class 文件中除了有類(lèi)的版本/字段/方法/接口等描述信息外,,還有一項(xiàng)信息是常量池(Constant Pool Table),,用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將類(lèi)在加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放,。運(yùn)行期間也可能將新的常量放入池中,,這種特性被開(kāi)發(fā)人員利用得比較多的是 String.intern()方法。受方法區(qū)內(nèi)存的限制,,當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)會(huì)拋出 OutOfMemoryError 異常,。

  • 方法區(qū)的大小和堆空間一樣,可以選擇固定大小也可選擇可擴(kuò)展,,方法區(qū)的大小決定了系統(tǒng)可以放多少個(gè)類(lèi),,如果系統(tǒng)類(lèi)太多,,導(dǎo)致方法區(qū)溢出,,虛擬機(jī)同樣會(huì)拋出內(nèi)存溢出錯(cuò)誤

  • JVM 關(guān)閉后方法區(qū)即被釋放

5.1 解惑

你是否也有看不同的參考資料,有的內(nèi)存結(jié)構(gòu)圖有方法區(qū),,有的又是永久代,,元數(shù)據(jù)區(qū),一臉懵逼的時(shí)候,?

  • 方法區(qū)(method area)只是 JVM 規(guī)范中定義的一個(gè)概念,,用于存儲(chǔ)類(lèi)信息、常量池,、靜態(tài)變量,、JIT編譯后的代碼等數(shù)據(jù),并沒(méi)有規(guī)定如何去實(shí)現(xiàn)它,,不同的廠(chǎng)商有不同的實(shí)現(xiàn),。而永久代(PermGen)Hotspot 虛擬機(jī)特有的概念,, Java8 的時(shí)候又被元空間取代了,永久代和元空間都可以理解為方法區(qū)的落地實(shí)現(xiàn),。

  • 永久代物理是堆的一部分,,和新生代,老年代地址是連續(xù)的(受垃圾回收器管理),,而元空間存在于本地內(nèi)存(我們常說(shuō)的堆外內(nèi)存,,不受垃圾回收器管理),這樣就不受 JVM 限制了,,也比較難發(fā)生OOM(都會(huì)有溢出異常)

  • Java7 中我們通過(guò)-XX:PermSize-xx:MaxPermSize 來(lái)設(shè)置永久代參數(shù),,Java8 之后,隨著永久代的取消,,這些參數(shù)也就隨之失效了,,改為通過(guò)-XX:MetaspaceSize-XX:MaxMetaspaceSize 用來(lái)設(shè)置元空間參數(shù)

  • 存儲(chǔ)內(nèi)容不同,元空間存儲(chǔ)類(lèi)的元信息,,靜態(tài)變量和常量池等并入堆中,。相當(dāng)于永久代的數(shù)據(jù)被分到了堆和元空間中

  • 如果方法區(qū)域中的內(nèi)存不能用于滿(mǎn)足分配請(qǐng)求,則 Java 虛擬機(jī)拋出 OutOfMemoryError

  • JVM 規(guī)范說(shuō)方法區(qū)在邏輯上是堆的一部分,,但目前實(shí)際上是與 Java 堆分開(kāi)的(Non-Heap)

所以對(duì)于方法區(qū),,Java8 之后的變化:

  • 移除了永久代(PermGen),替換為元空間(Metaspace),;

  • 永久代中的 class metadata 轉(zhuǎn)移到了 native memory(本地內(nèi)存,,而不是虛擬機(jī));

  • 永久代中的 interned Strings 和 class static variables 轉(zhuǎn)移到了 Java heap,;

  • 永久代參數(shù) (PermSize MaxPermSize) -> 元空間參數(shù)(MetaspaceSize MaxMetaspaceSize)

5.2 設(shè)置方法區(qū)內(nèi)存的大小

JDK8 及以后:

  • 元數(shù)據(jù)區(qū)大小可以使用參數(shù) -XX:MetaspaceSize-XX:MaxMetaspaceSize 指定,,替代上述原有的兩個(gè)參數(shù)

  • 默認(rèn)值依賴(lài)于平臺(tái)。Windows 下,,-XX:MetaspaceSize 是 21M,,-XX:MaxMetaspacaSize 的值是 -1,即沒(méi)有限制

  • 與永久代不同,,如果不指定大小,,默認(rèn)情況下,虛擬機(jī)會(huì)耗盡所有的可用系統(tǒng)內(nèi)存,。如果元數(shù)據(jù)發(fā)生溢出,,虛擬機(jī)一樣會(huì)拋出異常 OutOfMemoryError:Metaspace

  • -XX:MetaspaceSize :設(shè)置初始的元空間大小。對(duì)于一個(gè) 64 位的服務(wù)器端 JVM 來(lái)說(shuō),,其默認(rèn)的 -XX:MetaspaceSize 的值為20.75MB,,這就是初始的高水位線(xiàn),一旦觸及這個(gè)水位線(xiàn),,F(xiàn)ull GC 將會(huì)被觸發(fā)并卸載沒(méi)用的類(lèi)(即這些類(lèi)對(duì)應(yīng)的類(lèi)加載器不再存活),,然后這個(gè)高水位線(xiàn)將會(huì)重置,,新的高水位線(xiàn)的值取決于 GC 后釋放了多少元空間。如果釋放的空間不足,,那么在不超過(guò) MaxMetaspaceSize時(shí),,適當(dāng)提高該值。如果釋放空間過(guò)多,,則適當(dāng)降低該值

  • 如果初始化的高水位線(xiàn)設(shè)置過(guò)低,,上述高水位線(xiàn)調(diào)整情況會(huì)發(fā)生很多次,通過(guò)垃圾回收的日志可觀(guān)察到 Full GC 多次調(diào)用,。為了避免頻繁 GC,,建議將 -XX:MetaspaceSize 設(shè)置為一個(gè)相對(duì)較高的值。

5.3 方法區(qū)內(nèi)部結(jié)構(gòu)

方法區(qū)用于存儲(chǔ)已被虛擬機(jī)加載的類(lèi)型信息,、常量,、靜態(tài)變量、即時(shí)編譯器編譯后的代碼緩存等,。

類(lèi)型信息

對(duì)每個(gè)加載的類(lèi)型(類(lèi) class,、接口 interface、枚舉 enum,、注解 annotation),,JVM 必須在方法區(qū)中存儲(chǔ)以下類(lèi)型信息

  • 這個(gè)類(lèi)型的完整有效名稱(chēng)(全名=包名.類(lèi)名)

  • 這個(gè)類(lèi)型直接父類(lèi)的完整有效名(對(duì)于 interface或是 java.lang.Object,都沒(méi)有父類(lèi))

  • 這個(gè)類(lèi)型的修飾符(public,,abstract,,final 的某個(gè)子集)

  • 這個(gè)類(lèi)型直接接口的一個(gè)有序列表

域(Field)信息

  • JVM 必須在方法區(qū)中保存類(lèi)型的所有域的相關(guān)信息以及域的聲明順序

  • 域的相關(guān)信息包括:域名稱(chēng)、域類(lèi)型,、域修飾符(public,、private、protected,、static,、final、volatile,、transient 的某個(gè)子集)

方法(Method)信息

JVM 必須保存所有方法的

  • 方法名稱(chēng)

  • 方法的返回類(lèi)型

  • 方法參數(shù)的數(shù)量和類(lèi)型

  • 方法的修飾符(public,,private,,protected,,static,final,,synchronized,,native,abstract 的一個(gè)子集)

  • 方法的字符碼(bytecodes),、操作數(shù)棧,、局部變量表及大?。╝bstract 和 native 方法除外)

  • 異常表(abstract 和 native 方法除外)

    • 每個(gè)異常處理的開(kāi)始位置、結(jié)束位置,、代碼處理在程序計(jì)數(shù)器中的偏移地址,、被捕獲的異常類(lèi)的常量池索引

棧、堆,、方法區(qū)的交互關(guān)系

5.4 運(yùn)行時(shí)常量池

運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分,,理解運(yùn)行時(shí)常量池的話(huà),我們先來(lái)說(shuō)說(shuō)字節(jié)碼文件(Class 文件)中的常量池(常量池表)

常量池

一個(gè)有效的字節(jié)碼文件中除了包含類(lèi)的版本信息,、字段,、方法以及接口等描述信息外,還包含一項(xiàng)信息那就是常量池表(Constant Pool Table),,包含各種字面量和對(duì)類(lèi)型,、域和方法的符號(hào)引用。

為什么需要常量池,?

一個(gè) Java 源文件中的類(lèi),、接口,編譯后產(chǎn)生一個(gè)字節(jié)碼文件,。而 Java 中的字節(jié)碼需要數(shù)據(jù)支持,,通常這種數(shù)據(jù)會(huì)很大以至于不能直接存到字節(jié)碼里,換另一種方式,,可以存到常量池,,這個(gè)字節(jié)碼包含了指向常量池的引用。在動(dòng)態(tài)鏈接的時(shí)候用到的就是運(yùn)行時(shí)常量池,。

如下,,我們通過(guò) jclasslib 查看一個(gè)只有 Main 方法的簡(jiǎn)單類(lèi),字節(jié)碼中的 #2 指向的就是 Constant Pool

常量池可以看作是一張表,,虛擬機(jī)指令根據(jù)這張常量表找到要執(zhí)行的類(lèi)名,、方法名、參數(shù)類(lèi)型,、字面量等類(lèi)型,。

運(yùn)行時(shí)常量池

  • 在加載類(lèi)和結(jié)構(gòu)到虛擬機(jī)后,就會(huì)創(chuàng)建對(duì)應(yīng)的運(yùn)行時(shí)常量池

  • 常量池表(Constant Pool Table)是 Class 文件的一部分,,用于存儲(chǔ)編譯期生成的各種字面量和符號(hào)引用,,這部分內(nèi)容將在類(lèi)加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中

  • JVM 為每個(gè)已加載的類(lèi)型(類(lèi)或接口)都維護(hù)一個(gè)常量池。池中的數(shù)據(jù)項(xiàng)像數(shù)組項(xiàng)一樣,,是通過(guò)索引訪(fǎng)問(wèn)的

  • 運(yùn)行時(shí)常量池中包含各種不同的常量,,包括編譯器就已經(jīng)明確的數(shù)值字面量,也包括到運(yùn)行期解析后才能夠獲得的方法或字段引用,。此時(shí)不再是常量池中的符號(hào)地址了,,這里換為真實(shí)地址

    • 運(yùn)行時(shí)常量池,,相對(duì)于 Class 文件常量池的另一個(gè)重要特征是:動(dòng)態(tài)性,Java 語(yǔ)言并不要求常量一定只有編譯期間才能產(chǎn)生,,運(yùn)行期間也可以將新的常量放入池中,,String 類(lèi)的 intern() 方法就是這樣的

  • 當(dāng)創(chuàng)建類(lèi)或接口的運(yùn)行時(shí)常量池時(shí),如果構(gòu)造運(yùn)行時(shí)常量池所需的內(nèi)存空間超過(guò)了方法區(qū)所能提供的最大值,,則 JVM 會(huì)拋出 OutOfMemoryError 異常,。

5.5 方法區(qū)在 JDK6、7,、8中的演進(jìn)細(xì)節(jié)

只有 HotSpot 才有永久代的概念

jdk1.6及之前有永久代,,靜態(tài)變量存放在永久代上
jdk1.7有永久代,但已經(jīng)逐步“去永久代”,,字符串常量池,、靜態(tài)變量移除,保存在堆中
jdk1.8及之后取消永久代,,類(lèi)型信息,、字段、方法,、常量保存在本地內(nèi)存的元空間,,但字符串常量池、靜態(tài)變量仍在堆中

移除永久代原因

http://openjdk./jeps/122

  • 為永久代設(shè)置空間大小是很難確定的,。

    在某些場(chǎng)景下,,如果動(dòng)態(tài)加載類(lèi)過(guò)多,容易產(chǎn)生 Perm 區(qū)的 OOM,。如果某個(gè)實(shí)際 Web 工程中,,因?yàn)楣δ茳c(diǎn)比較多,在運(yùn)行過(guò)程中,,要不斷動(dòng)態(tài)加載很多類(lèi),,經(jīng)常出現(xiàn) OOM。而元空間和永久代最大的區(qū)別在于,,元空間不在虛擬機(jī)中,,而是使用本地內(nèi)存,所以默認(rèn)情況下,,元空間的大小僅受本地內(nèi)存限制

  • 對(duì)永久代進(jìn)行調(diào)優(yōu)較困難

5.6 方法區(qū)的垃圾回收

方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:常量池中廢棄的常量和不再使用的類(lèi)型,。

先來(lái)說(shuō)說(shuō)方法區(qū)內(nèi)常量池之中主要存放的兩大類(lèi)常量:字面量和符號(hào)引用。字面量比較接近 java 語(yǔ)言層次的常量概念,,如文本字符串,、被聲明為 final 的常量值等。而符號(hào)引用則屬于編譯原理方面的概念,,包括下面三類(lèi)常量:

  • 類(lèi)和接口的全限定名

  • 字段的名稱(chēng)和描述符

  • 方法的名稱(chēng)和描述符

HotSpot 虛擬機(jī)對(duì)常量池的回收策略是很明確的,,只要常量池中的常量沒(méi)有被任何地方引用,就可以被回收

判定一個(gè)類(lèi)型是否屬于“不再被使用的類(lèi)”,,需要同時(shí)滿(mǎn)足三個(gè)條件:

  • 該類(lèi)所有的實(shí)例都已經(jīng)被回收,,也就是 Java 堆中不存在該類(lèi)及其任何派生子類(lèi)的實(shí)例

  • 加載該類(lèi)的類(lèi)加載器已經(jīng)被回收,這個(gè)條件除非是經(jīng)過(guò)精心設(shè)計(jì)的可替換類(lèi)加載器的場(chǎng)景,,如 OSGi,、JSP 的重加載等,否則通常很難達(dá)成

  • 該類(lèi)對(duì)應(yīng)的 java.lang.Class 對(duì)象沒(méi)有在任何地方被引用,,無(wú)法在任何地方通過(guò)反射訪(fǎng)問(wèn)該類(lèi)的方法

Java 虛擬機(jī)被允許堆滿(mǎn)足上述三個(gè)條件的無(wú)用類(lèi)進(jìn)行回收,,這里說(shuō)的僅僅是“被允許”,而并不是和對(duì)象一樣,,不使用了就必然會(huì)回收,。是否對(duì)類(lèi)進(jìn)行回收,HotSpot 虛擬機(jī)提供了 -Xnoclassgc 參數(shù)進(jìn)行控制,,還可以使用 -verbose:class 以及 -XX: TraceClassLoading ,、-XX: TraceClassUnLoading 查看類(lèi)加載和卸載信息。

在大量使用反射,、動(dòng)態(tài)代理,、CGLib 等 ByteCode 框架、動(dòng)態(tài)生成 JSP 以及 OSGi 這類(lèi)頻繁自定義 ClassLoader 的場(chǎng)景都需要虛擬機(jī)具備類(lèi)卸載的功能,,以保證永久代不會(huì)溢出,。

參考與感謝

算是一篇學(xué)習(xí)筆記,共勉,,主要來(lái)源:

《深入理解 Java 虛擬機(jī) 第三版》

宋紅康老師的 JVM 教程

https://docs.oracle.com/javase/specs/index.html

https://www.cnblogs.com/wicfhwffg/p/9382677.html

https://www.cnblogs.com/hollischuang/p/12501950.html

來(lái)源:https://www./content-3-721751.html

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,,所有內(nèi)容均由用戶(hù)發(fā)布,不代表本站觀(guān)點(diǎn),。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式,、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙,。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶(hù) 評(píng)論公約

    類(lèi)似文章 更多