PermGenSpace problem? No problem!

Tento článek vyšel na našem firemním intranetu. Jelikož je jeho obsah velmi přínosný ve své jednoduchosti a agregace poznatků z řady roztříštěných zdrojů po internetu, požádal jsem autora Michala France o svolení k jeho zveřejnění. Jak to dopadlo, můžete vytušit už sami. Výsledkem je že se s Vámi mohu podělit o zkušenosti s (vy)řešením problémů OutOfMemory v oblasti PermGenSpace při redeploy našich aplikací v aplikačních kontejnerech. Před aplikací těchto znalostí jsme vcelku pravidelně po dvou "redeployích" restartovali celý server, protože docházela PermGenSpace. V současném stavu aplikační server žije i po několika desítkách redeployů.

A nyní slíbený článek

Každého programátora to jednou čeká. Jeho aplikace začne padat na OutOfMemoryError. Dá se krčit rameny se slovy "já vážně nevím čím to je", nebo s tím něco udělat.

Tak mu dej víc paměti

V ideálním případě je chyba způsobena pouze poddimenzovaným nastavením limitů paměti. Pak stačí nastavit JVM následovně (hodnoty doplnit dle uvážení).

Pro java.lang.OutOfMemoryError: Java heap space

-Xmx256m

Pro java.lang.OutOfMemoryError: PermGen space

-XX:MaxPermSize=128m

Další volby v dokumentaci Java HotSpot VM.

Tak tohle nepomohlo, prubni jmap

Většinou ale zvýšení přiřazené paměti problém jen oddálí. Pak přijdou na řadu diagnostické nástroje. Tím nejjednodušším je jmap.

Pomocí jmap je možné získat užitečné informace o využití paměti a umožňuje získat HEAP dump.

Více informací o jmap s příklady naleznete v sumarizaci na Blog O'Matty

S jmap jsem narazil na problém pod Windows Vista. Nepodařilo se mi ho donutit komunikovat s java procesem, který běžel jako service. Pořád tvrdošíjně tvrdil, že „Not enough storage is available to process this command”. Jediné řešení které znám je spustit proces přímo z příkazové řádky.

Můžu zjistit něco i bez jmap?

Na JDK 1.5 lze použít následující JSP (je možné ho nakopírovat přímo do deploynuté aplikace)


<%@ page import="java.lang.management.*" %>
<%@ page import="java.util.*" %>
<h1>Memory</h1>
<table border="1">
<tr><th width="100"><b>Name</b></th><th width="150">Type</th><th colspan="6">Usage</th></tr>
<tr>
        <td colspan="2"><b>Heap Memory summary</b></td>
        <td><b>Usage</b></td><td><%= String.valueOf( ManagementFactory.getMemoryMXBean().getHeapMemoryUsage() ).replace(")",")</td><td>")%></td>
</tr>
<tr>
        <td colspan="2"><b>Non-Heap Memory summary</b></td>
        <td><b>Usage</b></td><td><%= String.valueOf( ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage() ).replace(")",")</td><td>")%></td>
</tr>
<tr><th colspan="8"></th></tr>
<%
Iterator iter = ManagementFactory.getMemoryPoolMXBeans().iterator();
while (iter.hasNext()) {
        MemoryPoolMXBean item = (MemoryPoolMXBean) iter.next(); %>
<tr>
        <td rowspan="2"><b><%= item.getName() %></b></td>
        <td rowspan="2"><%= item.getType() %></td>
        <td><b>Usage</b></td><td><%= String.valueOf( item.getUsage() ).replace(")",")</td><td>") %></td>
</tr>
<tr><td><b>Peak</b></td><td><%= String.valueOf( item.getPeakUsage() ).replace(")",")</td><td>") %></td></tr>
<%}%>
</table>

Výpis informací může vypadat nějak takhle:

JMX memory information

Co je to ten perm gen?

