Pohled pod kapotu JVM – čtení informací o parametrech i o lokálních proměnných metod s využitím rozhraní JDI

30. 4. 2013
Doba čtení: 24 minut

Sdílet

V dnešní části seriálu o programovacím jazyku Java i o virtuálním stroji Javy si popíšeme způsob přečtení informací o parametrech i o lokálních proměnných metod s využitím rozhraní JDI (Java Debugger Interface). Využijeme přitom velkou část funkcionality demonstračního příkladu popsaného minule.

Obsah

1. Pohled pod kapotu JVM – čtení informací o parametrech i o lokálních proměnných metod s využitím rozhraní JDI

2. Zásobník operandů

3. Struktura zásobníkového rámce (a zásobníku operandů) při zavolání metody

4. Informace o zásobníkovém rámci metody uložené v bajtkódu

5. Zdrojový kód testovací třídy Test3

6. Rozhraní com.sun.jdi.LocalVariable

7. Rozhraní com.sun.jdi.Type a všechna odvozená rozhraní

8. Výpis informací o lokálních proměnných v metodě printMethodLocalVariablesInfo()

9. Kompletní zdrojový kód demonstračního příkladu JDIMethodVariables

10. Spuštění demonstračního příkladu a ukázka informací vypsaných na standardní výstup

11. Repositář se zdrojovými kódy demonstračního příkladu JDIMethodVariables i podpůrných skriptů

12. Odkazy na Internetu

1. Pohled pod kapotu JVM – čtení informací o parametrech i o lokálních proměnných metod s využitím rozhraní JDI

V předchozí části seriálu o programovacím jazyku Java i o virtuálním stroji Javy jsme si popsali, jakým způsobem je možné s využitím rozhraní JDI (Java Debugger Interface) získat základní informace o zásobníkových rámcích. Víme již, že pro každé vlákno se ve virtuálním stroji Javy vytváří v průběhu volání metod takzvané zásobníkové rámce (stack frames). Jednotlivé zásobníkové rámce jednoho vlákna jsou vzájemně propojeny a v každém zásobníkovém rámci se nachází jak parametry předané volané metodě, tak i oblast vyhrazená pro lokální proměnné této metody. Velikost této paměťové oblasti, na níž se můžeme s určitým zjednodušením dívat jako na pole s možností indexace jednotlivých prvků, je zjištěna již při překladu zdrojových kódů a virtuální stroj Javy automaticky provádí kontrolu, zda nedochází k překročení indexu, tj. ke čtení či zápisu mimo vyhrazenou oblast (jedná se ovšem o pole nehomogenní, protože jeho prvky mohou mít různý typ). Navíc je součástí zásobníkového rámce i takzvaný zásobník operandů (operand stack) používaný při vyhodnocování výrazů či při volání dalších metod – bližší informace o zásobníku operandů budou uvedeny v následující kapitole.

Logická struktura paměti spravované virtuálním strojem Javy, která nás nyní bude zajímat, je zobrazena pod tímto odstavcem:

+============================================+
|              Zásobník (stack)              |
+============================================+
|                                            |
|   +------------------------------------+   |
|   | Zásobníkový rámec #1 (stack frame) |   |
|   +------------------------------------+   |
|   |                                    |   |
|   |      +---------------------+       |   |
|   |      |  Zásobník operandů  |       |   |
|   |      +---------------------+       |   |
|   |                                    |   |
|   |      +---------------------+       |   |
|   |      |  Parametry metody   |       |   |
|   |      |.....................|       |   |
|   |      |  Lokální proměnné   |       |   |
|   |      +---------------------+       |   |
|   |                                    |   |
|   +------------------------------------+   |
|                                            |
|   +------------------------------------+   |
|   | Zásobníkový rámec #2 (stack frame) |   |
|   +------------------------------------+   |
|   |                                    |   |
|   |      +---------------------+       |   |
|   |      |  Zásobník operandů  |       |   |
|   |      +---------------------+       |   |
|   |                                    |   |
|   |      +---------------------+       |   |
|   |      |  Parametry metody   |       |   |
|   |      |.....................|       |   |
|   |      |  Lokální proměnné   |       |   |
|   |      +---------------------+       |   |
|   |                                    |   |
|   +------------------------------------+   |
|                                            |
|   +------------------------------------+   |
|   | Zásobníkový rámec #3 (stack frame) |   |
|   +------------------------------------+   |
|   |                                    |   |
|   |      +---------------------+       |   |
|   |      |  Zásobník operandů  |       |   |
|   |      +---------------------+       |   |
|   |                                    |   |
|   |      +---------------------+       |   |
|   |      |  Parametry metody   |       |   |
|   |      |.....................|       |   |
|   |      |  Lokální proměnné   |       |   |
|   |      +---------------------+       |   |
|   |                                    |   |
|   +------------------------------------+   |
|                                            |
|  ::::::::::::::::::::::::::::::::::::::::  |
|  :: Další zásobníkové rámce vytvářené  ::  |
|  :: v čase běhu aplikace při volání    ::  |
|  :: metod.                             ::  |
|  ::::::::::::::::::::::::::::::::::::::::  |
+============================================+

2. Zásobník operandů

