java中的各種數據類型在內存中存儲的方式 二

6. 垃圾回收機制

問題一:什麼叫垃圾回收機制?
垃圾回收是一種動態存儲管理技術,它自動地釋放不再被程序引用的對象,按照特定的垃圾收集算法來實現資源自動回收的功能。當一個對象不再被引用的時候,內存回收它佔領的空間,以便空間被後來的新對象使用,以免造成內存泄露。

問題二:java的垃圾回收有什麼特點?
jAVA
語言不允許程序員直接控制內存空間的使用。內存空間的分配和回收都是由JRE負責在後臺自動進行的,尤其是無用內存空間的回收操作(garbagecollection,也稱垃圾回收),只能由運行環境提供的一個超級線程進行監測和控制。

問題三:垃圾回收器什麼時候會運行?
一般是在CPU空閒或空間不足時自動進行垃圾回收,而程序員無法精確控制垃圾回收的時機和順序等。、

問題四:什麼樣的對象符合垃圾回收條件?
當沒有任何獲得線程能訪問一個對象時,該對象就符合垃圾回收條件。

問題五:垃圾回收器是怎樣工作的?
垃圾回收器如發現一個對象不能被任何活線程訪問時,他將認爲該對象符合刪除條件,就將其加入回收隊列,但不是立即銷燬對象,何時銷燬並釋放內存是無法預知的。垃圾回收不能強制執行,然而java提供了一些方法(如:System.gc()方法),允許你請求JVM執行垃圾回收,而不是要求,虛擬機會盡其所能滿足請求,但是不能保證JVM從內存中刪除所有不用的對象。

問題六:一個java程序能夠耗盡內存嗎?
可以。垃圾收集系統嘗試在對象不被使用時把他們從內存中刪除。然而,如果保持太多活的對象,系統則可能會耗盡內存。垃圾回收器不能保證有足夠的內存,只能保證可用內存儘可能的得到高效的管理。

問題七:如何顯示的使對象符合垃圾回收條件?
(1)
空引用:當對象沒有對他可到達引用時,他就符合垃圾回收的條件。也就是說如果沒有對他的引用,刪除對象的引用就可以達到目的,因此我們可以把引用變量設置爲null,來符合垃圾回收的條件。

1.StringBuffer sb =newStringBuffer("hello");

2.System.out.println(sb);

3.sb=null;

(2)重新爲引用變量賦值:可以通過設置引用變量引用另一個對象來解除該引用變量與一個對象間的引用關係。
StringBuffer sb1 = new StringBuffer(“hello”);
StringBuffer sb2 = new StringBuffer(“goodbye”);
System.out.println(sb1);
sb1=sb2;//
此時”hello”符合回收條件
(3)
方法內創建的對象:所創建的局部變量僅在該方法的作用期間內存在。一旦該方法返回,在這個方法內創建的對象就符合垃圾收集條件。有一種明顯的例外情況,就是方法的返回對象。

1.publicstaticvoid main(String[] args){

2.Date d = getDate();

3.System.out.println("d="+d);

4.}

5.privatestaticDate getDate(){

6.Date d2 =newDate();

7.StringBuffer now =newStringBuffer(d2.toString());

8.System.out.println(now);

9.return d2;

10.}

(4)隔離引用:這種情況中,被回收的對象仍具有引用,這種情況稱作隔離島。若存在這兩個實例,他們互相引用,並且這兩個對象的所有其他引用都刪除,其他任何線程無法訪問這兩個對象中的任意一個。也可以符合垃圾回收條件。

1.publicclassIsland{

2.Island i;

3.publicstaticvoid main(String[] args){

4.Island i2 =newIsland();

5.Island i3 =newIsland();

6.Island i4 =newIsland();

7.i2. i =i3;

8.i3. i =i4;

9.i4. i =i2;

10.i2=null;

11.i3=null;

12.i4=null;

13.}

14.}

問題八:垃圾收集前進行清理——finalize()方法
java
提供了一種機制,使你能夠在對象剛要被垃圾回收之前運行一些代碼。這段代碼位於名爲finalize()的方法內,所有類從Object類繼承這個方法。由於不能保證垃圾回收器會刪除某個對象。因此放在finalize()中的代碼無法保證運行。因此建議不要重寫finalize();

7.final問題

