Hlavní navigace

Pohled pod kapotu JVM - architektura JPDA a rozhraní JDI

26. 3. 2013
Doba čtení: 12 minut

Sdílet

V dnešní části seriálu o jazyku Java i o virtuálním stroji Javy si řekneme základní informace o architektuře JPDA (Java Platform Debugger Architecture). Ukážeme si, jaký je vztah již popsaného rozhraní JVM TI k JPDA a taktéž si popíšeme základní vlastnosti javovského rozhraní JDI (Java Debugger Interface).

Obsah

1. Vlastnosti, přednosti a zápory rozhraní JVM TI (shrnutí)

2. Architektura JPDA (Java Platform Debugger Architecture)

3. JDWP – Java Debug Wire Protocol

4. JDI – Java Debugger Interface

5. Vytvoření připojení přes rozhraní JDI k cílovému virtuálnímu stroji Javy

6. Demonstrační příklad – výpis všech konektorů, které jsou přes rozhraní JDI k dispozici

7. Spuštění demonstračního příkladu na Linuxu i na MS Windows

8. Obsah následující části seriálu

9. Odkazy na Internetu

1. Vlastnosti, přednosti a zápory rozhraní JVM TI (shrnutí)

V předchozích osmnácti částech seriálu o programovacím jazyku Java i o virtuálním stroji Javy jsme si popsali většinu funkcí nabízených rozhraním JVM TI (Java Virtual Machine Tool Interface ). Připomeňme si, že JVM TI umožňuje připojení nativní binární knihovny (naprogramované typicky v céčku či C++) k virtuálnímu stroji Javy. Tato nativní knihovna, která se nazývá agent, může přes funkce nabízené JVM TI sledovat stav JVM – může například získat informace o všech načtených třídách, přečíst a prostudovat obsah haldy (heapu), zastavit libovolné vlákno na nastaveném breakpointu a samozřejmě taktéž prozkoumat obsah zásobníkových rámců vybraného vlákna (například při vzniku výjimky). Nespornou předností rozhraní JVM TI je fakt, že JVM TI agent může začít sledovat či ovlivňovat činnost virtuálního stroje Javy ještě předtím, než JVM začne inicializovat classloadery a než začne načítat a inicializovat bajtkódy jednotlivých tříd, které jsou využívány spouštěnou javovskou aplikací.

