Hlavní navigace

Monitorování procesů a správa paměti v JDK 6 a JDK 7 (2)

27. 1. 2011
Doba čtení: 16 minut

Sdílet

V deváté části seriálu o vlastnostech JDK 6 a 7 se budeme zabývat především principem práce správců paměti. Řekneme si, jaké veličiny se při sledování chování aplikace v reálném provozu mohou měřit a jak jsou tyto veličiny ovlivněny nastavením parametrů správců paměti při spuštění prostředí Javy (JRE).

Obsah

1. Implementace správců paměti v JDK a OpenJDK

2. Nejdůležitější veličiny sledované při nastavování parametrů JRE

3. Trasovací algoritmy

4. Zápory naivních trasovacích algoritmů

5. Všechny objekty jsou si rovné, ale některé jsou si rovnější

6. Metoda „rozděl a panuj“

7. Kopie (přesuny) objektů v průběhu správy paměti

8. Parametry ovlivňující efektivitu správců paměti

9. Odkazy na Internetu

1. Implementace správců paměti v JDK a OpenJDK

V předchozí části poněkud nepravidelně vycházejícího seriálu o programovacím jazyce Java a vlastnostech JDK 6 a JDK 7 jsme se mj. věnovali i tématu implementace správců paměti (gc – garbage collectors). I když je programovací jazyk Java a samozřejmě i jeho běhové prostředí (JRE) navrženo takovým způsobem, aby se programátoři ve svých aplikacích nemuseli (většinou!) zabývat problematikou alokace a dealokace paměti pro vytvářené objekty, s nimiž se v aplikaci pracuje, je znalost fungování správců paměti důležitá při nastavování parametrů běhového prostředí Javy, především v těch případech, pokud se jedná o aplikaci běžící na výkonných systémech s více mikroprocesory nebo s více jádry umístěnými na jednom čipu (takový hardware je již zcela běžný jak na serverech, tak i na výkonnějších osobních počítačích). V současnosti je v JDK a samozřejmě taktéž v OpenJDK implementováno větší množství typů správců paměti, přičemž každý z nich je vhodný pro jiné typy aplikací a odlišné konfigurace počítačů.

Obrázek 1: Při použití správce paměti využívajícího počítání referencí (reference counting) je ke každému objektu přiřazeno celé kladné číslo vyjadřující počet existujících referencí na tento objekt. Pokud počitadlo klesne k hodnotě 0, je objekt z paměti odstraněn. Jak jsme si však již řekli minule, tento typ správy paměti se v JDK nepoužívá, i když v některých oblastech (například při tvorbě systémů běžících v reálném čase) má své využití.

V následujících kapitolách si vysvětlíme především princip fungování správců paměti založených na takzvaném generačním algoritmu, při jehož použití se objekty umístěné na haldě (heapu) heuristicky rozdělují podle jejich chování zjištěného přímo za běhu aplikace (nejedná se tedy o vlastnost objektů, která by byla zjišťována již při překladu, i když by to bylo v některých případech možné). Tito správci paměti mohou běžet buď v jednom vlákně, nebo je možné jejich funkci rozdělit do většího množství vláken, přičemž strategie jejich práce se liší podle toho, jaké parametry jsou nastaveny při startu JRE – ne všechny aplikace totiž mají shodné požadavky na to, jakým způsobem mají správci paměti pracovat (někdy se preferuje spíše celková propustnost, u systémů reálného času se však dbá na minimalizaci pauz způsobených správci paměti při běhu aplikace). Bližší informace o tom, jaké veličiny se většinou při nastavování parametrů JRE sledují, si řekneme v další kapitole.

2. Nejdůležitější veličiny sledované při nastavování parametrů JRE

