java - thread - tomcat memory leak analyzer




使用Java創建內存洩漏 (20)

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

一個例子是什麼?


靜態字段保持對象引用[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設置


一個簡單的事情是使用具有不正確(或不存在) hashCode()equals()的HashSet,然後繼續添加“重複”。 而不是忽略重複,它只會增長,你將無法刪除它們。

如果你想讓這些壞鍵/元素閒逛,你可以使用靜態字段

class BadKey {
   // no hashCode or equals();
   public final String key;
   public BadKey(String key) { this.key = key; }
}

Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.

也許通過JNI使用外部本機代碼?

使用純Java,幾乎是不可能的。

但這是關於“標準”類型的內存洩漏,當您無法再訪問內存時,它仍然由應用程序擁有。您可以保留對未使用對象的引用,或者在不關閉它們的情況下打開流。


什麼是內存洩漏:

  • 這是由錯誤糟糕的設計引起的
  • 這是浪費記憶。
  • 隨著時間的推移會變得更糟
  • 垃圾收集器無法清理它。

典型例子:

對象緩存是弄亂事物的好起點。

private static final Map<String, Info> myCache = new HashMap<>();

public void getInfo(String key)
{
    // uses cache
    Info info = myCache.get(key);
    if (info != null) return info;

    // if it's not in cache, then fetch it from the database
    info = Database.fetch(key);
    if (info == null) return null;

    // and store it in the cache
    myCache.put(key, info);
    return info;
}

您的緩存增長和增長。很快整個數據庫就被吸進了內存。更好的設計使用LRUMap(僅在緩存中保留最近使用的對象)。

當然,你可以讓事情變得更複雜:

  • 使用ThreadLocal構造。
  • 添加更複雜的參考樹
  • 第三方圖書館造成的洩密。

經常發生的事情:

如果此Info對象具有對其他對象的引用,則其他對像也會引用其他對象。在某種程度上,你也可以認為這是某種內存洩漏(由不良設計引起)。


如果您不了解JDBC ,以下是一個非常毫無意義的示例。 或者至少JDBC希望開發人員在丟棄它們或丟失對它們的引用之前關閉ConnectionStatementResultSet實例,而不是依賴於finalize的實現。

void doWork()
{
   try
   {
       Connection conn = ConnectionFactory.getConnection();
       PreparedStatement stmt = conn.preparedStatement("some query"); // executes a valid query
       ResultSet rs = stmt.executeQuery();
       while(rs.hasNext())
       {
          ... process the result set
       }
   }
   catch(SQLException sqlEx)
   {
       log(sqlEx);
   }
}

上面的問題是Connection像沒有關閉,因此物理連接將保持打開狀態,直到垃圾收集器出現並看到它無法訪問。 GC將調用finalize方法,但是有些JDBC驅動程序沒有實現finalize ,至少與實現Connection.close方式不同。 由此產生的行為是,由於收集了無法訪問的對象而將回收內存,因此可能無法回收與Connection對象關聯的資源(包括內存)。

Connectionfinalize方法沒有清理所有內容的情況下,實際上可能會發現與數據庫服務器的物理連接將持續幾個垃圾收集週期,直到數據庫服務器最終發現連接不活動為止(如果確實如此),應該關閉。

即使JDBC驅動程序要實現finalize ,也可能在最終確定期間拋出異常。 由此產生的行為是,與現在“休眠”對象關聯的任何內存都不會被回收,因為finalize確保只調用一次。

在對象最終化期間遇到異常的上述情況與另一個可能導致內存洩漏的情況有關 - 對象復活。 對象復活通常是通過從另一個對象創建對象的強引用來有意識地完成的。 當對象復活被濫用時,它將導致內存洩漏以及其他內存洩漏源。

你可以想出更多的例子 - 比如

  • 管理一個List實例,您只添加到列表中而不是從列表中刪除(儘管您應該刪除不再需要的元素),或者
  • 打開SocketFile ,但不再需要它們時關閉它們(類似於涉及Connection類的上述示例)。
  • 在關閉Java EE應用程序時不卸載單例。 顯然,加載單例類的類加載器將保留對類的引用,因此永遠不會收集單例實例。 當部署應用程序的新實例時,通常會創建一個新的類加載器,並且由於單例,前一個類加載器將繼續存在。

您可以使用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();
        }
    }

}

