堆與內存優化
今天測了一個項目的數據自動整理功能,對數據庫中幾萬條記錄及圖片進行整理操作,運行接近到最后,爆出了java.lang.outOfMemoryError,java heap space方面的錯誤,以前寫程序很少遇到這種內存上的錯誤,因為java有垃圾回收器機制,就一直沒太關注。今天上網找了點資料,在此基礎上做了個整理。
一、堆和棧
堆—用new建立,垃圾回收器負責回收
1、程序開始運行時,JVM從OS獲取一些內存,部分是堆內存。堆內存通常在存儲地址的底層,向上排列。
2、堆是一個"運行時"數據區,類實例化的對象就是從堆上去分配空間的;
3、在堆上分配空間是通過"new"等指令建立的,堆是動態分配的內存大小,生存期也不必事先告訴編譯器;
4、與C++不同的是,Java自動管理堆和棧,垃圾回收器可以自動回收不再使用的堆內存;
5、缺點是,由于要在運行時動態分配內存,所以內存的存取速度較慢。
棧—存放基本類型和引用類型,速度快
1、先進后出的數據結構,通常用于保存方法中的參數,局部變量;
2、在java中,所有基本類型(short,int, long, byte, float, double,boolean, char)和引用類型的變量都在棧中存儲;
3、棧中數據的生存空間一般在當前scopes內(由{...}括起來的區域;
4、棧的存取速度比堆要快,僅次于直接位于CPU中的寄存器;
5、棧中的數據可以共享,多個引用可以指向同一個地址;
6、缺點是,棧的數據大小與生存期必須是確定的,缺乏靈活性。
二、內存設置
1、查看虛擬機內存情況
1
2
|
long maxControl = Runtime.getRuntime().maxMemory(); //獲取虛擬機可以控制的最大內存數量 long currentUse = Runtime.getRuntime().totalMemory(); //獲取虛擬機當前已使用的內存數量 |
默認情況下,java虛擬機的maxControl=66650112B=63.5625M;
什么都不做的情況,在我的機子上測得的currentUse=5177344B=4.9375M;
2、設置內存大小的命令
-Xms<size> set initial Java heap size :設置JVM初始化堆內存大小;此值可以設置與-Xmx相同,以避免每次垃圾回收完成后JVM重新分配內存。
-Xmx<size> set maximum Java heap size:設置JVM最大的堆內存大小;
-Xmn<size>:設置年輕代大小,整個堆大小=年輕代大小+ 年老代大小+ 持久代大小。
-Xss<size> set java thread stack size:設置JVM線程棧內存大小;
3、具體操作
(1)JVM內存設置:
打開MyEclipse(Eclipse) window-preferences-Java -Installed JREs -Edit -Default VM Arguments
在VM自變量中輸入:-Xmx128m -Xms64m -Xmn32m -Xss16m
(2)IDE內存設置:
在MyEclipse根目錄下的myeclipse.ini(或Eclipse根目錄下的eclipse.ini)中修改-vmargs 下的配置:
(3)Tomcat內存設置
打開Tomcat根目錄下的bin文件夾,編輯catalina.bat
修改為:set JAVA_OPTS= -Xms256m -Xmx512m
三、Java堆中的OutOfMemoryError錯誤分析
當JVM啟動時,使用了-Xms 參數設置的堆內存。當程序繼續進行,創建更多對象,JVM開始擴大堆內存以容納更多對象。JVM也會使用垃圾回收器來回收內存。當快達到-Xmx設置的最大堆內存時,如果沒有更多的內存可被分配給新對象的話,JVM就會拋出java.lang.outofmemoryerror,程序就會宕掉。在拋出 OutOfMemoryError之前,JVM會嘗試著用垃圾回收器來釋放足夠的空間,但是發現仍舊沒有足夠的空間時,就會拋出這個錯誤。為了解決這個問題,需要清楚程序對象的信息,例如,你創建了哪些對象,哪些對象占用了多少空間等等。可以使用profiler或者堆分析器來處理OutOfMemoryError錯誤。"java.lang.OutOfMemoryError: Java heap space”表示堆沒有足夠的空間了,不能繼續擴大了。"java.lang.OutOfMemoryError: PermGen space”表示permanent generation已經裝滿了,你的程序不能再裝載類或者再分配一個字符串了。
四、堆和垃圾回收
我們知道對象創建在堆內存中,垃圾回收這樣一個進程,它將已死對象清除出堆空間,并將這些內存再還給堆。為了給垃圾回收器使用,堆主要分成三個區域,分別叫作New Generation,Old Generation或叫Tenured Generation,以及Perm space。New Generation是用來存放新建的對象的空間,在對象新建的時候被使用。如果長時間還使用的話,它們會被垃圾回收器移動到Old Generation(或叫Tenured Generation)。Perm space是JVM存放Meta數據的地方,例如類,方法,字符串池和類級別的詳細信息。
五、總結:
1、Java堆內存是操作系統分配給JVM的內存的一部分。
2、當我們創建對象時,它們存儲在Java堆內存中。
3、為了便于垃圾回收,Java堆空間分成三個區域,分別叫作New Generation, Old Generation或叫作Tenured Generation,還有Perm Space。
4、你可以通過用JVM的命令行選項 -Xms, -Xmx, -Xmn來調整Java堆空間的大小。
5、可以用JConsole或者Runtime.maxMemory(),Runtime.totalMemory(),Runtime.freeMemory()來查看Java中堆內存的大小。
6、可以使用命令“jmap”來獲得heap dump,用“jhat”來分析heap dump。
7、Java堆空間不同于棧空間,棧空間是用來儲存調用棧和局部變量的。
8、Java垃圾回收器是用來將死掉的對象(不再使用的對象)所占用的內存回收回來,再釋放到Java堆空間中。
9、當遇到java.lang.outOfMemoryError時,不必緊張,有時候僅僅增加堆空間就可以了,但如果經常出現的話,就要看看Java程序中是不是存在內存泄露了。
10、使用Profiler和Heap dump分析工具來查看Java堆空間,可以查看給每個對象分配了多少內存。
棧存儲詳解
Java棧存儲具有以下幾個特點:
一、存在棧中的數據大小和生命周期必須是確定的。
如基本類型的存儲:int a = 1; 這種變量存的是字面值,a是一個指向int類型的引用,指向3這個字面值。這些字面值的數據,由于大小可知,生存期可知(這些字面值固定定義在某個程序塊里面,程序塊退出后,字面值就消失了),出于追求速度的原因,就存在于棧中。
二、存在棧中的數據可以共享。
(1)、基本類型數據存儲:
如:
1
2
|
int a = 3 ; int b = 3 ; |
編譯器先處理int a = 3;首先它會在棧中創建一個變量為a的引用,然后查找有沒有字面值為3的地址,沒找到,就開辟一個存放3這個字面值的地址,然后將a指向3的地址。接著處理int b = 3;在創建完b的引用變量后,由于在棧中已經有3這個字面值,便將b直接指向3的地址。這樣,就出現了a與b同時均指向3的情況。
注意:這種字面值的引用與類對象的引用不同。假定兩個類對象的引用同時指向一個對象,如果一個對象引用變量修改了這個對象的內部狀態,那么另一個對象引用變量也即刻反映出這個變化。相反,通過字面值的引用來修改其值,不會導致另一個指向此字面值的引用的值也跟著改變的情況。如上例,我們定義完a 與b的值后,再令a=4;那么,b不會等于4,還是等于3。在編譯器內部,遇到a=4;時,它就會重新搜索棧中是否有4的字面值,如果沒有,重新開辟地址存放4的值;如果已經有了,則直接將a指向這個地址。因此a值的改變不會影響到b的值。
(2)、包裝類數據存儲:
如Integer, Double, String等將相應的基本數據類型包裝起來的類。這些類數據全部存在于堆中,Java用new()語句來顯示地告訴編譯器,在運行時才根據需要動態創建,因此比較靈活,但缺點是要占用更多的時間。
如:以String為例。
String是一個特殊的包裝類數據。即可以用String str = new String("abc");的形式來創建,也可以用String str = "abc";的形式來創建。前者是規范的類的創建過程,即在Java中,一切都是對象,而對象是類的實例,全部通過new()的形式來創建。Java 中的有些類,如DateFormat類,可以通過該類的getInstance()方法來返回一個新創建的類,似乎違反了此原則。其實不然。該類運用了單例模式來返回類的實例,只不過這個實例是在該類內部通過new()來創建的,而getInstance()向外部隱藏了此細節。
那為什么在String str = "abc";中,并沒有通過new()來創建實例,是不是違反了上述原則?其實沒有。
關于String str = "abc"的內部工作。Java內部將此語句轉化為以下幾個步驟:
a、先定義一個名為str的對String類的對象引用變量:String str;
b、在棧中查找有沒有存放值為"abc"的地址,如果沒有,則開辟一個存放字面值為"abc"的地址,接著創建一個新的String類的對象O,并將O的字符串值指向這個地址,而且在棧中這個地址旁邊記下這個引用的對象O。如果已經有了值為"abc"的地址,則查找對象O,并返回O的地址。
c、將str指向對象O的地址。
值得注意的是,通常String類中字符串值都是直接存值的。但像String str = "abc";這種場合下,其字符串值卻是保存了一個指向存在棧中數據的引用(即:String str = "abc";既有棧存儲,又有堆存儲)。
為了更好地說明這個問題,我們可以通過以下的幾個代碼進行驗證。
1
2
3
|
String str1 = "abc" ; String str2 = "abc" ; System.out.println(str1==str2); //true |
(只有在兩個引用都指向了同一個對象時才返回真值。str1與str2是否都指向了同一個對象)
結果說明,JVM創建了兩個引用str1和str2,但只創建了一個對象,而且兩個引用都指向了這個對象。
1
2
3
4
5
|
String str1 = "abc" ; String str2 = "abc" ; str1 = "bcd" ; System.out.println(str1 + "," + str2); //bcd, abc System.out.println(str1==str2); //false |
這就是說,賦值的變化導致了類對象引用的變化,str1指向了另外一個新對象,而str2仍舊指向原來的對象。上例中,當我們將str1的值改為"bcd"時,JVM發現在棧中沒有存放該值的地址,便開辟了這個地址,并創建了一個新的對象,其字符串的值指向這個地址。
事實上,String類被設計成為不可改變(immutable)的類。如果你要改變其值,可以,但JVM在運行時根據新值悄悄創建了一個新對象(沒法在原來內存的基礎上改變其值),然后將這個對象的地址返回給原來類的引用。這個創建過程雖說是完全自動進行的,但它畢竟占用了更多的時間。在對時間要求比較敏感的環境中,會帶有一定的不良影響。
1
2
3
4
5
6
7
|
String str1 = "abc" ; String str2 = "abc" ; str1 = "bcd" ; String str3 = str1; System.out.println(str3); //bcd String str4 = "bcd" ; System.out.println(str1 == str4); //true |
str3這個對象的引用直接指向str1所指向的對象(注意,str3并沒有創建新對象)。當str1改完其值后,再創建一個String的引用str4,并指向因str1修改值而創建的新的對象。可以發現,這回str4也沒有創建新的對象,從而再次實現棧中數據的共享。
1
2
3
|
String str1 = new String( "abc" ); String str2 = "abc" ; System.out.println(str1==str2); //false |
創建了兩個引用。創建了兩個對象。兩個引用分別指向不同的兩個對象。
1
2
3
|
String str1 = "abc" ; String str2 = new String( "abc" ); System.out.println(str1==str2); //false |
創建了兩個引用。創建了兩個對象。兩個引用分別指向不同的兩個對象。
以上兩段代碼說明,只要是用new()來新建對象的,都會在堆中創建,而且其字符串是單獨存值的,即使與棧中的數據相同,也不會與棧中的數據共享。
總結:
(1)我們在使用諸如String str = "abc";的格式定義類時,總是想當然地認為,我們創建了String類的對象str。擔心陷阱!對象可能并沒有被創建!唯一可以肯定的是,指向 String類的引用被創建了。至于這個引用到底是否指向了一個新的對象,必須根據上下文來考慮,除非你通過new()方法來顯要地創建一個新的對象。因此,更為準確的說法是,我們創建了一個指向String類的對象的引用變量str,這個對象引用變量指向了某個值為"abc"的String類。清醒地認識到這一點對排除程序中難以發現的bug是很有幫助的。
(2)使用String str = "abc";的方式,可以在一定程度上提高程序的運行速度,因為JVM會自動根據棧中數據的實際情況來決定是否有必要創建新對象。而對于String str = new String("abc");的代碼,則一概在堆中創建新對象,而不管其字符串值是否相等,是否有必要創建新對象,從而加重了程序的負擔。
(3)由于String類的immutable性質(因為包裝類的值不可修改),當String變量需要經常變換其值時,應該考慮使用StringBuffer類,以提高程序效率。