úterý 27. února 2007

Za tajemstvím classloaderu

Classloader, je jednou ze základních komponent Javy, o které nepanuje tak široké povědomí. Classloader je většinou kolečko, které se někde tiše otáčí a člověk s ním nemá problém. Jenže o to horší je to v případě, že se tohle kolečko zasekne. Vědět jak classloader funguje není na škoda a pokud děláte webové nebo enterprise aplikace, tak je to dokonce nutností.

Classloader (abstraktní třída java.lang.ClassLoader), jak již jeho název napovídá, je zodpovědný za nahrávání tříd. Pokud si kladete otázku, proč je vlastně classloader potřeba a proč to nedělá Java tak nějak automaticky, tak odpověď je, že dělá. Nicméně aplikace mají různé potřeby, představte si například aplikační server, který musí umět deploynout aplikaci za běhu. Jedním z požadavků, který classloader umožňuje realizovat, je například dynamické nahrávání tříd za běhu aplikace.

Dalším případem užití classloaderu může být nahrání třídy z různých úložišť, disku, sítě a nebo z databáze. Z tohoto důvodu java.lang.ClassLoader definuje metodu defineClass, která dostává definici třídy jako pole bytes.

JVM zaručuje, že jednou nahraná třída nebude vícekrát nahraná. Každá třída je uvnitř JVM identifikovaná plně kvalifikovaným jménem, tedy package a název třídy a classloaderem, který ji nahrál. To je velice důležité, pokud budeme mít například třídu Foo v package hoo, kterou nahrál classloader C1 a třídu Foo v package hoo, kterou nahrál classloader C2, jedná se o dvě rozdílné třídy. Pokud bych jejich instance zkusili přetypovat, dostali bychom ClassCastException.

Z historického hlediska je zajímavostí, že Java 1.1 nepoužívala pro identifikaci plně kvalifikované jméno třídy a classloader, který ji nahrál, ale pouze plně kvalifikované jméno. Jak se záhy ukázalo, bylo to typově nebezpečné, protože mohlo docházet podvržení tříd viz článek Java is not type-safe.

Hierarchie classloaderu je stromová a díky tomu umožňuje delegování nahrání tříd. Hierarchie classloaderu také zaručuje omezenou viditelnost třídy v prostoru, který classloader definuje. Classloader umožňuje v daném prostoru vidět třídy, které nahrál on sám a nebo některý z jeho předků a naopak nevidí na třídy, které nahrál kterýkoliv z jeho potomků. Vztah předek-potomek není myšlen na úrovni dědičnosti, ale na úrovni vazeb mezi objekty.

Hierarchie classloaderů

  • Bootstrap classloader - boostrapový classloader je zodpovědný za nahrání základních tříd Javy
  • Ext classloader - classloader pro Java rozšíření, který nahrává třídy z extension adresáře Javy
  • Classpath classloader - classloader nahrává třídy uvedené na classpath
  • Custom Classloader - implementace classloaderu, který může nahrávat třídy z libovolného uložište a defacto libovolným způsobem

Jak jsme si řekli JVM musí zaručit, že daná třída je nahrána pouze jednou v doménovém prostoru classloaderu, to zaručuje, že se její byte code postupem času nezmění. Java definuje mechanismus delegování mezi classloadery jako rodič dříve (parent first). To znamená, že classloader nejdříve deleguje nahrání třída na nadřazený classloader a teprve v případě, že třídu nenajde, zkusí to classloader sám.

Následuje kód metody loadClass z třídy java.lang.ClassLoader

    protected synchronized Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        // First, check if the class has already been loaded
        Class c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                else {
                    c = findBootstrapClass0(name);
                }
            catch (ClassNotFoundException e) {
                // If still not found, then invoke findClass in order
                // to find the class.
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }

Nebylo by pravidla, aby z něj nebyla výjimka, ve světě J2EE je totiž toto pravidlo obráceno naruby a to dokonce podle specifikace. Každý aplikační server má hierarchii classloaderů. V případě serveru Apache Tomcat, vypadá hierarchie následovně.

        Bootstrap
          |
       System
          |
       Common
      /      \
 Catalina   Shared
             /   \
        Webapp1  Webapp2
    

Jak je zřejmé ze schématu, každá webová aplikace má vlastní classloader a tento classloader nejdříve hledá třídu u sebe teprve potom deleguje hledání na rodiče. To umožňuje, že aplikace může mít vlastní verzi třídy stejného kvalifikovaného jména, která mohla být před tím nahrána v některém z rodičů. V této souvislosti je dobré vzpomenout, že java.lang.Thread definuje metodu setContextClassLoader, pomocí které je možné vláknu určit classloader, který se použije při hledání tříd.

S classloadery je spojená i vlastnost zvaná hot redeployment, tedy nahrání nové verze aplikace bez nutnosti restartu aplikace. JVM sice neumožňuje jednu a tu samou třídu pro daný prostor classloaderu nahrát opakovaně, ale je možné udělat malý trik. Třída se prostě nahraje znovu jiným classloaderem a původní reference na třídy classloader se nastaví na null, tím pádem je garbage collector může uklidit.

    Class foo = Class.forName("Foo", false, newClassloader) 
    

Poznámka: bohužel v Tomcatu potažmo JBossu je nepříjemná chyba, že ne všechny reference jsou zrušeny a proto dochází při neustálem redeployi aplikace v běžícím serveru k OutOfMemoryError výjimce na PermGen space. PermGen space je část heapu (pamětové úložiště Javy), které je určené pro definici tříd a metod. Workaround je čas od času server restartnout a nebo navýšit tuto oblast heapu JVM parametrem -XX:MaxPermSize=velikost.

Classloadery jsou velice zajímavým tématem, které do detailu přesahuje rámec tohoto článku. Navíc například v J2EE světě mají aplikační servery svá specifika ohledně konfigurace a chování classloaderů. Z tohoto důvodu na konci článku najdete seznam užitečných linků, které Vás navedou na další informace o problematice classloaderů.

Související linky