剛剛做完了一個項目的性能測試,“有幸”也遇到了內存泄露的案例,所以在此和大家分享一下。
主要從以下幾部分來說明,關于內存和內存泄露、溢出的概念,區分內存泄露和內存溢出;內存的區域劃分,了解GC回收機制;重點關注如何去監控和發現內存問題;此外分析出問題還要如何解決內存問題。
下面就開始本篇的內容:
第一部分 概念
眾所周知,java中的內存java虛擬機自己去管理的,他不想C++需要自己去釋放?;\統地去講,java的內存分配分為兩個部分,一個是數據堆,一個是棧。程序在運行的時候一般分配數據堆,把局部的臨時的變量都放進去,生命周期和進程有關系。但是如果程序員聲明了static的變量,就直接在棧中運行的,進程銷毀了,不一定會銷毀static變量。
另外為了保證java內存不會溢出,java中有垃圾回收機制。 System.gc()即垃圾收集機制是指jvm用于釋放那些不再使用的對象所占用的內存。java語言并不要求jvm有gc,也沒有規定gc如何工作。垃圾收集的目的在于清除不再使用的對象。gc通過確定對象是否被活動對象引用來確定是否收集該對象。
而其中,內存溢出就是你要求分配的java虛擬機內存超出了系統能給你的,系統不能滿足需求,于是產生溢出。
內存泄漏是指你向系統申請分配內存進行使用(new),可是使用完了以后卻不歸還(delete),結果你申請到的那塊內存你自己也不能再訪問,該塊已分配出來的內存也無法再使用,隨著服務器內存的不斷消耗,而無法使用的內存越來越多,系統也不能再次將它分配給需要的程序,產生泄露。一直下去,程序也逐漸無內存使用,就會溢出。
第二部分 原理
JAVA垃圾回收及對內存區劃分
在Java虛擬機規范中,提及了如下幾種類型的內存空間:
◇ 棧內存(Stack):每個線程私有的。
◇ 堆內存(Heap):所有線程公用的。
◇ 方法區(Method Area):有點像以前常說的“進程代碼段”,這里面存放了每個加載類的反射信息、類函數的代碼、編譯時常量等信息。
◇ 原生方法棧(Native Method Stack):主要用于JNI中的原生代碼,平時很少涉及。
而Java的使用的是堆內存,java堆是一個運行時數據區,類的實例(對象)從中分配空間。Java虛擬機(JVM)的堆中儲存著正在運行的應用程序所建立的所有對象,“垃圾回收”也是主要是和堆內存(Heap)有關。
垃圾回收的概念就是JAVA虛擬機(JVM)回收那些不再被引用的對象內存的過程。一般我們認為正在被引用的對象狀態為“alive”,而沒有被應用或者取不到引用屬性的對象狀態為“dead”。垃圾回收是一個釋放處于”dead”狀態的對象的內存的過程。而垃圾回收的規則和算法被動態的作用于應用運行當中,自動回收。
JVM的垃圾回收器采用的是一種分代(generational )回收策略,用較高的頻率對年輕的對象(young generation)進行掃描和回收,這種叫做minor collection,而對老對象(old generation)的檢查回收頻率要低很多,稱為major collection。這樣就不需要每次GC都將內存中所有對象都檢查一遍,這種策略有利于實時觀察和回收。
(Sun JVM 1.3 有兩種最基本的內存收集方式:一種稱為copying或scavenge,將所有仍然生存的對象搬到另外一塊內存后,整塊內存就可回收。這種方法有效率,但需要有一定的空閑內存,拷貝也有開銷。這種方法用于minor collection。另外一種稱為mark-compact,將活著的對象標記出來,然后搬遷到一起連成大塊的內存,其他內存就可以回收了。這種方法不需要占用額外的空間,但速度相對慢一些。這種方法用于major collection. )
一些對象被創建出來只是擁有短暫的生命周期,比如 iterators 和本地變量。
另外一些對象被創建是擁有很長的生命周期,比如 高持久化對象等。
垃圾回收器的分代策略是把內存區劃分為幾個代,然后為每個代分配一到多個內存區塊。當其中一個代用完了分配給他的內存后,JVM會在分配的內存區內執行一個局部的GC(也可以叫minor collection)操作,為了回收處于“dead”狀態的對象所占用的內存。局部GC通常要不Full GC要快很多。
JVM定義了兩個代,年輕代(yong generation)(有時稱為“nursery”托兒所)和老年代(old generation)。年輕代包括 “Eden space(伊甸園)”和兩個“survivor spaces”。虛擬內存初始化的時候會把所有對象都分配到 Eden space,并且大部分對象也會在該區域被釋放。 當進行 minor GC的時候,VM會把剩下的沒有釋放的對象從Eden space移動到其中一個survivor spaces當中。此外,VM也會把那些長期存活在survivor spaces 里的對象移動到 老生代的“tenured” space中。當 tenured generation 被填滿后,就會產生Full GC,Full GC會相對比較慢因為回收的內容包括了所有的 live狀態的對象。pemanet generation這個代包括了所有java虛擬機自身使用的相對比較穩定的數據對象,比如類和對象方法等。
關于代的劃分,可以從下圖中獲得一個概況:
如果垃圾回收器影響了系統的性能,或者成為系統的瓶頸,你可以通過自定義各個代的大小來優化它的性能。使用JConsole,可以方便的查看到當前應用所配置的垃圾回收器的各個參數。想要獲得更詳細的參數,可以參考以下調優介紹:
Tuning Garbage collection with the 5.0 HotSpot VM
http://java.sun.com/docs/hotspot/gc/index.html
最后,總結一下各區內存:
Eden Space (heap): 內存最初從這個線程池分配給大部分對象。
Survivor Space (heap):用于保存在eden space內存池中經過垃圾回收后沒有被回收的對象。
Tenured Generation (heap):用于保持已經在 survivor space內存池中存在了一段時間的對象。
Permanent Generation (non-heap): 保存虛擬機自己的靜態(refective)數據,例如類(class)和方法(method)對象。Java虛擬機共享這些類數據。這個區域被分割為只讀的和只寫的,