Při nastavování parametrů správců paměti či při optimalizacích aplikace (tj. úpravách vlastního algoritmu a/nebo používaných datových typů) se většinou sledují tři veličiny. První z těchto veličin představuje maximální kapacitu operační paměti, která může být alokována při vytváření objektů nebo při správě haldy (heap) správcem paměti. Druhou důležitou veličinou je propustnost (throughput), která – poněkud zjednodušeně řečeno – vyjadřuje, kolik procent strojového času proces (tj. běžící Javovská aplikace) stráví vlastními výpočty. Přitom se předpokládá, že zbylá procenta strojového času jsou využita právě správcem paměti při zjišťování a následném uvolňování nepotřebných objektů. Propustnost se většinou měří po velmi dlouhý časový interval, kdy se již aplikace „ustálí“ a kdy mají správci paměti založení na heuristice možnost si objekty roztřídit podle jejich délky existence (viz též další kapitoly). Třetí sledovanou veličinou je průměrná či maximální doba, po kterou musí být aplikace pozastavena (pause time), aby mohl správce paměti uvolnit nepotřebné objekty z haldy, popř. přeorga­nizovat zde umístěné objekty podle nastavené strategie organizace haldy. Na tomto místě je možná dobré zdůraznit, že naprostá většina správců paměti musí aplikaci alespoň na krátké okamžiky pozastavit.

Podobně jako je tomu i v mnoha dalších oblastech IT (a nejenom tam :-), způsobí většinou úprava nastavení JRE tak, aby se preferovala jedna z těchto veličin, zhoršení obou zbývajících veličin. Například po zvýšení maximální kapacity paměti pro haldu se může snížit frekvence volání „plného“ úklidu haldy (viz další text); snížení průměrné či maximální doby, po kterou je aplikace kvůli správě paměti pozastavena, může snížit její celkovou propustnost apod. Při nastavování parametrů JRE je tedy vhodné si ujasnit, o jaký typ aplikace se jedná a jaké chování je ještě pro uživatele/zákazníka přijatelné a které již ne. Pokud by se kupříkladu jednalo o interaktivní aplikaci s grafickým uživatelským rozhraním (v extrémním případě o hru nebo o multimediální přehrávač), je většinou nutné minimalizovat pauzy způsobené správcem paměti a naopak – u dlouhotrvajících výpočtů se bude v naprosté většině případů klást větší důraz na celkovou propustnost, protože u výpočtu trvajícího například tři hodiny bez interakce uživatele asi nikoho nebude trápit, když se celá aplikace na několik sekund zastaví, aby správce paměti mohl vykonat všechny potřebné činnosti.

3. Trasovací algoritmy

V moderních implementacích běhového prostředí jazyka Java (JRE) se velmi často můžeme setkat se správci paměti, které jsou založeny na testování, které objekty uložené na haldě jsou skutečně „živé“, tj. dostupné z alespoň jednoho vlákna aplikace. Tito správci paměti postupně prochází stromem všech objektů, přičemž svoji činnost začínají u objektů, jež jsou v daném okamžiku aktivní ve všech vláknech. Mohou začínat například u všech objektů, jejichž reference jsou uloženy v zásobníkových rámcích vláken. Připomeňme si, že virtuální stroj jazyka Java vytváří pro každé vlákno aplikace vlastní virtuální zásobník (virtual stack), v němž jsou uloženy zásobníkové rámce (stack frames). Pro každou zavolanou metodu je vytvořen nový zásobníkový rámec, ve kterém jsou uložené jak lokální proměnné, tak i mezivýsledky aritmetických a logických operací (tyto operace jsou prováděny nad takzvaným zásobníkem operandů – operand stack – jenž je součástí zásobníkového rámce).

Obrázek 2: Stav haldy (heapu) před spuštěním správce paměti využívajícího (naivní) trasovací algoritmus.

Zásobníkový rámec je taktéž použit při volání metod (statických, virtuálních i konstruktorů) pro předávání parametrů volaným metodám (u nativních metod je situace komplikovanější). Vzhledem k tomu, že se s ukazateli na zásobníkové rámce nedá v Javě přímo manipulovat, je možné, aby byly tyto rámce postupně vytvářeny přímo na haldě, a to na libovolném místě (to je však již implementační detail). V každém případě mohou správci paměti nejprve označit všechny objekty uložené v jednotlivých virtuálních zásobnících jako objekty „živé“, protože jsou z daného vlákna dostupné. Správci paměti posléze postupně prochází všemi atributy těchto objektů, které mnohdy obsahují reference na objekty další, a u všech nalezených objektů nastavují bitový příznak říkající, že je objekt skutečně „živý“, tj. nějakým způsobem dostupný. Po provedení celého trasování mohou být ty objekty, které nemají bitový příznak „je živý“ nastaveny na hodnotu true, odstraněny z haldy, protože neexistuje způsob, jakým by byly tyto objekty dosažitelné z libovolného vlákna aplikace.

