一有巧、JVM內(nèi)存區(qū)域模型
1.堆
也叫做java 堆鹉胖、GC堆是java虛擬機所管理的內(nèi)存中最大的一塊內(nèi)存區(qū)域,也是被各個線程共享的內(nèi)存區(qū)域绒尊,在JVM啟動時創(chuàng)建畜挥。
該內(nèi)存區(qū)域存放了對象實例及數(shù)組(所有new的對象),每個對象都包含一個與之對應的class的信息婴谱。(class的目的是得到操作指令)
其大小通過-Xms(最小值)和-Xmx(最大值)參數(shù)設置蟹但,-Xms為JVM啟動時申請的最小內(nèi)存,默認為操作系統(tǒng)物理內(nèi)存的1/64但小于1G谭羔,-Xmx為JVM可申請的最大內(nèi)存华糖,默認為物理內(nèi)存的1/4但小于1G,默認當空余堆內(nèi)存小于40%時瘟裸,JVM會增大Heap到-Xmx指定的大小客叉,可通過-XX:MinHeapFreeRation=來指定這個比列;當空余堆內(nèi)存大于70%時话告,JVM會減小heap的大小到-Xms指定的大小兼搏,可通過XX:MaxHeapFreeRation=來指定這個比列,對于運行系統(tǒng)沙郭,為避免在運行時頻繁調(diào)整Heap的大小佛呻,通常-Xms與-Xmx的值設成一樣。
由于現(xiàn)在收集器都是采用分代收集棠绘,堆被劃分為新生代和老年代件相。新生代主要存儲新創(chuàng)建的對象和尚未進入老年代的對象。老年代存儲經(jīng)過多次新生代GC(Minor GC)任然存活的對象氧苍。
新生代:
程序新創(chuàng)建的對象都是從新生代分配內(nèi)存夜矗,新生代由Eden Space和兩塊相同大小的Survivor Space(From Survivor Space 和 To Survivor Space)構成,可通過-Xmn參數(shù)來指定新生代的大小让虐,也可以通過-XX:SurvivorRation來調(diào)整Eden Space及Survivor Space的大小紊撕。
由于新生代采用復制算法進行垃圾回收,根據(jù)IBM研究表明新生代中的對象98%是“朝生夕死”的赡突,所以并不需要按照1:1的比例來劃分內(nèi)存空間对扶,而是將內(nèi)存分為較大的Eden Space和兩塊較小的Survivor Space区赵,每次使用Eden和其中一塊Survivor。當回收時浪南,將Eden和Survivor中還存活的對象復制到另一塊Survivor笼才,最后清理掉Eden和剛才用過的Survivor。
如果-XX:SurvivorRation=8則表示Eden空間和其中一塊Survivor空間大小比是8:1络凿,也就是說新生代的可用空間是整個新生代容量的90%(80%+10%)骡送,只有10%的空間會浪費。
老年代:
用于存放經(jīng)過多次新生代GC任然存活的對象絮记,例如緩存對象摔踱,新建的對象也有可能直接進入老年代,主要有兩種情況:
(1)大對象怨愤,可通過啟動參數(shù)設置-XX:PretenureSizeThreshold=1024(單位為字節(jié)派敷,默認為0)來代表超過多大時就不在新生代分配,而是直接在老年代分配
(2)大數(shù)組對象撰洗,且數(shù)組中無引用外部對象
老年代所占的內(nèi)存大小為-Xmx對應的值減去-Xmn對應的值篮愉。
2. 方法區(qū)
也稱"永久代” 、“非堆”了赵, 它用于存儲虛擬機加載的類信息潜支、常量、靜態(tài)變量柿汛、是各個線程共享的內(nèi)存區(qū)域。默認最小值為16MB埠对,最大值為64MB络断,可以通過-XX:PermSize 和 -XX:MaxPermSize 參數(shù)限制方法區(qū)的大小。
3. 運行時常量池
是方法區(qū)的一部分项玛,Class文件中除了有類的版本貌笨、字段、方法襟沮、接口等描述信息外锥惋,還有一項信息是常量池,用于存放編譯器生成的字面量和符號引用开伏,這部分內(nèi)容將在類加載后放到方法區(qū)的運行時常量池中膀跌。
存放基本類型常量和字符串常量。
4. 虛擬機棧
描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法被執(zhí)行的時候 都會創(chuàng)建一個“棧幀”用于存儲局部變量表(包括參數(shù))固灵、操作棧捅伤、方法出口等信息。每個方法被調(diào)用到執(zhí)行完的過程巫玻,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程丛忆。聲明周期與線程相同祠汇,是線程私有的。
基礎數(shù)據(jù)類型直接在椣ü睿空間分配可很。
引用數(shù)據(jù)類型,需要用new來創(chuàng)建凰浮,既在椄睿空間分配一個地址空間,又在堆空間分配對象的類變量 导坟。
局部變量 new 出來時屿良,在棧空間和堆空間中分配空間惫周,當局部變量生命周期結(jié)束后尘惧,棧空間立刻被回收递递,堆空間區(qū)域等待GC回收喷橙。
數(shù)組既在棧空間分配數(shù)組名稱登舞, 又在堆空間分配數(shù)組實際的大小
方法的形式參數(shù)贰逾,直接在棧空間分配菠秒,當方法調(diào)用完成后從椄斫#空間回收。
方法的引用參數(shù)践叠,在椦早停空間分配一個地址空間,并指向堆空間的對象區(qū)禁灼,當方法調(diào)用完成后從椆苄空間回收。
5. 程序計數(shù)器
是最小的一塊內(nèi)存區(qū)域弄捕,它的作用是當前線程所執(zhí)行的字節(jié)碼的行號指示器僻孝,在虛擬機的模型里,字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令守谓,分支穿铆、循環(huán)、異常處理分飞、線程恢復等基礎功能都需要依賴計數(shù)器完成悴务。
二、代碼示例存儲分配
JAVA的JVM的內(nèi)存可分為3個區(qū):堆(heap)、棧(stack)和方法區(qū)(method)
堆區(qū):
- 存儲的全部是對象讯檐,每個對象都包含一個與之對應的class的信息羡疗。(class的目的是得到操作指令)
- jvm只有一個堆區(qū)(heap)被所有線程共享,堆中不存放基本類型和對象引用别洪,只存放對象本身
棧區(qū):
- 每個線程包含一個棧區(qū)叨恨,棧中只保存基礎數(shù)據(jù)類型的對象和自定義對象的引用(不是對象),對象都存放在堆區(qū)中
- 每個棧中的數(shù)據(jù)(原始類型和對象引用)都是私有的挖垛,其他棧不能訪問
- 棧分為3個部分:基本類型變量區(qū)痒钝、執(zhí)行環(huán)境上下文、操作指令區(qū)(存放操作指令)
方法區(qū):
- 又叫靜態(tài)區(qū)痢毒,跟堆一樣送矩,被所有的線程共享。方法區(qū)包含所有的class和static變量
- 方法區(qū)中包含的都是在整個程序中永遠唯一的元素哪替,如class栋荸,static變量
//運行時, jvm把appmain的信息都放入方法區(qū)
public class AppMain {
//main 方法本身放入方法區(qū)
public static void main(String[] args) {
//test1是引用,所以放到棧區(qū)里凭舶, Sample是自定義對象應該放到堆里面
Sample test1 = new Sample("測試1");
Sample test2 = new Sample("測試2");
test1.printName();
test2.printName();
}
}
//運行時, jvm 把appmain的信息都放入方法區(qū)
public class Sample {
/** 范例名稱 */
// new Sample實例后晌块, name 引用放入棧區(qū)里, name 對象放入堆里
private String name;
/** 構造方法 */
public Sample(String name) {
this.name = name;
}
/** 輸出 */
// print方法本身放入 方法區(qū)里
public void printName() {
System.out.println(name);
}
}
系統(tǒng)收到了我們發(fā)出的指令帅霜,啟動了一個Java虛擬機進程匆背,這個進程首先從classpath中找到AppMain.class文件,讀取這個文件中的二進制數(shù)據(jù)身冀,然后把Appmain類的類信息存放到運行時數(shù)據(jù)區(qū)的方法區(qū)中钝尸。這一過程稱為AppMain類的加載過程。
接著闽铐,Java虛擬機定位到方法區(qū)中AppMain類的Main()方法的字節(jié)碼蝶怔,開始執(zhí)行它的指令。這個main()方法的第一條語句就是:
Sample test1=new Sample("測試1");
語句很簡單啦兄墅,就是讓java虛擬機創(chuàng)建一個Sample實例,并且使引用變量test1引用這個實例澳叉。貌似小case一樁哦隙咸,就讓我們來跟蹤一下Java虛擬機,看看它究竟是怎么來執(zhí)行這個任務的:
Java虛擬機一看成洗,不就是建立一個Sample實例嗎五督,簡單,于是就直奔方法區(qū)而去瓶殃,先找到Sample類的類型信息再說充包。結(jié)果并沒有找到,因為這會兒的方法區(qū)里還沒有Sample類(Sample類沒加載),于是Java虛擬機立馬加載了Sample類基矮,把Sample類的類型信息存放在方法區(qū)里淆储。
Sample類加載好后,Java虛擬機做的第一件事情就是在堆區(qū)中為一個新的Sample實例分配內(nèi)存, 這個Sample實例持有著指向方法區(qū)的Sample類的類型信息的引用家浇。這里所說的引用本砰,實際上指的是Sample類的類型信息在方法區(qū)中的內(nèi)存地址,而這個地址呢钢悲,就存放了在Sample實例的數(shù)據(jù)區(qū)里点额。
在Java虛擬機進程中,每個線程都會擁有一個方法調(diào)用棧莺琳,用來跟蹤線程運行中一系列的方法調(diào)用過程还棱,棧中的每一個元素就被稱為棧幀,每當線程調(diào)用一個方法的時候就會向方法棧壓入一個新幀惭等。這里的幀用來存儲方法的參數(shù)珍手、局部變量和運算過程中的臨時數(shù)據(jù)。OK咕缎,原理講完了珠十,就讓我們來繼續(xù)我們的跟蹤行動!位于“=”前的test1是一個在main()方法中定義的變量凭豪,可見焙蹭,它是一個局部變量,因此嫂伞,它被會添加到了執(zhí)行main()方法的主線程的JAVA方法調(diào)用棧中孔厉。而“=”將把這個test1變量指向堆區(qū)中的Sample實例,也就是說帖努,它持有指向Sample實例的引用撰豺。
OK,到這里為止呢拼余,JAVA虛擬機就完成了這個簡單語句的執(zhí)行任務污桦。參考行動向?qū)D,我們終于初步摸清了JAVA虛擬機的一點點底細了匙监,COOL凡橱!
接下來,JAVA虛擬機將繼續(xù)執(zhí)行后續(xù)指令亭姥,在堆區(qū)里繼續(xù)創(chuàng)建另一個Sample實例稼钩,然后依次執(zhí)行它們的printName()方法。當Java虛擬機執(zhí)行test1.printName()方法時达罗,JAVA虛擬機根據(jù)局部變量test1持有的引用坝撑,定位到堆區(qū)中的Sample實例,再根據(jù)Sample實例持有的引用,定位到方法去中Sample類的類型信息巡李,從而獲得printName()方法的字節(jié)碼抚笔,接著執(zhí)行printName()方法包含的指令。
三击儡、Java虛擬機中堆和棧的區(qū)別
(1)棧(stack)與堆(heap)都是Java用來在Ram中存放數(shù)據(jù)的地方塔沃。與C++不同,Java自動管理棧和堆阳谍,程序員不能直接地設置椫瘢或堆。
(2)棧的優(yōu)勢是矫夯,存取速度比堆要快鸽疾,僅次于直接位于CPU中的寄存器。但缺點是训貌,存在棧中的數(shù)據(jù)大小與生存期必須是確定的制肮,缺乏靈活性。另外递沪,棧數(shù)據(jù)可以共享豺鼻,詳見第3點。堆的優(yōu)勢是可以動態(tài)地分配內(nèi)存大小款慨,生存期也不必事先告訴編譯器儒飒,Java的垃圾收集器會自動收走這些不再使用的數(shù)據(jù)。但缺點是檩奠,由于要在運行時動態(tài)分配內(nèi)存桩了,存取速度較慢。
(3)Java中的數(shù)據(jù)類型有兩種埠戳,一種是基本類型(primitive types), 共有8種井誉,即int, short, long, byte, float, double, boolean, char(注意,并沒有string的基本類型)整胃。這種類型的定義是通過諸如
int a = 3;
long b = 255L;
的形式來定義的颗圣,稱為自動變量。值得注意的是屁使,自動變量存的是字面值欠啤,不是類的實例,即不是類的引用屋灌,這里并沒有類的存在。如int a = 3; 這里的a是一個指向int類型的引用应狱,指向3這個字面值共郭。這些字面值的數(shù)據(jù),由于大小可知,生存期可知(這些字面值固定定義在某個程序塊里面除嘹,程序塊退出后写半,字段值就消失了),出于追求速度的原因尉咕,就存在于棧中叠蝇。另外,棧有一個很重要的特殊性年缎,就是存在棧中的數(shù)據(jù)可以共享悔捶。假設我們同時定義
int a = 3;
int b = 3;
編譯器先處理int a = 3,首先它會在棧中創(chuàng)建一個變量為a的引用单芜,然后查找有沒有字面值為3的地址蜕该,沒找到,就開辟一個存放3這個字面值的地址洲鸠,然后將a指向3的地址堂淡。接著處理int b = 3;在創(chuàng)建完b的引用變量后扒腕,由于在棧中已經(jīng)有3這個字面值绢淀,便將b直接指向3的地址。這樣瘾腰,就出現(xiàn)了a與b同時均指向3的情況皆的。
特別注意的是,這種字面值的引用與類對象的引用不同居灯。假定兩個類對象的引用同時指向一個對象祭务,如果一個對象引用變量修改了這個對象的內(nèi)部狀態(tài),那么另一個對象引用變量也即刻反映出這個變化怪嫌。相反义锥,通過字面值的引用來修改其值,不會導致另一個指向此字面值的引用的值也跟著改變的情況岩灭。如上例拌倍,我們定義完a與 b的值后,再令a=4噪径;那么窘拯,b不會等于4奋蔚,還是等于3。在編譯器內(nèi)部,遇到a=4舆乔;時,它就會重新搜索棧中是否有4的字面值祟牲,如果沒有,重新開辟地址存放4的值仑鸥;如果已經(jīng)有了,則直接將a指向這個地址变屁。因此a值的改變不會影響到b的值眼俊。
(4)棧是線程私有的,堆是線程共享的粟关。
四疮胖、Java代碼運行過程實例
有如下代碼:
(1)運行main方法前,Test類已經(jīng)加載到方法區(qū)了
(2)首先new一個Test對象闷板,內(nèi)存分配如圖所示
由于Visio軟件有問題了澎灸,往后在更新!