我最近遇到了log4j造成的內存洩漏情況。

Log4j具有這種稱為嵌套診斷上下文(NDC)的機制,它是一種區分不同來源的交叉日誌輸出的工具。NDC工作的粒度是線程,因此它分別區分不同線程的日誌輸出。

為了存儲特定於線程的標記,log4j的NDC類使用一個由Thread對象本身鍵入的Hashtable(而不是線程id),因此直到NDC標記在內存中保留所有掛起線程的對象對像也留在記憶中。在我們的Web應用程序中,我們使用NDC標記帶有請求ID的logoutput,以區分日誌和單個請求。將NDC標記與線程相關聯的容器也會在從請求返迴響應時將其刪除。在處理請求的過程中,生成了一個子線程,類似於以下代碼:

pubclic class RequestProcessor {
    private static final Logger logger = Logger.getLogger(RequestProcessor.class);
    public void doSomething()  {
        ....
        final List<String> hugeList = new ArrayList<String>(10000);
        new Thread() {
           public void run() {
               logger.info("Child thread spawned")
               for(String s:hugeList) {
                   ....
               }
           }
        }.start();
    }
}    

因此,NDC上下文與生成的內聯線程相關聯。作為此NDC上下文的鍵的線程對像是內聯線程,其中有hugeList對象。因此,即使在線程完成其正在執行的操作之後,對NDC上下文Hastable仍然保持對hugeList的引用,從而導致內存洩漏。


我認為一個有效的例子可能是在線程被池化的環境中使用ThreadLocal變量。

例如,使用Servlet中的ThreadLocal變量與其他Web組件進行通信,讓容器創建線程並在池中維護空閒的線程。ThreadLocal變量如果沒有被正確清理,將會存在,直到可能相同的Web組件覆蓋它們的值。

當然,一旦確定,問題就可以輕鬆解決。


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

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

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

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

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

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


這裡的大多數例子都“過於復雜”。 他們是邊緣案件。 使用這些示例,程序員犯了一個錯誤(比如不重新定義equals / hashcode),或者被JVM / JAVA的一個角落案例(帶有靜態的類加載......)所困擾。 我認為這不是面試官想要的例子,甚至是最常見的案例。

但是內存洩漏的情況確實比較簡單。 垃圾收集器只釋放不再引用的內容。 我們作為Java開發人員並不關心內存。 我們在需要時分配它並讓它自動釋放。 精細。

但任何長期存在的應用程序都傾向於共享狀態。 它可以是任何東西,靜力學,單身......通常非平凡的應用程序往往會製作複雜的對像圖。 只是忘記設置null或更多的引用經常忘記從集合中刪除一個對象足以導致內存洩漏。

當然,如果處理不當,所有類型的偵聽器(如UI偵聽器),緩存或任何長期共享狀態都會產生內存洩漏。 應該理解的是,這不是Java角落案例,也不是垃圾收集器的問題。 這是一個設計問題。 我們設計我們為一個長期存在的對象添加一個監聽器,但是當不再需要時我們不會刪除監聽器。 我們緩存對象,但我們沒有策略將它們從緩存中刪除。

我們可能有一個複雜的圖形來存儲計算所需的先前狀態。 但是之前的狀態本身與之前的狀態有關,依此類推。

就像我們必須關閉SQL連接或文件一樣。 我們需要設置對null的正確引用並從集合中刪除元素。 我們將有適當的緩存策略(最大內存大小,元素數量或計時器)。 允許偵聽器得到通知的所有對像都必須同時提供addListener和removeListener方法。 當這些通知符不再使用時,他們必須清除他們的聽眾列表。

