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