Dokonce platí, že funkce dostupné přes rozhraní JVM TI bude možné využít i tehdy, pokud nepůjde inicializovat javovskou aplikaci, například z toho důvodu, že se používá špatný rt.jar (zde jsou uloženy třídy a rozhraní definované v Java SE API), pokud tento soubor nebyl nalezen apod. Ovšem při pohledu na zdrojové kódy testovacích JVM TI agentů (http://icedtea.classpath­.org/people/ptisnovs/jvm-tools/) začnou být zřejmé i některé nevýhody tohoto rozhraní. Ty spočívají v tom, že při volání funkcí JVM TI je nutné provádět ruční alokaci a dealokaci jednotlivých datových struktur (typicky řetězců či polí), při vstupu do kritických sekcí musí programátor explicitně zamykat a odmykat zámky atd. Funkce a datové struktury JVM TI jsou navíc odlišné od tříd a metod nabízených samotným standardním Java (SE) API, což znamená, že se vývojář musí detailně seznámit s oběma „světy“ – jak s tím javovským, tak i céčkovým.

2. Architektura JPDA (Java Platform Debugger Architecture)

Tvůrci specifikace technologie virtuálního stroje Javy si byli této vlastnosti rozhraní JVM TI vědomi a proto ve skutečnosti JVM TI tvoří pouze nejnižší vrstvu speciálního API virtuálního stroje Javy sloužícího k monitorování i k řízení JVM. Nad rozhraním JVM TI je vytvořen komunikační kanál tvořený protokolem JDWP (Java Debug Wire Protocol) [kapitola 3] a nad tímto protokolem bylo vytvořeno plnohodnotné javovské rozhraní nazvané JDI (Java Debug Interface) [kapitola 4]. JVM TI, JDWP (společně s poměrně jednoduchým rozhraním k tomuto protokolu JDWPI) a JDI tvoří ucelenou třívrstvou architekturu nazvanou Java Platform Debugger Architecture neboli zkráceně JPDA. Jak již název této architektury naznačuje, lze ji použít například pro implementaci různých ladicích nástrojů (debuggerů), ale i nástrojů monitorovacích, nástrojů umožňujících „hotswap“ tříd či jejich metod do běžícího virtuálního stroje Javy apod.

Z tohoto důvodu jsou funkce nabízené jednou ze tří zmíněných technologií velmi často využívány i integrovanými vývojovými prostředími (IDE). Na provádění nízkoúrovňových operací je přitom používáno rozhraní JVM TI a pro implementaci vysokoúrovňových funkcí pak JDI, popř. se s virtuálním strojem Javy, v němž byla spuštěna vyvíjená aplikace, komunikuje přímo s využitím protokolu JDWP (to je však většinou zbytečně komplikované). Vzájemný vztah mezi rozhraním JVM TI, protokolem JDWP a rozhraním JDI je zobrazen na následujícím schématu:

Obrázek 1: Vzájemný vztah mezi (céčkovým) rozhraním JVM TI, protokolem JDWP a javovským rozhraním JDI.

3. JDWP – Java Debug Wire Protocol

Jedním z důležitých prvků architektury JPDA je protokol nazvaný JDWP, neboli Java Debug Wire Protocol. Ze schématu vyobrazeného v předchozí kapitole je patrné, že protokol JDWP je v typické konfiguraci použit pro komunikaci mezi nativním agentem využívajícím rozhraní JVM TI na jedné straně a debuggerem využívajícím javovské rozhraní JDI na straně druhé. V terminologii JPDA se nativní agent nazývá back-end a jeho standardní implementaci nabízející céčkové rozhraní nazvané jdwpTransport většinou nalezneme v instalačním adresáři JRE pod jménem libjdwp.so popř. jdwp.dll (to, že je tato knihovna součástí JRE znamená, že lze JDWP využít například i na klientských počítačích či serverech, kde nelze předpokládat existenci instalace plné JDK). Zatímco se nativní agent nazývá back-end, front-endem je nazývána javovská knihovna, která s debuggerem či jiným vyvíjeným ladicím/monitorovacím nástrojem komunikuje přes javovské rozhraní JDI.

Důležité je, že zatímco back-end je přímo navázán s virtuálním strojem Javy, na němž běží laděná či sledovaná javovská aplikace (takzvaná „cílová JVM“), je front-end vetšinou spuštěn v jiném virtuálním stroji Javy, který navíc může běžet i na odlišném počítači – protokol JDWP umožňuje jak lokální komunikaci přes sdílenou paměť, tak i komunikaci se vzdáleným počítačem přes sockety, sériovou linku atd. Díky existenci JDWP je dokonce možné, aby byl front-end implementován v jiném programovacím jazyce, než je Java (viz též schéma zobrazené pod odstavcem). Po implementaci front-endu se pouze očekává dodržení specifikace tohoto protokolu, která mj. vyžaduje i asynchronní obousměrnou komunikaci založenou na (obousměrném) posílání paketů typu příkaz–odpověď. JDWP je bezstavovým protokolem a posílané pakety obsahují binární data, v nichž jsou hodnoty reprezentované dvoubajtovými či čtyřbajtovými hodnotami uloženy ve formátu big-endian, jak je tomu ostatně v JDK/JRE zvykem. Základní příkazy protokolu JDWP si popíšeme v některém z dalších pokračování tohoto seriálu.

Obrázek 2: S využitím protokolu JDWP může s běžící (cílovou) JVM komunikovat prakticky libovolný proces; nemusí se tedy jednat o aplikaci naprogramovanou v Javě.

4. JDI – Java Debugger Interface

JDI, neboli Java Debug Interface, je rozhraní implementované v Javě, které je možné použít pro monitorování a ladění aplikací spuštěných v samostatném („cílovém“) virtuálním stroji Javy. JDI lze použít například pro implementaci debuggeru naprogramovaného v Javě. Tento debugger se může s využitím tříd a metod poskytovaných rozhraním JDI připojit k virtuálnímu stroji s testovanou aplikací, popř. dokonce může tento stroj přímo spustit, což je téma, kterému se budeme podrobněji věnovat v následující části tohoto seriálu. Ve chvíli, kdy debugger volá nějakou metodu nabízenou JDI, vyšle se příkazový paket přes protokol JDWP do příslušného JVM TI agenta, který je již připojen k testovanému virtuálnímu stroji Javy. Odpovědí na příkazový paket je paket s vyžádanými daty (například hodnotami lokálních proměnných sledovaného vlákna), který je přenesen přes protokol JDWP zpět do debuggeru, který tato data získá přes JDI jako návratovou hodnotu nějaké metody.

Tento postup sice může na první pohled vypadat poněkud komplikovaně, ve skutečnosti je však velmi flexibilní. Programátor vyvíjející debugger či specializovaný monitorovací nástroj se totiž může sám rozhodnout, zda raději využije nízkoúrovňové rozhraní JVM TI (což jsme si ukázali v předchozích osmnácti částech tohoto seriálu), zda bude raději implementovat svůj program tak, aby používal komunikační protokol JDWP či zda naopak využije třídy a metody nabízené vysokoúrovňovým rozhraním JDI, což je ve většině případů nejrychlejší a z implementačního hlediska i nejjednodušší řešení. Ostatně čtenáři tohoto seriálu se o relativní jednoduchosti rozhraní JDI v porovnání s nízkoúrovňovým JVM TI budou moci přesvědčit v dalších částech, v nichž si ukážeme poměrně velké množství demonstračních příkladů využívajících rozhraní JDI pro monitorování i řízení JVM.

5. Vytvoření připojení přes rozhraní JDI k cílovému virtuálnímu stroji

Před použitím tříd a metod nabízených rozhraním JDI je nejprve nutné navázat spojení mezi debuggerem (tak budeme pro zjednodušení označovat všechny nástroje volající JDI) a takzvanou cílovou JVM, tj. virtuálním strojem, v němž běží monitorovaná či laděná aplikace. Způsob komunikace mezi debuggerem a cílovou JVM se v terminologii JPDA nazývá transport; s tímto slovem se ostatně můžeme setkat i při startu cílové JVM (java -agentlib:jdwp=transport=???). Implementace transportu může být různá, někdy i systémově závislá. Mezi dvě hlavní metody však patří transport implementovaný s využitím socketů a druhou metodou je transport implementovaný sdílenou pamětí. Jak však uvidíme dále, není transport používající sdílenou paměť podporován na všech operačních systémech. V případě, že se komunikace navazuje přes rozhraní JDI, se používá abstrakce transportu nazvaná jednoduše connector, která je definována v rozhraní com.sun.jdi.connect.Connector (pozor – nejde o součást standardního API Javy).

Většina virtuálních strojů Javy nabízí pro propojení mezi debuggerem a cílovou JVM hned několik typů konektorů, z nichž ty nejpoužívanější jsou vypsány v následující tabulce:

# Konektor Plné jméno (identifikace) Popis
1 CommandLineLaunch „com.sun.jdi.CommandLineLaunch“ cílová JVM je spuštěna přímo přes JDI a ihned poté se naváže spojení přes socket či sdílenou paměť
2 RawCommandLineLaunch „com.sun.jdi.RawCommandLineLaunch“ podobné předchozímu konektoru, ovšem příkaz pro spuštění JVM se předává přes jediný řetězec (liší se jen způsob spuštění)
3 SocketAttach „com.sun.jdi.SocketAttach“ připojení k již běžící cílové JVM přes socket
4 SharedMemoryAttach „com.sun.jdi.SharedMemoryAttach“ připojení k již běžící cílové JVM přes sdílenou paměť
5 SocketListen „com.sun.jdi.SocketListen“ připojení k běžící cílové JVM na základě požadavku přijatého od této JVM
6 SharedMemoryListen „com.sun.jdi.SharedMemoryListen“ dtto ale využije se sdílená paměť
7 ProcessAttach „com.sun.jdi.ProcessAttach“ připojení k již běžící cílové JVM, která je spuštěna s parametrem agentlib:jdwp=server=y

6. Demonstrační příklad – výpis všech konektorů, které jsou přes rozhraní JDI k dispozici

V předchozí kapitole jsme si řekli, že nabídka konektorů dostupných v konkrétní verzi JVM a na konkrétní architektuře, se může lišit. Při použití rozhraní JDI však máme možnost zjistit dostupné (nabízené) konektory v čase běhu debuggeru, a to s využitím javovského rozhraní com.sun.jdi.VirtualMachineManager. Instanci jedináčka implementujícího toto rozhraní získáme přes statickou metodu com.sun.jdi.Bootstrap.vir­tualMachineManager() a následně můžeme všechny dostupné konektory přečíst metodou com.sun.jdi.VirtualMachine­Manager.allConnectors(), která vrátí seznam (list) objektů typu com.sun.jdi.connect.Connector. Následně je již možné jeden z konektorů použít, popř. jen získaný seznam konektorů vypsat na standardní výstup. Přesně tento algoritmus je implementován v prvním demonstračním příkladu využívajícím třídy a rozhraní nabízené JDI:

import com.sun.jdi.Bootstrap;
import com.sun.jdi.VirtualMachineManager;
import com.sun.jdi.connect.Connector;
 
/**
 * Trida, ktera vypise vsechny dostupne JDI konektory.
 *
 * @author Pavel Tisnovsky
 */
public class JDIListAllConnectors {
 
    public static void main(String[] args) {
        // ziskat (jedinou) instanci tridy VirtualMachineManager
        VirtualMachineManager virtualMachineManager = Bootstrap.virtualMachineManager();
 
        // vypsat vsechny dostupne konektory
        for (Connector connector : virtualMachineManager.allConnectors()) {
            System.out.println(connector.toString());
        }
    }
 
}

Vzhledem k tomu, že třídy a rozhraní com.sun.jdi.Bootstrap, com.sun.jdi.VirtualMachineManager a com.sun.jdi.connect.Connector nejsou součástí standardního API Java SE, je nutné při překladu specifikovat, v jakém adresáři či v jakém Java archivu (JAR) se nachází bajtkódy těchto tříd. Standardně se jedná o Java archiv nazvaný tools.jar, který nalezneme v adresáři /usr/lib/jvm/{verze-JDK}/lib/. Překlad tedy může vypadat například následovně:

javac -classpath /usr/lib/jvm/java-6-openjdk/lib/tools.jar:. JDIListAllConnectors.java

Obrázek 3: Ukázka nastavení konfigurace projektu v Eclipse takovým způsobem, aby se při překladu i spuštění správně nalezly všechny potřebné třídy JDI.

7. Spuštění demonstračního příkladu na Linuxu i na MS Windows

Po kompilaci demonstračního příkladu se již můžeme pokusit o jeho spuštění. Podobně jako při samotném překladu je nutné i při spouštění specifikovat cestu k Java archivu, v němž jsou uloženy třídy JDI. Pokud je například nainstalováno dnes již poněkud historické OpenJDK6, provede se překlad následujícím způsobem:

java -cp /usr/lib/jvm/java-6-openjdk/lib/tools.jar:. JDIListAllConnectors

Po spuštění na Linuxu většinou získáme seznam pěti konektorů (povšimněte si předvyplněných parametrů konektorů; podrobněji se s nimi seznámíme příště):

CS24_early

com.sun.jdi.CommandLineLaunch (defaults: home=/usr/lib/jvm/java-6-openjdk/jre, options=, main=, suspend=true, quote=", vmexec=java)
com.sun.jdi.RawCommandLineLaunch (defaults: command=, quote=", address=)
com.sun.jdi.SocketAttach (defaults: timeout=, hostname=bender, port=)
com.sun.jdi.SocketListen (defaults: timeout=, port=, localAddress=)
com.sun.jdi.ProcessAttach (defaults: pid=, timeout=)

Na operačním systému Microsoft Windows bývá nabídka dostupných konektorů větší, protože jsou nabízeny i dva konektory využívající pro komunikaci sdílenou paměť namísto socketů:

com.sun.jdi.CommandLineLaunch (defaults: home=C:\Program Files\Java\jdk1.6.0_01\jre, options=, main=, suspend=true, quote=", vmexec=java)
com.sun.jdi.RawCommandLineLaunch (defaults: command=, quote=", address=)
com.sun.jdi.SocketAttach (defaults: timeout=, hostname=bender, port=)
com.sun.jdi.SocketListen (defaults: timeout=, port=, localAddress=)
com.sun.jdi.SharedMemoryAttach (defaults: timeout=, name=)
com.sun.jdi.SharedMemoryListen (defaults: timeout=, name=)
com.sun.jdi.ProcessAttach (defaults: pid=, timeout=)

8. Obsah následující části seriálu

V následující části seriálu o programovacím jazyku Java i o virtuálním stroji Javy si na několika demonstračních příkladech ukážeme, jakým způsobem lze využít konektor typu CommandLineLaunch, tj. konektor, který nejprve spustí cílovou JVM a ihned poté s ní naváže spojení. Dále si ukážeme použití konektoru typu SocketAttach, protože se jedná o velmi často používaný způsob připojení debuggeru či jiného monitorovacího nástroje k již běžícímu virtuálnímu stroji Javy.

9. Odkazy na Internetu

  1. Breakpoint (Wikipedia)
    http://cs.wikipedia.org/wi­ki/Breakpoint
  2. JVM Tool Interface Version 1.2 Documentation
    http://docs.oracle.com/ja­vase/7/docs/platform/jvmti/jvmti­.html
  3. JVM Tool Interface Version 1.2 Documentation – SetBreakpoint
    http://docs.oracle.com/ja­vase/6/docs/platform/jvmti/jvmti­.html#SetBreakpoint
  4. JVM Tool Interface Version 1.2 Documentation – ClearBreakpoint
    http://docs.oracle.com/ja­vase/6/docs/platform/jvmti/jvmti­.html#ClearBreakpoint
  5. JVM Tool Interface Version 1.2 Documentation – Breakpoint (callback funkce)
    http://docs.oracle.com/ja­vase/6/docs/platform/jvmti/jvmti­.html#Breakpoint
  6. The JVM Tool Interface (JVM TI): How VM Agents Work
    http://www.oracle.com/technet­work/articles/javase/jvm-ti-141370.html
  7. Creating a Debugging and Profiling Agent with JVMTI
    http://www.oracle.com/technet­work/articles/javase/jvmti-136367.html
  8. JVM TI (Wikipedia)
    http://en.wikipedia.org/wiki/JVM_TI
  9. IBM JVMTI extensions
    http://publib.boulder.ibm­.com/infocenter/realtime/v2r0/in­dex.jsp?topic=%2Fcom.ibm.sof­trt.doc%2Fdiag%2Ftools%2Fjvmti_ex­tensions.html
  10. An empirical study of Java bytecode programs
    http://www.mendeley.com/research/an-empirical-study-of-java-bytecode-programs/
  11. Java quick guide: JVM Instruction Set (tabulka všech instrukcí JVM)
    http://www.mobilefish.com/tu­torials/java/java_quickgu­ide_jvm_instruction_set.html
  12. The JVM Instruction Set
    http://mpdeboer.home.xs4a­ll.nl/scriptie/node14.html
  13. Control Flow in the Java Virtual Machine
    http://www.artima.com/under­thehood/flowP.html
  14. The class File Format
    http://java.sun.com/docs/bo­oks/jvms/second_edition/html/Clas­sFile.doc.html
  15. javap – The Java Class File Disassembler
    http://docs.oracle.com/ja­vase/1.4.2/docs/tooldocs/win­dows/javap.html
  16. javap-java-1.6.0-openjdk(1) – Linux man page
    http://linux.die.net/man/1/javap-java-1.6.0-openjdk
  17. Using javap
    http://www.idevelopment.in­fo/data/Programming/java/mis­cellaneous_java/Using_javap­.html
  18. Examine class files with the javap command
    http://www.techrepublic.com/ar­ticle/examine-class-files-with-the-javap-command/5815354
  19. Bytecode Engineering
    http://book.chinaunix.net/spe­cial/ebook/Core_Java2_Volu­me2AF/0131118269/ch13lev1sec6­.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.