Většina instrukcí virtuálního stroje Javy pracuje s operandy uloženými na takzvaném zásobníku operandů (operand stack). Zásobník operandů (v tomto případě se již jedná o skutečný zásobník typu LIFO – Last In, First Out) je vytvářen v čase běhu aplikace pro každou zavolanou metodu, což mj. znamená, že je při spuštění metody vždy prázdný (zásobník operandů je podle specifikace součástí zásobníkového rámce, jeho konkrétní umístění však je libovolné). Již v čase překladu zdrojového kódu je pro každou metodu zjištěno, jak velká oblast paměti má být pro zásobník operandů vyhrazena a samozřejmě je prováděna kontrola, zda se v době běhu aplikace tato velikost nepřekročí (to by se nemělo u validního bajtkódu v žádném případě stát). Virtuální stroj Javy kontroluje typy operandů uložených na zásobník operandů a zajišťuje, že se nad těmito operandy budou provádět pouze typově bezpečné operace. V praxi to například znamená, že není možné na zásobník uložit dvě hodnoty typu float a následně provést instrukci iadd, protože tato instrukce vyžaduje, aby na zásobníku byly uloženy dvě hodnoty typu int (i když floatint mají shodnou bitovou šířku).

Podívejme se nyní na jednoduchou ukázku využití zásobníku operandů při vyhodnocování složitějšího aritmetického výrazu:

class Test {
    void test() {
        int a=10;
        int b=20;
        int c=30;
        int d=40;
        int z=a+b*(c-d);
    }
}

Přeložený bajtkód metody Test.test() je následující:

void test();
  Code:
   0:   bipush  10
   2:   istore_1           // naplnění proměnné a hodnotou 10
   3:   bipush  20
   5:   istore_2           // naplnění proměnné b hodnotou 20
   6:   bipush  30
   8:   istore_3           // naplnění proměnné c hodnotou 30
   9:   bipush  40
   11:  istore  4          // naplnění proměnné d hodnotou 40
 
   13:  iload_1            // vložení a na zásobník      [a]
   14:  iload_2            // vložení b na zásobník      [a b]
   15:  iload_3            // vložení c na zásobník      [a b c]
   16:  iload   4          // vložení d na zásobník      [a b c d]
   18:  isub               // provedení operace x=c-d    [a b c-d]
   19:  imul               // provedení operace y=b*x    [a b*(c-d)]
   20:  iadd               // provedení operace a+y      [a+b*(c-d)]
   21:  istore  5          // uložení výsledku do z      []
 
   23:  return
}

3. Struktura zásobníkového rámce (a zásobníku operandů) při zavolání metody

Zásobník operandů je taktéž použit při volání metod pro předání parametrů těmto metodám a pro vyzvednutí jejich návratových hodnot. Záleží jen na konkrétní implementaci virtuálního stroje Javy, jakým způsobem dojde při volání metody ke konverzi mezi zásobníkem operandů volající metody a lokálními parametry metody volané, většinou však můžeme předpokládat, že se minimalizují přesuny dat. Ukažme si způsob použití zásobníku operandů jak pro provádění aritmetické operace součtu, tak i pro předání parametrů volající metodě a získání výsledku z volané metody. Mějme jednoduchou třídu Test s dvojicí metod add() a callAdd():

class Test {
    // provedení součtu dvou celých čísel
    static int add(int a, int b) {
        int c=a+b;
        return c;
    }
 
    // zavolá metodu pro provedení součtu dvou čísel
    // a uloží návratovou hodnotu do své lokální proměnné
    static void callAdd() {
        int result = add(1234,5678);
    }
}

Přeložený bajtkód obou metod vypadá následovně. Všechny poznámky jsou do disasemblovaného bajtkódu samozřejmě dopsány ručně:

// v metodě add je zásobník operandů použit pouze pro provedení operace součtu
static int add(int, int);
  Code:
   0:   iload_0         // uložení prvního parametru metody na zásobník operandů
   1:   iload_1         // uložení druhého parametru metody na zásobník operandů
   2:   iadd            // provedení operace součtu s odstraněním obou operandů
   3:   istore_2        // vyzvednutí výsledku součtu a uložení do lokální proměnné
   4:   iload_2         // opětovné uložení obsahu lokální proměnné na zásobník
   5:   ireturn         // při operaci ireturn se využije hodnota z vrcholu zásobníku
// v této metodě se zásobník operandů používá i pro komunikaci s metodou Test.add(int, int)
static void callAdd();
  Code:
   0:   sipush  1234    // uložení konstanty 1234 na zásobník operandů
   3:   sipush  5678    // uložení konstanty 5678 na zásobník operandů
   6:   invokestatic #2 // zavolání statické metody Test.add(int, int)
   9:   istore_0        // výsledná hodnota je umístěna na vrchol zásobníku operandů,
                        // tak ji odtud vyzvedneme a uložíme do lokální proměnné
   10:  return          // návrat z metody

Povšimněte si, že překladač javac neprovedl žádné optimalizace, i když se nám například může zdát dvojice po sobě jdoucích instrukcí istore2 a iload2 nadbytečná (ve skutečnosti je to nutné pro správnou funkci debuggeru).

V dalším testovacím příkladě si opět ukážeme způsob předávání parametrů do volaných metod:

class Test2 {
 
    static void method1(int a, int b, boolean vypln1, boolean vypln2, int c) {
        a = b;
        b = c;
        c = a;
    }
 
    static void method2(long a, long b, boolean vypln1, boolean vypln2, long c) {
        a = b;
        b = c;
        c = a;
    }
 
    static void method3(float a, float b, boolean vypln1, boolean vypln2, float c) {
        a = b;
        b = c;
        c = a;
    }
 
    static void method4(double a, double b, boolean vypln1, boolean vypln2, double c) {
        a = b;
        b = c;
        c = a;
    }
 
    static void method5(String a, String b, boolean vypln1, boolean vypln2, String c) {
        a = b;
        b = c;
        c = a;
    }
 
}