Obrázek 3: Objekt číslo 1 (zde zvýrazněný žlutě) byl nalezen ve virtuálním zásobníku jediného vlákna hypotetické aplikace a z tohoto důvodu byl označený bitovým příznakem „živý“. U modře označených objektů ještě jejich stav neznáme.

4. Zápory naivních trasovacích algoritmů

Výše uvedený způsob práce správců paměti je ovšem vhodný pouze pro teoretické úvahy a v praxi se (tak jak je popsaný) příliš často nepoužívá, protože má několik nepříjemných vlastností. Jednou ze špatných vlastností je to, že projití celého stromu dosažitelných (živých) objektů může být poměrně náročné jak na výpočetní výkon daného počítače, tak i na využití jeho vyrovnávacích pamětí, protože správce musí při své činnosti projít i těmi objekty, které například nejsou v aplikaci nikdy použity, i když jsou stále dostupné (živé). Představme si například serverovou aplikaci napsanou v Javě, která nikdy nepoužije pole args, v němž jsou aplikaci – přesněji řečeno statické metodě public static void main() – předány parametry zadané na příkazové řádce. Popř. aplikace tyto parametry skutečně zpracuje, ale pouze ihned po svém spuštění. Ovšem pole args se ani v tomto případě nemůže odstranit, protože reference na něj je uložena prakticky na samotném začátku virtuálního zásobníku hlavního vlákna aplikace.

Obrázek 4: Objekt číslo 1 obsahoval dva atributy – reference na objekt číslo 2 a objekt číslo 3. I tyto objekty jsou označeny příznakem „živý“, podobně jako objekt číslo 7, jenž je dostupný nepřímo, konkrétně přes dvě reference Objekt 1→Objekt 3→Objekt 7.

Tyto objekty – pole řetězců – jsou „živé“ po celou dobu práce serverové aplikace, ovšem ve skutečnosti se k nim nemusí i několik týdnů či měsíců přistupovat (ano, i Javovské aplikace mnohdy musí vydržet běžet tak dlouho ;-). Navíc, což je v praxi mnohem důležitější, může projití celého stromu objektů trvat velmi dlouho. To by způsobovalo nepříjemné a v mnoha případech i nepřípustné prodlevy – u desktopových aplikací je to nepříjemná vlastnost, u mnoha aplikací serverových či aplikací ovládajících nějaké periferní zařízení pak vlastnost zcela nepřípustná. Z tohoto důvodu se hledal vhodný způsob, jak by bylo možné chování „naivních“ trasovacích algoritmů, které neustále otrocky prochází všemi objekty na haldě, vylepšit. Jedna z vhodných a implementačně relativně nenáročných metod se skutečně našla. Tato metoda je založena na poznatku získaném v praxi: ne všechny objekty, které jsou při běhu aplikace vytvořeny, se chovají stejným způsobem. Existuje totiž (statistická) závislost mezi dobou existence objektu a pravděpodobností, že se tento objekt stane nedostupný (neaktivní). Na tomto poznatku jsou založeny takzvané heuristické trasovací algoritmy.

Obrázek 5: Všechny ostatní objekty (na předchozím obrázku měly stále modré pozadí) neměly příznak „živý“ nastaven na logickou jedničku a proto je správce paměti mohl odstranit z haldy. Před odstraněním samozřejmě provedl finalizaci objektů, tj. zavolal jejich metody finalize().

5. Všechny objekty jsou si rovné, ale některé jsou si rovnější

V současných verzích správců paměti, které jsou používané v JDK 6 i JDK 7, se sice stále používají trasovací algoritmy, ovšem tyto algoritmy nepracují se všemi objekty uloženými na haldě. Namísto toho jsou objekty rozděleny do několika navzájem oddělených částí haldy. Nejhrubší dělení je na objekty „mladé“ a objekty „staré“. Objekty jsou tedy rozděleny do několika oblastí na haldě podle toho, jak dlouho již existují – čas jejich existence se ovšem neměří pomocí nějakých časovačů, ale podle toho, kolik spuštění správce paměti daný objekt „přežil“, tj. kolikrát se objekt nacházel v aktivním (dosažitelném) stavu. Využívá se zde přitom faktu zjištěného při zkoumání chování reálných aplikací v praxi: poměrně mnoho objektů se vyznačuje velmi krátkou dobou existence a naopak – čím déle je objekt aktivní (dosažitelný alespoň z jednoho vlákna aplikace), tím menší je pravděpodobnost, že při dalším běhu správce paměti bude muset být odstraněn.