內存洩漏確實是可能的,並且是完全可預測的。 無需特殊語言功能或角落案例。 內存洩漏可能表明某些內容可能缺失甚至是設計問題。


GUI代碼中的一個常見示例是創建窗口小部件/組件並向某個靜態/應用程序範圍對象添加偵聽器,然後在窗口小部件被銷毀時不刪除偵聽器。您不僅會遇到內存洩漏,而且還會受到性能影響,因為無論您何時正在收聽火災事件,您的所有舊聽眾都會被調用。


創建一個靜態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); }
}

我曾經有過一次與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假設它將永遠存在,並且如果再也無法在代碼中訪問它,它實際上永遠不會嘗試清理它。但這完全未經證實。


最近我遇到了一種更為微妙的資源洩漏。我們通過類加載器的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,與迭代次數無關。

非常簡單和令人驚訝。


有許多不同的情況,內存會洩漏。我遇到的一個,它暴露了一個不應該在其他地方暴露和使用的地圖。

public class ServiceFactory {

private Map<String, Service> services;

private static ServiceFactory singleton;

private ServiceFactory() {
    services = new HashMap<String, Service>();
}

public static synchronized ServiceFactory getDefault() {

    if (singleton == null) {
        singleton = new ServiceFactory();
    }
    return singleton;
}

public void addService(String name, Service serv) {
    services.put(name, serv);
}

public void removeService(String name) {
    services.remove(name);
}

public Service getService(String name, Service serv) {
    return services.get(name);
}

// the problematic api, which expose the map.
//and user can do quite a lot of thing from this api.
//for example, create service reference and forget to dispose or set it null
//in all this is a dangerous api, and should not expose 
public Map<String, Service> getAllServices() {
    return services;
}

}

// resource class is a heavy class
class Service {

}

每個人總是忘記本機代碼路由。這是洩漏的簡單公式:

  1. 聲明本機方法。
  2. 在本機方法中,請致電malloc。不要打電話free
  3. 調用本機方法。

請記住,本機代碼中的內存分配來自JVM堆。


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

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

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

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

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


這是一個簡單/險惡的通過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實例之後,這樣做也會將原始長字符串和派生子字符串保留在內存中。


這是在純Java中創建真正的內存洩漏(通過運行代碼但仍然存儲在內存中無法訪問的對象)的好方法:

  1. 應用程序創建一個長時間運行的線程(或使用線程池來更快地洩漏)。
  2. 該線程通過(可選的自定義)ClassLoader加載一個類。
  3. 該類分配一大塊內存(例如new byte[1000000] ),在靜態字段中存儲對它的強引用,然後在ThreadLocal中存儲對它自己的引用。 分配額外的內存是可選的(洩漏Class實例就足夠了),但它會使洩漏工作更快。
  4. 該線程清除對自定義類或從中加載的ClassLoader的所有引用。
  5. 重複。

這是有效的,因為ThreadLocal保留對該對象的引用,該對象保持對其Class的引用,而Class又保持對其ClassLoader的引用。 反過來,ClassLoader保持對它已加載的所有類的引用。

(在許多JVM實現中,尤其是在Java 7之前,情況更糟,因為Classes和ClassLoader直接分配到permgen並且根本就沒有GC。但是,無論JVM如何處理類卸載,ThreadLocal仍然會阻止被回收的類對象。)

此模式的一個變體是,如果您經常重新部署碰巧以任何方式使用ThreadLocals的應用程序,那麼應用程序容器(如Tomcat)可能會像篩子那樣洩漏內存。 (由於應用程序容器使用了所描述的線程,每次重新部署應用程序時都會使用新的ClassLoader。)

更新 :由於很多人不斷要求它, 這裡有一些示例代碼顯示了這種行為





memory-leaks