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