Obrázek 6: Většina běžných objektů je vytvořena v menší oblasti haldy nazvané young generation. Teprve poté, co objekt několikrát „přežije“ spuštění správce paměti nad touto oblastí haldy, může být přesunut do (obecně) větší oblasti haldy nazvané old generation či taktéž tenured generation.

V praxi se tedy při rozdělení haldy na několik oblastí musí změnit především způsob alokace paměti při vytváření (konstrukci) objektů. Halda je totiž rozdělena na tři oblasti: relativně malou oblast nazvanou young generation, větší oblast nazvanou old generation (též tenured generation) a třetí oblast se jménem permanent generation (tato oblast má poněkud jiný význam a budeme se jí proto zabývat až v některé následující části tohoto seriálu). Většina nových objektů je vytvářena v oblasti young generation, pouze některé typy velkých objektů musí být vytvořeny přímo v old/tenured generation. Nad oblastí young generation se relativně často spouští správce paměti optimalizovaný s ohledem na to, že velká část objektů bude z této oblasti skutečně odstraněna. Objekty, které přežijí několik spuštění tohoto „rychlého“ správce paměti, jsou přesunuty do oblasti old generation. I nad objekty uloženými v oblasti old generation se samozřejmě spouští správce paměti, ovšem s mnohem menší frekvencí (pokud se náhodou spouští s velkou frekvencí, značí to buď problém v aplikaci, nebo problém v parametrech JRE). Taktéž algoritmus tohoto správce paměti je poněkud upravený, protože se počítá s tím, že objekty uložené v old generation s větší pravděpodobností „přežijí“, na rozdíl od většiny objektů uložených v young generation.

Obrázek 7: Oblast haldy nazvaná young generation je většinou rozdělena na tři podoblasti: eden, survivor space #1 a survivor space #2. Na některých architekturách je však namísto toho použita větší kapacita haldy pro eden a jedinou podoblast survivor space.

6. Metoda „rozděl a panuj“

Popis rozdělení haldy (heapu) na tři oblasti, který byl uveden v předchozí kapitole, není zcela přesný. Týká se to zejména oblasti young generation, které je ve skutečnosti ještě rozdělena na několik dalších podoblastí. První z těchto podoblastí se nazývá eden a právě v této podoblasti jsou vytvářeny nové objekty. V případě, že je celá kapacita edenu již alokována, zavolá se správce paměti, který celým edenem projde a vyřadí ty objekty, které již nejsou aktivní (živé). Zbylé objekty, kterých je (alespoň podle statistických měření provedených nad typickými aplikacemi) méně, jsou zkopírovány do jedné z oblastí nazvaných survivor space. V této oblasti jsou tedy umístěny pouze ty objekty, které přežily masakr způsobený správcem paměti v edenu :-). U architektur, v níž se používají dvě podoblasti survivor space, je vždy jedna z těchto podoblastí prázdná a druhá podoblast obsahuje objekty zkopírované z edenu popř. z podoblasti první (kopírováním objektů se totiž survivor space kromě jiného též defragmentuje).

Obrázek 8: Objekty běžné velikosti jsou vždy vytvářeny v edenu. Pouze příliš velké objekty jsou alokovány již přímo v oblasti old generation, což však může způsobovat problémy – fragmentaci této oblasti, frekventované odstraňování objektů z old generation atd.

7. Kopie (přesuny) objektů v průběhu správy paměti

Každý objekt může být po svém přesunu z edenu do jedné z podoblastí survivor space, mezi těmito oběma podoblastmi survivor space přesunut několikrát, než je uznáno jeho právo být zkopírován do oblasti old generation (samozřejmě v závislosti na nastavených parametrech správce paměti). Ke kopiím objektu ze survivor space do edenu však nedochází, protože eden je vyhrazen pouze pro nově vytvářené objekty. Po každém proběhnutí jedné iterace „rychlého“ správce paměti tedy vypadá celá oblast young generation na haldě následovně: eden je vyprázdněný, jedna z podoblastí survivor space je taktéž vyprázdněná a druhá z těchto podoblastí obsahuje objekty, které jsou sice stále aktivní, ovšem ještě nedosáhly stavu dospělosti (navíc je tato podoblast defragmentována). Při spuštění další iterace „rychlého“ správce paměti se význam obou podoblastí survivor space otočí, tj. objekty umístěné v částečně zaplněné podoblasti i přeživší objekty z edenu jsou kopírovány do podoblasti, která byla původně prázdná.