Disassemlovaný bajtkód všech pěti testovacích metod s ručně dopsanými komentáři vypadá následovně:

static void method1(int, int, boolean, boolean, int);
  Code:
   Stack=1, Locals=5, Args_size=5
   0:   iload_1           // index je součástí operačního kódu instrukce
   1:   istore_0          // index je součástí operačního kódu instrukce
   2:   iload   4         // index je uveden v bajtu za operačním kódem
   4:   istore_1          // index je součástí operačního kódu instrukce
   5:   iload_0           // index je součástí operačního kódu instrukce
   6:   istore  4         // index je uveden v bajtu za operačním kódem
   8:   return            // návrat do volající metody
static void method2(long, long, boolean, boolean, long);
  Code:
   Stack=2, Locals=8, Args_size=5
   0:   lload_2           // index je součástí operačního kódu instrukce
   1:   lstore_0          // index je součástí operačního kódu instrukce
   2:   lload   6         // index je uveden v bajtu za operačním kódem
   4:   lstore_2          // index je součástí operačního kódu instrukce
   5:   lload_0           // index je součástí operačního kódu instrukce
   6:   lstore  6         // index je uveden v bajtu za operačním kódem
   8:   return            // návrat do volající metody
static void method3(float, float, boolean, boolean, float);
  Code:
   Stack=1, Locals=5, Args_size=5
   0:   fload_1           // index je součástí operačního kódu instrukce
   1:   fstore_0          // index je součástí operačního kódu instrukce
   2:   fload   4         // index je uveden v bajtu za operačním kódem
   4:   fstore_1          // index je součástí operačního kódu instrukce
   5:   fload_0           // index je součástí operačního kódu instrukce
   6:   fstore  4         // index je uveden v bajtu za operačním kódem
   8:   return            // návrat do volající metody
static void method4(double, double, boolean, boolean, double);
  Code:
   Stack=2, Locals=8, Args_size=5
   0:   dload_2           // index je součástí operačního kódu instrukce
   1:   dstore_0          // index je součástí operačního kódu instrukce
   2:   dload   6         // index je uveden v bajtu za operačním kódem
   4:   dstore_2          // index je součástí operačního kódu instrukce
   5:   dload_0           // index je součástí operačního kódu instrukce
   6:   dstore  6         // index je uveden v bajtu za operačním kódem
   8:   return            // návrat do volající metody
static void method5(java.lang.String, java.lang.String, boolean, boolean, java.lang.String);
  Code:
   Stack=1, Locals=5, Args_size=5
   0:   aload_1           // index je součástí operačního kódu instrukce
   1:   astore_0          // index je součástí operačního kódu instrukce
   2:   aload   4         // index je uveden v bajtu za operačním kódem
   4:   astore_1          // index je součástí operačního kódu instrukce
   5:   aload_0           // index je součástí operačního kódu instrukce
   6:   astore  4         // index je uveden v bajtu za operačním kódem
   8:   return            // návrat do volající metody

4. Informace o zásobníkovém rámci metody uložené v bajtkódu

Při práci s parametry metod i s jejich lokálními proměnnými velmi brzy zjistíme, že při použití rozhraní JDI lze získat všechny potřebné informace o těchto objektech pouze v případě, že třídy byly přeloženy s přepínačem -g, tedy s vložením přídavných metainformací od generovaného bajtkódu. Ve skutečnosti totiž virtuální stroj Javy nepotřebuje při běhu znát přesná jména ani typy jednotlivých parametrů/lokálních proměnných. Typy jsou kontrolovány při každé operaci a jména není nutné znát vůbec – pouze pozice (indexy) parametrů a lokálních proměnných. Aby tedy náš demonstrační příklad pracoval korektně, musíme použít překlad s použitím již zmíněného přepínače -g. Podívejme se nyní na to, jakým způsobem se změní bajtkód při překladu testovací třídy Test3, jejíž zdrojový kód naleznete v následující kapitole.

Překlad třídy Test3 bez přepínače -g:

Compiled from "Test3.java"
public class Test3 extends java.lang.Object
  SourceFile: "Test3.java"
  minor version: 0
  major version: 50
  Constant pool: === vynecháno ===
{
public Test3();
  Code:
   Stack=1, Locals=1, Args_size=1
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return
  LineNumberTable:
   line 1: 0
 
 
 
public void run(int, char);
  Code:
   Stack=3, Locals=5, Args_size=3
   0:   bipush  10
   2:   istore_3
   3:   aconst_null
   4:   astore  4
   6:   aload_0
   7:   fconst_1
   8:   iload_1
   9:   i2f
   10:  fdiv
   11:  invokevirtual   #2; //Method foo:(F)V
   14:  return
  LineNumberTable:
   line 4: 0
   line 5: 3
   line 6: 6
   line 7: 14
 
 
 
public void foo(float);
  Code:
   Stack=6, Locals=3, Args_size=2
   0:   aload_0
   1:   astore_2
   2:   aload_0
   3:   fload_1
   4:   iconst_2
   5:   newarray float
   7:   dup
   8:   iconst_0
   9:   fload_1
   10:  fastore
   11:  dup
   12:  iconst_1
   13:  fload_1
   14:  fastore
   15:  ldc     #3; //String Hello world!
   17:  invokevirtual   #4; //Method bar:(F[FLjava/lang/String;)V
   20:  return
  LineNumberTable:
   line 10: 0
   line 11: 2
   line 12: 20
 
 
 
public void bar(float, float[], java.lang.String);
  Code:
   Stack=2, Locals=4, Args_size=4
   0:   getstatic       #5; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   aload_3
   4:   invokevirtual   #6; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   7:   goto    0
  LineNumberTable:
   line 16: 0
 
  StackMapTable: number_of_entries = 1
   frame_type = 0 /* same */
 
 
 
public static void main(java.lang.String[]);
  Code:
   Stack=3, Locals=1, Args_size=1
   0:   new     #7; //class Test3
   3:   dup
   4:   invokespecial   #8; //Method "<init>":()V
   7:   bipush  42
   9:   bipush  97
   11:  invokevirtual   #9; //Method run:(IC)V
   14:  return
  LineNumberTable:
   line 21: 0
   line 22: 14
}

