一、jvm概述
jvm又叫java虛擬機(jī)(The Java Virtual Machine),主要有三種功能:運(yùn)行編譯的class文件廊镜,內(nèi)存分配和垃圾回收即碗,提供多線程的同步。參考:Java Virtual Machine Technology Overview
二搞疗、jvm內(nèi)存模型
jvm內(nèi)存模型是指java虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)里面包含了哪些東西嗓蘑。
三、JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)
Java 虛擬機(jī)定義了若干種程序運(yùn)行期間會(huì)使用到的運(yùn)行時(shí)數(shù)據(jù)區(qū)匿乃,其中有一些會(huì)隨著虛擬機(jī)啟動(dòng)而創(chuàng)建桩皿,隨著虛擬機(jī)退出而銷毀。另外一些則是與線程一一對(duì)應(yīng)的幢炸,這些與線程一一對(duì)應(yīng)的數(shù)據(jù)區(qū)域會(huì)隨著線程開始和結(jié)束而創(chuàng)建和銷毀泄隔。
線程私有:程序計(jì)數(shù)器、棧宛徊、本地棧
線程共享:堆佛嬉、堆外內(nèi)存(永久代或元空間、代碼緩存)
方法區(qū):方法區(qū)(method area)只是JVM規(guī)范中定義的一個(gè)概念闸天,用于存儲(chǔ)類信息暖呕、常量池、靜態(tài)變量苞氮、JIT編譯后的代碼等數(shù)據(jù)湾揽,并沒有規(guī)定如何去實(shí)現(xiàn)它,不同的廠商有不同的實(shí)現(xiàn)。而永久代(PermGen)是?Hotspot?虛擬機(jī)特有的概念库物, Java8 的時(shí)候又被元空間取代了霸旗,永久代和元空間都可以理解為方法區(qū)的落地實(shí)現(xiàn)。
堆內(nèi)存:Java 堆是 Java 虛擬機(jī)管理的內(nèi)存中最大的一塊戚揭,被所有線程共享定硝。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例以及數(shù)據(jù)都在這里分配內(nèi)存毫目。
棧內(nèi)存:主管 Java 程序的運(yùn)行蔬啡,它保存方法的局部變量、部分結(jié)果镀虐,并參與方法的調(diào)用和返回箱蟆。
本地方法棧:一個(gè) Native Method 就是一個(gè) Java 調(diào)用非 Java 代碼的接口。我們知道的 Unsafe 類就有很多本地方法刮便。Java 虛擬機(jī)棧用于管理 Java 方法的調(diào)用空猜,而本地方法棧用于管理本地方法的調(diào)用。
程序計(jì)數(shù)器:程序計(jì)數(shù)器是一塊較小的內(nèi)存空間恨旱,可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器辈毯。由字節(jié)碼執(zhí)行引擎直接修改。
四搜贤、程序計(jì)數(shù)器
PC 寄存器用來存儲(chǔ)指向下一條指令的地址谆沃,即將要執(zhí)行的指令代碼。由執(zhí)行引擎讀取下一條指令仪芒。
程序計(jì)數(shù)器是一塊較小的內(nèi)存空間唁影,可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。
使用PC寄存器存儲(chǔ)字節(jié)碼指令地址有什么用呢掂名?為什么使用PC寄存器記錄當(dāng)前線程的執(zhí)行地址呢据沈?
???♂?:因?yàn)镃PU需要不停的切換各個(gè)線程,這時(shí)候切換回來以后饺蔑,就得知道接著從哪開始繼續(xù)執(zhí)行锌介。JVM的字節(jié)碼解釋器就需要通過改變PC寄存器的值來明確下一條應(yīng)該執(zhí)行什么樣的字節(jié)碼指令。
PC寄存器為什么會(huì)被設(shè)定為線程私有的猾警?
???♂?:多線程在一個(gè)特定的時(shí)間段內(nèi)只會(huì)執(zhí)行其中某一個(gè)線程方法孔祸,CPU會(huì)不停的做任務(wù)切換,這樣必然會(huì)導(dǎo)致經(jīng)常中斷或恢復(fù)肿嘲。為了能夠準(zhǔn)確的記錄各個(gè)線程正在執(zhí)行的當(dāng)前字節(jié)碼指令地址融击,所以為每個(gè)線程都分配了一個(gè)PC寄存器,每個(gè)線程都獨(dú)立計(jì)算雳窟,不會(huì)互相影響。
五、虛擬機(jī)棧
Java 虛擬機(jī)棧(Java Virtual Machine Stacks)封救,早期也叫 Java 棧拇涤。每個(gè)線程在創(chuàng)建的時(shí)候都會(huì)創(chuàng)建一個(gè)虛擬機(jī)棧,其內(nèi)部保存一個(gè)個(gè)的棧幀(Stack Frame)誉结,對(duì)應(yīng)著一次次 Java 方法調(diào)用鹅士,是線程私有的,生命周期和線程一致惩坑。
作用:主管 Java 程序的運(yùn)行掉盅,它保存方法的局部變量、部分結(jié)果以舒,并參與方法的調(diào)用和返回趾痘。
Java 虛擬機(jī)規(guī)范允許?Java虛擬機(jī)棧的大小是動(dòng)態(tài)的或者是固定不變的
如果采用固定大小的 Java 虛擬機(jī)棧,那每個(gè)線程的 Java 虛擬機(jī)棧容量可以在線程創(chuàng)建的時(shí)候獨(dú)立選定蔓钟。如果線程請(qǐng)求分配的棧容量超過 Java 虛擬機(jī)棧允許的最大容量永票,Java 虛擬機(jī)將會(huì)拋出一個(gè)?StackOverflowError?異常
如果 Java 虛擬機(jī)棧可以動(dòng)態(tài)擴(kuò)展滥沫,并且在嘗試擴(kuò)展的時(shí)候無法申請(qǐng)到足夠的內(nèi)存侣集,或者在創(chuàng)建新的線程時(shí)沒有足夠的內(nèi)存去創(chuàng)建對(duì)應(yīng)的虛擬機(jī)棧,那 Java 虛擬機(jī)將會(huì)拋出一個(gè)OutOfMemoryError異常
可以通過參數(shù)-Xss來設(shè)置線程的最大椑夹澹空間世分,棧的大小直接決定了函數(shù)調(diào)用的最大可達(dá)深度∽罕纾可參考JVMMemorySetting.
每個(gè)**棧幀(Stack Frame)**中存儲(chǔ)著:
局部變量表(Local Variables):是一組變量值存儲(chǔ)空間罚攀,主要用于存儲(chǔ)方法參數(shù)和定義在方法體內(nèi)的局部變量,包括編譯器可知的各種 Java 虛擬機(jī)基本數(shù)據(jù)類型(boolean雌澄、byte斋泄、char、short镐牺、int炫掐、float、long睬涧、double)募胃、對(duì)象引用(reference類型,它并不等同于對(duì)象本身畦浓,可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔槺允部赡苁侵赶蛞粋€(gè)代表對(duì)象的句柄或其他與此相關(guān)的位置)和returnAddress?類型(指向了一條字節(jié)碼指令的地址,已被異常表取代)讶请,局部變量表中的變量也是重要的垃圾回收根節(jié)點(diǎn)祷嘶,只要被局部變量表中直接或間接引用的對(duì)象都不會(huì)被回收.
操作數(shù)棧(Operand Stack)(或稱為表達(dá)式棧):操作數(shù)棧,在方法執(zhí)行過程中,根據(jù)字節(jié)碼指令论巍,往操作數(shù)棧中寫入數(shù)據(jù)或提取數(shù)據(jù)烛谊,即入棧(push)、出棧(pop)嘉汰,操作數(shù)棧丹禀,主要用于保存計(jì)算過程的中間結(jié)果,同時(shí)作為計(jì)算過程中變量臨時(shí)的存儲(chǔ)空間鞋怀。
動(dòng)態(tài)鏈接(Dynamic Linking):指向運(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í)辛友,就是通過常量池中指向方法的符號(hào)引用來表示的薄扁,那么動(dòng)態(tài)鏈接的作用就是為了將這些符號(hào)引用轉(zhuǎn)換為調(diào)用方法的直接引用。
方法返回地址(Return Address):方法正常退出或異常退出的地址
一些附加信息.
六废累、本地方法棧(Native Method Stack)
Java 虛擬機(jī)棧用于管理 Java 方法的調(diào)用邓梅,而本地方法棧用于管理本地方法的調(diào)用
本地方法棧也是線程私有的
允許線程固定或者可動(dòng)態(tài)擴(kuò)展的內(nèi)存大小,本地方法可以通過本地方法接口來訪問虛擬機(jī)內(nèi)部的運(yùn)行時(shí)數(shù)據(jù)區(qū)邑滨,它甚至可以直接使用本地處理器中的寄存器日缨,直接從本地內(nèi)存的堆中分配任意數(shù)量的內(nèi)存。
棧是運(yùn)行時(shí)的單位掖看,而堆是存儲(chǔ)的單位匣距。棧解決程序的運(yùn)行問題,即程序如何執(zhí)行哎壳,或者說如何處理數(shù)據(jù)毅待。堆解決的是數(shù)據(jù)存儲(chǔ)的問題,即數(shù)據(jù)怎么放归榕、放在哪尸红。
七、堆內(nèi)存
對(duì)于大多數(shù)應(yīng)用刹泄,Java 堆是 Java 虛擬機(jī)管理的內(nèi)存中最大的一塊外里,被所有線程共享。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例特石,幾乎所有的對(duì)象實(shí)例以及數(shù)據(jù)都在這里分配內(nèi)存盅蝗。
為了進(jìn)行高效的垃圾回收,虛擬機(jī)把堆內(nèi)存邏輯上劃分成三塊區(qū)域(分代的唯一理由就是優(yōu)化 GC 性能):
新生帶(年輕代):新對(duì)象和沒達(dá)到一定年齡的對(duì)象都在新生代
老年代(養(yǎng)老區(qū)):被長時(shí)間使用的對(duì)象姆蘸,老年代的內(nèi)存空間應(yīng)該要比年輕代更大
元空間(JDK1.8之前叫永久代):像一些方法中的操作臨時(shí)對(duì)象等墩莫,JDK1.8之前是占用JVM內(nèi)存芙委,JDK1.8之后直接使用物理內(nèi)存。
7.1贼穆、 對(duì)象的分配過程
為對(duì)象分配內(nèi)存是一件非常嚴(yán)謹(jǐn)和復(fù)雜的任務(wù)题山,JVM 的設(shè)計(jì)者們不僅需要考慮內(nèi)存如何分配兰粉、在哪里分配等問題故痊,并且由于內(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)伊甸園的空間填滿時(shí)戴甩,程序又需要?jiǎng)?chuàng)建對(duì)象,JVM 的垃圾回收器將對(duì)伊甸園區(qū)進(jìn)行垃圾回收(Minor GC)闪彼,將伊甸園區(qū)中的不再被其他對(duì)象所引用的對(duì)象進(jìn)行銷毀甜孤。再加載新的對(duì)象放到伊甸園區(qū)
3、然后將伊甸園中的剩余對(duì)象移動(dòng)到幸存者 0 區(qū)
4畏腕、如果再次觸發(fā)垃圾回收缴川,此時(shí)上次幸存下來的放到幸存者 0 區(qū),如果沒有回收描馅,就會(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)依然無法進(jìn)行對(duì)象的保存知市,就會(huì)產(chǎn)生 OOM 異常
7.2傻盟、 GC 垃圾回收簡介
可以使用jvisualvm來查看當(dāng)前線程分配內(nèi)存的情況。
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ū)域又分為兩大類:部分收集(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ū)的垃圾, stop world.
垃圾回收機(jī)制比較復(fù)雜、堆內(nèi)存分配推薦看視頻JAVA虛擬機(jī)和垃圾回收機(jī)制大講解?和?垃圾回收機(jī)制候址。
full gc?stop world的目的是防止用戶線程結(jié)束吕粹,gc的內(nèi)存無效導(dǎo)致繁瑣的重復(fù)gc.
gc的一個(gè)例子:當(dāng)對(duì)象大小超過servivor的50%,直接進(jìn)入老年代。
八岗仑、方法區(qū)
運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分匹耕,理解運(yùn)行時(shí)常量池的話,我們先來說說字節(jié)碼文件(Class 文件)中的常量池(常量池表)
常量池:一個(gè)有效的字節(jié)碼文件中除了包含類的版本信息荠雕、字段稳其、方法以及接口等描述信息外,還包含一項(xiàng)信息那就是常量池表(Constant Pool Table)炸卑,包含各種字面量和對(duì)類型既鞠、域和方法的符號(hào)引用。
為什么需要常量池矾兜?
答:一個(gè) java 源文件中的類损趋、接口,編譯后產(chǎn)生一個(gè)字節(jié)碼文件椅寺。而 Java 中的字節(jié)碼需要數(shù)據(jù)支持浑槽,通常這種數(shù)據(jù)會(huì)很大以至于不能直接存到字節(jié)碼里,換另一種方式返帕,可以存到常量池桐玻,這個(gè)字節(jié)碼包含了指向常量池的引用。在動(dòng)態(tài)鏈接的時(shí)候用到的就是運(yùn)行時(shí)常量池荆萤。
運(yùn)行時(shí)常量池
在加載類和結(jié)構(gòu)到虛擬機(jī)后镊靴,就會(huì)創(chuàng)建對(duì)應(yīng)的運(yùn)行時(shí)常量池
常量池表(Constant Pool Table)是 Class 文件的一部分,用于存儲(chǔ)編譯期生成的各種字面量和符號(hào)引用链韭,這部分內(nèi)容將在類加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中
JVM 為每個(gè)已加載的類型(類或接口)都維護(hù)一個(gè)常量池偏竟。池中的數(shù)據(jù)項(xiàng)像數(shù)組項(xiàng)一樣,是通過索引訪問的
運(yùn)行時(shí)常量池中包含各種不同的常量敞峭,包括編譯器就已經(jīng)明確的數(shù)值字面量踊谋,也包括到運(yùn)行期解析后才能夠獲得的方法或字段引用。此時(shí)不再是常量池中的符號(hào)地址了旋讹,這里換為真實(shí)地址
運(yùn)行時(shí)常量池殖蚕,相對(duì)于 Class 文件常量池的另一個(gè)重要特征是:動(dòng)態(tài)性轿衔,Java 語言并不要求常量一定只有編譯期間才能產(chǎn)生,運(yùn)行期間也可以將新的常量放入池中睦疫,String 類的?intern()?方法就是這樣的
當(dāng)創(chuàng)建類或接口的運(yùn)行時(shí)常量池時(shí)害驹,如果構(gòu)造運(yùn)行時(shí)常量池所需的內(nèi)存空間超過了方法區(qū)所能提供的最大值,則 JVM 會(huì)拋出 OutOfMemoryError 異常
參考文檔:
https://docs.oracle.com/en/java/javase/14/
https://docs.oracle.com/javase/specs/jvms/se14/html/jvms-2.html