Java內(nèi)存機(jī)制講解

一 概述

眾所周知c語(yǔ)言是鼻祖.而讓c語(yǔ)言的特點(diǎn)之一就是指針.那在Java中是沒(méi)有指針這個(gè)概念的.但是沒(méi)有并不表示不存在,在Java中,每次new一個(gè)對(duì)象的時(shí)候,其實(shí)就是在內(nèi)存中開辟了一塊控件,對(duì)象的引用實(shí)際上就是指針.只不過(guò)java中把對(duì)內(nèi)存的操作交給了JVM.

c語(yǔ)言的指針讓你可以操作內(nèi)存,但是同時(shí)你也要去維護(hù)這個(gè)指針.而java中操作內(nèi)存的工作交給了JVM.所以一定程度上減輕了程序猿的負(fù)擔(dān).當(dāng)然若是在這種情況下出現(xiàn)內(nèi)存泄漏和內(nèi)存溢出問(wèn)題.如果沒(méi)有了解JVM是怎樣使用內(nèi)存的,將會(huì)導(dǎo)致異常排除變得非常困難.

二 JVM內(nèi)存分布


如圖.JVM的內(nèi)存主要包括兩個(gè)子系統(tǒng)和兩個(gè)組件.兩個(gè)子系統(tǒng)分別是CLASS LOADER(類加載器)和EXECUTION ENGINE(執(zhí)行引擎).兩個(gè)組件分別是RUNTIME DATA AREA(運(yùn)行數(shù)據(jù)區(qū)域)和NATIVE INTERFACE(本地接口庫(kù))組件.

1.CLASS LOADER。java運(yùn)行的時(shí)候并不是直接運(yùn)行代碼,而是要通過(guò)類加載器把java class加載到JVM中然后運(yùn)行.負(fù)責(zé)加載的這部分就是CLASS LOADER.我們可以通過(guò)重寫ClassLoader來(lái)拓展程序的功能.例如:1)在執(zhí)行非置信代碼之前,自動(dòng)驗(yàn)證數(shù)字簽名.2)動(dòng)態(tài)地創(chuàng)建符合用戶特定需要的定制化構(gòu)建類.3)從特定的場(chǎng)所取得java class,例如數(shù)據(jù)庫(kù)中.4) 等等

2.EXECUTION ENGINE。執(zhí)行classes中的指令。任何JVM specification實(shí)現(xiàn)(JDK)的核心都是Execution engine,不同的JDK例如Sun 的JDK 和IBM的JDK好壞主要就取決于他們各自實(shí)現(xiàn)的Execution engine的好壞。

3.NATIVE INTERFACE .用作與其他語(yǔ)言編程的接口.可以通過(guò)這個(gè)組件調(diào)用其他語(yǔ)言的程序.

4.Runtime Data Area組件 這就是我們常說(shuō)的java內(nèi)存了
? ? ?? 1、Heap (堆):一個(gè)Java虛擬實(shí)例中只存在一個(gè)堆空間,是Java內(nèi)存管理中最大的一塊.也是被所有的線程所共享的一塊.在虛擬機(jī)啟動(dòng)的時(shí)候創(chuàng)建.
??????? 堆內(nèi)存中存儲(chǔ)著基本上所有的對(duì)象.因?yàn)榧夹g(shù)的更新 現(xiàn)在也不是絕對(duì)的存放在堆內(nèi)存中,java中的gc也是主要對(duì)堆內(nèi)存進(jìn)行操作,所以堆內(nèi)存也可以稱為GC內(nèi)存.gc采用的算法是分代收集算法,所以在堆中又可以分為,年青代(Young)、年老代(Tenured).更細(xì)致的可以劃分為 Eden空間,From Survivor空間和To Survivor空間.不論怎么劃分.堆都是用來(lái)存放對(duì)象.
??? ? ? 根據(jù)Java虛擬機(jī)規(guī)范規(guī)定,java堆可以處于物理上不連續(xù)的內(nèi)存空間中(多條內(nèi)存條),只要邏輯連續(xù)即可.在創(chuàng)建堆的時(shí)候我們可以固定堆的大小,也可以使用可拓展的堆,如果堆中沒(méi)有足夠的內(nèi)存來(lái)分配對(duì)象就會(huì)報(bào)出OutOfMemory的錯(cuò)誤

? ? ? ? 2、Method Area(方法區(qū)域):Method Area和Heap一樣,也是被所有線程所共享的一塊,它是用于存儲(chǔ)已被虛擬機(jī)加載的類的信息,常量,靜態(tài)變量,即時(shí)編譯器編譯后的代碼等數(shù)據(jù),java對(duì)這個(gè)區(qū)域的管理比較輕松,除了和堆一樣可以選擇固定內(nèi)存和拓展以外,還可以對(duì)方法區(qū)選擇不進(jìn)行垃圾回收機(jī)制,相對(duì)而言,gc在這里觸發(fā)的概率相對(duì)較小,但也不意味著不會(huì)被回收,這個(gè)區(qū)域的回收工作主要是針對(duì)常量池的回收和類型的卸載.當(dāng)方法區(qū)無(wú)法滿足內(nèi)存分配的時(shí)候,會(huì)拋出OutOfMemory的錯(cuò)誤,