Obrázek 9: Jakmile je eden zcela zaplněn, nebo pokud v něm již není místo pro vytvoření dalšího objektu, spustí se správce paměti, který poctivě projde všechny zde uložené objekty. Aktivní objekty jsou uloženy do jedné z oblastí survivor space, objekty neaktivní jsou odstraněny.

8. Parametry ovlivňující efektivitu správců paměti

Efektivita správy paměti implementované podle popisu uvedeného v předchozích kapitolách je do značné míry závislá na nastavení velikostí jednotlivých oblastí na haldě, tj. zejména poměru mezi velikostí oblasti young generation a old (tenured) generation. V neposlední řadě efektivitu správců paměti ovlivňuje velikost edenu. Pokud by například byla jeho kapacita příliš malá, docházelo by často k jeho zaplňování, což znamená, že by se správce paměti volal s velkou frekvencí. Na tom by sice nemuselo být při prvním pohledu nic špatného, ovšem v praxi se ukazuje, že je vhodné dát některým objektům určitý čas na to, aby se staly neaktivní. Příliš malá kapacita edenu by tedy mohla způsobit, že by se i objekty s krátkou dobou existence zkopírovaly do survivor space a v extrémním případě i do oblasti old generation. Další problém by nastal u objektů, které se již při svém vytváření do kapacity edenu nedostanou. Takové objekty totiž jsou, jak jsme si již řekli o několik odstavců výše, přímo vytvářeny v oblasti old generation.

Obrázek 10: Před či po projití edenu se navíc projdou i všechny objekty uložené v jedné ze survivor space a opět je na základě trasování objektů zhodnoceno, který objekt je aktivní, který neaktivní a který dostatečně starý na to, aby byl přesunut do oblasti old generation.

root_podpora

To ovšem opět může způsobit menší efektivitu při údržbě paměti, protože algoritmus správce paměti pracujícího nad oblastí old generation není optimalizován na to, že se v této oblasti budou nacházet objekty s krátkou dobou existence. Dalším parametrem, který v některých případech může i významně ovlivnit chování aplikace, je volba, zda se má použít správce paměti běžící v jediném vlákně nebo multivláknový (multithreadový) správce paměti. Zatímco jednovláknový (sériový) správce paměti je vhodný pro většinu desktopových aplikací běžících na počítačích s malým počtem jader, může být použití tohoto správce (který je například standardně použit v JDK 1.4.2!) na výkonných serverech s osmi či šestnácti procesorovými jádry velmi problematické, protože se zde začne negativně uplatňovat Amdahlův zákon. Podrobnosti si řekneme v navazující části tohoto seriálu, spolu s vysvětlením voleb, které ovlivňují způsob práce správců paměti (a vlastně i to, který správce paměti se skutečně použije).

Obrázek 11: Sledování stavu zaplnění jednotlivých oblastí haldy lze provádět i z nástroje jconsole (viz pravý dolní roh okna).