final使得被修飾的變量不變,但是由於對象型變量的本質是引用,使得不變也有了兩種含義:引用本身的不變和引用指向的對象不變。
引用本身的不變:

1.finalStringBuffer a=newStringBuffer("immutable");

2.finalStringBuffer b=newStringBuffer("notimmutable");

3.a=b;//編譯期錯誤

4.finalStringBuffer a=newStringBuffer("immutable");

5.finalStringBuffer b=newStringBuffer("notimmutable");

a=b;//編譯期錯誤

引用指向的對象不變:

1.finalStringBuffer a=newStringBuffer("immutable");

2.a.append"broken!");//編譯通過

3.finalStringBuffer a=newStringBuffer("immutable");

4.a.append("broken!");//編譯通過

可見,final只對引用的”(也即它所指向的那個對象的內存地址)有效,它迫使引用只能指向初始指向的那個對象,改變它的指向會導致編譯期錯誤。至於它所指向的對象的變化,final是不負責的。這很類似==操作符:==操作符只負責引用的相等,至於這個地址所指向的對象內容是否相等,==操作符是不管的。在舉一個例子:

1.publicclassName{

2.privateString firstname;

3.privateString lastname;

4.publicString getFirstname(){

5.return firstname;

6.}

7.publicvoid setFirstname(String firstname){

8.this.firstname = firstname;

9.}

10.publicString getLastname(){

11.return lastname;

12.}

13.publicvoid setLastname(String lastname){

14.this.lastname = lastname;

15.}

16.}

17.

18.publicclassName{

19.privateString firstname;

20.privateString lastname;

21.publicString getFirstname(){

22.return firstname;

23.}

24.publicvoid setFirstname(String firstname){

25.this.firstname = firstname;

26.}

27.publicString getLastname(){

28.return lastname;

29.}

30.publicvoid setLastname(String lastname){

31.this.lastname = lastname;

32.}

33.}

編寫測試方法:

1.publicstaticvoid main(String[] args){

2.finalName name =newName();

3.name.setFirstname("JIM");

4.name.setLastname("Green");

5.System.out.println(name.getFirstname()+" "+name.getLastname());

6.}

7.publicstaticvoid main(String[] args){

8.finalName name =newName();

9.name.setFirstname("JIM");

10.name.setLastname("Green");

11.System.out.println(name.getFirstname()+" "+name.getLastname());

12.}

理解final問題有很重要的含義。許多程序漏洞都基於此—-final只能保證引用永遠指向固定對象,不能保證那個對象的狀態不變。在多線程的操作中,一個對象會被多個線程共享或修改,一個線程對對象無意識的修改可能會導致另一個使用此對象的線程崩潰。一個錯誤的解決方法就是在此對象新建的時候把它聲明爲final,意圖使得它永遠不變。其實那是徒勞的.final還有一個值得注意的地方,
先看以下示例程序:

1.classSomething{

2.finalint i ;

3.publicvoid doSomething(){

4.System.out.println("i = "+ i );

5.}

6.}

7.classSomething{

8.finalint i;

9.publicvoid doSomething(){

10.System.out.println("i = "+ i);

11.}

12.}

對於類變量,java虛擬機會自動進行初始化。如果給出了初始值,則初始化爲該初始值。如果沒有給出,則把它初始化爲該類型變量的默認初始值。但是對於用final修飾的類變量,虛擬機不會爲其賦予初值,必須在constructor(構造器)結束之前被賦予一個明確的值。可以修改爲”finalint i = 0;”

8.如何把程序寫得更健壯

(1)儘早釋放無用對象的引用。
好的辦法是使用臨時變量的時候,讓引用變量在退出活動域後,自動設置爲null,暗示垃圾收集器來收集該對象,防止發生內存泄露。對於仍然有指針指向的實例,jvm就不會回收該資源,因爲垃圾回收會將值爲null的對象作爲垃圾,提高GC回收機制效率;

(2)定義字符串應該儘量使用String str=”hello”;的形式,避免使用String str = new String(“hello”);的形式。因爲要使用內容相同的字符串,不必每次都new一個String。例如我們要在構造器中對一個名叫sString引用變量進行初始化,把它設置爲初始值,應當這樣做:

1.publicclassDemo{

2.privateString s;

3.publicDemo(){

4.s ="InitialValue";

5.}

6.}

7.