? ? ? ? 3、JavaStack(java的棧):Java Virtual Machine Stacks 是屬于線程私有的,他的生命周期和線程的相同.每個(gè)方法被執(zhí)行的時(shí)候都會(huì)在棧中創(chuàng)建一個(gè)棧幀.棧幀中包括局部變量表,操作棧,動(dòng)態(tài)鏈接,方法出口等.
局部變量表用于存儲(chǔ)方法的參數(shù)和局部變量.
操作棧,也成為"基于棧的執(zhí)行引擎",主要是運(yùn)行過(guò)程中的算數(shù)計(jì)算和調(diào)用其他方法參數(shù)的傳遞.
動(dòng)態(tài)鏈接是每個(gè)棧幀在執(zhí)行時(shí)常量池中都有一個(gè)引用,Class文件中會(huì)有大量的符號(hào)引用,字節(jié)碼中方法調(diào)用就是使用這些符號(hào)引用,這些符號(hào)引用在第一次加載的時(shí)候就轉(zhuǎn)換為直接引用的成為靜態(tài)解析(靜態(tài)方法),和每一次調(diào)用的時(shí)候才轉(zhuǎn)換為直接引用的成為動(dòng)態(tài)鏈接.
方法出口,當(dāng)方法運(yùn)行的時(shí)候只有兩種情況會(huì)退出方法,一種是異常退出,另一種是執(zhí)行到方法出口(return).當(dāng)方法執(zhí)行完可能進(jìn)行的操作是1.恢復(fù)上層方法的局部變量表和操作數(shù)棧.2.把返回值壓入調(diào)用者調(diào)用者棧幀的操作數(shù)棧.3.調(diào)整 PC 計(jì)數(shù)器的值以指向方法調(diào)用指令后面的一條指令.

? ? ? ? 4、Program Counter(程序計(jì)數(shù)器):每一個(gè)線程都有它自己的PC寄存器,也是該線程啟動(dòng)時(shí)創(chuàng)建的。PC寄存器的內(nèi)容總是指向下一條將被執(zhí)行指令的餓地址,這里的地址可以是一個(gè)本地指針,也可以是在方法區(qū)中相對(duì)應(yīng)于該方法起始指令的偏移量。由于Java虛擬機(jī)的多線程是通過(guò)線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來(lái)實(shí)現(xiàn)的,在任何一個(gè)確定的時(shí)刻,一個(gè)處理器(對(duì)于多核處理器來(lái)說(shuō)是一個(gè)內(nèi)核)只會(huì)執(zhí)行一條線程中的指令。因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間的計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ),我們稱這類內(nèi)存區(qū)域?yàn)椤熬€程私有”的內(nèi)存。這塊內(nèi)存不會(huì)拋出OutOfMemory

? ? ? ? 5、Native method stack(本地方法棧):保存native方法進(jìn)入?yún)^(qū)域的地址

三 Java對(duì)象調(diào)用

介紹完java內(nèi)存的分布,現(xiàn)在來(lái)說(shuō)說(shuō)java的對(duì)象是怎樣的一個(gè)存在.最普通創(chuàng)建對(duì)象的方法是
Object object=new Object();
這里面包含了前面所說(shuō)的三塊內(nèi)存的使用.Object object 這句話就是值在棧中保存了Object對(duì)象的引用,new Object();這段話則表示在堆內(nèi)存中開辟了一段內(nèi)存存儲(chǔ)Object的對(duì)象.Object() 這個(gè)方法是存在于方法區(qū)中.

在Java中reference類型引用對(duì)象有兩種主流的方式,一種是通過(guò)句柄訪問(wèn),一種是直接訪問(wèn).
1.句柄訪問(wèn)會(huì)在堆內(nèi)存中劃分出一塊區(qū)域用于存儲(chǔ)句柄,reference只需要指向這個(gè)句柄,而句柄會(huì)指向?qū)ο蠛瓦@個(gè)對(duì)象的方法


2.直接引用 reference中直接存儲(chǔ)對(duì)象的地址


這兩種方式各有優(yōu)缺點(diǎn),采用句柄的形式,是在對(duì)象發(fā)生變化的時(shí)候(被回收了)只需要改變句柄的指向?qū)ο?不需要修改reference的指向.


采用直接指針訪問(wèn)的時(shí)候,因?yàn)樯倭艘粚又羔樀闹赶?所以速度更快.

四 對(duì)象的回收

說(shuō)完對(duì)象的調(diào)用,接下來(lái)就是對(duì)象的回收,在Java中g(shù)c的回收機(jī)制還是比較常見的.一下以堆內(nèi)存中的分代收集算法做舉例.

Sun的JVM Generational Collecting(垃圾回收)原理是這樣的:把對(duì)象分為年青代(Young)、年老代(Tenured)、持久代(Perm),對(duì)不同生命周期的對(duì)象使用不同的算法。(基于對(duì)對(duì)象生命周期分析)

如上圖所示,為Java堆中的各代分布。

1. Young(年輕代)

年輕代分三個(gè)區(qū)。一個(gè)Eden區(qū),兩個(gè)Survivor區(qū)。大部分對(duì)象在Eden區(qū)中生成。當(dāng)Eden區(qū)滿時(shí),還存活的對(duì)象將被復(fù)制到Survivor區(qū)(兩個(gè)中的一個(gè)),當(dāng)這個(gè)Survivor區(qū)滿時(shí),此區(qū)的存活對(duì)象將被復(fù)制到另外一個(gè)Survivor區(qū),當(dāng)這個(gè)Survivor去也滿了的時(shí)候,從第一個(gè)Survivor區(qū)復(fù)制過(guò)來(lái)的并且此時(shí)還存活的對(duì)象,將被復(fù)制年老區(qū)(Tenured。需要注意,Survivor的兩個(gè)區(qū)是對(duì)稱的,沒(méi)先后關(guān)系,所以同一個(gè)區(qū)中可能同時(shí)存在從Eden復(fù)制過(guò)來(lái) 對(duì)象,和從前一個(gè)Survivor復(fù)制過(guò)來(lái)的對(duì)象,而復(fù)制到年老區(qū)的只有從第一個(gè)Survivor去過(guò)來(lái)的對(duì)象。而且,Survivor區(qū)總有一個(gè)是空的。

2. Tenured(年老代)

年老代存放從年輕代存活的對(duì)象。一般來(lái)說(shuō)年老代存放的都是生命期較長(zhǎng)的對(duì)象。

3. Perm(持久代)

用于存放靜態(tài)文件,如今Java類、方法等。持久代對(duì)垃圾回收沒(méi)有顯著影響,但是有些應(yīng)用可能動(dòng)態(tài)生成或者調(diào)用一些class,例如hibernate等,在這種時(shí)候需要設(shè)置一個(gè)比較大的持久代空間來(lái)存放這些運(yùn)行過(guò)程中新增的類。持久代大小通過(guò)-XX:MaxPermSize=進(jìn)行設(shè)置。

舉個(gè)例子:當(dāng)在程序中生成對(duì)象時(shí),正常對(duì)象會(huì)在年輕代中分配空間,如果是過(guò)大的對(duì)象也可能會(huì)直接在年老代生成(據(jù)觀測(cè)在運(yùn)行某程序時(shí)候每次會(huì)生成一個(gè)十兆的空間用收發(fā)消息,這部分內(nèi)存就會(huì)直接在年老代分配)。年輕代在空間被分配完的時(shí)候就會(huì)發(fā)起內(nèi)存回收,大部分內(nèi)存會(huì)被回收,一部分幸存的內(nèi)存會(huì)被拷貝至Survivor的from區(qū),經(jīng)過(guò)多次回收以后如果from區(qū)內(nèi)存也分配完畢,就會(huì)也發(fā)生內(nèi)存回收然后將剩余的對(duì)象拷貝至to區(qū)。等到to區(qū)也滿的時(shí)候,就會(huì)再次發(fā)生內(nèi)存回收然后把幸存的對(duì)象拷貝至年老區(qū)。

通常我們說(shuō)的JVM內(nèi)存回收總是在指堆內(nèi)存回收,確實(shí)只有堆中的內(nèi)容是動(dòng)態(tài)申請(qǐng)分配的,所以以上對(duì)象的年輕代和年老代都是指的JVM的Heap空間,而持久代則是之前提到的Method Area,不屬于Heap。

了解完這些之后,以下的轉(zhuǎn)載一熱衷于鉆研技術(shù)的哥們Richen Wang關(guān)于內(nèi)存管理的一些建議——

1、手動(dòng)將生成的無(wú)用對(duì)象,中間對(duì)象置為null,加快內(nèi)存回收。

2、對(duì)象池技術(shù) 如果生成的對(duì)象是可重用的對(duì)象,只是其中的屬性不同時(shí),可以考慮采用對(duì)象池來(lái)較少對(duì)象的生成。如果有空閑的對(duì)象就從對(duì)象池中取出使用,沒(méi)有再生成新的對(duì)象,大大提高了對(duì)象的復(fù)用率。

3、JVM調(diào)優(yōu) 通過(guò)配置JVM的參數(shù)來(lái)提高垃圾回收的速度,如果在沒(méi)有出現(xiàn)內(nèi)存泄露且上面兩種辦法都不能保證內(nèi)存的回收時(shí),可以考慮采用JVM調(diào)優(yōu)的方式來(lái)解決,不過(guò)一定要經(jīng)過(guò)實(shí)體機(jī)的長(zhǎng)期測(cè)試,因?yàn)椴煌膮?shù)可能引起不同的效果。如-Xnoclassgc參數(shù)等。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容