9. Odkazy na Internetu

  1. Amdahl's law
    http://en.wiki­pedia.org/wiki/Am­dahl_law
  2. Garbage collection (computer science)
    http://en.wiki­pedia.org/wiki/Gar­bage_collecti­on_(computer_sci­ence)
  3. Dr. Dobb's | G1: Java's Garbage First Garbage Collector
    http://www.drdob­bs.com/article/prin­tableArticle.jhtml?ar­ticleId=219401061­&dept_url=/ja­va/
  4. Java's garbage-collected heap
    http://www.ja­vaworld.com/ja­vaworld/jw-08–1996/jw-08-gc.html
  5. Compressed oops in the Hotspot JVM
    http://wikis.sun­.com/display/Hot­SpotInternals/Com­pressedOops
  6. 32-bit or 64-bit JVM? How about a Hybrid?
    http://blog.ju­ma.me.uk/2008/10/­14/32-bit-or-64-bit-jvm-how-about-a-hybrid/
  7. Compressed object pointers in Hotspot VM
    http://blogs.sun­.com/nike/entry/com­pressed_objec­t_pointers_in_hot­spot
  8. Java HotSpot™ Virtual Machine Performance Enhancements
    http://downlo­ad.oracle.com/ja­vase/7/docs/techno­tes/guides/vm/per­formance-enhancements-7.html
  9. Using jconsole
    http://downlo­ad.oracle.com/ja­vase/1.5.0/doc­s/guide/manage­ment/jconsole­.html
  10. jconsole – Java Monitoring and Management Console
    http://downlo­ad.oracle.com/ja­vase/1.5.0/doc­s/tooldocs/sha­re/jconsole.html
  11. Great Computer Language Shootout
    http://c2.com/cgi/wi­ki?GreatCompu­terLanguageSho­otout
  12. x86–64
    http://en.wiki­pedia.org/wiki/X86–64
  13. Physical Address Extension
    http://en.wiki­pedia.org/wiki/Phy­sical_Address_Ex­tension
  14. Java performance
    http://en.wiki­pedia.org/wiki/Ja­va_performance
  15. 1.6.0_14 (6u14)
    http://www.ora­cle.com/technet­work/java/java­se/6u14–137039.html?ssSou­rceSiteId=otncn
  16. Update Release Notes
    http://www.ora­cle.com/technet­work/java/java­se/releasenotes-136954.html
  17. 4.10 Limitations of the Java Virtual Machine
    http://java.sun­.com/docs/book­s/jvms/second_e­dition/html/Clas­sFile.doc.html#88659
  18. Java™ Platform, Standard Edition 7 Binary Snapshot Releases
    http://dlc.sun­.com.edgesuite­.net/jdk7/bina­ries/index.html
  19. Trying the prototype
    http://mail.o­penjdk.java.net/pi­permail/lambda-dev/2010-August/002179.html
  20. Better closures (for Java)
    http://blogs.sun­.com/jrose/en­try/better_clo­sures
  21. Lambdas in Java: An In-Depth Analysis
    http://www.in­foq.com/articles/lam­bdas-java-analysis
  22. Class ReflectiveOpe­rationExcepti­on
    http://downlo­ad.java.net/jdk7/doc­s/api/java/lan­g/ReflectiveO­perationExcep­tion.html
  23. Proposal: Indexing access syntax for Lists and Maps
    http://mail.o­penjdk.java.net/pi­permail/coin-dev/2009-March/001108.html
  24. Proposal: Elvis and Other Null-Safe Operators
    http://mail.o­penjdk.java.net/pi­permail/coin-dev/2009-March/000047.html
  25. Java 7 : Oracle pushes a first version of closures
    http://www.bap­tiste-wicht.com/2010/05­/oracle-pushes-a-first-version-of-closures/
  26. Groovy: An agile dynamic language for the Java Platform
    http://groovy­.codehaus.org/O­perators
  27. Better Strategies for Null Handling in Java
    http://www.sli­deshare.net/Step­han.Schmidt/bet­ter-strategies-for-null-handling-in-java
  28. Control Flow in the Java Virtual Machine
    http://www.ar­tima.com/under­thehood/flowP­.html
  29. Java Virtual Machine
    http://en.wiki­pedia.org/wiki/Ja­va_virtual_machi­ne
  30. ==, .equals(), compareTo(), and compare()
    http://leepoin­t.net/notes-java/data/expres­sions/22compa­reobjects.html
  31. New JDK7 features
    http://openjdk­.java.net/pro­jects/jdk7/fe­atures/
  32. Project Coin: Bringing it to a Close(able)
    http://blogs.sun­.com/darcy/en­try/project_co­in_bring_close
  33. ClosableFinder source code
    http://blogs.sun­.com/darcy/re­source/Projec­tCoin/Closeable­Finder.java
  34. Joe Darcy blog about JDK
    http://blogs.sun­.com/darcy
  35. Java 7 – more dynamics
    http://www.bap­tiste-wicht.com/2010/04­/java-7-more-dynamics/
  36. ArrayList (JDK 1.4)
    http://downlo­ad.oracle.com/ja­vase/1.4.2/doc­s/api/java/util/A­rrayList.html

Byl pro vás článek přínosný?

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.