TÉMATICKÝ OKRUH Softwarové inženýrství
Číslo otázky : Otázka :
28. Vyšší programovací jazyky a jejich moderní rysy (správa paměti, implementace objektově orientovaných prvků, výjimky)
Obsah : 1. 2. 3. 4.
Rozdělení Strukturované a objektové programování Objektově orientované prostředky Správa paměti 4.1 Problémy správy paměti 4.2 Manuální správa paměti 4.3 Automatická správa paměti 4.4 Garbage collection 4.4.1 Algoritmus počítání referencí 4.4.2 Sledovací algoritmy (Mark & Sweep, kopírovací collector) 4.4.3 Generační algoritmus 5. Výjimka
1. Rozdělení Nižší programovací jazyky jsou jazyky primitivní, jejichž instrukce (víceméně přesně) odpovídají příkazům procesoru. To znamená, že procesor bude vykonávat ty instrukce, které programátor napíše. Jsou závislé na svém procesoru a nepřenositelné. V praxi to vypadá tak, že programátor musí vypisovat "každou pitomost", i jednoduchý program má neúměrně složitý zdrojový kód. Výhodou je, že programátor má takto přístup i k funkcím počítače, které by měl ve vyšším programovacím jazyce nedosažitelné. Patří sem jazyk symbolických adres (Assembler) a strojový kód. Vzláštním typem nižšího jazyka je tzv. autokód, který spojuje prvky nižších a vyšších jazyků. Vznikl rozšířením Assembleru o jednoduché příkazy pro často používané skupiny instrukcí. Vyšší (problémově orientované) programovací jazyky jsou podstatně srozumitelnější, struktura jejich zdrojáků je logická, nejsou závislé na strojových principech počítače. Do strojového kódu se převádějí kompilátorem (případně se rovnou spouštějí interpretrem). V praxi je vyšší programovací jazyk vše, co není Assembler, to znamená: Pascal, Basic, Prolog, Lisp, Algol, Fortran, ... Často se uvádí, že jazyk C je jakýmsi přechodem mezi vyššími a nižšími jazyky, má však blíže k vyšším. Vyšší programovací jazyky se dělí dále na: – Procedurální (imperativní) – Strukturované (C, BASIC) – Objektově orientované (Smalltalk, Java) – Neprocedurální (deskriptivní) – Funkcionální (Lisp, Haskell) – Logické (Prolog, Goedel)
2. Strukturované a objektové programování Strukturované programování definovalo několik základních pravidel pro psaní programu. Tato specifikace s sebou přinesla přehled a srozumitelnost zdrojového kódu. Hlavním nositelem činnosti jsou algoritmy. Bohužel tento způsob programování měl i jednu nevýhodu. Tou je obtížná práce se strukturami (např. vytváření a práce s databázemi). Z tohoto důvodu vznikl směr objektového programování. U objektového programování se nositelem aktivity staly data (vytvořily se objekty, kterým se definovaly vlastnosti a činnosti). Dále přineslo nové vlastnosti jako jsou zapouzdření, dědičnost a polymorfismus. Vytváření objektů (tříd) vede k zpřehlednění zdrojového kódu, snížení chyb v programech a navýšení flexibility a stability.
3. Objektově orientované prostředky Nejlepší prostředky pro systematický návrh a implementaci abstraktních datových typů poskytují objektově orientované jazyky pomocí tříd a objektů (objekt je instancí třídy). Třídy umožňují realizovat: – zapouzdření: zaručuje, že objekt nemůže přímo přistupovat k „vnitřnostem“ jiných objektů, což by mohlo vést k nekonzistenci. Každý objekt navenek zpřístupňuje rozhraní, pomocí kterého (a nijak jinak) se s objektem pracuje. – polymorfismus: označuje vlastnost přiřadit instanci typu předek referenci na instanci potomka, což dovoluje, že potomek může zastoupit/překrýt implementaci předka. Díky dynamicky vázaným metodám pak můžeme zajistit různé chování dle aktuálně přiřazeného objektu.
– dědičnost: Třída může být potomkem jiné třídy (příp. více tříd) - pak tato odvozená třída převezme všechny atributy, operace a asociace bázové třídy. Tuto sadu zděděných operací může rozšířit o vlastní prvky. Odvozenou třídu můžeme použít všude tam, kde je přípustné použití třídy bázové. U staticky vázané metody je adresa volané metody známa již při překladu (implicitně v C++, C#). U dynamicky vázané metody je adresa volané metody vyhodnocena při běhu (implicitně v Javě) . Toto je nejčastěji implementováno tabulkou virtuálních metod. Abstraktní třída obvykle obsahuje alespoň jednu abstraktní metodu - dynamicky vázanou metodu bez definovaného těla. Definici musí provést potomci této třídy. Nelze vytvořit instanci takové třídy.
4. Správa paměti Většina současných aplikací ke své činnosti využivá dynamického přidělování paměti, které umožňuje efektivně využívat paměť i v případě předem neznámých požadavků na její velikost nutnou pro konkrétní běh aplikace. Správa paměti je obvykle rozdělena do tří úrovní: – technického vybavení: správa paměti zabývá elektronickými prvky, v nichž jsou data skutečně uložena. Tato oblast zahrnuje zejména paměťové prvky RAM a paměti typu cache. – úroveň operačního systému: paměť musí být přidělována uživatelským programům a neníli dále programem vyžadována, pak je znovu použita pro jiné programy. Operační systém může předstírat, že má počítač mnohem více paměti než odpovídá skutečnosti, případně že každý program má celou dostupnou paměť pouze pro svou potřebu - tyto situace řeší systém virtuální paměti. – úroveň aplikačních programů: správa paměti zahrnuje přidělování úseků omezené dostupné paměti pro objekty a datové struktury programu a obvykle i opakované použití paměti, která již není obsazena. Řeší dva hlaví úkoly: – přidělování paměti: vyžaduje-li program blok paměti, musí správce vyhledat a přidělit úsek odpovídající délky z většího bloku paměti získané od operačního systému. – regenerace paměti: není-li úsek paměti přidělené programu dále využíván, může být uvolněn a dán k dispozici pro opakované použití. V zásadě zde existují dva možné přístupy: o uvolnění paměti musí rozhodnout programátor (tzv. manuální správa paměti), případně musí správa paměti o uvolnění rozhodnout sama (tzv. automatická správa paměti). Během přidělování a uvolňování paměti je třeba dodržovat jisté omezující podmínky, které zahrnují mimo jiné i časovou režii (dodatečný čas spotřebovaný správou paměti během činnosti programu), dobu pozdržení interaktivity (zpoždění, které pozoruje interaktivní uživatel), a paměťovou režii (množství paměti, které se spotřebuje pro administraci, zaokrouhlování velikosti bloků).
4.1 Problémy správy paměti Základním problémem správy paměti je správné rozhodnutí o tom, zda je v nějakém úseku paměti třeba ponechat data, která obsahuje, případně zda je možné tato data zahodit a úsek paměti znovu využít pro jiné účely. Mezi typické problémy patří: – předčasné uvolnění paměti: mnoho programů uvolní paměť, avšak pokouší se k ní přistupovat později, což může být příčinou havárie nebo neočekávaného chování programu. Tento problém nastává obvykle při manuální správě paměti.
– únik paměti: dochází k němu tehdy, pokud program neustále přiděluje novou paměť, aniž by ji zase uvolňoval. To může vést až k havárii programu v důsledku vyčerpání dostupné volné paměti. – externí fragmentace: nelze přidělit dostatečně velký blok volné paměti, i když celkové množství volné paměti je větší. Tato situace vzniká tehdy, pokud je volná paměť rozdělena na mnoho malých bloků, mezi nimiž jsou stále používané bloky. – špatná lokalita odkazů: přístupy k paměti jsou rychlejší, pokud pracujeme s ne příliš od sebe vzdálenými místy. Pokud správa paměti umístí bloky, k nimž program přistupuje současně, daleko od sebe, může to vést ke zhoršení výkonu programu. – nepřizpůsobivý návrh: pokud metoda přidělování paměti předem předpokládá jisté vlastnosti programu, například typickou velikost bloků, posloupnost odkazů nebo dobu života přidělovaných objektů a nejsou-li tyto předpoklady splněny, může se celková režie správy paměti zvýšit.
4.2 Manuální správa paměti Programátor má plnou kontrolu nad tím, zda a ve kterém okamžiku bude paměť uvolněna a případně využita pro opakované přidělení. To obvykle nastává buď explicitním voláním funkcí pro přidělování a uvolňování paměti z hromady (např. malloc/free v jazyce C), nebo jazykovými konstrukcemi ovlivňujícími zásobník (např. pro lokální proměnné). Klíčovou vlastností manuální správy paměti je možnost, aby program sám vrátil část paměti a oznámil, že ji již dále nepotřebuje. Bez tohoto oznámení správa paměti žádný úsek opakovaně nevyužije. Manuální správa paměti přenechává veškerou zodpovědnost za správné uvolňování paměti na programátorovi, což může vést k obtížně odhalitelným chybám
4.3 Automatická správa paměti Automatická správa paměti je služba, která je součástí jazyka nebo jeho rozšířením, a která automaticky regeneruje paměť, kterou by program již znovu nevyužil. Automatická správa programu (garbage collector) tuto činnost provádí opakovaným použitím těch bloků, které již nejsou živé, tj. program s nimi již dále nepracuje. Většinou se pracuje konzervativněji s bloky, na které není možné se dostat pomocí ukazatelů. Je-li například blok paměti sice dosažitelný, ale program jej již dále nepoužívá, je tato situace obecně těžko odhalitelná, i když v některých případech může pomoci překladač použitím mnohem přesnějších metod analýzy toku dat. Výhody automatické správy paměti jsou následující: – programátor se může věnovat řešení skutečného problému – rozhraní modulů jsou přehlednější - není třeba řešit problém zodpovědnosti za uvolnění paměti pro objekty vytvořené různými moduly – nastává menší množství chyb spojených s přístupem do paměti – správa paměti je často mnohem efektivnější. Tento přístup má však i své nevýhody: – paměť může být zachována jen proto, že je dostupná, i když není dále využita – automatická správa paměti není k dispozici ve starších, ale často používaných jazycích. 4.4 Garbage collection Základní princip garbage collecting: – vyhledají se v programu takové datové objekty, které nebudou v budoucnu použity – vrácení zdrojů, kde se vyskytovaly nalezené objekty
Uvolňování paměti garbage collecting osvobozuje prográmatora od uvolňování objektů, které již dále nejsou zapotřebí, což ho většinou stojí značné úsilí. Je to vlastně pomůcka pro stabilnější program, protože zabraňuje některým třídám provozních chyb. Například zabraňuje chybám ukazatelů, které ukazují na již nepoužívaný objekt, nebo který je již zrušen a tato paměť se dále k ničemu nevyužívá. 4.4.1 Algoritmus počítání referencí Ke každému objektu je přiřazen čítač referencí. Když je objekt vytvořen, jeho čítači je nastavena hodnota 1. V okamžiku, kdy si nějaký jiný objekt nebo kořen programu (kořeny jsou hledány v programových registrech, v lokálních proměnných uložených v zásobnících jednotlivých vláken a ve statických proměnných) uloží referenci na tento objekt, hodnota čítače je zvětšena o 1. Ve chvíli, kdy je reference mimo rozsah platnosti (např. po opuštění funkce, která si referenci uložila), nebo když je referenci přiřazena nová hodnota, čítač je snížen o 1. Jestliže je hodnota čítače některého objektu nulová, může být tento objekt uvolněn z paměti. 4.4.2 Sledovací algoritmy (Mark & Sweep, kopírovací collector) Sledovací algoritmy zastaví běh programu a začnou vyhledávat objekty. Začínají v kořenové množině programu a pokračují po referencích, dokud neprozkoumají všechny dosažitelné objekty. Algoritmy, založené na tomto principu, se používají téměř výlučně pro implementaci garbage collectorů v dnešních programovacích jazycích. 4.4.3 Generační algoritmus Generační garbage collector si rozděluje paměť programu do několika částí, tzv. generací. Objekty jsou vytvářeny ve spodní (nejmladší) generaci a po splnění určité podmínky, obvykle stáří, jsou přeřazeny do starší generace. Pro každou generaci může být úklid prováděn v rozdílných časových intervalech (obvykle nejkratší intervaly budou pro nejmladší generaci) a dokonce pro rozdílné generace mohou být použity rozdílné algoritmy úklidu. V okamžiku, kdy se prostor pro spodní generaci zaplní, všechny dosažitelné objekty v nejmladší generaci jsou zkopírovány do starší generace. I tak bude množství kopírovaných objektů pouze zlomkem z celkového množství mladších objektů, jelikož většina z nich se již stala odpadem.
5. Výjimka Je definována jako událost, která nastane během provádění programu a která naruší normální běh instrukcí. Výjimka je vyvolána například při chybném otevření souboru, při překročení mezí pole, při aritmetické chybě apod. Mechanismus výjimek umožňuje tyto chybové stavy zachytit a ošetřit. K tomu slouží dvojice bloků: – hlídaný: uzavírá kritickou část programu, kde "může" být vyvolána výjimka. – záchytný: ošetřuje výjimku, musí následovat bezprostředně za blokem try a může jich být libovolný počet. Hlídané a záchytné bloky je možné do sebe vnořovat. Pokud není výjimka vyvolána, pokračuje program za posledním záchytným blokem. Pokud dojde k vyvolání, pokračuje program prvním záchytným blokem, který ošetřuje odpovídající třídu výjimek. Po opuštění záchytného bloku pokračuje program za posledním záchytným blokem. Za pomocí výjimek je důsledně oddělen "užitečný kód" od kódu, který pouze ošetřuje chyby. Výjimka jedné třídy může být navíc vyvolána v hlídaném bloku na několika místech a k ošetření postačuje jeden blok catch. Ve srovnání s klasickým způsobem testování chyb (příkazem if) je mnohem přehlednější.
Dvojici výše zmíněných bloků je možné rozšířit o max. jeden nepovinný koncový blok. Tímto blokem program pokračuje po ukončení hlídaného i záchytného bloku (tj. ať už výjimka nastane nebo ne). Výjimky lze i programově vyvolávat (např. příkaz throw v Jave).