? Java虛擬機(jī)在執(zhí)行Java程序的過(guò)程中會(huì)把它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域罢缸,總共包括以下幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)域。
1 、程序計(jì)數(shù)器(Program Counter Register)
程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,它的作用:
1.1. 可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的信號(hào)指示器。字節(jié)碼解釋器就是通過(guò)改變?cè)撚?jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼指令, 分支唁情、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需依賴計(jì)數(shù)器來(lái)完成。注:但是贾漏,如果當(dāng)前線程正在執(zhí)行的是一個(gè)本地方法隐圾,那么此時(shí)程序計(jì)數(shù)器為空伍掀。
1.2. 在多線程的情況下,程序計(jì)數(shù)器用于記錄當(dāng)前線程執(zhí)行的位置暇藏,從而當(dāng)線程被切換回來(lái)的時(shí)候能夠知道該線程上次運(yùn)行到哪兒了。
特點(diǎn):線程私有的盐碱, 生命周期隨著線程的創(chuàng)建而創(chuàng)建把兔,隨著線程的結(jié)束而死亡。 此內(nèi)存區(qū)域是唯一一個(gè)在 Java 虛擬機(jī)規(guī)范中沒有規(guī)定任何 OutOfMemoryError 情況的區(qū)域瓮顽。
2缕贡、Java 虛擬機(jī)棧(Java Virtual Machine Stack)
Java 虛擬機(jī)棧與程序計(jì)數(shù)器一樣剂跟,也是線程私有的送淆,其生命周期與線程相同为居。虛擬機(jī) 棧描述的是 Java 方法執(zhí)行的內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候都會(huì)同時(shí)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表蒙畴、操作數(shù)棧、動(dòng)態(tài)鏈接呜象、方法出口等信息膳凝。每一個(gè)方法被調(diào)用 直至執(zhí)行完成的過(guò)程就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過(guò)程。
局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean董朝、byte鸠项、char、short子姜、int祟绊、 float、long哥捕、double)牧抽、對(duì)象引用(reference 類型)和 returnAddress 類型(指向了一條字 節(jié)碼指令的地址)。其中 64 位長(zhǎng)度的 long 和 double 會(huì)占用 2 個(gè)局部變量空間(Slot遥赚,一個(gè) 32 位)扬舒,其余數(shù)據(jù)類型只占用 1 個(gè)。局部變量表所需的空間在編譯期間完成分配凫佛,當(dāng)進(jìn)入一 個(gè)方法時(shí)讲坎,其需要在幀中分配多大的局部變量空間是確定的孕惜,方法運(yùn)行期間不會(huì)改變局部變 量表的大小。局部變量表的創(chuàng)建是在方法被執(zhí)行的時(shí)候晨炕,隨著棧幀的創(chuàng)建而創(chuàng)建衫画。而且,局部變量表的大小在編譯時(shí)期就確定下來(lái)了瓮栗,在創(chuàng)建的時(shí)候只需分配事先規(guī)定好的大小即可削罩。此外,在方法運(yùn)行的過(guò)程中局部變量表的大小是不會(huì)發(fā)生改變的费奸。
Java 虛擬機(jī)規(guī)范中對(duì)該區(qū)域規(guī)定了兩種異常情況:
2.1.? 如 線 程 請(qǐng) 求 的 深 度 大 于 虛 擬 機(jī) 所 允 許 的 深 度 弥激, 棧 溢 出 , 如 遞 歸 時(shí) 愿阐, 拋 出
StackOverflowError 異常微服。
2.2.? 虛擬機(jī)棧動(dòng)態(tài)擴(kuò)展無(wú)法申請(qǐng)到足夠的內(nèi)存時(shí),拋出 OutOfMemoryError 異常缨历。
當(dāng)方法傳遞參數(shù)時(shí)實(shí)際上是一個(gè)方法將自己棧幀中局部變量表的副本傳遞給另一個(gè)方法棧幀中的局 部變量表(注意是副本职辨,而不是其本身),不管數(shù)據(jù)類型是什么(基本類型戈二,引用類型)
3、本地方法棧(Native Method Stack)
Java 虛擬機(jī)可能會(huì)使用到傳統(tǒng)的棧來(lái)支持 native 方法(使用 Java 語(yǔ)言以外的其它語(yǔ)言 編寫的方法)的執(zhí)行喳资。線程私有的觉吭,如 Sun HotSpot 虛擬機(jī)直接把本地方法棧和虛擬機(jī)棧合 二為一。Java 虛擬機(jī)規(guī)范中對(duì)該區(qū)域規(guī)定了兩種異常情況:
3.1.如線程請(qǐng)求的深度大于虛擬機(jī)所允許的深度仆邓,拋出 StackOverflowError 異常鲜滩。
3.2.虛擬機(jī)棧動(dòng)態(tài)擴(kuò)展無(wú)法申請(qǐng)到足夠的內(nèi)存時(shí),拋出 OutOfMemoryError 異常节值。
4徙硅、Java 堆(Java Heap)
Java 堆是 Java 虛擬機(jī)管理內(nèi)存中最大的一塊,是所有線程共享的內(nèi)存區(qū)域搞疗,隨虛擬機(jī)的啟動(dòng)而創(chuàng)建嗓蘑。該區(qū)域唯一目的是存放對(duì)象實(shí)例,幾乎所有對(duì)象的實(shí)例都在堆里面分配匿乃。 Java 虛擬機(jī)規(guī)范規(guī)定桩皿,Java 堆可以出于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上連續(xù)即可幢炸,如同磁盤空間一樣泄隔,既可以實(shí)現(xiàn)成固定大小,也可以是擴(kuò)展的宛徊,當(dāng)前主流虛擬機(jī)都是 按照擴(kuò)展來(lái)實(shí)現(xiàn)的(通過(guò)-Xmx 和-Xms 控制)佛嬉。
Java堆是垃圾收集器管理的主要區(qū)域逻澳,因此也叫"GC堆",細(xì)分一點(diǎn)可以分為新生代和老年代暖呕;再細(xì)致一點(diǎn)新生代可以分為Eden空間斜做、From Survivor空間、ToSurvivor空間缰揪。
Java 虛擬機(jī)規(guī)范中對(duì)該區(qū)域規(guī)定了 OutOfMemoryError 異常:如果堆中沒有 內(nèi)存完成實(shí)例分配陨享,并且堆無(wú)法再擴(kuò)展則拋出 OutOfMemoryError 異常。(當(dāng) Ol d 區(qū)被放滿的之后钝腺,進(jìn)行 Full GC抛姑,F(xiàn)ull GC 后,若 Survivor 及 old 區(qū)仍然無(wú)法存放 從 Eden 復(fù)制過(guò)來(lái)的部分對(duì)象艳狐,則出現(xiàn) OOM 錯(cuò)誤/或者直接存放大對(duì)象定硝、大數(shù)組,導(dǎo)致老年代空間不足)
5毫目、方法區(qū)(Method Area)
方法區(qū)與 Java 堆一樣蔬啡,是各個(gè)線程共享的內(nèi)存區(qū)域,用于存儲(chǔ)已被虛擬機(jī)加載的類信 息镀虐、常量箱蟆、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)刮便。在 HotSpot 中用永久代來(lái)實(shí)現(xiàn)方法區(qū)空猜,而其他虛擬機(jī)(如 BEA JRockit、IBM J9 等)是不存在永久代的恨旱。
Java7 中已經(jīng)將運(yùn)行時(shí)常量池從永久代移除辈毯,在 Java 堆(Heap)中開辟了一塊區(qū)域存放運(yùn)行時(shí)常量池。而在 Java8 中搜贤,已經(jīng)徹底沒有了永久代谆沃,將方法區(qū)直接放在一個(gè)與堆不相連的本地內(nèi)存區(qū)域,這個(gè)區(qū)域被叫做元空間仪芒。
元空間的本質(zhì)和永久代類似唁影,都是對(duì) JVM 規(guī)范中方法區(qū)的實(shí)現(xiàn)。不過(guò)元空間與永久代 之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中掂名,而是使用本地內(nèi)存夭咬。因此,默認(rèn)情況下铆隘,元 空間的大小僅受本地內(nèi)存限制卓舵,但可以通過(guò)以下參數(shù)來(lái)指定元空間的大小:-XX:MetaspaceSize膀钠,初始空間大小掏湾。-XX:MaxMetaspaceSize裹虫,最大空間,默認(rèn)是沒有 限制的融击。Java 虛擬機(jī)規(guī)范中對(duì)方法區(qū)規(guī)定了 OutOfMemoryError 異常: 如果方法區(qū)的內(nèi)存空間 不能滿足內(nèi)存分配請(qǐng)求筑公,那 Java 虛擬機(jī)將拋出一個(gè) OutOfMemoryError 異常。
6尊浪、運(yùn)行時(shí)常量池(Runtime Constant Pool)
運(yùn)行時(shí)常量池是方法區(qū)的一部分匣屡。線程共享。Class 文件中除了有類的版本拇涤、字段捣作、方 法、接口等信息外鹅士,還有一項(xiàng)信息是常量池券躁,用于存放編譯期生成的各種字面常量和符號(hào)引 用,這部分內(nèi)容在類加載后存放到方法區(qū)的常量池中掉盅。
static 修飾的靜態(tài)變量也存放在方法區(qū)中也拜,但不是在常量池中(不能修飾局部變量),不 能在一個(gè)方法內(nèi)部定義 static 變量(final 可以)趾痘,只能定義為成員變量慢哈。
當(dāng)這個(gè)類被Java虛擬機(jī)加載后,class文件中的常量就存放在方法區(qū)的運(yùn)行時(shí)常量池中永票。而且在運(yùn)行期間岸军,可以向常量池中添加新的常量。如:String類的intern()方法就能在運(yùn)行期間向常量池中添加字符串常量瓦侮。
當(dāng)運(yùn)行時(shí)常量池中的某些常量沒有被對(duì)象引用,同時(shí)也沒有被變量引用佣谐,那么就需要垃圾收集器回收肚吏。
Java 虛擬機(jī)規(guī)范中對(duì)該區(qū)域規(guī)定了 OutOfMemoryError 異常: 當(dāng)常量池?zé)o法申請(qǐng)到內(nèi) 存時(shí)拋出 OutOfMemoryError 異常。
7狭魂、直接內(nèi)存
直接內(nèi)存是除Java虛擬機(jī)之外的內(nèi)存罚攀,但也有可能被Java使用。
在NIO中引入了一種基于通道和緩沖的IO方式雌澄。它可以通過(guò)調(diào)用本地方法直接分配Java虛擬機(jī)之外的內(nèi)存斋泄,然后通過(guò)一個(gè)存儲(chǔ)在Java堆中的DirectByteBuffer對(duì)象直接操作該內(nèi)存,而無(wú)需先將外面內(nèi)存中的數(shù)據(jù)復(fù)制到堆中再操作镐牺,從而提升了數(shù)據(jù)操作的效率炫掐。
直接內(nèi)存的大小不受Java虛擬機(jī)控制,但既然是內(nèi)存睬涧,當(dāng)內(nèi)存不足時(shí)就會(huì)拋出OOM異常募胃。
8旗唁、棧幀
棧幀是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū) 的虛擬機(jī)棧的棧元素痹束。棧幀存儲(chǔ)了方法的局部變量表检疫,操作數(shù)棧,動(dòng)態(tài)連接和方法返回地址 等信息祷嘶。第一個(gè)方法從調(diào)用開始到執(zhí)行完成屎媳,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧 的過(guò)程。在編譯代碼的時(shí)候论巍,棧幀中需要多大的局部變量表烛谊,多深的操作數(shù)棧都已經(jīng)完全確 定了,并且寫入到了方法表的 Code 屬性中环壤,因此一個(gè)棧幀需要分配多少內(nèi)存晒来,不會(huì)受到程 序運(yùn)行期變量數(shù)據(jù)的影響,而僅僅取決于具體虛擬機(jī)的實(shí)現(xiàn)郑现。一個(gè)線程中的方法調(diào)用鏈可能會(huì)很長(zhǎng)湃崩,很多方法都同時(shí)處理執(zhí)行狀態(tài)。對(duì)于執(zhí)行引擎來(lái) 講接箫,活動(dòng)線程中攒读,只有虛擬機(jī)棧頂?shù)臈攀怯行У模Q為當(dāng)前棧幀(Current Stack Frame)辛友,這個(gè)棧幀所關(guān)聯(lián)的方法稱為當(dāng)前方法(Current Method)薄扁。
8.1、局部變量表
局部標(biāo)量表是一組變量值的存儲(chǔ)空間废累,一個(gè)以字長(zhǎng)為單位邓梅,從 0 開始計(jì)數(shù)的數(shù)組,用于 存放方法參數(shù)和局部變量邑滨。變量槽 (Variable Slot)是局部變量表的最小單位日缨,沒有強(qiáng)制規(guī) 定大小為 32 位,雖然 32 位足夠存放大部分類型的數(shù)據(jù)掖看。一個(gè) Slot 可以存放 boolean匣距、 byte、char哎壳、short毅待、int、float归榕、reference 和 returnAddress 8 種類型尸红。其中 reference 表 示對(duì)一個(gè)對(duì)象實(shí)例的引用。returnAddress 則指向了一條字節(jié)碼指令的地址。 對(duì)于 64 位的 long 和 double 變量而言驶乾,虛擬機(jī)會(huì)為其分配兩個(gè)連續(xù)的 Slot 空間邑飒。虛擬機(jī)通過(guò)索引定位的方式使用局部變量表。之前我們知道级乐,局部變量表存放的是方法參數(shù)和局部變量疙咸。當(dāng)調(diào)用方法是非 static 方法時(shí),局部變量表中第 0 位索引的 Slot 默認(rèn)是用于傳遞方法所屬對(duì)象實(shí)例的引用风科,即“this”關(guān)鍵字指向的對(duì)象撒轮。分配完方法參數(shù)后,便會(huì) 依次分配方法內(nèi)部定義的局部變量贼穆。
為了節(jié)省棧幀空間题山,局部變量表中的 Slot 是可以重用的。當(dāng)離開了某些變量的作用域 之后故痊,這些變量對(duì)應(yīng)的 Slot 就可以交給其他變量使用顶瞳。
8.2、操作數(shù)棧
操作數(shù)棧被組織成一個(gè)以字長(zhǎng)為單位的數(shù)組愕秫。但不是通過(guò)索引來(lái)訪問慨菱,而是通過(guò)標(biāo)準(zhǔn)棧 操作--壓棧和出棧來(lái)訪問。方法執(zhí)行中進(jìn)行算術(shù)運(yùn)算或者是調(diào)用其他的方法進(jìn)行參數(shù)傳遞的 時(shí)候是通過(guò)操作數(shù)棧進(jìn)行的戴甩。
在概念模型中符喝,兩個(gè)棧幀是相互獨(dú)立的。但是大多數(shù)虛擬機(jī)的實(shí)現(xiàn)都會(huì)進(jìn)行優(yōu)化甜孤,令兩 個(gè)棧幀出現(xiàn)一部分重疊协饲。令下面的部分操作數(shù)棧與上面的局部變量表重疊在一塊,這樣在方 法調(diào)用的時(shí)候可以共用一部分?jǐn)?shù)據(jù)缴川,無(wú)需進(jìn)行額外的參數(shù)復(fù)制傳遞茉稠。
8.3、幀數(shù)據(jù)區(qū)
棧幀需要一些數(shù)據(jù)來(lái)支持常量池解析把夸、正常方法返回和異常處理等而线。在幀數(shù)據(jù)區(qū)中保存
著訪問常量池的指針,方便程序訪問常量池扎即。此外,當(dāng)函數(shù)返回或者出現(xiàn)異常時(shí)况凉,虛擬機(jī)必 須恢復(fù)調(diào)用者函數(shù)的棧幀谚鄙,并讓調(diào)用者函數(shù)繼續(xù)執(zhí)行下去。對(duì)于異常處理刁绒,虛擬機(jī)必須有一 個(gè)異常處理表闷营,方便在發(fā)生異常的時(shí)候找到處理異常的代碼,因此異常處理表也是幀數(shù)據(jù)區(qū) 中重要的一部分。
8.3.1 動(dòng)態(tài)連接
每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用傻盟,持有這個(gè)引用是為了
支持方法調(diào)用過(guò)程中的動(dòng)態(tài)連接速蕊。符號(hào)引用一部分會(huì)在類加載階段或者第一次使用的時(shí)候就 轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化成為靜態(tài)解析娘赴。另外一部分在每一次運(yùn)行期間轉(zhuǎn)化為直接引用规哲, 這部分稱為動(dòng)態(tài)連接。
8.3.2 方法返回地址
當(dāng)一個(gè)方法開始執(zhí)行后诽表,只有兩種方式可以退出這個(gè)方法唉锌。第一種是執(zhí)行引擎遇到任意 一個(gè)方法返回的字節(jié)碼指令,這時(shí)候可能會(huì)有返回值傳遞給上層的方法調(diào)用者竿奏。
另一種退出方式是袄简,在方法執(zhí)行過(guò)程中遇到了異常,并且這個(gè)異常沒有在方法體內(nèi)得到 處理泛啸。無(wú)論采用何種退出方式绿语,在方法退出之后,都需要返回到方法被調(diào)用的位置候址,程序才 能繼續(xù)執(zhí)行吕粹。
8.4 OutOfMemoryError
在 eclipse 中設(shè)置-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError(堆最小 值、最大值設(shè)置成一樣為了避免自動(dòng)擴(kuò)展宗雇,輸出內(nèi)存溢出時(shí)信息)
Java 堆用于存儲(chǔ)對(duì)象實(shí)例昂芜,只要不斷創(chuàng)建對(duì)象,并且保證 GC Roots 到對(duì)象之間有可達(dá)路 徑來(lái)避免垃圾回收赔蒲,當(dāng)對(duì)象數(shù)量達(dá)到最大堆容量后就產(chǎn)生內(nèi)存溢出異常泌神。