memory-leaks java記憶體洩漏 memory - 使用Java創建內存洩漏



15 Answers

靜態字段保持對象引用[esp final field]

class MemorableClass {
    static final ArrayList list = new ArrayList(100);
}

在冗長的String上調用String.intern()

String str=readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can't remove
str.intern();

(未封閉的)開放流(文件,網絡等......)

try {
    BufferedReader br = new BufferedReader(new FileReader(inputFile));
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

未封閉的連接

try {
    Connection conn = ConnectionFactory.getConnection();
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

JVM的垃圾收集器無法訪問的區域 ,例如通過本機方法分配的內存

在Web應用程序中,某些對象存儲在應用程序範圍中,直到明確停止或刪除應用程序。

getServletContext().setAttribute("SOME_MAP", map);

不正確或不適當的JVM選項 ,例如IBM JDK上的noclassgc選項,用於防止未使用的類垃圾回收

請參閱IBM jdk設置

example detection thread

我剛接受采訪,並被要求用Java創建內存洩漏。 毋庸置疑,我覺得自己很傻,甚至不知道如何開始創建一個。

一個例子是什麼?




下面將有一個非顯而易見的案例,其中Java洩漏,除了被遺忘的偵聽器的標準情況,靜態引用,哈希映射中的虛假/可修改鍵,或者只是沒有任何機會結束其生命週期的線程。

  • File.deleteOnExit() - 總是洩漏字符串, 如果字符串是子字符串,則洩漏更嚴重(底層char []也洩漏) - 在Java 7子串中也複製了char[] ,所以後者不適用 ; @Daniel,不需要投票。

我將專注於線程,以顯示大多數非託管線程的危險,不希望甚至觸摸揮桿。

  • Runtime.addShutdownHook並沒有刪除...然後甚至使用removeShutdownHook由於ThreadGroup類中的一個關於未啟動線程的錯誤,它可能無法收集,有效地洩漏了ThreadGroup。 JGroup在GossipRouter中有漏洞。

  • 創建但未啟動的Thread與上面的類別相同。

  • 創建一個線程繼承ContextClassLoaderAccessControlContext ,加上ThreadGroup和任何InheritedThreadLocal ,所有這些引用都是潛在的洩漏,以及類加載器和所有靜態引用加載的整個類,以及ja-ja。 整個jucExecutor框架具有超級簡單的ThreadFactory接口,但效果特別明顯,但大多數開發人員都不知道潛伏的危險。 此外,許多庫都會根據請求啟動線程(太多行業流行的庫)。

  • ThreadLocal緩存; 那些在許多情況下是邪惡的。 我確信每個人都已經看到了很多基於ThreadLocal的簡單緩存,這也是壞消息:如果線程持續超過預期生命的上下文ClassLoader,那麼這是一個純粹的小漏洞。 除非確實需要,否則不要使用ThreadLocal緩存。

  • 當ThreadGroup本身沒有線程時調用ThreadGroup.destroy() ,但它仍保留子ThreadGroups。 一個錯誤的洩漏將阻止ThreadGroup從其父級中刪除,但所有子級都變為不可枚舉。

  • 使用WeakHashMap和值(in)直接引用鍵。 沒有堆轉儲,這是一個很難找到的。 這適用於所有可能將硬引用保留回受保護對象的擴展Weak/SoftReference

  • 使用帶有HTTP(S)協議的java.net.URL並從(!)加載資源。 這個是特殊的, KeepAliveCache在系統ThreadGroup中創建一個新線程,它洩漏當前線程的上下文類加載器。 當沒有活動線程存在時,線程是在第一個請求時創建的,所以你可能會幸運或只是洩漏。 洩漏已經在Java 7中得到修復,並且創建線程的代碼正確地刪除了上下文類加載器。 還有更多的案例( 喜歡ImageFetcher 也固定 )創建類似的線程。

  • 使用PNGImageDecoder在構造函數中傳遞new java.util.zip.Inflater() (例如PNGImageDecoder )而不調用PNGImageDecoderend() 。 好吧,如果你只使用new傳遞構造函數,那就沒有機會了......是的,如果它作為構造函數參數手動傳遞,則在流上調用close()不會關閉inflater。 這不是真正的洩漏,因為它會被終結者釋放......當它認為有必要時。 直到那一刻牠吃掉本機內存如此糟糕,它可能導致Linux oom_killer肆無忌憚地殺死進程。 主要問題是Java的最終確定非常不可靠,G1在7.0.2之前變得更糟。 故事的道德:盡快釋放本地資源; 終結者太窮了。

  • java.util.zip.Deflater相同的情況。 由於Deflater在Java中存在內存不足,即總是使用15位(最大值)和8個內存級別(最大值為9),因此分配數百KB的本機內存,這一點要糟糕得多。 幸運的是, Deflater沒有被廣泛使用,據我所知,JDK沒有濫用。 如果手動創建DeflaterInflater始終調用end() 。 最後兩個中最好的部分: 您無法通過常規的分析工具找到它們。

(我可以根據要求添加更多時間浪費。)

祝你好運,保持安全; 洩漏是邪惡的!




答案完全取決於面試官的想法。

在實踐中是否有可能使Java洩漏? 當然是,並且在其他答案中有很多例子。

但是有多個元問題可能會被問到?

  • 理論上“完美”的Java實現是否容易受到洩漏?
  • 候選人是否理解理論與現實之間的區別?
  • 候選人是否了解垃圾收集的工作原理?
  • 或者垃圾收集如何在理想情況下起作用?
  • 他們知道他們可以通過本地接口調用其他語言嗎?
  • 他們知道用其他語言洩漏記憶嗎?
  • 候選人是否知道內存管理是什麼,以及Java中幕後發生了什麼?

我正在讀你的元問題:“在這次面試中我能用到什麼答案”。 因此,我將專注於面試技巧而不是Java。 我相信你更有可能在面試中重複不知道問題答案的情況,而不是在需要知道如何使Java洩漏的地方。 所以,希望這會有所幫助。

您可以為面試開發的最重要的技能之一是學習積極傾聽問題並與面試官合作以提取他們的意圖。 這不僅可以讓你以他們想要的方式回答他們的問題,而且還表明你有一些重要的溝通技巧。 當談到許多同樣有才華的開發人員之間的選擇時,我會聘請那些在每次回復之前傾聽,思考和理解的人。




可能是潛在內存洩漏的最簡單示例之一,以及如何避免它,是ArrayList.remove(int)的實現:

public E remove(int index) {
    RangeCheck(index);

    modCount++;
    E oldValue = (E) elementData[index];

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    elementData[--size] = null; // (!) Let gc do its work

    return oldValue;
}

如果你自己實現它,你會想到清除不再使用的數組元素( elementData[--size] = null )? 這個參考可能會讓一個巨大的物體活著......




您可以使用sun.misc.Unsafe類進行內存洩漏。 實際上,此服務類用於不同的標準類(例如,在java.nio類中)。 您無法直接創建此類的實例 ,但您可以使用反射來執行此操作

代碼無法在Eclipse IDE中編譯 - 使用命令javac編譯它(在編譯期間,您將收到警告)

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import sun.misc.Unsafe;


public class TestUnsafe {

    public static void main(String[] args) throws Exception{
        Class unsafeClass = Class.forName("sun.misc.Unsafe");
        Field f = unsafeClass.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);
        System.out.print("4..3..2..1...");
        try
        {
            for(;;)
                unsafe.allocateMemory(1024*1024);
        } catch(Error e) {
            System.out.println("Boom :)");
            e.printStackTrace();
        }
    }

}



這是一個簡單/險惡的通過http://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29

public class StringLeaker
{
    private final String muchSmallerString;

    public StringLeaker()
    {
        // Imagine the whole Declaration of Independence here
        String veryLongString = "We hold these truths to be self-evident...";

        // The substring here maintains a reference to the internal char[]
        // representation of the original string.
        this.muchSmallerString = veryLongString.substring(0, 1);
    }
}

因為子串指的是原始字符串的內部表示,所以原始字符串保留在內存中。因此,只要你有一個StringLeaker,你就可以將整個原始字符串放在內存中,即使你可能認為你只是堅持使用單字符字符串。

避免將不需要的引用存儲到原始字符串的方法是執行以下操作:

...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...

為了增加不良,您也可以.intern()使用子字符串:

...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...

即使在丟棄StringLeaker實例之後,這樣做也會將原始長字符串和派生子字符串保留在內存中。




獲取在任何servlet容器中運行的任何Web應用程序(Tomcat,Jetty,Glassfish,等等......)。連續重新部署應用程序10次或者20次(可能只需觸摸服務器的autodeploy目錄中的WAR即可。

除非有人實際測試過這個,否則很有可能在經過幾次重新部署後你會得到一個OutOfMemoryError,因為應用程序沒有註意自己清理。您甚至可以通過此測試在服務器中找到錯誤。

問題是,容器的生命週期比應用程序的生命週期長。您必須確保容器可能對應用程序的對像或類的所有引用都可以進行垃圾回收。

如果只有一個引用在您的Web應用程序取消部署後仍然存在,則相應的類加載器將導致您的Web應用程序的所有類都無法進行垃圾回收。

您的應用程序啟動的線程,ThreadLocal變量,日誌記錄附加程序是導致類加載器洩漏的一些常見嫌疑。




我曾經有過一次與PermGen和XML解析有關的“內存洩漏”。我們使用的XML解析器(我不記得它是哪一個)在標記名稱上做了一個String.intern(),以便更快地進行比較。我們的一位客戶最好不要將數據值存儲在XML屬性或文本中,而是存儲為標記名,因此我們有一個文檔,如:

<data>
   <1>bla</1>
   <2>foo</>
   ...
</data>

實際上,他們並沒有使用數字,而是使用更長的文本ID(大約20個字符),這些ID是獨一無二的,每天的速度為10-15百萬。每天產生200 MB的垃圾,這是永遠不需要的,而且從來沒有GCed(因為它是在PermGen)。我們將permgen設置為512 MB,因此內存異常(OOME)需要大約兩天才能到達......




我覺得有趣的是沒有人使用內部類的例子。如果你有內部課程; 它本身就維護了對包含類的引用。當然,從技術上講,它不是內存洩漏,因為Java最終會將其清理乾淨; 但這可能會導致課程比預期更長時間。

public class Example1 {
  public Example2 getNewExample2() {
    return this.new Example2();
  }
  public class Example2 {
    public Example2() {}
  }
}

現在,如果您調用Example1並獲取Example2丟棄Example1,您將固有地仍然擁有指向Example1對象的鏈接。

public class Referencer {
  public static Example2 GetAnExample2() {
    Example1 ex = new Example1();
    return ex.getNewExample2();
  }

  public static void main(String[] args) {
    Example2 ex = Referencer.GetAnExample2();
    // As long as ex is reachable; Example1 will always remain in memory.
  }
}

我也聽說過一個謠言,如果你的變量存在的時間超過特定的時間; Java假設它將永遠存在,並且如果再也無法在代碼中訪問它,它實際上永遠不會嘗試清理它。但這完全未經證實。




創建一個靜態Map並繼續添加對它的硬引用。那些永遠不會是GC。

public class Leaker {
    private static final Map<String, Object> CACHE = new HashMap<String, Object>();

    // Keep adding until failure.
    public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }
}



您可以通過在該類的finalize方法中創建類的新實例來創建移動內存洩漏。如果終結器創建多個實例,則獎勵積分。這是一個簡單的程序,它會在幾秒到幾分鐘之間的某個時間內洩漏整個堆,具體取決於您的堆大小:

class Leakee {
    public void check() {
        if (depth > 2) {
            Leaker.done();
        }
    }
    private int depth;
    public Leakee(int d) {
        depth = d;
    }
    protected void finalize() {
        new Leakee(depth + 1).check();
        new Leakee(depth + 1).check();
    }
}

public class Leaker {
    private static boolean makeMore = true;
    public static void done() {
        makeMore = false;
    }
    public static void main(String[] args) throws InterruptedException {
        // make a bunch of them until the garbage collector gets active
        while (makeMore) {
            new Leakee(0).check();
        }
        // sit back and watch the finalizers chew through memory
        while (true) {
            Thread.sleep(1000);
            System.out.println("memory=" +
                    Runtime.getRuntime().freeMemory() + " / " +
                    Runtime.getRuntime().totalMemory());
        }
    }
}



最近我遇到了一種更為微妙的資源洩漏。我們通過類加載器的getResourceAsStream打開資源,並且輸入流句柄沒有關閉。

嗯,你可能會說,多麼愚蠢。

那麼,有趣的是:這樣,你可以洩漏底層進程的堆內存,而不是來自JVM的堆。

您只需要一個jar文件,其中包含一個文件,該文件將從Java代碼中引用。jar文件越大,分配的內存越快。

您可以使用以下類輕鬆創建此類jar:

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class BigJarCreator {
    public static void main(String[] args) throws IOException {
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
        zos.putNextEntry(new ZipEntry("resource.txt"));
        zos.write("not too much in here".getBytes());
        zos.closeEntry();
        zos.putNextEntry(new ZipEntry("largeFile.out"));
        for (int i=0 ; i<10000000 ; i++) {
            zos.write((int) (Math.round(Math.random()*100)+20));
        }
        zos.closeEntry();
        zos.close();
    }
}

只需粘貼到名為BigJarCreator.java的文件中,從命令行編譯並運行它:

javac BigJarCreator.java
java -cp . BigJarCreator

Etvoilà:你在當前的工作目錄中找到一個jar存檔,裡面有兩個文件。

讓我們創建第二個類:

public class MemLeak {
    public static void main(String[] args) throws InterruptedException {
        int ITERATIONS=100000;
        for (int i=0 ; i<ITERATIONS ; i++) {
            MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
        }
        System.out.println("finished creation of streams, now waiting to be killed");

        Thread.sleep(Long.MAX_VALUE);
    }

}

這個類基本上什麼都不做,但創建了未引用的InputStream對象。這些對象將立即被垃圾收集,因此不會對堆大小產生影響。對於我們的示例來說,從jar文件加載現有資源非常重要,大小在這裡很重要!

如果您有疑問,請嘗試編譯並啟動上面的類,但請確保選擇合適的堆大小(2 MB):

javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak

這裡不會遇到OOM錯誤,因為沒有保留引用,無論您在上面的示例中選擇了多大的ITERATIONS,應用程序都將繼續運行。除非應用程序進入wait命令,否則進程的內存消耗(在頂層(RES / RSS)或進程資源管理器中可見)會增長。在上面的設置中,它將在內存中分配大約150 MB。

如果您希望應用程序安全播放,請在創建它的位置關閉輸入流:

MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();

並且您的過程不會超過35 MB,與迭代次數無關。

非常簡單和令人驚訝。




正如許多人所建議的那樣,資源洩漏很容易引起 - 就像JDBC示例一樣。實際的內存洩漏有點困難 - 特別是如果你不依賴於JVM的破碎位來為你做這件事......

創建具有非常大的佔用空間然後無法訪問它們的對象的想法也不是真正的內存洩漏。如果沒有什麼可以訪問它那麼它將被垃圾收集,如果有東西可以訪問它那麼它不是洩漏...

過去工作的一種方式- 我不知道它是否仍然存在 - 是有一個三深的圓形鏈。如在對象A中引用了對象B,對象B引用了對象C,對象C引用了對象A.GC非常聰明地知道兩條深鏈 - 如在A < - > B中 - 如果A和B無法通過其他任何方式訪問,可以安全地收集,但無法處理三向鏈......




線程在終止之前不會被收集。它們是垃圾收集的roots。它們是為數不多的僅通過忘記它們或清除對它們的引用而無法回收的對象之一。

考慮:終止工作線程的基本模式是設置線程看到的一些條件變量。線程可以定期檢查變量並將其用作終止信號。如果未聲明變量volatile,則線程可能無法看到對變量的更改,因此它不會知道要終止。或者想像一下,如果某些線程想要更新共享對象,但在嘗試鎖定它時會出現死鎖。

如果您只有少數線程,這些錯誤可能會很明顯,因為您的程序將無法正常工作。如果您有一個根據需要創建更多線程的線程池,那麼可能不會注意到過時/卡住的線程,並且將無限累積,從而導致內存洩漏。線程可能會在您的應用程序中使用其他數據,因此也會阻止他們直接引用的任何數據被收集。

作為一個玩具示例:

static void leakMe(final Object object) {
    new Thread() {
        public void run() {
            Object o = object;
            for (;;) {
                try {
                    sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {}
            }
        }
    }.start();
}

打電話給System.gc()你喜歡,但傳遞給的對象leakMe永遠不會死。

(*編輯*)




面試官可能正在尋找循環參考解決方案:

    public static void main(String[] args) {
        while (true) {
            Element first = new Element();
            first.next = new Element();
            first.next.next = first;
        }
    }

這是引用計數垃圾收集器的典型問題。然後,您可以禮貌地解釋JVM使用更複雜的算法,但沒有此限制。

-Wes Tarle




Related