浅析java内存管理机制
內(nèi)存管理是計(jì)算機(jī)編程中的一個(gè)重要問(wèn)題,一般來(lái)說(shuō),內(nèi)存管理主要包括內(nèi)存分配和內(nèi)存回收兩個(gè)部分。不同的編程語(yǔ)言有不同的內(nèi)存管理機(jī)制,本文在對(duì)比C++和java語(yǔ)言?xún)?nèi)存管理機(jī)制的不同的基礎(chǔ)上,淺析java中的內(nèi)存分配和內(nèi)存回收機(jī)制,包括java對(duì)象初始化及其內(nèi)存分配,內(nèi)存回收方法及其注意事項(xiàng)等……
java與C++內(nèi)存管理機(jī)制對(duì)比
在C++中,所有的對(duì)象都會(huì)被銷(xiāo)毀,局部對(duì)象的銷(xiāo)毀發(fā)生在以右花括號(hào)為界的對(duì)象作用域的末尾處,而程序猿new出來(lái)的對(duì)象則應(yīng)該主動(dòng)調(diào)用delete操作符從而調(diào)用析構(gòu)函數(shù)去回收對(duì)象占用的內(nèi)存。但是C++這種直接操作內(nèi)存的方式存在很大內(nèi)存泄露風(fēng)險(xiǎn),而且人為管理內(nèi)存復(fù)雜且困難。
在java中,內(nèi)存管理由JVM完全負(fù)責(zé),java中的“垃圾回收器”負(fù)責(zé)自動(dòng)回收無(wú)用對(duì)象占據(jù)的內(nèi)存資源,這樣可以大大減少程序猿在內(nèi)存管理上花費(fèi)的時(shí)間,可以更集中于業(yè)務(wù)邏輯和具體功能實(shí)現(xiàn);但這并不是說(shuō)java有了垃圾回收器程序猿就可以高枕無(wú)憂,將內(nèi)存管理拋之腦外了!一方面,實(shí)際上java中還存在垃圾回收器沒(méi)法回收以某種“特殊方式”分配的內(nèi)存的情況(這種特殊方式我們將在下文中進(jìn)行詳細(xì)描述);另一方面,java的垃圾回收是不能保證一定發(fā)生的,除非JVM面臨內(nèi)存耗盡的情況。所以java中部分對(duì)象內(nèi)存還是需要程序猿手動(dòng)進(jìn)行釋放,合理地對(duì)部分對(duì)象進(jìn)行管理可以減少內(nèi)存占用與資源消耗。
java內(nèi)存分配
java程序執(zhí)行過(guò)程
首先Java源代碼文件(.java后綴)會(huì)被Java編譯器編譯為字節(jié)碼文件(.class后綴),然后由JVM中的類(lèi)加載器加載各個(gè)類(lèi)的字節(jié)碼文件,加載完畢之后,交由JVM執(zhí)行引擎執(zhí)行(執(zhí)行過(guò)程還包括將字節(jié)碼編譯成機(jī)器碼),JVM執(zhí)行引擎在執(zhí)行字節(jié)碼時(shí)首先會(huì)掃描四趟class文件來(lái)保證定義的類(lèi)型的安全性,再檢查空引用,數(shù)據(jù)越界,自動(dòng)垃圾收集等。在整個(gè)程序執(zhí)行過(guò)程中,JVM會(huì)用一段空間來(lái)存儲(chǔ)程序執(zhí)行期間需要用到的數(shù)據(jù)和相關(guān)信息,這段空間一般被稱(chēng)作為Runtime Data Area(運(yùn)行時(shí)數(shù)據(jù)區(qū)),也就是我們常說(shuō)的JVM內(nèi)存
類(lèi)加載器分為啟動(dòng)類(lèi)加載器(不繼承classLoader,屬于虛擬機(jī)的一部分;負(fù)責(zé)加載原生代碼實(shí)現(xiàn)的Java核心庫(kù),包括加載JAVA_HOME中jre/lib/rt.jar里所有的 class);擴(kuò)展類(lèi)加載器(負(fù)責(zé)在JVM中擴(kuò)展庫(kù)目錄中去尋找加載Java擴(kuò)展庫(kù),包括JAVA_HOME中jre/lib/ext/xx.jar或-Djava.ext.dirs指定目錄下的 jar 包);應(yīng)用程序類(lèi)加載器(ClassLoader.getSystemClassLoader()負(fù)責(zé)加載Java類(lèi)路徑classpath中的類(lèi))
- 加載:查找裝載二進(jìn)制文件,通過(guò)一個(gè)類(lèi)的全限定名獲取類(lèi)的二進(jìn)制字節(jié)流,并將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu);在 Java 堆中生成一個(gè)代表這個(gè)類(lèi)的 java.lang.Class 對(duì)象,作為對(duì)方法區(qū)中這些數(shù)據(jù)的訪問(wèn)入口。
- 驗(yàn)證:為了確保Class文件中的字節(jié)流包含的信息符合當(dāng)前虛擬機(jī)的要求,完成以下四個(gè)階段的驗(yàn)證:文件格式的驗(yàn)證、元數(shù)據(jù)的驗(yàn)證、字節(jié)碼驗(yàn)證和符號(hào)引用驗(yàn)證。
- 準(zhǔn)備:準(zhǔn)備階段是正式為類(lèi)變量分配內(nèi)存并設(shè)置類(lèi)變量初始值的階段,這些內(nèi)存都將在方法區(qū)中分配
- 解析:解析階段是虛擬機(jī)將常量池中的符號(hào)引用轉(zhuǎn)化為直接引用的過(guò)程
- 初始化:初始化階段是根據(jù)程序員通過(guò)程序指定的主觀計(jì)劃去初始化類(lèi)變量和其他資源,也就是執(zhí)行類(lèi)構(gòu)造器()方法的過(guò)程
現(xiàn)代硬件內(nèi)存架構(gòu)
java內(nèi)存模型劃分
一般來(lái)講,我們將java內(nèi)存劃分為以下幾個(gè)區(qū)域, 如圖:
GC備注:
下文中將要提到的內(nèi)存分配與回收主要是指對(duì)象所占據(jù)的堆內(nèi)存的釋放與回收。
java對(duì)象創(chuàng)建及初始化
java對(duì)象創(chuàng)建之后,就會(huì)在堆內(nèi)存擁有自己的一塊區(qū)域,接著就是對(duì)象的初始化過(guò)程。對(duì)象一般通過(guò)構(gòu)造器來(lái)進(jìn)行初始化,構(gòu)造器是一種與類(lèi)名相同的沒(méi)有返回值的特殊方法;如果一個(gè)類(lèi)中沒(méi)有定義構(gòu)造函數(shù),則系統(tǒng)會(huì)自動(dòng)生成一個(gè)不接受任何參數(shù)的默認(rèn)構(gòu)造器;但是如果已經(jīng)定義一個(gè)構(gòu)造器(無(wú)論是否有參數(shù)),編譯器就不會(huì)再自動(dòng)創(chuàng)建默認(rèn)構(gòu)造器了;我們可以對(duì)構(gòu)造函數(shù)進(jìn)行多次重載(即傳遞不同數(shù)目或不同順序的參數(shù)列表),也可以在一個(gè)構(gòu)造器中調(diào)用另一個(gè)構(gòu)造器,但是只能調(diào)用一次,并且必須將構(gòu)造器放在最起始處,否則編譯器會(huì)報(bào)錯(cuò)。
那么類(lèi)成員初始化又是怎么做的呢?順序是怎樣的呢?java中所有變量在使用前都應(yīng)該得到恰當(dāng)?shù)某跏蓟?#xff0c;即使是方法的局部變量,如果不進(jìn)行初始化就會(huì)發(fā)生編譯錯(cuò)誤;而如果是類(lèi)的成員變量,即使你不進(jìn)行初始化賦值,系統(tǒng)也是會(huì)給與其一個(gè)初始值的,例如char、int類(lèi)型的初始值都是0,對(duì)象引用不進(jìn)行初始化則默認(rèn)為null。
類(lèi)成員初始化順序總結(jié):先靜態(tài)后普通再構(gòu)造, 先父類(lèi)后子類(lèi),同級(jí)看書(shū)寫(xiě)順序
1.先執(zhí)行父類(lèi)靜態(tài)變量和靜態(tài)代碼塊,再執(zhí)行子類(lèi)靜態(tài)變量和靜態(tài)代碼塊 2.先執(zhí)行父類(lèi)普通變量和代碼塊,再執(zhí)行父類(lèi)構(gòu)造器(static方法) 3.先執(zhí)行子類(lèi)普通變量和代碼塊,再執(zhí)行子類(lèi)構(gòu)造器(static方法) 4.static方法初始化先于普通方法,靜態(tài)初始化只有在必要時(shí)刻才進(jìn)行且只初始化一次。注意:子類(lèi)的構(gòu)造方法,不管這個(gè)構(gòu)造方法帶不帶參數(shù),默認(rèn)的它都會(huì)先去尋找父類(lèi)的不帶參數(shù)的構(gòu)造方法。如果父類(lèi)沒(méi)有不帶參數(shù)的構(gòu)造方法,那么子類(lèi)必須用supper關(guān)鍵子來(lái)調(diào)用父類(lèi)帶參數(shù)的構(gòu)造方法,否則編譯不能通過(guò)。java內(nèi)存回收
垃圾回收器(4種收集器)和finalize()方法
java中垃圾回收器可以幫助程序猿自動(dòng)回收無(wú)用對(duì)象占據(jù)的內(nèi)存,但它只負(fù)責(zé)釋放java中創(chuàng)建的對(duì)象所占據(jù)的所有內(nèi)存,通過(guò)某種創(chuàng)建對(duì)象之外的方式為對(duì)象分配的內(nèi)存空間則無(wú)法被垃圾回收器回收;而且垃圾回收本身也有開(kāi)銷(xiāo),GC的優(yōu)先級(jí)比較低,所以如果JVM沒(méi)有面臨內(nèi)存耗盡,它是不會(huì)去浪費(fèi)資源進(jìn)行垃圾回收以恢復(fù)內(nèi)存的。最后我們會(huì)發(fā)現(xiàn),只要程序沒(méi)有瀕臨存儲(chǔ)空間用完那一刻,對(duì)象占用的空間就總也得不到釋放。我們可以通過(guò)代碼System.gc()來(lái)主動(dòng)啟動(dòng)一個(gè)垃圾回收器(雖然JVM不會(huì)立刻去回收),在釋放new分配內(nèi)存空間之前,將會(huì)通過(guò)finalize()釋放用其他方法分配的內(nèi)存空間。
finalize()方法的工作原理是:一旦垃圾回收器準(zhǔn)備好釋放對(duì)象占用的存儲(chǔ)空間,將首先調(diào)用并且只能調(diào)用一次該對(duì)象的finalize()方法(通過(guò)代碼System.gc()實(shí)現(xiàn)),并且在下一次垃圾回收動(dòng)作發(fā)生時(shí),才會(huì)真正回收對(duì)象占用的內(nèi)存。所以如果我們重載finalize()方法就能在垃圾回收時(shí)刻做一些重要的清理工作或者自救該對(duì)象一次(只要在finalize()方法中讓該對(duì)象重新和引用鏈上的任何一個(gè)對(duì)象建立關(guān)聯(lián)即可)。finalize()方法用于釋放用特殊方式分配的內(nèi)存空間,這是因?yàn)槲覀兛赡茉趈ava中調(diào)用非java代碼來(lái)分配內(nèi)存,比如Android開(kāi)發(fā)中調(diào)用NDK。那么,當(dāng)我們調(diào)用C中的malloc()函數(shù)分配了存儲(chǔ)空間,我們就只能用free()函數(shù)來(lái)釋放這些內(nèi)存,這樣就需要我們?cè)趂inalize()函數(shù)中用本地方法調(diào)用它。
對(duì)象內(nèi)存狀態(tài)&&引用形式及回收時(shí)機(jī)
-
java對(duì)象內(nèi)存狀態(tài)轉(zhuǎn)換圖
-
如何判斷java對(duì)象需要被回收?GC判斷方法
- 引用計(jì)數(shù),引用計(jì)數(shù)法記錄著每一個(gè)對(duì)象被其它對(duì)象所持有的引用數(shù),被引用一次就加一,引用失效就減一;引用計(jì)數(shù)器為0則說(shuō)明該對(duì)象不再可用;當(dāng)一個(gè)對(duì)象被回收后,被該對(duì)象所引用的其它對(duì)象的引用計(jì)數(shù)都應(yīng)該相應(yīng)減少,它很難解決對(duì)象之間的相互循環(huán)引用問(wèn)題循環(huán)引用實(shí)例
- 可達(dá)性分析算法:從GC Root對(duì)象向下搜索其所走過(guò)的路徑稱(chēng)為引用鏈,當(dāng)一個(gè)對(duì)象不再被任何的GC root對(duì)象引用鏈相連時(shí)說(shuō)明該對(duì)象不再可用,GC root對(duì)象包括四種:方法區(qū)中常量和靜態(tài)變量引用的對(duì)象,虛擬機(jī)棧中變量引用的對(duì)象,本地方法棧中引用的對(duì)象;?解決循環(huán)引用是因?yàn)?/strong>GC Root通常是一組特別管理的指針,這些指針是tracing GC的trace的起點(diǎn)。它們不是對(duì)象圖里的對(duì)象,對(duì)象也不可能引用到這些“外部”的指針。
- 采用引用計(jì)數(shù)算法的系統(tǒng)只需在每個(gè)實(shí)例對(duì)象創(chuàng)建之初,通過(guò)計(jì)數(shù)器來(lái)記錄所有的引用次數(shù)即可。而可達(dá)性算法,則需要再次GC時(shí),遍歷整個(gè)GC根節(jié)點(diǎn)來(lái)判斷是否回收
- java對(duì)象的四種引用
1.強(qiáng)引用?:創(chuàng)建一個(gè)對(duì)象并把這個(gè)對(duì)象直接賦給一個(gè)變量,eg :Person person = new Person(“sunny”); 不管系統(tǒng)資源有么的緊張,強(qiáng)引用的對(duì)象都絕對(duì)不會(huì)被回收,即使他以后不會(huì)再用到。
2.軟引用?:通過(guò)SoftReference類(lèi)實(shí)現(xiàn),eg : SoftReference?p = new SoftReference(new Person(“Rain”));內(nèi)存非常緊張的時(shí)候會(huì)被回收,其他時(shí)候不會(huì)被回收,所以在使用之前要判斷是否為null從而判斷他是否已經(jīng)被回收了。
3.弱引用?:通過(guò)WeakReference類(lèi)實(shí)現(xiàn),eg : WeakReference?p = new WeakReference(new Person(“Rain”));不管內(nèi)存是否足夠,系統(tǒng)垃圾回收時(shí)必定會(huì)回收
4.虛引用?:不能單獨(dú)使用,主要是用于追蹤對(duì)象被垃圾回收的狀態(tài),為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的是希望能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知。通過(guò)PhantomReference類(lèi)和引用隊(duì)列ReferenceQueue類(lèi)聯(lián)合使用實(shí)現(xiàn)
常見(jiàn)垃圾回收算法參考圖
- 停止-復(fù)制算法
這是一種非后臺(tái)回收算法,將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊,內(nèi)存浪費(fèi)嚴(yán)重.它先暫停程序的運(yùn)行,然后將所有存活的對(duì)象從當(dāng)前堆復(fù)制到另外一個(gè)堆,沒(méi)被復(fù)制的死對(duì)象則全部是垃圾,存活對(duì)象被復(fù)制到新堆之后全部緊密排列,就可以直接分配新空間了。此方法耗費(fèi)空間且效率低,適用于存活對(duì)象少。 - 標(biāo)記-清掃算法
同樣是非后臺(tái)回收算法,該算法從堆棧區(qū)和靜態(tài)域出發(fā),遍歷每一個(gè)引用去尋找所有需要回收的對(duì)象,對(duì)每個(gè)找到需要回收對(duì)象都進(jìn)行標(biāo)記。標(biāo)記結(jié)束之后,開(kāi)始清理工作,被標(biāo)記的對(duì)象都會(huì)被釋放掉,如果需要連續(xù)堆空間,則還需要對(duì)剩下的存貨對(duì)象進(jìn)行整理;否則會(huì)產(chǎn)生大量?jī)?nèi)存碎片 -
標(biāo)記-整理算法
先標(biāo)記需要回收的對(duì)象,但是不會(huì)直接清理那些可回收的對(duì)象,而是將存活對(duì)象向內(nèi)存區(qū)域的一端移動(dòng),然后清理掉端以外的內(nèi)存。適用于存活對(duì)象多。 -
分代算法
在新生代中,每次垃圾收集時(shí)都會(huì)發(fā)現(xiàn)有大量對(duì)象死去,只有少量存活,因此可選用停止復(fù)制算法來(lái)完成收集,而老年代中因?yàn)閷?duì)象存活率高、沒(méi)有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用標(biāo)記—清除算法或標(biāo)記—整理算法來(lái)進(jìn)行回收。
JVM性能調(diào)優(yōu)
Linux下面查看Jvm性能信息的命令
內(nèi)存相關(guān)問(wèn)題
參考鏈接
總結(jié)
以上是生活随笔為你收集整理的浅析java内存管理机制的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 一个面试官对JVM面试问题的分析
- 下一篇: 从表到里学习JVM实现