J A V A V I S U A L I Z A T I O N
|
Graphical Visualization of Java Objects, Threads, and LocksIsabelle
Attali, Denis Caromel, and Marjorie Russo The Java language combines object-oriented and concurrency features. Both sets of features are difficult to understand and handle, and combining them increases their complexity. A program developer must be aware of objects that become locked and unreachable. Another concern is static variables, which objects can access without an explicit reference to the variables and which consequently might require unexpected locking. Objects shared by two threads can lead to a concurrent access, raising a potential synchronization problem. Deadlocks are difficult to track and handle, so debugging threaded applications is very difficult. Jitan, a visualization environment for concurrent, object-oriented programming-more specifically, for Java-provides interesting and useful solutions to these problems. Developed in the INRIA (France's national computer science research institute) Oasis project, Jitan provides both textual and graphical visualization of objects and thread activities at execution, including the object graph's topology, thread activities and status, locks, and synchronizations. Graph-oriented object visualization provides important information simply and effectively-showing, for instance, that two threads share an object. One of Jitan's main research contributions is its formal foundation and its specific architecture based on interpretation and animation of an executable, operational Java semantics. Another important contribution is its techniques for graphically visualizing objects, threads, and locks. From the tool designer's point of view, Jitan's formal foundation has these advantages:
The formal foundation also has advantages from the Java developer's point of view:
The principles we use for graphical visualization of objects, threads, and locks apply to other Java environments, as well as to other languages with object-oriented and concurrent features. Objects and concurrency in JavaJava is a concurrent, class-based, object-oriented programming language, which includes classical concurrent, multithread, monitorlike locks and synchronization features.[1] Executing a Java program creates objects (the main entities representing the program's execution), modifies them, and updates object references. A program's behavior during execution is therefore described by the evolution of its object list. We can distinguish particular objects called threads. Threads have an activity (a state)-that is, a stack of successive calls (a continuation)-and a status (created, runnable, executing, blocked, dormant, dead).[2] A thread in runnable status can become executing at the next execution step, whereas a blocked thread must wait for a lock release on the considered object. Threads can execute simultaneously (on a multiprocessor architecture) or with interleaving (on a monoprocessor computer). Java provides a mechanism by which programmers can obtain a deterministic behavior for a given Java program in critical sections, using monitorlike synchronizations. This mechanism relies on locks; one lock is associated with each Java object. Because these characteristics can be difficult to understand, we illustrate the model's main features using the classical Java producer-consumer program[3] shown in Figure 1. The program involves two consumers and one producer of data (an integer). The producer must put successive data items into a cubbyhole; two consumers then read the data items. Consumers cannot read a data item before the producer puts its first data item into the cubbyhole, and the producer must not put in a new data item if the previous one has not been read by both consumers. This example introduces several Java concepts: object and thread creation, concurrent execution, synchronized methods, and notifications.
The program
consists of four classes: Jitan vs. other toolsSeveral systems have been developed to help Java programmers write Java programs or, more generally, object-oriented and concurrent programs. These systems mainly provide visualization and debugging tools. (See http://www.andromeda.com/people/ddyer/java/Reviews.html for Dave Dyer's exhaustive review of Java tools. Also see Carl Dichter's ?We Test the Top Java Visual IDEs? at http://www.javaworld.com/javaworld/jw-04-1998/jw-04-visual-ides.html.) Comparing Jitan with other Java development environments, we can highlight several common features: assistance for syntactic constructs, interactive debugging, examination of data contents, and visualization of thread activities and status. These features are minimal requirements for a modern development environment. Our tool's more advanced features make a significant contribution to the field of development environments. The first is its graphical visualization of the object graph's topology. Other graphical environments exist-Javelin and CodeWarrior, for example, are commercial tools that are radically graphical. They present classes as boxes connected by lines representing inheritance. The boxes contain data and methods of the class and can be graphically edited. But this information, such as class hierarchy, is static; graphical visualization of dynamic information, such as references between objects, is more useful to the programmer. Moreover, visualizing synchronizations between threads is of major importance in threaded programming. Some systems-SuperCede Java, CodeWarrior, Kawa, and NuMega-show dynamic information (objects), but their textual presentation is tree-oriented, not graph-oriented. Metamata mostly presents textual information (such as program source, a command line interpreter, and values of chosen variables). It presents no global view, so the user must compare the values of variables to detect shared objects or cycles. Jitan's graph-oriented topology visualization makes this kind of information immediately obvious. The second important feature is interactive debugging. Jitan requires no compilation at all, in contrast to the incremental recompilation after modification or addition of a breakpoint that is usually necessary in other tools. (The exception is SuperCede Java, in which you can modify a running program without restarting it.) Jitan owes this capability to the fact that its debugger is not based on bytecode execution, possibly instrumented, and does not use a particular Java virtual machine (unlike most commercial products). Instead, it directly uses an operational semantics, instrumented with notifications. It therefore requires no user instrumentation. Deriving a visualization environmentWe derived Jitan from Java's syntactic specifications and an operational semantics of the language based on Natural Semantics[4] and Structural Operational Semantics.[5] We wrote these specifications within Centaur, a generic programming environment.[6] From the syntactic and semantic specifications of a given language, Centaur automatically produces a syntactic editor and semantic tools such as type checkers and interpreters for the language. Syntactic specificationsJava's syntactical specification includes a concrete and an abstract syntax of the language. From this specification (written in Metal[7]), we derived a parser that transforms a program's textual form (a source file) into a structural representation (a syntactically correct abstract syntax tree). Jitan represents every structured object as an abstract syntax tree. For example, Figure
2 shows the abstract syntax tree corresponding to the expression Figure 2. An abstract syntax tree for a method call. Semantic specificationsWe adopted the Natural Semantics style (also called big-step semantics) to describe object-oriented features. We used Structural Operational Semantics (or small-step semantics) to specify the multithreading semantics. Centaur handles semantics with the Typol formalism, an implementation of the Natural Semantics approach. Because this formalism is based on a logical framework, it is highly declarative and expressive but also executable. Our operational semantics simulates concurrency with a deterministic thread interleaving. Currently, programmers can't choose a particular interleaving or act on the scheduler. We express Java operational semantics in terms of a transition system, modeling possible transitions from one configuration to another. By configuration, we mean all thread activities at a given program execution time. We model objects, threads, and configurations (a pair of objects and static variables) as semantic structures. Description
of Typol Each inference
rule consists of a finite set of premises (which is empty for an
axiom) and a conclusion. The premises (above the line) and the
conclusion (below the line) are relations represented by sequents
that express properties. Typol rules indicate how to deduce a sequent
from other sequents. We manipulate object languages via their
abstract syntax. A sequent expresses the fact that some hypothesis
(the term list to the left of the sequent symbol) is necessary to
prove a particular property about an abstract syntax term called the
subject. In the The Semantic
structures Figure
3 establishes the structure of objects and threads. In the case
of a simple object (not a thread), the only difference between
objects and threads is that the activity is
Figure 3. An abstract syntax for objects and threads. This structure
changes during program execution, as shown in Figure 4.
The figure shows the source code of a Figure 4. An example thread activity. Semantic
modules Figure 5. Semantic modules. The
transition system <ObjL0, ClVarL0 > ®<ObjL1, ClVarL1 > ®¼ which we interpret as A system in an initial configuration <ObjL0, ClVarL0> performs an execution step and changes its configuration into <ObjL1, ClVarL1>. Execution is therefore a sequence of transitions. The initial configuration consists of a single thread object list (this thread will execute the main method) and the static variable list obtained by class loading. Figure 6. The Typol rules that specify our transition system. The first rule, Program animation and graphical visualizationFrom semantic specifications, Centaur automatically generates a simulator that takes as input a syntactically correct Java program and outputs a list of objects and threads denoting the program's behavior (or meaning). Jitan shows the object list using two visualization engines, both based on the semantic structure modeling the object list. The textual view is a direct prettyprinting of the list of objects and threads. To generate the graphical view, which shows the object graph's complete topology, we use Centaur's graph server. It builds the graph's nodes and edges by traversing the abstract syntax tree representing the object list. For each object, it creates a node representing the object, and for each attribute or local variable value that is a reference, it creates an edge between the two involved objects. The Jitan user does not directly control the drawing of the graph. It is the graph server's responsibility to use a specific node placement heuristic and to determine the edges' form (curved or straight). But the user can interactively modify node and edge placement with the mouse. The semantics interpreter reports semantic structure changes to the environment, which then sends the corresponding graphic structure to the graph server. The environment also sends the graph server instructions to create edges and arrows between nodes. The graph server defines structures for each graph entity. To provide graphical animation during program execution, the semantics interpreter notifies the two textual and graphical visualization engines of important events (state changes). A notification is a call to highlighting primitives, with the concerned entity (where a change occurs) passed as a parameter. The semantics is equipped with notifications. The successful execution (proof) of appropriate semantic rules triggers the notification (if it exists), and the visualization engines become aware of a modification in the semantic structures. Notifications tell the engines to change a field's value or a thread's status or to add or remove a lock on a given object or an arc between two objects. Altogether, we needed to equip less than 10 semantic rules with notifications. For the graphical engine, the rules triggering notifications are
The notifications are not scattered throughout the specifications but concentrated in two modules: object list and concurrency management (see Figure 5). A critical aspect of visualization is incrementality. To achieve efficient, high-quality visualizations (avoiding flashing), Jitan computes and displays changes incrementally in both views. Without program instrumentation by the user, execution can proceed step by step, stop and resume, or abort at any time. Thus, the user controls and paces the animation. VisualizationFigure 7 shows the windows of the Jitan interpretation environment during execution of the producer-consumer program presented in Figure 1. In Figure 7a, a control panel provides debugging tools (step-by-step execution, interpretation speed, and so on). In Figure 7b, the programmer can study a detailed call stack of each thread (stack or continuation). Figure 7c presents the object list in textual form: objects (including thread objects) with their type and attribute values. The graphical view in Figure 7d features the object graph's topology. It shows thread status (dead, dormant, blocked, executing), and locks. The final two windows provide summary views: object status (Figure 7e) and thread and object interactions (Figure 7f).
Object list: Textual visualizationThe windows provide redundant information to help the programmer find it easily at various stages. For example, checking the value of a given field or whether an object is currently locked is often a problem. So Jitan provides two windows (Figures 7b and 7c) that give a textual presentation of the current object list during program execution. Programmers can track a problem if they know what has been executed and by which thread. In sequential applications, following the execution with the program text might not be difficult, but it is nearly impossible when several threads are executing several methods at once. The window in Figure 7b lets programmers see each thread's status and current continuation. The continuation's instruction list is updated at each execution step, displaying only instructions yet to execute, not those that have already executed. Thus, the programmer can focus on the next instruction. And, more important for concurrent applications, the programmer has the status information for each thread. When programmers encounter problems during program execution, their first idea is to check objects that have already been created and their attribute values. The textual window in Figure 7c presents this information without any instrumentation:
Graphical presentationAnother problem in object-oriented programming is the object topology's dynamic evolution. With a textual presentation, discovering which objects are directly or indirectly connected by a reference can be difficult. So a graphical depiction of references between objects is very useful. Jitan provides a graph-oriented object list representation that makes connection visualization obvious and immediate. Most systems offer only a tree-oriented representation that compels the programmer to compare field values to follow connections between objects. Figure 7d illustrates the graphical visualization of an object list obtained during the producer-consumer program interpretation. In the graph, elliptical nodes stand for objects or classes, and black arrows symbolize references between objects. Threads are distinguished by a rectangle around thread names. The visualization highlights references (arrows) between objects and object types (labels). Different colors make thread status visible. By clicking on an object, the programmer can use a zooming process to examine the object's references-that is, fields and local variable values. In this mode, variable names (field or local variable) appear in a rectangular node attached to the corresponding arrows. A smaller font and a dark blue frame distinguish local variables. To debug a program or to understand its behavior, the programmer must notice directed acyclic graphs and reference cycles between objects. A DAG can represent a potential shared access on a given object and race conditions in the presence of threads. Jitan's graphical view makes it easy to detect such situations. If two threads reach the same object through several successive references, the two threads share the object, potentially leading to a problem of protecting the shared data. For example, one thread might call a method that modifies this data during its reading by the other thread, leading to a coherence problem. In that case, synchronization is necessary to avoid concurrent data access problems. With the zooming feature of our graphical visualization, the programmer will see that several threads can access an object. This indicates the need for synchronization to protect the object's fields. Visualization of static variables requires special treatment. Static variables are attached to their class and therefore are common to all objects that are instances of this class, whereas instance variables (nonstatic fields) are attached to their instance object. The graphical view must represent static variables, but the chosen representation must let programmers easily differentiate static and instance variables. Our solution is to display classes that have static variables-as shown, for instance, on the left side of Figure 8. Objects referenced by such a class (or the static field values) are accessible to every object of this type. Figure 8. Objects, classes, locks, and thread status. To keep the
topology simple, we do not show a link between an object and its
static variables. But a programmer can easily locate such static
references. Considering an object, the programmer knows its type by
the object label. If the corresponding class is represented in the
graph, the object has static variables, which the programmer can
visualize by zooming on the class. In Figure 8,
for example, Summary viewsConcurrent systems have many threads and objects with complex interactions between them. Jitan's summary views of objects and threads facilitate the task of understanding complex concurrent behaviors, especially when the graphical presentation contains too many nodes. The ability to see, for instance, that no thread can proceed with execution, or that at least one thread is not dead or blocked, is useful in detecting deadlocks or livelocks. Moreover, a created threat that never becomes runnable may indicate a problem in the program. The window in Figure 7e presents objects and threads classified by status. We can see that thread #T4 is executing, #T3 is blocked, #T2 is dormant, and #T0 is dead. This window also shows which objects are locked-in the considered example, object #1. Errors in recursive method calls are common. Detecting a thread that successively calls methods on a single object or on a cycle of objects can be very helpful to the programmer. In Figure 7f, the window presents the thread stacks and the object activations. In the upper part of the window, the programmer can see which object each thread is currently working on and the chain of objects leading to that point. For example thread #T2 is executing a method on object #1. Placing synchronizations is a crucial problem in concurrent applications. To avoid nondeterminism, an access to a shared data item (an object field) must be enclosed in a critical section (a synchronized block). It is therefore useful for the programmer to know of any concurrent accesses to objects. Our tool lists all the threads currently accessing each object in the system (including threads that are objects). From the lower half of Figure 7f, we know that object #1 is currently accessed by threads #T2, #T3, and #T4. Therefore, synchronization might be required on all methods accessing this shared object. Synchronizations and locksTo understand, at a given execution step, why there is a deadlock or if a critical section is required, the programmer must know which objects are locked and which threads are blocked. The textual information is a first answer to that question. However, this information is not sufficient on its own, so Jitan graphically visualizes synchronization mechanisms and locks. First, Jitan visualizes thread status by color coding (red for blocked, black for dead, purple for dormant, and so on). During execution, it also show locked objects (light blue for unlocked objects, dark blue for locked objects). The graphical presentation highlights synchronization information in an animated fashion. This feature requires the introduction of the following arrows between objects:
Figure 8 presents the color and graphical codes corresponding to object, class, lock, and thread status. For lock
visualization, a first possibility was to make the lock visible on
the reference between the thread and the concerned object. But a
thread that has no direct reference on an object can hold a lock on
it indirectly through other objects. The objects on which a given
thread can hold a lock are the transitive closure of accessible
objects from that thread attribute (including transient), statics,
and variables of its Figure 9 shows an example of a thread that locks an object on which it has no direct reference. This program creates a communication chain, shown topologically in Figure 10. Thread #T0 holds a lock on object #3, but this lock is obtained through three successive method calls (via objects #1, #2, and #3). This implies that the information cannot be attached to a reference but must have its own representation.
Figure 10. An example of an indirectly locked object. Synchronization animationFigure 11 demonstrates how the animated graphical view shows synchronization and status information during the producer-consumer program's execution. This program has four threads and one object. The main thread (#T0) creates the three other threads (two consumers, #T2 and #T4, and one producer, #T3) and the object (CubbyHole #1), and starts the thread Consumer #T2.
In Figure 11a, the three threads have started. Consumer #T2 and Producer #T3 are runnable and Consumer #T4 is executing. CubbyHole #1 is locked by Consumer #T2 (indicated by an L arrow and a dark blue color for CubbyHole #1). Later (Figure 11b), the main thread (#T0) is dead (black node). Consumer #T4 has tried to access CubbyHole #1 (a call to the synchronized get method) and is now blocked (red and a B arrow). Consumer #T4 is waiting for the lock on CubbyHole #1 (which is still owned by Consumer #T2). Consumer #T2 has locked CubbyHole #1 before Producer #T3 could put a value in it (and therefore Consumer #T2 has no value to read). In Figure 11c, Consumer #T2 has called its wait method to enable Producer #T3 (perhaps after several execution steps) to finally get the lock on CubbyHole #1 and to put a data item in it. Consumer #T2 releases the lock (CubbyHole #1 and its lock are now in light blue) and becomes dormant (indicated by the purple color and the D arrow link to the CubbyHole lock). In Figure 11d, Consumer #T4 resumes and gets the lock on CubbyHole #1. Producer #T3 is therefore blocked, waiting for this lock. For the same reason as before, Consumer #T4 will become dormant, and Producer #T3 will resume and hold the lock on CubbyHole #1. Later, Producer #T3 will release the lock. Consumers #T2 and #T4 will resume and compete to get the lock on CubbyHole #1 and to read the data. Example of deadlock detectionThe previous
section described the producer-consumer program's normal execution.
To describe the deadlock detection mechanism, we transform the
program as follows: Two producers put a data item in a cubbyhole.
This data is read by two intermediate consumers ( Figure 12. Deadlock example in a producer-consumer pattern. When we interpret
this new program, we get the graphical view presented in Figure
13, which informs us that there is a deadlock. Indeed, all
threads are either dead (DLProdCons #T0) or dormant (other threads).
So no thread can execute itself. Looking carefully at Figure
13, we observe that a group of dormant threads and objects
appears disconnected from the rest of the program. That is, there is
no reference between CubbyHole #1 and InterConsumers #T3 and #T6. To
get the data placed in the first cubbyhole, and to wake up dormant
threads, the two intermediate consumers must have, at least
indirectly, a reference on this cubbyhole. So the problem most likely
concerns the intermediate consumers. Zooming on InterConsumer #T6 (Figure
14) shows us that its two attributes Figure 13. Deadlock detection. Figure 14. Zoom on deadlock cause. The error is
therefore related to these attribute values. We can see that the Now that we
understand the deadlock cause, we must correct the program-that is,
find where the intermediate consumers are created and initialized.
They are created by the main thread, in its main method-the statement Figure 15. Topology of corrected version. Abstract views of the object graphJitan users can abstract an object graph that represents a Java program's execution. An abstraction lets programmers consider real Java programs with a large number of objects and threads. It also provides a clearer view of a selected part of the constructed graph. An abstraction can focus on an object subgraph-for example, by graying out the other parts of the global graph without changing shapes. An abstract view also can visualize only this subgraph and what happens to it during program execution. An abstraction of the global object graph can visualize only threads and objects that are directly or indirectly referenced by a given thread (indicated by the user). During program debugging, we may find it interesting to precisely analyze a new thread in the graph execution without seeing the other threads, which are known not to cause any problem. On the other hand, to precisely study interactions and possible deadlocks between two threads on a shared object, we must see both thread subgraphs,. Figures 16 and 17 give an example of an abstraction of a given program. The main thread #T0 (the thread related to the main method) creates two well-known patterns: a binary tree and a producer-consumer system. Every BinTree object is built through an object factory method, which explains the references between the main thread and all the BinTree objects. The obtained object graph, visualized in Figure 16 without user instrumentation, is complex. In such a view, detecting patterns or independent subgraphs is difficult. But if we ask for an abstraction of thread #T2, we see a partial view of the object graph, as shown in Figure 17. In this view, we can immediately recognize a classical binary-tree pattern. Figure 16. Global view of an object graph. Figure 17. View of the object graph abstracted to the thread #T1 subgraph. The additional insights offered by abstraction are very important for the understanding of a large application. This capability enables programmers to find recurrent patterns or to precisely study independent subgraphs at runtime. Our current platform demonstrates the feasibility of the approach we have described here. Other environments can also use these principles. Graphical visualization can be useful even for single-threaded systems. For instance, in the context of smart cards, only a few (less than 20) objects are involved, but security issues make program understanding crucial. Discovering an object shared by two different applications on a card would be a major security issue. Moreover, we can imagine designers using Jitan to automatically produce an object topology for use in reverse engineering. Teachers might use the environment for teaching concurrent and object-oriented concepts. In terms of performance and scalability, Jitan is operational and already supports medium-size programs. It has handled applications with more than 100 objects, running on standard workstations. For the sake of animation, we must slow down the execution speed, so that the user can see and understand what is happening during program execution. When needed, we can show only an abstract view of the object graph (a subgraph of a given thread). This feature lets us handle complex systems efficiently and increase scalability. (For further information, see http://www.inria.fr/oasis/jitan.) Our further experiments will focus on two main goals. First, in graphical abstraction, we are exploring techniques for displaying only threads (not objects) or only certain types of objects. These techniques require specific rules and treatments because graph connectivity is not obvious and is therefore difficult to maintain. We also want to add a feature that would let the user control interleaving between all threads (for example, artificially blocking or slowing threads). This feature would allow programmers to explore various potential behaviors such as infrequent deadlock conditions. References
Isablle
Attali is a senior researcher and the project leader of the Oasis
team at INRIA (Institut National de Recherche en Informatique et
Automatique), Sophia Antipolis, France. Her main research interests
are the semantics of object-oriented programming languages,
concurrent object-oriented programming languages, and parallel
functional languages. Attali holds a master's degree in computer
science from the University of Bordeaux and a PhD in computer science
from the University of Nice. Contact her at isabelle.attali@sophia.inria.fr.
Denis Caromel
is a professor in the Computer Sciences Department at the University
of Nice, Sophia Antipolis, France. His main research interests are
parallel concurrent and distributed object-oriented programming
languages. Caromel received an MS and a PhD, both in computer
science, from the University of Nancy, France. Contact him at denis.caromel@sophia.inria.fr.
Marjorie
Russo is a PhD candidate at the University of Nice, Sophia
Antipolis. Her main research interests are the formal semantics of
concurrent object-oriented programming languages and the proofs of
their properties. Russo holds an engineer diploma from the Ecole Supérieure
d'Ingénieurs de Luminy and an MS from the University of
Marseille, both in computer science. Contact her at marjorie.russo@sophia.inria.fr.
|