Telegraficky - popis jednotlivých oblastí paměťi JVM (formulace dávají přednost srozumitelnosti před přesnou charakteristikou):

  • Eden Space - oblast kde jsou instance objektů umístěny po jejich vytvoření (young generation)
  • Survivor Space, Tenured Gen, From/To space - instance objektů s delší platností
  • Perm Gen - interní data JVM, zde se ukládají načtené třídy (class)
  • Code Cache - oblast obsahující bytecode přeložený do nativní podoby

Mapa paměti JVM

Více se dá dočíst v Tuning Garbage Collection with the 5.0 Java Virtual Machine

Paměti to žere dost, ale kde?

Přehled o objektech v paměti lze získat pomocí profilerů. Našinec asi vynechá klasickou komerční řadu produktů (YourKit, JProbe, JProfiler) a bude hledat něco zdarma (NetBeans Profiler). Problémy se ale nejčastěji objevují na produkčních prostředích. A zde je použití profilerů mnohem složitější. Většinou zbývá jediná možnost, získat dump paměti a ten následně analyzovat.

Použití jmap

Příklady použití jmap pod Linuxem


jmap -heap:format=b 18302
Attaching to process ID 18302, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 1.5.0_11-b03
heap written to heap.bin

A pod windows (JDK 1.6)


jmap -dump:format=b,file=heap.bin 18302

Hodnota 18302 je číslo procesu (PID).

Dump přímo z JVM

Následující užitečná nastavení:

JVM provede heap dump pokud dojde k OutOfMemoryError

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/dump.hprof

JVM provede heap dump pokud obdrží Ctrl Break

-XX:+HeapDumpOnCtrlBreak

Generování dumpu může trvat poměrně dlouho (jednotky minut), proto pozor v jaké době dump děláte. JVM v průběhu generování samozřejmně nepracuje.

Mrkneš se na ten dump? Já se v tom nevyznám.

Zbývá už „jen zanalyzovat” jaké objekty jsou v paměti. Lze využít některý z profilerů, ale jsou zde i další nástroje.

jhat - Java Heap Analysis Tool

Příklad použití (s nastavením 512MB pro jhat).


> jhat -J-mx512m dump.hprof
Reading from dump.hprof...
Dump file created Thu Apr 10 18:27:40 CEST 2008
Snapshot read, resolving...
Resolving 639872 objects...
Chasing references, expect 127 dots
...............................................................................................................................
Eliminating duplicate references
...............................................................................................................................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.

JHAT nastartuje webovou aplikaci. Nasměrujte prohlížeč na http://localhost:7000/

JHat console

JHat console

Sap Memory Analyzer

Tento nástroj je skutečně to nejlepší co se mi podařilo pro analýzu heap dumpu najít.

SAP Memory Analyzer

Program je volně ke stažení zde.

To neřeš, perm gen dochází jen po redeploy

Častý problém - na aplikačním serveru dochází Perm Gen po několika restartech aplikačního kontextu.

Na serveru nedochází k uvolnění classloaderů a v perm gen zůstávají třídy staré verze aplikace (více popisuje Frank Kieviet).

Ano i já jsem objevil již objevené.

Problém první - commons logging

Problémy s commons-logging jsou dobře popsané, celkem i srozumitelné, ale přece jen. Vzhledem k tomu, že existuje jednoduchý způsob jak se commons-logging úplně zbavit, proč to neudělat.

SLF4J umí nahradit commons logging beze změny v aplikaci, pouze nahrazením knihoven. S Maven je řešení úžasně jednoduché. Jediný problém je, jak zajistit aby se do výsledného buildu nedostal commons-logging z tranzitivních dependencí. Řešení je nastavit scope na provided.

Příslušná část pom.xml pak vypadá následovně:


<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.1</version>
    <scope>provided</scope>
    <!-- provided knihovny se nedostanou do vysledného WARu/EARu -->
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.5.0</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl104-over-slf4j</artifactId>
    <version>1.5.0</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.5.0</version>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.14</version>
    <!-- SLF4J vyžaduje min. verzi 1.2.12 -->
</dependency>

Problém druhý - JDBC drivery

