灰色的為單線程私有的,紅色的為多個線程共享的
每個線程:獨立包括程序計數(shù)器丶棧丶本地棧
線程間共享:堆丶堆外內(nèi)存(永久代或元空間[方法區(qū)]丶代碼緩存)
程序計數(shù)器(PC寄存器)
PC寄存器用來存儲指向下一條指令的地址舰褪,也即將要執(zhí)行的指令代碼皆疹。由執(zhí)行引擎讀取下一條指令。
一塊很小的內(nèi)存空間占拍,幾乎可以忽略不計略就。運行速度最快的存儲區(qū)域
它是程序控制流的指示器,分支丶循環(huán)丶跳轉(zhuǎn)丶異常處理丶線程恢復等基礎功能都需要依賴這個計數(shù)器來完成
字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令
它是唯一一個JVM規(guī)范中沒有規(guī)定任何OutOtMemoryError情況的區(qū)域晃酒。
使用PC寄存器存儲字節(jié)碼指令地址有什么用呢表牢?
因為CPU需要不停的切換各個線程,這時候切換回來以后贝次,就得知道接著從哪開始繼續(xù)執(zhí)行崔兴。JVM的字節(jié)碼解釋器就需要通過改變PC寄存器的值來明確下一條應該執(zhí)行什么樣的字節(jié)碼執(zhí)行。
虛擬機棧
優(yōu)點:跨平臺,指令集小敲茄,編譯器很容易實現(xiàn)位谋;缺點是性能下降,實現(xiàn)同樣的功能需要更多的指令
棧是運行時的單位而堆是存儲的單位
Java虛擬機棧堰燎,每個線程在創(chuàng)建時都會創(chuàng)建一個虛擬機棧掏父,其內(nèi)部保存一個個棧幀(一個棧幀對應一個Java方法),對應著一次次的Java方法調(diào)用秆剪。? 線程是私有的.? ? ? ? 生命周期和線程一致
作用:主管Java程序的運行赊淑,它保存方法的局部變量(8種基本數(shù)據(jù)類型丶對象的引用地址),部分結(jié)果仅讽,并參與方法的調(diào)用和返回陶缺。
JVM允許Java棧的大小是動態(tài)的或者固定不變的。
棧的優(yōu)點
棧是一種快速有效的分配存儲方式何什,訪問速度僅次于程序計數(shù)器
JVM直接對Java棧的操作只有兩個:
每個方法執(zhí)行组哩,伴隨著進棧(入棧丶壓棧);方法結(jié)束后的出棧工作
對于棧來說不存在垃圾回收問題
棧中存儲
每個線程都有自己的棧,棧中的數(shù)據(jù)都是以棧幀的格式存在处渣,在這個線程上正在執(zhí)行的每個方法都各自對應一個棧幀伶贰,棧幀是一個內(nèi)存區(qū)塊,是一個數(shù)據(jù)集罐栈,維系著方法執(zhí)行過程中的各種數(shù)據(jù)信息黍衙。
棧運行原理
jvm直接對Java棧的操作只有兩個,就是對棧幀的壓棧和出棧,遵循“先進后出”/“后進先出”原則荠诬。
在一個活動線程中琅翻,一個時間點上,只有有一個活動的棧幀柑贞。即只有當前正在執(zhí)行的方法的棧幀(棧頂棧幀)是有效的方椎,這個棧幀被稱為當前棧幀,與當前棧幀相對應的方法就是當前方法钧嘶,定義這個方法的類就是當前類
執(zhí)行引擎運行的所有字節(jié)碼指令只針對當前棧幀進行操作
如果在該方法中調(diào)用了其他方法棠众,對應的新的棧幀會被創(chuàng)建出來,放在棧的頂端有决,成為新的棧幀闸拿。
不同的線程中所包含的棧幀是不允許存在相互引用的,即不可能在一個棧幀之中引用另外一個線程的棧幀书幕。
如果當前方法調(diào)用了其他方法新荤,方法返回之際,當前棧幀會傳回此方法的執(zhí)行結(jié)果給前一個棧幀台汇,接著苛骨,虛擬機會丟棄當前棧幀篱瞎,是的前一個棧幀重新成為當前棧幀。
Java方法有兩種返回函數(shù)的方式智袭,一種是正常的函數(shù)返回奔缠,使用return指令;另外一種是拋出異常吼野。不管使用哪種方式校哎,都會導致棧幀被彈出
棧幀的內(nèi)部結(jié)構(gòu)
局部變量表
操作數(shù)棧(或表達式棧)
動態(tài)鏈接(或執(zhí)行運行時常量池的方法引用)
方法返回地址(或方法正常退出或者異常退出的定義)
一些附加信息
局部變量表
局部變量表也稱之為局部變量數(shù)組或本地變量表
定義為一個數(shù)字數(shù)組,主要用于存儲方法參數(shù)和定義在方法內(nèi)的局部變量瞳步,這些數(shù)據(jù)類型包括各類基本數(shù)據(jù)類型丶對象引用闷哆,以及returnAddress類型。
由于局部變量表時簡歷在線程的棧上单起,是線程的私有數(shù)據(jù)抱怔,因此不存在數(shù)據(jù)安全問題
局部變量表所需的容量大小是在編譯期確定下來的,并保存在方法的Code屬性的maximum local variables數(shù)據(jù)項中。在方法運行期間是不會改變局部變量表的大小的嘀倒。
方法嵌套調(diào)用的次數(shù)由棧的大小決定屈留。一般來說,棧越大测蘑,方法嵌套調(diào)用次數(shù)越多灌危。
局部變量表中的變量只在當前方法調(diào)用中有效。在方法執(zhí)行時碳胳,虛擬機通過使用局部變量表完成參數(shù)值到闡述變量列表的傳遞過程勇蝙。當方法調(diào)用結(jié)束后,隨著方法棧幀的銷毀挨约,局部變量表也會隨之銷毀味混。
關(guān)于Slot的理解
參數(shù)值的存放總是在局部變量數(shù)組的index0開始,到數(shù)組長度-1的索引結(jié)束诫惭。
局部變量表翁锡,最基本的存儲單元是Slot(變量槽)
局部變量表中存放編譯期可知的各種基本數(shù)據(jù)類型(8種),引用類型夕土,returnAddress類型的變量馆衔。
在局部變量表里,32位以內(nèi)的類型只占用一個slot隘弊,64位的類型(long和double)占用兩個slot哈踱。
棧幀中的局部變量表中的槽位是可以重用的荒适,如果一個局部變量過了其作用域梨熙,那么在其作用域之后申明的新的局部變量就很可能會重復過期局部變量的槽位,從而達到節(jié)省資源的目的刀诬。
在棧幀中咽扇,與性能調(diào)優(yōu)關(guān)系最為密切的部分就是前面提到的局部變量表邪财。在方法執(zhí)行時,虛擬機使用局部變量表完成方法的傳遞质欲。
局部變量表中的變量也是重要的垃圾回收根節(jié)點树埠,只要被局部變量表中直接或間接引用的對象都不會被回收。
操作數(shù)棧
操作數(shù)棧嘶伟,在方法的執(zhí)行過程中怎憋,根據(jù)字節(jié)碼指令,往棧中寫入數(shù)據(jù)或提取數(shù)據(jù)九昧,即入棧/出棧绊袋。
只要用于保存計算過程的中間結(jié)果,同時作為計算過程中變量臨時的存儲空間铸鹰。
操作數(shù)棧就是JVM執(zhí)行引擎的一個工作區(qū)癌别,當一個方法剛開始執(zhí)行的時候,一個新的棧幀也會隨之被創(chuàng)建出來蹋笼,這個方法的操作數(shù)棧是空的展姐。
操作數(shù)棧并非采用訪問索引的方式來進行數(shù)據(jù)訪問的,而是只能通過標準的入棧和出棧操作來完成一次數(shù)據(jù)訪問剖毯。
如果被調(diào)用的方法帶有返回值的話圾笨,其返回值將會被壓入當前棧幀的操作數(shù)棧中,并更新PC寄存器中下一條需要執(zhí)行的字節(jié)碼指令速兔。
棧頂緩存技術(shù)
將棧頂元素全部緩存在物理CPU的寄存器中墅拭,以此降低對內(nèi)存的讀/寫次數(shù),提升執(zhí)行引擎的執(zhí)行效率
動態(tài)鏈接
每一個棧幀內(nèi)部都包含一個指向運行時常量池中該幀所屬方法的引用涣狗。包含這個引用的目的就是為了支持當前方法的代碼能夠?qū)崿F(xiàn)動態(tài)鏈接
動態(tài)鏈接的作用就是為了將這些符號引用轉(zhuǎn)換為調(diào)用方法的直接引用
方法的調(diào)用
在JVM中谍婉,將符號引用轉(zhuǎn)換為調(diào)用方法的直接引用與方法的綁定機制相關(guān)。
靜態(tài)鏈接
當一個字節(jié)碼文件被裝載進JVM內(nèi)部時镀钓,如果被調(diào)用的目標方法在編譯期可知穗熬,且運行期保持不變時。這種情況下將調(diào)用方法的符號引用轉(zhuǎn)換為直接引用的過程稱之為靜態(tài)鏈接丁溅。
動態(tài)鏈接
如果被調(diào)用的方法在編譯期無法被確定下來唤蔗,也就是說,只能夠在程序運行期將調(diào)用方法的符號引用轉(zhuǎn)換為直接引用窟赏,由于這種引用轉(zhuǎn)換過程具備動態(tài)性妓柜,因此也就被稱之為動態(tài)鏈接。
Java語言中方法重寫的本質(zhì)
找到操作數(shù)棧頂?shù)牡谝粋€元素所執(zhí)行的對象的實際類型涯穷,記作C
如果在過程結(jié)束棍掐;如果不通過類型C中找到與常量中的描述符合簡單名稱都相符的方法,則進行訪問權(quán)限校驗拷况,如果通過則返回這個方法的直接引用片部,查找過,則返回java.lang.IllegalAccessError異常
否則按照繼承關(guān)系從下往上依次對C的各個父類進行第二部的搜索和驗證過程
如果始終沒有找到合適的方法寺晌,則拋出java.lang.AbstractMethodError異常
為了提高性能噪沙,JVM采用在類的方法區(qū)建立一個虛方法表(非虛方法不會出現(xiàn)在表中)來實現(xiàn)。使用索引表來代替查找。每個類中都有一個虛方法表,表中存放著各個方法的實際入口。
幀數(shù)據(jù)區(qū)
方法返回地址
存放調(diào)用該方法的PC寄存器(該方法要執(zhí)行的下一條指令的值)的值病瞳。
正常完成出口和異常完成出口的區(qū)別在于:通過異常完成出口退出的不會給他的上層調(diào)用者產(chǎn)生任何的返回值。
動態(tài)鏈接
一些附加信息
棧幀中還允許攜帶與Java虛擬機實現(xiàn)相關(guān)的一些附加信息悲酷。例如仍源,對程序調(diào)試提供支持的信息。
棧的相關(guān)面試題
舉例棧溢出的情況舔涎?(StackIverFlowError)
通過-Xss設置棧的大辛取;OOM
調(diào)整棧大小亡嫌,就能保證不出現(xiàn)溢出嗎嚎于?不能
分配的棧內(nèi)存越大越好嗎? 不是挟冠,因為會擠占其他的內(nèi)存空間于购。
垃圾回收是否會涉及到虛擬機棧?不會的知染!
方法中定義的局部變量是否線程安全肋僧?
本地方法棧
本地方法
一個native method就是一個Java調(diào)用非Java代碼的接口.一個本地方法是這樣一個Java方法,該方法由非Java語言實現(xiàn)控淡,比如C語言嫌吠。使用native關(guān)鍵字來修飾的方法,不能和abstract一起修飾方法掺炭,可以與其他標識符連用(public辫诅,private)
本地方法的使用原因
與Java環(huán)境交互-? Java應用需要與Java外面的環(huán)境交互,這時本地方法存在的主要原因
與操作系統(tǒng)的交互? -通過使用本地方法涧狮,我們得以用Java實現(xiàn)了jre的底層系統(tǒng)的交互炕矮,甚至JVM的一部分就是用C寫的。
Sun's Java-? Sun的解釋器是用C實現(xiàn)的者冤,這使得它可以向一些普通的C一樣與外部交互肤视。
Java虛擬機棧用于管理Java方法的調(diào)用,而本地方法棧就是用于管理本地方法的調(diào)用涉枫。
本地方法棧也是線程私有的邢滑,允許被實現(xiàn)成固定或者是可動態(tài)擴展的內(nèi)存大小。
當某個線程調(diào)用一個本地方法時拜银,它就進入了一個全新的并且不再受虛擬機限制的世界殊鞭。它和虛擬機擁有同樣的權(quán)限。
本地方法可以通過本地方法接口來訪問虛擬機內(nèi)部的運行時數(shù)據(jù)區(qū)
甚至可以直接使用本地處理器中的寄存器
直接從本地內(nèi)存的堆中分配任意數(shù)量的內(nèi)存
并不是所有的JVM都支持本地方法尼桶。因為Java虛擬機規(guī)范并沒有明確要求本地方法棧的使用語言操灿,具體實現(xiàn)方式,數(shù)據(jù)結(jié)構(gòu)等泵督。
Hotspot JVM中趾盐,直接將本地方法棧和虛擬機棧合二為一。
堆
堆的核心概述
一個JVM實例只存在一個堆內(nèi)存小腊,堆也是Java內(nèi)存管理的核心區(qū)域救鲤。
Java堆區(qū)在JVM啟動的時候即被創(chuàng)建,其空間大小也就確定了秩冈。是JVM管理的最大一塊內(nèi)存空間本缠。堆內(nèi)存的大小是可以調(diào)節(jié)的。
《Java虛擬機規(guī)范》規(guī)定入问,堆可以處于物理上不連續(xù)的內(nèi)存空間中丹锹,但在邏輯上它應該被視為連續(xù)的。
所有的線程共享Java堆芬失,在這里還可以劃分線程私有的緩沖區(qū)(TLAB)楣黍。
(幾乎)所有的對象實例以及數(shù)組都應該在運行時分配在堆上。
數(shù)組和對象可能永遠都不會存儲在棧上棱烂,因為棧幀中保存引用租漂,這個引用指向?qū)ο蠡蛘邤?shù)組在堆中的位置。
在方法結(jié)束后颊糜,堆中的對象不會馬上被移除哩治,僅僅在垃圾收集的時候才會被移除
堆,是GC執(zhí)行垃圾回收的重點區(qū)域
內(nèi)存細分
JDK7及以前堆內(nèi)存邏輯上分為三部分:新生代+老年代+永久代
JDK8及之后堆內(nèi)存邏輯上分為三部分:新生代+老年代+元空間
-Xms 用來設置堆空間(新生代+老年代)的初始內(nèi)存大小
-Xmx 用來設置堆空間(新生代+老年代)的最大內(nèi)存大小
-X 是JVM的運行參數(shù)? ? ? ? ? ? ? ? ms是memory start
開發(fā)中建議將初始堆內(nèi)存和最大堆內(nèi)存設置相同大小衬鱼,避免頻繁的擴容和釋放造成不必要的系統(tǒng)壓力锚扎。
新生代與老年代
存儲在JVM中的Java對象可以被劃分為兩類:
一類是生命周期較短的瞬時對象,這類對象的創(chuàng)建和消亡都非常迅速
另一類對象的生命周期很長馁启,在某些極端的情況下還能夠與JVM的生命周期保持一致驾孔。
Java堆區(qū)進一步可以劃分為新生代與老年代
其中新生代又可以劃分為Eden空間,from區(qū)和to區(qū)惯疙,默認8:1:1 -XX:SurvivorRatio來修改默認設置
默認新生代與老年代的結(jié)構(gòu)占比為1:2翠勉,可以修改-XX:Ratio=4 即1:4
幾乎所有的Java對象都是在Eden區(qū)被new出來的。(如果Eden放不下就直接進入老年代了)
幾乎絕大部分的Java對象的銷毀都在新生代進行霉颠。
對象分配過程
概述:為新對象分配內(nèi)存是意見非常嚴謹和復雜的任務对碌,JVM的設計者們不僅需要考慮內(nèi)存如何分配,在哪里分配等問題蒿偎,并且由于內(nèi)存分配算法與內(nèi)存回收算法密切相關(guān)朽们,所以還需要考慮GC執(zhí)行完內(nèi)存回收后是否會在內(nèi)存空間中產(chǎn)生內(nèi)存碎片怀读。
new的對象先放Eden區(qū),此區(qū)有大小限制
當Eden的空間填滿時骑脱,程序又需要創(chuàng)建對象菜枷,JVM的垃圾回收期將對Eden區(qū)進行垃圾回收,將Eden中的不再被其他對象所引用的對象進行銷毀叁丧。再加載新的對象放到Eden區(qū)
讓后在將Eden中剩余的對象移動到from區(qū)
如果再次觸發(fā)GC啤誊,此時上次幸存下來的放到from區(qū),如果沒有回收拥娄,就會放到to區(qū)
如果再次經(jīng)歷垃圾回收蚊锹,此時會重新放回到from區(qū),再去to區(qū)
如果一個對象生命周期很長稚瘾,超過15次牡昆,就會進入到老年代√罚可以設置次數(shù)迁杨。
Minor GC,Major GC與Full GC
JVM在進行GC時凄硼,并非每次都對上面三個內(nèi)存(新生代铅协,老年代,方法區(qū))區(qū)域一起回收摊沉,大部分時候回收都指新生代狐史。
針對HotSpot VM的實現(xiàn),它里面的GC按照回收區(qū)域又分為兩大種類型:一部分是部分收集(Partial GC),一種是整堆收集(Full GC)
部分收集:不是完整收集整個Java堆的垃圾收集说墨。其中又分為:
新生代收集(Minor GC /Young GC):只是新生代的垃圾收集
老年代收集(Major GC/Old GC):只是老年代的垃圾收集
目前骏全,只有CMS GC會有單獨收集老年代的行為
很多時候Major GC會和Full GC混淆使用,需要具體分辨是老年代回收還是整堆回收
混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集?
目前只有G1 GC會有這種行為
整堆收集(Full GC):收集整個Java堆和方法區(qū)的垃圾收集尼斧。
新生代GC(Minor GC)觸發(fā)機制:
當新生代空間不足時姜贡,就會觸發(fā)Minor GC,這里的年輕代滿指的是Eden滿,Survivor滿不會引發(fā)GC棺棵。
因為Java對象大多都具備朝生夕滅的特性楼咳,所以Minor GC非常頻繁,一般回收速度也比較快烛恤。這一定義既清晰又易于理解母怜。
Minor GC會引發(fā)STW(Stop-The-World,在執(zhí)行垃圾回收算法時缚柏,Java應用程序的其他所有線程全部被掛機除了GC苹熏,所有Java代碼停止,native代碼可以執(zhí)行,但不能與JVM交互)轨域,暫停其他用戶的線程袱耽,等垃圾回收結(jié)束,用戶線程才恢復運行干发。
老年代GC(Major GC/Full GC)觸發(fā)機制:
指發(fā)生在老年代的GC朱巨,對象從老年代消失時,Major GC或Full GC發(fā)生了
出現(xiàn)了Major GC,經(jīng)常會伴隨至少一次的Minjor GC
Major GC的速度一般會比Minjor GC慢10倍以上铐然,STW的時間更長。
如果Major GC 后恶座,內(nèi)存還不足搀暑,就報OOM了。
Major GC的速度一般會比Minor GC慢10倍以上跨琳。
Full GC觸發(fā)機制:
調(diào)用System.gc()時自点,系統(tǒng)建議執(zhí)行Full GC,但是不必然 執(zhí)行
老年代空間不足
方法區(qū)空間不足
通過Minor GC后進入老年代的平均大小大于老年代的可用內(nèi)存
由Eden區(qū),from區(qū)向to區(qū)復制時脉让,對象大小大于to區(qū)可用內(nèi)存桂敛,則把該對象轉(zhuǎn)存到老年代,且老年代的可用內(nèi)存小于該對象大小溅潜。
full GC是開發(fā)或調(diào)優(yōu)中盡量避免的术唬。這樣STW時間會短一些
堆空間分代思想
其實不分代完全可以,分代的唯一理由就是優(yōu)化GC性能滚澜。如果沒有分代粗仓,那所有的對象都在一塊。GC的時候要找到哪些對象沒用设捐,就會對堆的所有區(qū)域進行掃描借浊。而很多對象都是朝生夕死,如果分代的話萝招,把新創(chuàng)建的對象放到某一地方蚂斤,當GC的時候先把這塊存儲朝生夕死對象的區(qū)域進行回收,就會騰出很大的空間出來槐沼。
內(nèi)存分配策略
優(yōu)先分配到Eden
大對象直接分配到老年代(開發(fā)中應盡量避免程序中出現(xiàn)過多的大對象)
長期存活的對象分配到老年代
動態(tài)對象年齡判斷
如果Survicor區(qū)中相同年齡的所有對象大小的總和大于Survicor空間的一半曙蒸,年齡大于或等于該年齡的對象可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡
空間分配擔保
-XX:HandlePromotionFailure
對象分配過程:TLAB
- 堆區(qū)是線程共享區(qū)域岗钩,任何線程都可以訪問到堆區(qū)中的共享數(shù)據(jù)
- 由于對象實例的創(chuàng)建在JVM中非常頻繁逸爵,因此在并發(fā)環(huán)境下從堆區(qū)中劃分內(nèi)存空間是線程不安全的
- 為避免多個線程操作同一地址,需要使用加鎖等機制凹嘲,進而影響分配速度师倔。
從內(nèi)存模型而不是垃圾收集的角度,對Eden區(qū)域繼續(xù)進行劃分,JVM為每個線程分配了一個私有緩沖區(qū)域趋艘,它包含在Eden空間內(nèi)疲恢。
多線程同時分配內(nèi)存時,使用TLAB可以避免一系列的非線程安全問題瓷胧,同時還能夠提升內(nèi)存分配的吞吐量显拳,因此我們可以將這種內(nèi)存分配方式稱之為快速分配策略
幾乎所有OpenJDK衍生出來的JVM都提供了TLAB的設計
測試堆空間常用的JVM參數(shù):
-XX:+PrintFlagsInitial :查看所有的參數(shù)的默認初始值
-XX:+PrintFlagsFinal :查看所有的參數(shù)的最終值(可能會存在修改,不再是初始值)
-Xms:初始堆空間內(nèi)存(默認為物理內(nèi)存的1/64)
-Xmx:最大堆空間內(nèi)存(默認為物理內(nèi)存的1/4)
-Xmn:設置新生代的大小(初始值及最大值)
-XX:NewRatio ;配置新生代與老年代在堆結(jié)構(gòu)的占比
-XX:SurvivorRatio:設置新生代中Eden和s0/s1空間的比例
-XX:MaxTenuringThreshold:設置新生代垃圾的最大年齡
-XX:+PrintGCDetails:輸出詳細的GC處理日志
打印GC簡要信息:① -XX:PrintGC? ②-verbose:gc
-XX:HandlePromotionFailure:是否設置空間分配擔保