8.publicclassDemo{

9.privateString s;

10....

11.publicDemo{

12.s ="InitialValue";

13.}

14....

15.}

16.而非

17.s =newString("Initial Value");

18.s =newString("InitialValue");

後者每次都會調用構造器,生成新對象,性能低下且內存開銷大,並且沒有意義,因爲String對象不可改變,所以對於內容相同的字符串,只要一個String對象來表示就可以了。也就說,多次調用上面的構造器創建多個對象,他們的String類型屬性s都指向同一個對象。

(3)我們的程序裏不可避免大量使用字符串處理,避免使用String,應大量使用StringBuffer,因爲String被設計成不可變(immutable)類,所以它的所有對象都是不可變對象,請看下列代碼;

1.String s ="Hello";

2.s = s +" world!";

3.String s ="Hello";

4.s = s +" world!";

在這段代碼中,s原先指向一個String對象,內容是”Hello”,然後我們對s進行了+操作,那麼s所指向的那個對象是否發生了改變呢?答案是沒有。這時,s不指向原來那個對象了,而指向了另一個String對象,內容爲”Hello world!”,原來那個對象還存在於內存之中,只是s這個引用變量不再指向它了。

通過上面的說明,我們很容易導出另一個結論,如果經常對字符串進行各種各樣的修改,或者說,不可預見的修改,那麼使用String來代表字符串的話會引起很大的內存開銷。因爲String對象建立之後不能再改變,所以對於每一個不同的字符串,都需要一個String對象來表示。這時,應該考慮使用StringBuffer類,它允許修改,而不是每個不同的字符串都要生成一個新的對象。並且,這兩種類的對象轉換十分容易。

(4)儘量少用靜態變量,因爲靜態變量是全局的,GC不會回收的;

(5)儘量避免在類的構造函數裏創建、初始化大量的對象,防止在調用其自身類的構造器時造成不必要的內存資源浪費,尤其是大對象,JVM會突然需要大量內存,這時必然會觸發GC優化系統內存環境;顯示的聲明數組空間,而且申請數量還極大。
以下是初始化不同類型的對象需要消耗的時間:

運算操作

示例

標準化時間

本地賦值

i = n

1.0

實例賦值

this.i = n

1.2

方法調用

Funct()

5.9

新建對象

New Object()

980

新建數組

New int[10]

3100

從表中可以看出,新建一個對象需要980個單位的時間,是本地賦值時間的980倍,是方法調用時間的166倍,而新建一個數組所花費的時間就更多了。

(6)儘量在合適的場景下使用對象池技術以提高系統性能,縮減縮減開銷,但是要注意對象池的尺寸不宜過大,及時清除無效對象釋放內存資源,綜合考慮應用運行環境的內存資源限制,避免過高估計運行環境所提供內存資源的數量。

(7)大集合對象擁有大數據量的業務對象的時候,可以考慮分塊進行處理,然後解決一塊釋放一塊的策略。

(8)不要在經常調用的方法中創建對象,尤其是忌諱在循環中創建對象。可以適當的使用hashtablevector創建一組對象容器,然後從容器中去取那些對象,而不用每次new之後又丟棄。

(9)一般都是發生在開啓大型文件或跟數據庫一次拿了太多的數據,造成Out OfMemory Error的狀況,這時就大概要計算一下數據量的最大值是多少,並且設定所需最小及最大的內存空間值。

(10)儘量少用finalize函數,因爲finalize()會加大GC的工作量,而GC相當於耗費系統的計算能力。

(11)不要過濫使用哈希表,有一定開發經驗的開發人員經常會使用hash表(hash表在JDK中的一個實現就是HashMap)來緩存一些數據,從而提高系統的運行速度。比如使用HashMap緩存一些物料信息、人員信息等基礎資料,這在提高系統速度的同時也加大了系統的內存佔用,特別是當緩存的資料比較多的時候。其實我們可以使用操作系統中的緩存的概念來解決這個問題,也就是給被緩存的分配一個一定大小的緩存容器,按照一定的算法淘汰不需要繼續緩存的對象,這樣一方面會因爲進行了對象緩存而提高了系統的運行效率,同時由於緩存容器不是無限制擴大,從而也減少了系統的內存佔用。現在有很多開源的緩存實現項目,比如ehcacheoscache等,這些項目都實現了FIFO MRU等常見的緩存算法。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章