DriverManager v JDBC způsobuje další leak. Pokud nelze přesunout JDBC drivery do knihoven aplikačního serveru, je zde možnost použít context listener pro uvolnění registrovaných driverů:


public class CleanupListener implements ServletContextListener {
    public void contextInitialized(ServletContextEvent event) { }
    public void contextDestroyed(ServletContextEvent event) {
        try {
            Introspector.flushCaches();
            for ( Enumeration e = DriverManager.getDrivers(); e.hasMoreElements(); ) {
                 Driver driver = (Driver) e.nextElement();
                if (driver.getClass().getClassLoader() == getClass().getClassLoader()) {
                     DriverManager.deregisterDriver(driver);
                }
            }
        } catch (Throwable e) {
            System.err.println("Failled to cleanup ClassLoader for webapp");
            e.printStackTrace();
        }
    }
}

Uvedené řešení, stejně jako problémy s dalšími knihovnami naleznete v článku Memory leak - classloader won’t let go.

Asi se zeptáte proč vše neřešit vhodným umístěním knihoven přímo do aplikačního serveru. Zní to jednoduše, nicméně úmyslně jsem řešil problém tak, aby aplikace nebyla závislá na konfiguraci prostředí.

Dovětek

Na závěr ještě doporučuji JavaTM 2 Platform - Troubleshooting and Diagnostic Guide a A day in the life of a memory leak hunter.

Většina nástrojů je závislá na JDK 1.5, dump lze získat i z posledních verzí JDK 1.4.2. Pokud někdo víte jak vymámit dump z JDK 1.4.2_04 podělte se prosím.

Pro ne-Sunovské implementace JVM může být vše jinak ;-)

Update k 9.5.2008 - další způsoby jak leakovat classloader

Následným zkoumáním našich web aplikací jsme přišli na další způsoby, jak spolehlivě přijít o možnost GC classloaderu:

  • Leakování Java Timeru

    při použití Timerů ze standardního balíku Javy se často používá strategie, že při naběhnutí aplikace se nastartují vlákna, která v pravidelných intervalech provádějí určité servisní činnosti. Častou chybou ale je, že při ukončení aplikace, se nastartované Timery neuzavřou. Tím pádem zůstanou instance vašich objektů živé i po stopnutí web aplikace a zamezí zGC classloaderu celé web aplikace.

  • Leakování referencí v ThreadLocal

    další poměrně často využívaná technika ve web aplikacích je používání ThreadLocal objektů, držících reference na objekty po celou dobu zpracování requestu. Pokud se tyto proměnné na konci zpracování nevyčistí, může v nich zůstat reference na objekt z web aplikace, což opět zabrání zGC jejího classloaderu. Tuto chybu obsahuje například i knihovna iBatis - chyba je již nahlášená z roku 2006, takže kdo ví, jestli se jejího vyřešení dočkáme.

Update k 19.6.2008 - způsobů jak leakovat je na tisíc ;-)

  • Leakování class v Introspectoru

    Pokud používáte (nebo některá z knihoven) JavaBeany a přistupuje te k nim reflexním způsobem (tzn. používáte např. Commons BeanUtils, Spring, nebo další frameworky, které pracují s JavaBeanami) je třeba starat se o vyčištění Introspector cache - což je cache, která udržuje meta informace o třídách, jejich property a dalších věcech, které jsou potřeba. Cache je životně důležitá z hlediska performance. Bohužel Introspector sídlí v classloaderu Javy a tudíž mimo kontext classloader webové aplikace. Classy uložené v cache tedy zabrání GC classloaderu web aplikace. Pokud používáte spring můžete využít IntrospectorCleanupListener, který je ale potřeba zaregistrovat do web.xml.

    Zajímavé počtení najdeme v dokumentaci k tomuto listeneru:

    Application classes hardly ever need to use the JavaBeans Introspector directly, so are normally not the cause of Introspector resource leaks. Rather, many libraries and frameworks do not clean up the Introspector: e.g. Struts and Quartz.

    Cache lze vyčistit také jednoduše vlasním kódem zavoláním: Introspector.flushCaches();