Překlad třídy Test3 s použitím přepínače -g:

Compiled from "Test3.java"
public class Test3 extends java.lang.Object
  SourceFile: "Test3.java"
  minor version: 0
  major version: 50
  Constant pool: === vynecháno ===
{
public Test3();
  Code:
   Stack=1, Locals=1, Args_size=1
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return
  LineNumberTable:
   line 1: 0
 
  LocalVariableTable:
   Start  Length  Slot  Name   Signature
   0      5       0    this    LTest3;
 
 
 
public void run(int, char);
  Code:
   Stack=3, Locals=5, Args_size=3
   0:   bipush  10
   2:   istore_3
   3:   aconst_null
   4:   astore  4
   6:   aload_0
   7:   fconst_1
   8:   iload_1
   9:   i2f
   10:  fdiv
   11:  invokevirtual   #2; //Method foo:(F)V
   14:  return
  LineNumberTable:
   line 4: 0
   line 5: 3
   line 6: 6
   line 7: 14
 
  LocalVariableTable:
   Start  Length  Slot  Name         Signature
   0      15      0     this         LTest3;
   0      15      1     value        I
   0      15      2     znak         C
   3      12      3     int_value    I
   6       9      4     object_value Ljava/lang/Object;
 
 
 
public void foo(float);
  Code:
   Stack=6, Locals=3, Args_size=2
   0:   aload_0
   1:   astore_2
   2:   aload_0
   3:   fload_1
   4:   iconst_2
   5:   newarray float
   7:   dup
   8:   iconst_0
   9:   fload_1
   10:  fastore
   11:  dup
   12:  iconst_1
   13:  fload_1
   14:  fastore
   15:  ldc #3; //String Hello world!
   17:  invokevirtual   #4; //Method bar:(F[FLjava/lang/String;)V
   20:  return
  LineNumberTable:
   line 10: 0
   line 11: 2
   line 12: 20
 
  LocalVariableTable:
   Start  Length  Slot  Name           Signature
   0      21      0     this           LTest3;
   0      21      1     value          F
   2      19      2     object_value   Ljava/lang/Object;
 
 
 
public void bar(float, float[], java.lang.String);
  Code:
   Stack=2, Locals=4, Args_size=4
   0:   getstatic   #5; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   aload_3
   4:   invokevirtual   #6; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   7:   goto    0
  LineNumberTable:
   line 16: 0
 
  LocalVariableTable:
   Start  Length  Slot  Name       Signature
   0      10      0     this       LTest3;
   0      10      1     value      F
   0      10      2     array      [F
   0      10      3     message    Ljava/lang/String;
 
  StackMapTable: number_of_entries = 1
   frame_type = 0 /* same */
 
 
 
public static void main(java.lang.String[]);
  Code:
   Stack=3, Locals=1, Args_size=1
   0:   new #7; //class Test3
   3:   dup
   4:   invokespecial   #8; //Method "<init>":()V
   7:   bipush  42
   9:   bipush  97
   11:  invokevirtual   #9; //Method run:(IC)V
   14:  return
  LineNumberTable:
   line 21: 0
   line 22: 14
 
  LocalVariableTable:
   Start  Length  Slot  Name       Signature
   0      15      0     args       [Ljava/lang/String;
}

Vidíme, že se ve druhém případě v bajtkódu objevila nová struktura s metainformacemi nazvaná LocalVariableTable. Právě tuto strukturu bude používat náš demonstrační příklad JDIMethodVariables popsaný v navazujících kapitolách.

5. Zdrojový kód testovací třídy Test3

Dnešní demonstrační příklad nazvaný JDIMethodVariables bude vypisovat parametry metod jednoduché testovací třídy Test3, v níž se postupně zavolají metody main()→run()→foo()→bar(). V poslední metodě je implementována nekonečná smyčka, která umožní snadné připojení k cílové JVM přes rozhraní JDI:

public class Test3 {
 
    public void run(int value, char znak) {
        int int_value = 10;
        Object object_value = null;
        foo(1.0f/value);
    }
 
    public void foo(float value) {
        Object object_value = this;
        bar(value, new float[] {value, value}, "Hello world!");
    }
 
    public void bar(float value, float[] array, String message) {
        while (true) {
            System.out.println(message);
        }
    }
 
    public static void main(String[] args) {
        new Test3().run(42, 'a');
    }
 
}

6. Rozhraní com.sun.jdi.LocalVariable

Seznam všech viditelných lokálních proměnných i parametrů metod lze ze zásobníkového rámce získat s využitím metody:

com.sun.jdi.StackFrame.visibleVariables()

Návratovou hodnotou této metody je seznam (List) objektů, jejichž třídy implementují rozhraní com.sun.jdi.LocalVariable. Tyto objekty nesou všechny důležité informace o proměnných/parametrech metod, které dokáže rozhraní JDI z cílového virtuálního stroje získat. V následující tabulce je seznam metod, jež lze s objekty typu com.sun.jdi.LocalVariable zavolat:

# Návratový typ Metoda + parametry Popis
1 boolean equals(Object obj) zjištění, zda se jedná o shodnou proměnnou/parametr
2 String genericSignature() vrátí signaturu generického typu, pokud je ovšem k proměnné přiřazen
3 int hashCode() vrací hashovací hodnotu (většinou nezajímavé)
4 boolean isArgument() dokáže rozlišit mezi parametrem a skutečnou lokální proměnnou
5 boolean isVisible(StackFrame frame) zjistí, zda je proměnná dostupná v předaném zásobníkovém rámci
6 String name() vrátí čitelné jméno proměnné
7 String signature() vrátí signaturu proměnné či parametru
8 Type type() vrátí typ proměnné (com.sun.jdi.Type)
9 String typeName() vrátí textovou (čitelnou) reprezentaci typu proměnné/parametru

Metoda signature() vrací údaje v následujícím formátu:

# Signatura Typ v Javě
1 Z boolean
2 B byte
3 C char
4 S short
5 I int
6 J long
7 F float
8 D double
9 L plně kvalifikované jméno třídy ukončené středníkem
10 [typ typ[]

Totéž platí i pro metodu genericSignature().

7. Rozhraní com.sun.jdi.Type a všechna odvozená rozhraní

Zastavme se na chvíli u metody com.sun.jdi.LocalVariable.type(). Tato metoda vrací objekt, jehož třída implementuje rozhraní com.sun.jdi.Type, ve skutečnosti se však vždy bude jednat o třídu implementující i nějaké odvozené rozhraní. Pro primitivní datové typy existuje celkem osm takových rozhraní:

# Datový typ Rozhraní
1 boolean com.sun.jdi.BooleanType
2 byte com.sun.jdi.ByteType
3 char com.sun.jdi.CharType
4 double com.sun.jdi.DoubleType
5 float com.sun.jdi.FloatType
6 int com.sun.jdi.IntegerType
7 long com.sun.jdi.LongType
8 short com.sun.jdi.ShortType

Pro reference či pro pole se bude jednat o tato rozhraní:

# Datový typ Rozhraní
1 třída com.sun.jdi.ClassType
2 rozhraní com.sun.jdi.InterfaceType
3 pole com.sun.jdi.ArrayType

Rozhraní com.sun.jdi.ArrayType navíc předepisuje metodu componentType(), z níž lze získat typ prvků pole.

8. Výpis informací o lokálních proměnných v metodě printMethodLocalVariablesInfo()

Podívejme se nyní, jakým způsobem je výpis informací o lokálních proměnných a parametrech metod implementován v našem demonstračním příkladu JDIMethodVariables. Tento příklad je založen na zdrojovém kódu minule popsaného příkladu JDIStackTraceList (http://icedtea.classpath­.org/people/ptisnovs/jvm-tools/file/8fad31d36206/jdi/JDIS­tackTraceList.java). Došlo však k úpravě metody printThreadInfo() takovým způsobem, že se podrobnější výpis obsahu zásobníkových rámců provede pouze pro vlákno se jménem „main“:

    /**
     * Vypis informaci o vlaknech existujicich ve sledovanem virtualnim stroji.
     *
     * @param virtualMachine sledovany virtualni stroj, k nemuz je debugger vzdalene pripojen
     */
    private static void printThreadInfo(VirtualMachine virtualMachine) {
        System.out.println("Thread info:");
        System.out.println("    UniqueID    Thread name            Status       Suspended");
        List<ThreadReference> threads = virtualMachine.allThreads();
        // projit vsemi vlakny
        for (ThreadReference thread : threads) {
            String threadName = thread.name();
            String threadStatus = getThreadStatus(thread);
            String threadSuspended = getThreadSuspended(thread);
            long uniqueID = thread.uniqueID();
            System.out.format("    %8d    %-20s   %-12s    %-5s\n", uniqueID, threadName, threadStatus, threadSuspended);
            // pro "hlavni" vlakno vypsat i informace o zasobnikovych ramcich
            if (MAIN_THREAD_NAME.equals(threadName)) {
                printDetailInfoAboutStackFrames(thread);
            }
        }
        System.out.println();
    }
 

Nás bude dnes zajímat především výpis jmen, typů a hodnot parametrů metod i jejich lokálních proměnných. Tato funkcionalita je implementována v metodě pojmenované printMethodLocalVariablesInfo(), v níž se nejprve zjistí seznam všech viditelných proměnných/parametrů a posléze se pro každý z těchto objektů zjistí jeho jméno, jméno typu a taktéž hodnota (zde se s využitím ""+… provedl převod hodnoty na řetězec, což bude skvěle fungovat i v případě, kdy je hodnota/reference nastavena na null):

    /**
     * Vypis informaci o lokalnich promennych.
     *
     * @param frame zasobnikovy ramec
     */
    private static void printMethodLocalVariablesInfo(StackFrame frame) {
        List<LocalVariable> variables;
        try {
            // precist seznam vsech viditelnych lokalnich promennych
            variables = frame.visibleVariables();
            System.out.format("%20sVariables %d\n", "", variables.size());
            // vypis informaci o kazde lokalni promenne
            for (LocalVariable variable : variables) {
                String name = variable.name();
                String type = variable.typeName();
                // pomerne primitivni zpusob ziskani retezcove podoby
                // hodnoty promenne
                String value = "" + frame.getValue(variable);
                // vypsat vsechny zjistene informace o promenne
                System.out.format("%20s%s %s = %s\n", "", type, name, value);
            }
        }
        catch (AbsentInformationException e) {
            // neudelame nic, protoze udaje o lokalnich promennych
            // skutecne nemuseji byt k dispozici
        }
    }

9. Kompletní zdrojový kód demonstračního příkladu JDIMethodVariables

V předchozích kapitolách byly popsány ty nejdůležitější metody, které jsou součástí dnešního demonstračního příkladu nazvaného JDIMethodVariables. Pod tímto odstavcem je vypsán celý zdrojový kód tohoto příkladu:

import java.io.IOException;
import java.util.List;
import java.util.Map;
 
import com.sun.jdi.AbsentInformationException;
import com.sun.jdi.Bootstrap;
import com.sun.jdi.IncompatibleThreadStateException;
import com.sun.jdi.LocalVariable;
import com.sun.jdi.Location;
import com.sun.jdi.StackFrame;
import com.sun.jdi.ThreadReference;
import com.sun.jdi.VirtualMachine;
import com.sun.jdi.VirtualMachineManager;
import com.sun.jdi.connect.AttachingConnector;
import com.sun.jdi.connect.Connector;
import com.sun.jdi.connect.IllegalConnectorArgumentsException;
 
/**
 * Pripojeni k bezicimu virtualnimu stroji Javy,
 * ktery byl spusten s parametry:
 * java -agentlib:jdwp=transport=dt_socket,server=y,address=6502,suspend=y Trida
 *
 * Po pripojeni se vypisou obsahy zasobnikovych ramcu vlakna "main"
 * vcetne jmen, typu a hodnot vsech lokalnich promennych
 * (pokud jsou tyto informace dostupne)
 *
 * @author Pavel Tisnovsky
 */
public class JDIMethodVariables {
 
    /**
     * Jmeno "hlavniho" vlakna.
     */
    private static final String MAIN_THREAD_NAME = "main";
 
    /**
     * Navratovy kod pouzity pri ukoncovani sledovane JVM.
     */
    private static final int EXIT_VALUE = 0;
 
    /**
     * Jmeno konektoru, ktery pro pripojeni pouziva sockety.
     */
    private static final String SOCKET_ATTACH_CONNECTOR_NAME = "com.sun.jdi.SocketAttach";
 
    public static void main(String[] args) {
        // ziskat (jedinou) instanci tridy VirtualMachineManager
        VirtualMachineManager virtualMachineManager = Bootstrap.virtualMachineManager();
 
        // ziskat vsechny konektory pouzite pro pripojeni k bezici JVM
        List<AttachingConnector> connectors = virtualMachineManager.attachingConnectors();
 
        // potrebujeme ziskat konektor pouzivajici pro pripojeni sockety
        AttachingConnector connector = getSocketAttachConnector(connectors);
 
        if (connector == null) {
            System.out.println("Socket connector is not available");
            return;
        }
 
        debugVirtualMachineUsingSocket(connector);
    }
 
    /**
     * Ziskat konektor pouzivajici pro pripojeni sockety
     */
    private static AttachingConnector getSocketAttachConnector(List<AttachingConnector> connectors) {
        for (AttachingConnector connector : connectors) {
            if (SOCKET_ATTACH_CONNECTOR_NAME.equals(connector.name())) {
                return connector;
            }
        }
        return null;
    }
 
    /**
     * Pripojeni k bezicimu virtualnimu stroji pres socket.
     * @throws InterruptedException 
     */
    private static void debugVirtualMachineUsingSocket(AttachingConnector connector) {
        // nastaveni argumentu pouzivanych konektorem
        Map<String, Connector.Argument> arguments = prepareConnectorArguments(connector);
 
        try {
            // pripojeni ke vzdalenemu bezicimu virtualnimu stroji Javy
            VirtualMachine virtualMachine = connectToVirtualMachine(connector, arguments);
 
            // spustit sledovany virtualni stroj po dobu jedne sekundy
            runVirtualMachineForOneSecond(virtualMachine);
 
            // vypis zakladnich informaci o pripojenem VM
            printVirtualMachineInfo(virtualMachine);
 
            // ukonceni behu vzdaleneho virtualniho stroje
            shutdownVirtualMachine(virtualMachine);
        }
        catch (IOException e) {
            e.printStackTrace();
        }
        catch (IllegalConnectorArgumentsException e) {
            e.printStackTrace();
        }
    }
 
    /**
     * Nastaveni portu na cilove JVM, jenz debugger pouzije pro navazani spojeni.
     *
     * @param connector konektor pouzity pro pripojeni
     * @return mapa obsahujici parametry konektoru
     */
    private static Map<String, Connector.Argument> prepareConnectorArguments(AttachingConnector connector) {
        Map<String, Connector.Argument> arguments = connector.defaultArguments();
        arguments.get("port").setValue("6502");
        return arguments;
    }
 
    /**
     * Pripojeni debuggeru ke sledovanemu virtualnimu stroji.
     *
     * @param connector konektor vyuzivajici pro spojeni sockety
     * @param arguments mapa obsahujici parametry pripojeni
     * @return sledovany virtualni stroj, k nemuz je debugger vzdalene pripojen
     *
     * @throws IOException vyvolane v pripade, ze se pripojeni k JVM nepodari
     * @throws IllegalConnectorArgumentsException vyvolane v pripade spatne zadanych parametru
     */
    private static VirtualMachine connectToVirtualMachine(AttachingConnector connector, Map<String, Connector.Argument> arguments)
        throws IOException, IllegalConnectorArgumentsException {
        System.out.println("Connecting to virtual machine");
        VirtualMachine virtualMachine = connector.attach(arguments);
        System.out.println("Connected");
        return virtualMachine;
    }
 
    /**
     * Spustit sledovany virtualni stroj po dobu jedne sekundy
     *
     * @param virtualMachine sledovany virtualni stroj, k nemuz je debugger vzdalene pripojen
     */
    private static void runVirtualMachineForOneSecond(VirtualMachine virtualMachine) {
        virtualMachine.resume();
        try {
            Thread.sleep(1000);
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
        virtualMachine.suspend();
    }
 
    /**
     * Ukonceni prace beziciho sledovaneho virtualniho stroje.
     *
     * @param virtualMachine sledovany virtualni stroj, k nemuz je debugger vzdalene pripojen
     */
    private static void shutdownVirtualMachine(VirtualMachine virtualMachine) {
        System.out.println("Calling exit");
        virtualMachine.exit(EXIT_VALUE);
    }
 
    /**
     * Vypis informaci ziskanych ze sledovaneho virtualniho stroje.
     *
     * @param virtualMachine sledovany virtualni stroj, k nemuz je debugger vzdalene pripojen
     */
    private static void printVirtualMachineInfo(VirtualMachine virtualMachine) {
        System.out.println("Basic virtual machine info:");
        printThreadInfo(virtualMachine);
    }
 
    /**
     * Vypis informaci o vlaknech existujicich ve sledovanem virtualnim stroji.
     *
     * @param virtualMachine sledovany virtualni stroj, k nemuz je debugger vzdalene pripojen
     */
    private static void printThreadInfo(VirtualMachine virtualMachine) {
        System.out.println("Thread info:");
        System.out.println("    UniqueID    Thread name            Status       Suspended");
        List<ThreadReference> threads = virtualMachine.allThreads();
        // projit vsemi vlakny
        for (ThreadReference thread : threads) {
            String threadName = thread.name();
            String threadStatus = getThreadStatus(thread);
            String threadSuspended = getThreadSuspended(thread);
            long uniqueID = thread.uniqueID();
            System.out.format("    %8d    %-20s   %-12s    %-5s\n", uniqueID, threadName, threadStatus, threadSuspended);
            // pro "hlavni" vlakno vypsat i informace o zasobnikovych ramcich
            if (MAIN_THREAD_NAME.equals(threadName)) {
                printDetailInfoAboutStackFrames(thread);
            }
        }
        System.out.println();
    }
 
    /**
     * Prevod stavu vlakna na retezec.
     *
     * @param thread JDI objekt predstavujici vlakno
     * @return stav vlakna v retezcove podobe
     */
    private static String getThreadStatus(ThreadReference thread) {
        switch (thread.status()) {
        case ThreadReference.THREAD_STATUS_NOT_STARTED:
            return "not started";
        case ThreadReference.THREAD_STATUS_RUNNING:
            return "running";
        case ThreadReference.THREAD_STATUS_SLEEPING:
            return "sleeping";
        case ThreadReference.THREAD_STATUS_MONITOR:
            return "wait/monitor";
        case ThreadReference.THREAD_STATUS_WAIT:
            return "Object.wait";
        case ThreadReference.THREAD_STATUS_ZOMBIE:
            return "zombie";
        case ThreadReference.THREAD_STATUS_UNKNOWN:
            return "*unkwnown*";
        default:
            return "should not happen!";
        }
    }
 
    /**
     * Informace (ve tvaru retezce) o tom, zda je vlakno pozastaveno ci nikoli.
     *
     * @param thread JDI objekt predstavujici vlakno
     * @return stav pozastaveni vlakna v retezcove podobe
     */
    private static String getThreadSuspended(ThreadReference thread) {
        return thread.isSuspended() ? "yes" : "no";
    }
 
    /**
     * Vypis zasobnikovych ramcu
     *
     * @param thread JDI objekt predstavujici vlakno
     */
    private static void printDetailInfoAboutStackFrames(ThreadReference thread) {
        try {
            System.out.format("%16s-------------------------------------------------\n", "");
            System.out.format("%16sStack frame count: %d\n", "", thread.frameCount());
            // vypsat detailni informace o vsech zasobnikovych ramcich
            for (StackFrame frame : thread.frames()) {
                printStackFrameDetailInfo(frame);
            }
            System.out.format("%16s-------------------------------------------------\n", "");
        }
        catch (IncompatibleThreadStateException e) {
            e.printStackTrace();
        }
    }
 
    /**
     * Vypis podrobnych informaci o vybranem zasobnikovem ramci
     *
     * @param frame zasobnikovy ramec
     */
    private static void printStackFrameDetailInfo(StackFrame frame) {
        // informace o volane metode
        printLocationInfo(frame);
        // informace o lokalnich promennych volane metody
        printMethodLocalVariablesInfo(frame);
    }
 
    /**
     * Vypis informaci o volane metode
     *
     * @param frame zasobnikovy ramec
     */
    private static void printLocationInfo(StackFrame frame) {
        Location location = frame.location();
        // ziskat vsechny informace o pozici v pozastavenem vlaknu
        String className = getClassName(location);
        String methodName = getMethodName(location);
        String sourceName = getSourceName(location);
        String lineNumber = getLineNumber(location);
 
        // nyni mame vsechny informace, lze je tedy vypsat
        System.out.format("%16s%s.%s (%s:%s)\n", "", className, methodName, sourceName, lineNumber);
    }
 
    /**
     * Jmeno tridy, jejiz metoda byla zavolana.
     */
    private static String getClassName(Location location) {
        return location.method().declaringType().name();
    }
 
    /**
     * Jmeno volane metody.
     */
    private static String getMethodName(Location location) {
        return location.method().name();
    }
 
    /**
     * Ziskani informaci o jmene zdrojoveho souboru pro danou lokaci.
     */
    private static String getSourceName(Location location) {
        try {
            return location.sourceName();
        }
        catch (AbsentInformationException e) {
            return "unknown";
        }
    }
 
    /**
     * Prevod cisla radku na retezec, pokud je to mozne.
     */
    private static String getLineNumber(Location location) {
        int lineNumber = location.lineNumber();
        // u nativnich metod nelze zjistit cisla radku
        return lineNumber >= 0 ? "" + lineNumber : "<native method>";
    }
 
    /**
     * Vypis informaci o lokalnich promennych.
     *
     * @param frame zasobnikovy ramec
     */
    private static void printMethodLocalVariablesInfo(StackFrame frame) {
        List<LocalVariable> variables;
        try {
            // precist seznam vsech viditelnych lokalnich promennych
            variables = frame.visibleVariables();
            System.out.format("%20sVariables %d\n", "", variables.size());
            // vypis informaci o kazde lokalni promenne
            for (LocalVariable variable : variables) {
                String name = variable.name();
                String type = variable.typeName();
                // pomerne primitivni zpusob ziskani retezcove podoby
                // hodnoty promenne
                String value = "" + frame.getValue(variable);
                // vypsat vsechny zjistene informace o promenne
                System.out.format("%20s%s %s = %s\n", "", type, name, value);
            }
        }
        catch (AbsentInformationException e) {
            // neudelame nic, protoze udaje o lokalnich promennych
            // skutecne nemuseji byt k dispozici
        }
    }
 
}

10. Spuštění demonstračního příkladu a ukázka informací vypsaných na standardní výstup

Před vlastním spuštěním demonstračního příkladu JDIMethodVariables je nejprve nutné spustit samostatnou cílovou JVM i s testovanou aplikací Test3, jejíž zdrojový kód byl vypsán v páté kapitole. Tato aplikace je velmi jednoduchá, protože se po své inicializaci dostane do nekonečné smyčky, což nám samozřejmě vyhovuje, neboť bude jisté, že po cca jednosekundovém běhu cílové JVM se bude hlavní vlákno aplikace skutečně nacházet uvnitř této smyčky.

Překlad a spuštění této aplikace v cílovém virtuálním stroji Javy se provede následujícím způsobem (důležité je zde především číslo portu, k němuž se následně připojíme s využitím rozhraní JDI):

javac Test3.java
java -agentlib:jdwp=transport=dt_socket,server=y,address=6502,suspend=y Test3

Nyní již zbývá provést překlad a spuštění našeho demonstračního příkladu v samostatné JVM:

bitcoin_skoleni

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

Po spuštění by se měl na standardním výstupu objevit přibližně tento text obsahující jména metod a parametrů metod třídy Test3:

 
Connecting to virtual machine
Connected
Basic virtual machine info:
Thread info:
    UniqueID    Thread name            Status       Suspended
          56    Attach Listener        running         yes
          57    Signal Dispatcher      running         yes
          58    Finalizer              Object.wait     yes
          59    Reference Handler      Object.wait     yes
           1    main                   running         yes
                -------------------------------------------------
                Stack frame count: 16
                java.io.FileOutputStream.writeBytes (FileOutputStream.java:)
                java.io.FileOutputStream.write (FileOutputStream.java:260)
                java.io.BufferedOutputStream.flushBuffer (BufferedOutputStream.java:65)
                java.io.BufferedOutputStream.flush (BufferedOutputStream.java:123)
                java.io.PrintStream.write (PrintStream.java:432)
                sun.nio.cs.StreamEncoder.writeBytes (StreamEncoder.java:202)
                sun.nio.cs.StreamEncoder.implFlushBuffer (StreamEncoder.java:272)
                sun.nio.cs.StreamEncoder.flushBuffer (StreamEncoder.java:85)
                java.io.OutputStreamWriter.flushBuffer (OutputStreamWriter.java:168)
                java.io.PrintStream.write (PrintStream.java:477)
                java.io.PrintStream.print (PrintStream.java:619)
                java.io.PrintStream.println (PrintStream.java:756)
                Test3.bar (Test3.java:16)
                    Variables 3
                    float value = 0.023809524
                    float[] array = instance of float[2] (id=65)
                    java.lang.String message = "Hello world!"
                Test3.foo (Test3.java:11)
                    Variables 2
                    float value = 0.023809524
                    java.lang.Object object_value = instance of Test3(id=68)
                Test3.run (Test3.java:6)
                    Variables 4
                    int value = 42
                    char znak = a
                    int int_value = 10
                    java.lang.Object object_value = null
                Test3.main (Test3.java:21)
                    Variables 1
                    java.lang.String[] args = instance of java.lang.String[0] (id=69)
                -------------------------------------------------
 
Calling exit
 

11. Repositář se zdrojovými kódy demonstračního příkladu JDIMethodVariables i podpůrných skriptů

Zdrojové kódy demonstračního příkladu JDIMethodVariables, testovací třídy i skriptů použitých pro překlad a spuštění tohoto demonstračního příkladu byly uloženy (podobně jako tomu bylo i v předchozích částech tohoto seriálu) do Mercurial repositáře dostupného na adrese http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/. Prozatím nejnovější verze všech zmíněných zdrojových souborů a skriptů můžete najít na adresách:

12. 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

Autor článku

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