Overblik

Debugging af en ekstern Java-applikation kan være praktisk i mere end ét tilfælde.

I denne vejledning vil vi se, hvordan vi kan gøre det ved hjælp af JDK’s værktøjer.

Applikationen

Lad os starte med at skrive en applikation. Vi kører det på et eksternt sted og debugger det lokalt via denne artikel:

public class OurApplication { private static String staticString = "Static String"; private String instanceString; public static void main(String args) { for (int i = 0; i < 1_000_000_000; i++) { OurApplication app = new OurApplication(i); System.out.println(app.instanceString); } } public OurApplication(int index) { this.instanceString = buildInstanceString(index); } public String buildInstanceString(int number) { return number + ". Instance String !"; }}

JDWP: Java Debug Wire Protocol

Java Debug Wire Protocol er en protokol, der anvendes i Java til kommunikation mellem en debuggee og en debugger. Debuggee er det program, der debugges, mens debuggeren er et program eller en proces, der opretter forbindelse til det program, der debugges.

Både programmerne kører enten på den samme maskine eller på forskellige maskiner. Vi vil fokusere på sidstnævnte.

3.1. JDWP’s indstillinger

Vi vil bruge JDWP i JVM-kommandolinjeargumenterne, når vi starter debuggeprogrammet.

Den kræver en liste over indstillinger:

  • transport er den eneste fuldt påkrævede indstilling. Den definerer, hvilken transportmekanisme der skal anvendes. dt_shmem fungerer kun på Windows, og hvis begge processer kører på den samme maskine, mens dt_socket er kompatibel med alle platforme og gør det muligt for processerne at køre på forskellige maskiner
  • server er ikke en obligatorisk indstilling. Dette flag, når det er aktiveret, definerer den måde, hvorpå den knytter sig til debuggeren. Det eksponerer enten processen gennem den adresse, der er defineret i address-indstillingen. Ellers eksponerer JDWP en standard
  • suspend definerer, om JVM’en skal suspendere og vente på, at en debugger tilknyttes eller ej
  • address er indstillingen, der indeholder den adresse, som regel en port, der eksponeres af debuggeren. Den kan også repræsentere en adresse oversat som en streng af tegn (som javadebug, hvis vi bruger server=y uden at angive en adresse på Windows)

3.2. Startkommando

Lad os starte med at starte fjernprogrammet. Vi giver alle de tidligere anførte muligheder:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 OurApplication

Igennem Java 5 skulle JVM-argumentet runjdwp bruges sammen med den anden mulighed debug:

java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000

Denne måde at bruge JDWP på understøttes stadig, men vil blive droppet i fremtidige udgivelser. Vi vil foretrække brugen af den nyere notation, når det er muligt.

3.3. Siden Java 9

Endeligt er en af mulighederne i JDWP blevet ændret med udgivelsen af version 9 af Java. Det er en ganske lille ændring, da den kun vedrører én indstilling, men den vil gøre en forskel, hvis vi forsøger at fejlfinde et fjernprogram.

Denne ændring påvirker den måde, som adressen opfører sig på for fjernprogrammer. Den ældre notation address=8000 gælder kun for localhost. For at opnå den gamle opførsel bruger vi en asterisk med et kolon som præfiks for adressen (f.eks. address=*:8000).

I henhold til dokumentationen er dette ikke sikkert, og det anbefales at angive debuggerens IP-adresse, når det er muligt:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=127.0.0.1:8000

JDB: Java Debugger

JDB, Java Debugger, er et værktøj, der er inkluderet i JDK’et, der er tænkt til at give en bekvem debuggerklient fra kommandolinjen.

For at starte JDB bruger vi attach-tilstand. Denne tilstand tilknytter JDB til en kørende JVM. Der findes andre kørselstilstande, f.eks. listen eller run, men de er mest praktiske ved debugging af et lokalt kørende program:

jdb -attach 127.0.0.1:8000> Initializing jdb ...

4.1. Breakpoints

Lad os fortsætte med at sætte nogle breakpoints i den applikation, der blev præsenteret i afsnit 1.

Vi sætter et breakpoint på konstruktøren:

> stop in OurApplication.<init>

Vi sætter et andet i den statiske metode main, hvor vi bruger det fuldt kvalificerede navn på String-klassen:

> stop in OurApplication.main(java.lang.String)

Slutteligt sætter vi det sidste på instansmetoden buildInstanceString:

> stop in OurApplication.buildInstanceString(int)

Vi bør nu bemærke, at serverprogrammet stopper, og at følgende bliver udskrevet i vores debuggerkonsol:

> Breakpoint hit: "thread=main", OurApplication.<init>(), line=11 bci=0

Lad os nu tilføje et breakpoint på en bestemt linje, nemlig den linje, hvor variablen app.instanceString bliver udskrevet:

> stop at OurApplication:7

Vi bemærker, at at bruges efter stop i stedet for in, når breakpointet er defineret på en specifik linje.

4.2. Naviger og evaluerer

Nu, hvor vi har indstillet vores breakpoints, skal vi bruge fortsætte udførelsen af vores tråd, indtil vi når breakpointet på linje 7.

Vi bør se følgende udskrevet i konsollen:

> Breakpoint hit: "thread=main", OurApplication.main(), line=7 bci=17

Som en påmindelse har vi stoppet på den linje, der indeholder følgende stykke kode:

System.out.println(app.instanceString);

Stoppet på denne linje kunne også have været gjort ved at stoppe på main-metoden og skrive step to gange. step udfører den aktuelle kodelinje og stopper debuggeren direkte på den næste linje.

Nu, hvor vi har stoppet, evaluerer debugen vores staticString, appens instanceString, den lokale variabel i og tager endelig et kig på, hvordan man evaluerer andre udtryk.

Lad os udskrive staticField til konsollen:

> eval OurApplication.staticStringOurApplication.staticString = "Static String"

Vi sætter eksplicit navnet på klassen før det statiske felt.

Lad os nu udskrive instansfeltet for app:

> eval app.instanceStringapp.instanceString = "68741. Instance String !"

Næst skal vi se på variablen i:

> print ii = 68741

I modsætning til de andre variabler kræver lokale variabler ikke, at der angives en klasse eller en instans. Vi kan også se, at print har nøjagtig samme opførsel som eval: De evaluerer begge et udtryk eller en variabel.

Vi evaluerer en ny instans af OurApplication, som vi har overgivet et heltal som konstruktørparameter:

> print new OurApplication(10).instanceStringnew OurApplication(10).instanceString = "10. Instance String !"

Nu, hvor vi har evalueret alle de variabler, vi havde brug for, vil vi slette de tidligere indstillede breakpoints og lade tråden fortsætte sin behandling. For at opnå dette bruger vi kommandoen clear efterfulgt af breakpointets identifikator:

Identifikatoren er nøjagtig den samme som den, der blev brugt tidligere med kommandoen stop:

> clear OurApplication:7Removed: breakpoint OurApplication:7

For at verificere, om breakpointet er blevet fjernet korrekt, bruger vi clear uden argumenter. Dette vil vise listen over eksisterende breakpoints uden det, vi netop har slettet:

> clearBreakpoints set: breakpoint OurApplication.<init> breakpoint OurApplication.buildInstanceString(int) breakpoint OurApplication.main(java.lang.String)

Konklusion

I:n denne hurtige artikel har vi opdaget, hvordan vi kan bruge JDWP sammen med JDB, begge JDK-værktøjer.