20. 3. 2014

Nezvyklá příčina NoClassDefFoundError

Zápisky z troubleshootingu zapeklitého problému, který mne stál den hledání. Aplikace (war v Tomcatu) vyhazovala NoClassDefFoundError na třídy, které evidentně ve waru jsou (byly buď přímo v classes, nebo v jarech v lib). Moc nesetříděno a nezaručuji, že nejsou zavádějící.

Příčina


V systému došlo souběžně k jiné chybě - vyčerpání maximálního počtu povolených souborů (včetně síťových a db připojení). To jsme sice věděli (chyby jako java.net.SocketException: Too many open files nebo java.io.FileNotFoundException: /soubor (Too many open files)
byly diagnostikovány a bylo nutné zkontrolovat potenciální resource leak a limit nechat navýšit (ulimit -n, cat /proc/5063/limits). Nenapadla mne ale spojitost s NCDFE. Pokud JVM při pokusu o nahrání třídy narazí na maximální počet otevřených souborů, dostane se zřejmě do nějak nekonzistentního stavu, kdy v classloaderu se třída jeví jako nahraná, ale pokus o přístup na ni končí NCDFE. Bližší rozbor jsem nikde nevygooglil, ale nejvíc se mu blíží tato otázka na SO, první stopa vedla z tohoto dotazu.

Řešením byl restart Tomcatu, ale bylo dobré zjistit příčinu.

Falešné stopy


Zkontrolovat filesystem


V minulosti se nám stalo, že na předprodukčním serveru (virtualizovaný server s Windows Server 2003 - dnes už je upgradován) došlo k poruše, která se projevila náhodným porušením náhodného souboru. Stalo se to u jaru z instalace Javy, kde rozdíl byl pouze o 1 bit! V dnešním případě šlo o produkční prostředí a Linux a tehdejší chyba se taky projevovala myslím jako InternalError, nicméně jsem zkontroloval, že jary jdou rozzipovat a že hlášené classy jsou validní (pro jistotu jsem se na ně podíval i přes oblíbený decompiler).

Hierarchie classloaderů byla v pořádku


Klasická tomcatí: všechny aplikančí třídy z waru jsou nahrány WebappClassLoaderem.
Odkazy: 1, 2, 3 - pěkný tutoriál, i když k IBM JVM


Nesnažit se o odstranění třídy z classloaderu


Nejde to. Je to dáno jednak prioritou classloaderů, jednak tím, že příslušnost classy v classloaderu je na úrovni nativního kódu. Nepomůže ani vlamovat se do classloaderu pomocí reflexe (Bad.class.getClassLoader().resourceEntries.remove("com.pkg.Bad")) ani snažit se třídu nahrát pomocí defineClass (protože už tam je a hodí to LinkageError: loader ... : attempted  duplicate class definition), ani totéž rodičovským classloaderem  (Bad.class.getClassLoader().resolveClass(Bad.class.getClassLoader().getParent().defineClass("com.pkg.Bad",new byte[] {...}),0,length))), protože by se tak musely zavléct všechny třídy, na kterých nahrávaná třída závisí.
Odkazy: 1, 2, 3, 4.


Třída šla instancovat z konzole


Na projektu je Beanshell konzole, v nímž šlo vytvořit přímo instanci třídy. Nešlo ale vytvořit factory a na ní zavolat metodu pro vytvoření třídy - to házelo NCDFE. Nezkoumal jsem dál, asi to je prostředím Beanshell interpreteru.

Nemá cenu zjišťovat všechny nahrané třídy


Reflexní triky, jak zjistit všechny nahrané třídy (viz např. zde) jsou založeny na zkoumání výsledku metody Class.getProtectionDomain() a ProtectionDomain.getCodeSource() a tady nepomáhají, protože dotčené třídy (pro které se vyhazuje NCDFE) mají tyto údaje i classloader v naprostém pořádku.


Class loading


Do JAVA_OPTS při spouštění aplikačního serveru jsem přidal -verbose:class. Těch pár tisíc řádků navíc v catalina.out ničemu nevadí a třeba informace o tom, zda třída byla nahraná, příště pomůže. Později jsem ještě objevil SO otázku s dalšími experimentálními přepínači.

Užitečné odkazy

O classloaderech na oracle.com



Žádné komentáře:

Okomentovat