Spring AOP - Pozor na AspectJExpressionPointcut!

Tento týden jsem řešil problém s nedostatkem paměti při spouštění testů jednoho projektu. Pro běh testů nestačilo výchozích 64MB paměti Javy na heapu, což mi připadlo v porovnání s velikostí projektu podezřelé. Začal jsem profilovat a jelikož mne výsledky poněkud překvapily, chci se s Vámi o ně v tomto článku podělit.

Hned na úvod řeknu, že jádrem problému byla třída AspectJExpressionPointcut. Tato třída je ve Spring dokumentaci zmiňována hned několikrát, velmi jednoduše se používá a ze všech dostupných materiálů jsem dospěl k názoru, že se jedná o doporučovaný a běžně používaný standard.

Identifikace problému

V mém případě jsem pomocí AOP ošetřil kolem deseti tříd (jednou kvůli implementaci security a podruhé kvůli transakcím), načež mi za integračním serveru začaly padat testy na OutOfMemoryError (Java heap space). Rozjel jsem si testy lokálně a došel jsem ke stejnému výsledku. Po spuštění testů s nastavením:


-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.bin

jsem z Javy dostal HeapDump v okamžiku kdy paměť došla a začal jsem dump analyzovat pomocí NetBeans Profileru. Okamžitě na mne vyskočilo, že třída java.lang.reflect.Method zabírá v paměti 15,2% místa o celkové velikosti téměř 10MB se 125tis. instancemi. Po krátkém zkoumání jsem zjistil, že většinu instancí drží právě AspectJExpressionPointcut ve své shadowMapCache. Tuto cache používá pro ukládání indexu, kde klíčem je právě Method a hodnotou je org.aspectj.weaver.tools.ShadowMatch, který udržuje informaci o tom, zda tato metoda odpovídá / neodpovídá deklaraci pointcutu. Celý index nepoužívá weak nebo soft reference a drží si objekty natvrdo.

Vysvětlení

Jistým uklidněním může být, že Spring testy se chovají k aplikačním kontextům poněkud odlišně, než je tomu v provozním systému. V testech se drží cache všech aplikačních kontextů z testů, které mají odlišnou "konfiguraci" (myšleno odlišné návratové hodnoty z getConfigLocations metody). Tím pádem je v paměti současně drženo větší množství kontextů (v mém případě jich bylo 24), které mají v sobě každý uvedené AspectJExpressionPoincuty, které defakto většinou drží stejnou shadowMapCache.

I tak se mi ale zdá, že je implementace AspectJExpressionPointcut poněkud nešetrná k paměti. Je důležité si uvědomit, že pro každou třídu je drženo v shadowMapCache tolik položek, kolik je metod dané třídy v celé hierarchii dědičnosti. Takže u mých "jednoduchých" DAO s deseti metodami, které dědí z org.springframework.orm.ibatis.support.SqlMapClientDaoSupport, jsem napočítal okolo 35 metod celkově. Při pronásobení se začínáme dostávat už na zajímavá čísla. Jen pro zajímavost jeden objekt typu java.lang.reflection.Method v paměti (podle profileru) drží 77B paměti. ShadowMatchImpl, který je jako hodnota v indexu, drží dalších 40 (celkově u mě dalších 4,5MB). Když jsem tedy sečetl cekovou náročnost AspectJExpressionPointcut pro mé testy zjistil jsem že se jednalo o 22,1% (6.9% + 15,2%) celkové obsazené paměti jen na tento jeden index!

Přestože se jedná o specifický případ v kombinaci s chováním Spring testů, jsem přesvědčen o tom, že masivnější používání AspectJExpressionPointcut v projektu může pozlobit i na produkci (zvlášť pokud byste v některých momentech měli více aplikačních kontextů najednou - jako je tomu v našem případě).

Řešení

Jakmile se podařilo objevit jádro problému, bylo řešení už jednoduché. Přepsal jsem deklarace AOP z použití AspectJExpression na implementaci standardních Spring AOP interfaců:

Dříve:

``` xml ```

Nyní:

``` xml ```

Problém samozřejmě zmizel.