程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> Java 垃圾回收,java垃圾回收

Java 垃圾回收,java垃圾回收

編輯:JAVA綜合教程

Java 垃圾回收,java垃圾回收


當程序創建對象、數組等引用類型的實體時,系統會在堆內存中為這一對象分配一塊內存,對象就保存在這塊內存中,當這塊內存不再被任何引用變量引用時,這塊內存就變成垃圾,等待垃圾回收機制進行回收。垃圾回收機制具有三個特征:

  • 垃圾回收機制只負責回收堆內存中的對象,不會回收任何物理資源(例如數據庫連接,打開的文件資源等),也不會回收以某種創建對象的方式以外的方式為該對像分配的內存,(例如對象調用本地方法中malloc的方式申請的內存)
  • 程序無法精確控制垃圾回收的運行,只可以建議垃圾回收進行,建議的方式有兩種System.gc() 和Runtime.getRuntime().gc()
  • 在垃圾回收任何對象之前,總會先調用它的finalize()方法,但是同垃圾回收的時機一致,調用finalize()方法的時機也不確定。

針對以上三個特征,有三個問題:

1、必須手動的進行清理工作,釋放除創建對象的方式以外的方式分配的內存和其它的物理資源。並且要注意消除過期的對象引用,否則可能引起OOM。

手動清理通常用到try...finally...這樣的代碼結構。

示例如下:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class ManualClear {

    public static void main(String[] args) {
        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream("./src/ManualClear.java");
        } catch (FileNotFoundException e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
            return;
        }

        try {
            byte[] bbuf = new byte[1024];
            int hasRead = 0;
            try {
                while ((hasRead = fileInputStream.read(bbuf)) > 0) {
                    System.out.println(new String(bbuf, 0, hasRead));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        } finally {
            try {
                fileInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

對於過期對象的引用,引起的OOM通常有三種常見的情況,這三種情況通常都不易發現,短時間內運行也不會有什麼問題,但是時間久了後,洩漏的對象增加後終會引起程序崩潰。

  • 類自己管理內存時,要警惕內存洩漏

示例如下:

import java.util.Arrays;
import java.util.EmptyStackException;

class Stack{
    private Object[] elements;
    private int size;
    private static final int DEFAULT_INITAL_CAPACITY = 16;
    
    public Stack() {
        elements = new Object[DEFAULT_INITAL_CAPACITY];
    }
    
    public void push(Object e){
        ensureCapacity();
        elements[size++] = e;
    }
    
    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        
        return elements[--size];
    }
    
    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

public class StackDemo {
    
    public static void main(String[] args) {
        Stack stack = new Stack();
        
        for (int i = 0; i < 10000; i++) {
            stack.push(new Object());
        }
        
        for(int i = 0; i < 10000; i++) {
            stack.pop();
        }
    }

}

之所以會內存洩漏,是因為那些出棧的對象即使程序其它對象不再引用,但是Stack類中的elements[]數組依然保存著這些對象的引用,導致這些對象不會被垃圾回收所回收,所以,當需要類自己管理內存事,要警惕內部維護的這些過期引用是否被及時解除了引用,本例中只需在出棧後,顯示的將

elements[size] = null;即可。

  • 緩存是要警惕內存洩漏

出現這樣情況通常是一旦將對象放入緩存,很可能長時間不使用很容易遺忘,通常可以用WakeHashMap代表緩存,在緩存中的項過期後,他們可以被自動刪除。或者可以由一個後台線程定期執行來清除緩沖中的過期項。

  • 監聽器或回調的注冊,最好可以顯示的取消注冊。

2、不要手動調用finalize(),它是給垃圾回收器調用的

3、避免使用finalize()方法,除非用來作為判斷終結條件以發現對象中沒有被適當清理的部分;用來作為安全網在手動清理忘記調用的情況下清理系統資源,延後清理總別永不清理要強,並且如果同時記錄下忘記清理資源的信息的話,也方便後面發現錯誤,並及時修改忘記清理的代碼;釋放對象中本地方法獲得的不是很關鍵的系統資源。

finalize()方法由於其執行時間以及是否確定被執行都不能准確確保,所以最好不用來釋放關鍵資源,但是可用於上面所說的三種情況。其中第一種情況,示例如下:

class Book {
    boolean checkout = false;
    public Book(boolean checkout) {
        this.checkout = checkout;
    }
    
    public void checkin(){
        checkout = false;
    }
    
    @Override
    protected void finalize() throws Throwable {
        if (checkout) {
            System.out.println("Error: check out");
        }
    }
}

public class FinalizeCheckObjectUse {

    public static void main(String[] args) {
        new Book(true);
        System.gc();
    }

}

執行結果:

Error: check out

例子中的Book對象,在釋放前必須處於checkIn的狀態,否則不能釋放,finalize中的實現可以幫助及時發現不合法的對象,或者更直接的,在finalize中直接使用某個引用變量引用,使其重新進入reachable的狀態,然後再次對其進行處理。

另一點需要注意的時,子類如果覆蓋了父類的finalize方法,但是忘了手工調用super.finalize或者子類的finalize過程出現異常導致沒有執行到super.finalize時,那麼父類的終結方法將永遠不會調到。

如下:

class Parent{
    @Override
    protected void finalize() throws Throwable {
        System.out.println(getClass().getName() + " finalize start");
    }
}

class Son extends Parent{
    @Override
    protected void finalize() throws Throwable {
        System.out.println(getClass().getName() + " finalize start");
    }
}
public class SuperFinalizeLost {

    public static void main(String[] args) {
        new Son();
        System.gc();
    }

}

運行結果:

Son finalize start

或者

class Parent{
    @Override
    protected void finalize() throws Throwable {
        System.out.println(getClass().getName() + " finalize start");
    }
}

class Son extends Parent{
    @Override
    protected void finalize() throws Throwable {
        System.out.println(getClass().getName() + " finalize start");
        int i = 5 / 0;
        super.finalize();
    }
}
public class SuperFinalizeLost {

    public static void main(String[] args) {
        new Son();
        System.gc();
    }

}

執行結果:

Son finalize start

對於第二種情況,可以使用try...finally...結構解決,但是對於第一種情況,最好使用一種叫終結方法守護者的方式。示例如下

class Parent2{
    private final Object finalizeGuardian = new Object() {
        protected void finalize() throws Throwable {
            System.out.println("在此執行父類終結方法中的邏輯");
        };
    };
}

class Son2 extends Parent2{
    @Override
    protected void finalize() throws Throwable {
        System.out.println(getClass().getName() + " finalize start");
        int i = 5 / 0;
        super.finalize();
    }
}

public class FinalizeGuardian {

    public static void main(String[] args) {
        new Son2();
        System.gc();
    }

}

執行結果:

在此執行父類終結方法中的邏輯
Son2 finalize start

這樣可以保證父類的終結方法中所需做的操作執行到。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved