Java言語は型安全な言語なので、標準的なJavaコンパイラが生成するのは有効なクラスファイルと型安全なバイトコードです。 しかし、JVMにはバイトコードが信頼できるコンパイラによって生成されたか分かりません。 そこで、リンク時のバイトコード検証(bytecode verification)という処理により、バイトコードの型安全性を再び担保することが必要になります。
バイトコード検証の仕様は、Java仮想マシン仕様書の4.8節で規定されています。 この仕様では、バイトコードにおいてJVMが検証を行う静的制約(static constraint)と動的制約(dynamic constraint)の両方が規定されています。 制約に対する違反が見つかった場合、VMはVerifyError例外を発生させ、そのバイトコードを含むクラスがリンクされないようにします。
バイトコードに対する制約の多くは、静的に検証することが可能です。 例えば、 ‘ldc' 命令のオペランドは CONSTANT_Integer 、 CONSTANT_String か CONSTANT_Float のいずれかの型を持つ、 有効なコンスタントプールのインデックスでなくてはならないという制約などです。 命令の引数の型と数についての制約で、バイトコードを動的に解析することが必要なものもあります。 どういうオペランドが式スタック上に存在するか実行時でないと決定できないためです。
現在二つの手法が、バイトコードを解析し、各命令のオペランドの型と数を決定するために用いられています。 従来用いられていた手法は「型推論(type inference)」と呼ばれます。 これは各バイトコードを抽象実行し、分岐命令のジャンプ先や例外ハンドルにおいて型情報を単一化することで動作します。 解析では、型情報が確定するまで何度もバイトコードが走査されます。 型情報を確定できない場合や、結果として得られた型がバイトコードの制約に違反している場合、 VerifyError が発生します。 この従来の検証手法は、外部ライブラリ libverify.so に含まれているため、クラスや型について必要な情報を収集するためにJNIを使用します。
JDK6において新しく導入された2つ目の検証手法は「型検証(type verification)」と呼ばれます。 この方法ではまずJavaコンパイラが、分岐命令のジャンプ先および例外処理ごとに型情報を確定し、 StackMapTable という属性としてバイトコードに付与します。 StackMapTable はいくつかのスタックマップフレームから構成されており、それぞれのフレームはメソッド内のある位置における式スタックの要素およびローカル変数の型を含みます。 その後JVMがバイトコードを一度だけ走査し、型の正しさを検証すればバイトコードの検証は完了です。 この検証手法はJavaME CLDCですでに採用されています。 この手法の処理は軽量で高速なため、VMの内部に実装されています。
JDK6より前に作成された場合など、バージョンが50未満のすべてのクラスファイルは、JVMは従来の手法である型推論を用いてクラスファイルを検証します。 バージョンが50以上のクラスファイルには StackMapTable 属性が存在するため、新しい検証手法である型検証が用いられます。 古い外部ツールは、バイトコードを操作したのにもかかわらず、 StackMapTable 属性を更新しないことがありうるので、新しい手法である型検証を実施している際にエラーが発生するかもしれません。 そのときは従来の手法である型推論に切り替わります。 解析処理が成功して終了すれば、クラスファイルの検証は終わりです。
Class data sharing (CDS) is a feature introduced in J2SE 5.0 that is intended to reduce the startup time for Java programming language applications, in particular smaller applications, as well as reduce footprint. When the JRE is installed on 32-bit platforms using the Sun provided installer, the installer loads a set of classes from the system jar file into a private internal representation, and dumps that representation to a file, called a “shared archive”. If the Sun JRE installer is not being used, this can be done manually, as explained below. During subsequent JVM invocations, the shared archive is memory-mapped in, saving the cost of loading those classes and allowing much of the JVM's metadata for these classes to be shared among multiple JVM processes.
Class data sharing is supported only with the Java HotSpot Client VM, and only with the serial garbage collector.
The primary motivation for including CDS is the decrease in startup time it provides. CDS produces better results for smaller applications because it eliminates a fixed cost: that of loading certain core classes. The smaller the application relative to the number of core classes it uses, the larger the saved fraction of startup time.
The footprint cost of new JVM instances has been reduced in two ways. First, a portion of the shared archive, currently between five and six megabytes, is mapped read-only and therefore shared among multiple JVM processes. Previously this data was replicated in each JVM instance. Second, since the shared archive contains class data in the form in which the Java Hotspot VM uses it, the memory which would otherwise be required to access the original class information inrt.jaris not needed. These savings allow more applications to be run concurrently on the same machine. On Microsoft Windows, the footprint of a process, as measured by various tools, may appear to increase, because a larger number of pages are being mapped in to the process' address space. This is offset by the reduction in the amount of memory (inside Microsoft Windows) which is needed to hold portions onrt.jar. Reducing footprint remains a high priority.
In HotSpot, the class data sharing implementation introduces new Spaces into the permanent generation which contain the shared data. The classes.jsa shared archive is memory mapped into these Spaces at VM startup. Subsequently, the shared region is managed by the existing VM memory management subsystem.
Read-only shared data includes constant method objects (constMethodOops), symbol objects (symbolOops), and arrays of primitives, mostly character arrays.
Read-write shared data consists of mutable method objects (methodOops), constant pool objects (constantPoolOops), VM internal representation of Java classes and arrays (instanceKlasses andarrayKlasses), and variousString,Class, andExceptionobjects.
The current HotSpot interpreter, which is used for executing bytecodes, is a template based interpreter. The HotSpot runtime a.k.a.InterpreterGeneratorgenerates an interpreter in memory at the startup using the information in theTemplateTable(assembly code corresponding to each bytecode). A template is a description of each bytecode. TheTemplateTabledefines all the templates and provides accessor functions to get the template for a given bytecode. The non-product flag-XX:+PrintInterpretercan be used to view the template table generated in memory during the VM's startup process.
The template design performs better than a classic switch-statement loop for several reasons. First, the switch statement performs repeated compare operations, and in the worst case it may be required to compare a given command with all but one bytecodes to locate the required one. Second, it uses a separate software stack to pass Java arguments, while the native C stack is used by the VM itself. A number of JVM internal variables, such as the program counter or the stack pointer for a Java thread, are stored in C variables, which are not guaranteed to be always kept in the hardware registers. Management of these software interpreter structures consumes a considerable share of total execution time.[5]
Overall, the gap between the VM and the real machine is significantly narrowed by the HotSpot interpreter, which makes the interpretation speed considerably higher. This, however, comes at a price of e.g. large machine-specific chunks of code (roughly about 10 KLOC (thousand lines of code) of Intel-specific and 14 KLOC of SPARC-specific code). Overall code size and complexity is also significantly higher, since e.g. the code supporting dynamic code generation is needed. Obviously, debugging dynamically generated machine code is significantly more difficult than static code. These properties certainly do not facilitate implementation of runtime evolution, but they don’t make it infeasible either.[5]
The interpreter calls out to the VM runtime for complex operations (basically anything too complicated to do in assembly language) such as constant pool lookup.
The HotSpot interpreter is also a critical part of the overall HotSpot adaptive optimization story. Adaptive optimization solves the problems of JIT compilation by taking advantage of an interesting program property. Virtually all programs spend the vast majority of their time executing a minority of their code. Rather than compiling method by method, just in time, the Java HotSpot VM immediately runs the program using an interpreter, and analyzes the code as it runs to detect the critical hot spots in the program. Then it focuses the attention of a global native-code optimizer on the hot spots. By avoiding compilation of infrequently executed code (most of the program), the Java HotSpot compiler can devote more attention to the performance-critical parts of the program, without necessarily increasing the overall compilation time. This hot spot monitoring is continued dynamically as the program runs, so that it literally adapts its performance on the fly to the user's needs.
Java virtual machines use exceptions to signal that a program has violated the semantic constraints of the Java language. For example, an attempt to index outside the bounds of an array will cause an exception. An exception causes a non-local transfer of control from the point where the exception occurred (or wasthrown) to a point specified by the programmer (or where the exception iscaught).[6]
The HotSpot interpreter, dynamic compilers, and runtime all cooperate to implement exception handling. There are two general cases of exception handling: either the exception is thrown or caught in the same method, or it's caught by a caller. The latter case is more complicated and requiresstack unwindingto find the appropriate handler.
Exceptions can be initiated by thethrowbytecode, a return from a VM-internal call, a return from a JNI call, or a return from a Java call. (The last case is really just a later stage of the first 3.) When the VM recognizes that an exception has been thrown, the runtime system is invoked to find the nearest handler for that exception. Three pieces of information are used to find the handler; the current method, the current bytecode, and the exception object. If a handler is not found in the current method, as mentioned above, the current activation stack frame is popped and the process is iteratively repeated for previous frames.
Once the correct handler is found, the VM execution state is updated, and we jump to the handler as Java code execution is resumed.
Broadly, we can define “synchronization” as a mechanism that prevents, avoids or recovers from the inopportune interleavings (commonly called “races”) of concurrent operations. In Java, concurrency is expressed through the thread construct. Mutual exclusion is a special case of synchronization where at most a single thread is permitted access to protected code or data.
HotSpot provides Java monitors by which threads running application code may participate in a mutual exclusion protocol. A monitor is either locked or unlocked, and only one thread may own the monitor at any one time. Only after acquiring ownership of a monitor may a thread enter the critical section protected by the monitor. In Java, critical sections are referred to as "synchronized blocks", and are delineated in code by thesynchronizedstatement.
If a thread attempts to lock a monitor and the monitor is in an unlocked state, the thread will immediately gain ownership of the monitor. If a subsequent thread attempts to gain ownership of the monitor while the monitor is locked that thread will not be permitted to proceed into the critical section until the owner releases the lock and the 2nd thread manages to gain (or is granted) exclusive ownership of the lock.
Some additional terminology: to “enter” a monitor means to acquire exclusive ownership of the monitor and enter the associated critical section. Likewise, to “exit” a monitor means to release ownership of the monitor and exit the critical section. We also say that a thread that has locked a monitor now “owns” that monitor. “Uncontended” refers to synchronization operations on an otherwise unowned monitor by only a single thread.
The HotSpot VM incorporates leading-edge techniques for both uncontended and contended synchronization operations which boost synchronization performance by a large factor.
Uncontended synchronization operations, which comprise the majority of synchronizations, are implemented with constant-time techniques. Withbiased locking, in the best case these operations are essentially free of cost. Since most objects are locked by at most one thread during their lifetime, we allow that thread tobiasan object toward itself. Once biased, that thread can subsequently lock and unlock the object without resorting to expensive atomic instructions.[7]
Contended synchronization operations use advanced adaptive spinning techniques to improve throughput even for applications with significant amounts of lock contention. As a result, synchronization performance becomes so fast that it is not a significant performance issue for the vast majority of real-world programs.
In HotSpot, most synchronization is handled through what we call ””fast-path” code. We have two just-in-time compilers (JITs) and an interpreter, all of which will emit fast-path code. The two JITs are “C1”, which is the-clientcompiler, and “C2”, which is the-servercompiler. C1 and C2 both emit fast-path code directly at the synchronization site. In the normal case when there's no contention, the synchronization operation will be completed entirely in the fast-path. If, however, we need to block or wake a thread (in monitorenter or monitorexit, respectively), the fast-path code will call into the slow-path. The slow-path implementation is in native C++ code while the fast-path is emitted by the JITs.
Per-object synchronization state is encoded in the first word (the so-calledmark word) of the VM's object representation. For several states, the mark word is multiplexed to point to additional synchronization metadata. (As an aside, in addition, the mark word is also multiplexed to contain GC age data, and the object's identity hashCode value.) The states are:
Thread management covers all aspects of the thread lifecycle, from creation through to termination, and the coordination of threads within the VM. This involves management of threads created from Java code (whether application code or library code), native threads that attach directly to the VM, or internal VM threads created for a range of purposes. While the broader aspects of thread management are platform independent, the details necessarily vary depending on the underlying operating system.
The basic threading model in Hotspot is a 1:1 mapping between Java threads (an instance ofjava.lang.Thread) and native operating system threads. The native thread is created when the Java thread is started, and is reclaimed once it terminates. The operating system is responsible for scheduling all threads and dispatching to any available CPU.
The relationship between Java thread priorities and operating system thread priorities is a complex one that varies across systems. These details are covered later.
There are two basic ways for a thread to be introduced into the VM: execution of Java code that callsstart()on ajava.lang.Threadobject; or attaching an existing native thread to the VM using JNI. Other threads created by the VM for internal purposes are discussed below.
There are a number of objects associated with a given thread in the VM (remembering that Hotspot is written in the C++ object-oriented programming language):
When ajava.lang.Threadis started the VM creates the associatedJavaThreadandOSThreadobjects, and ultimately the native thread. After preparing all of the VM state (such as thread-local storage and allocation buffers, synchronization objects and so forth) the native thread is started. The native thread completes initialization and then executes a start-up method that leads to the execution of thejava.lang.Threadobject'srun()method, and then, upon its return, terminates the thread after dealing with any uncaught exceptions, and interacting with the VM to check if termination of this thread requires termination of the whole VM. Thread termination releases all allocated resources, removes theJavaThreadfrom the set of known threads, invokes destructors for theOSThreadandJavaThreadand ultimately ceases execution when it's initial startup method completes.
A native thread attaches to the VM using the JNI callAttachCurrentThread. In response to this an associatedOSThreadandJavaThreadinstance is created and basic initialization is performed. Next ajava.lang.Threadobject must be created for the attached thread, which is done by reflectively invoking the Java code for theThreadclass constructor, based on the arguments supplied when the thread attached. Once attached, a thread can invoke whatever Java code it needs to via the other JNI methods available. Finally when the native thread no longer wishes to be involved with the VM it can call the JNIDetachCurrentThreadmethod to disassociate it from the VM (release resources, drop the reference to thejava.lang.Threadinstance, destruct theJavaThreadandOSThreadobjects and so forth).
A special case of attaching a native thread is the initial creation of the VM via the JNICreateJavaVMcall, which can be done by a native application or by the launcher (java.c). This causes a range of initialization operations to take place and then acts effectively as if a call toAttachCurrentThreadwas made. The thread can then invoke Java code as needed, such as reflective invocation of themainmethod of an application. See the JNI section for further details.
The VM uses a number of different internal thread states to characterize what each thread is doing. This is necessary both for coordinating the interactions of threads, and for providing useful debugging information if things go wrong. A thread's state transitions as different actions are performed, and these transition points are used to check that it is appropriate for a thread to proceed with the requested action at that point in time – see the discussion of safepoints below.
The main thread states from the VM perspective are as follows:
For debugging purposes additional state information is also maintained for reporting by tools, in thread dumps, stack traces etc. This is maintained in theOSThreadand some of it has fallen into dis-use, but states reported in thread dumps etc include:
Other subsystems and libraries impose their own state information, such as the JVMTI system and theThreadStateexposed by thejava.lang.Threadclass itself. Such information is generally not accessible to, nor relevant to, the management of threads inside the VM.
People are often surprised to discover that even executing a simple “Hello World” program can result in the creation of a dozen or more threads in the system. These arise from a combination of internal VM threads, and library related threads (such as reference handler and finalizer threads). The main kinds of VM threads are as follows:
All threads are instances of theThreadclass, and all threads that execute Java code areJavaThreadinstances (a subclass ofThread). The VM keeps track of all threads in a linked-list known as theThreads_list, and which is protected by theThreads_lock– one of the key synchronization locks used within the VM.
TheVMThreadspends its time waiting for operations to appear in theVMOperationQueue, and then executing those operations. Typically these operations are passed on to theVMThreadbecause they require that the VM reach asafepoint before they can be executed. In simple terms, when the VM is at safepoint all threads inside the VM have been blocked, and any threads executing in native code are prevented from returning to the VM while the safepoint is in progress. This means that the VM operation can be executed knowing that no thread can be in the middle of modifying the Java heap, and all threads are in a state such that their Java stacks are unchanging and can be examined.
The most familiar VM operation is for garbage collection, or more specifically for the “stop-the-world” phase of garbage collection that is common to many garbage collection algorithms. But many other safepoint based VM operations exist, for example: biased locking revocation, thread stack dumps, thread suspension or stopping (i.e. Thejava.lang.Thread.stop()method) and numerous inspection/modification operations requested through JVMTI.
Many VM operations are synchronous, that is the requestor blocks until the operation has completed, but some are asynchronous or concurrent, meaning that the requestor can proceed in parallel with theVMThread(assuming no safepoint is initiated of course).
Safepoints are initiated using a cooperative, polling-based mechanism. In simple terms, every so often a thread asks “should I block for a safepoint?”. Asking this question efficiently is not so simple. One place where the question is often asked is during a thread state transition. Not all state transitions do this, for example a thread leaving the VM to go to native code, but many do. The other places where a thread asks are in compiled code when returning from a method or at certain stages during loop iteration. Threads executing interpreted code don't usually ask the question, instead when the safepoint is requested the interpreter switches to a different dispatch table that includes the code to ask the question; when the safepoint is over, the dispatch table is switched back again. Once a safepoint has been requested, theVMThreadmust wait until all threads are known to be in a safepoint-safe state before proceeding to execute the VM operation. During a safepoint theThreads_lockis used to block any threads that were running, with theVMThreadfinally releasing theThreads_lockafter the VM operation has been performed.
In addition to the Java heap, which is maintained by the Java heap manager and garbage collectors, HotSpot also uses the C/C++ heap (also called the malloc heap) for storage of VM-internal objects and data. A set of C++ classes derived from the base classArenais used to manage C++ heap operations.
Arenaand its subclasses provide a fast allocation layer that sits on top of malloc/free. EachArenaallocates memory blocks (orChunks)out of 3 globalChunkPools.EachChunkPoolsatisfies allocation requests for a distinct range of allocation sizes. For example, a request for 1k of memory will be allocated from the “small”ChunkPool, while a 10K allocation will be made from the "medium"ChunkPool. This is done to avoid wasteful memory fragmentation.
TheArenasystem also provides better performance than pure malloc/free. The latter operations may require acquisition of global OS locks, which affects scalability and can hurt performance.Arenas are thread-local objects which cache a certain amount of storage, so that in the fast-path allocation case a lock is not required. Likewise,Arenafree operations do not require a lock in the common case.
Arenas are used for thread-local resource management (ResourceArea) and handle management (HandleArea). They are also used by both the client and server compilers during compilation.
The JNI is a native programming interface. It allows Java code that runs inside a Java virtual machine to interoperate with applications and libraries written in other programming languages, such as C, C++, and assembly.
While applications can be written entirely in Java, there are situations where Java alone does not meet the needs of an application. Programmers use the JNI to writeJava native methodsto handle those situations when an application cannot be written entirely in Java.
JNI native methods can be used to create, inspect, and update Java objects, call Java methods, catch and throw exceptions, load classes and obtain class information, and perform runtime type checking.
The JNI may also be used with theInvocation APIto enable an arbitrary native application to embed the Java VM. This allows programmers to easily make their existing applications Java-enabled without having to link with the VM source code. [9]
It is important to remember that once an application uses the JNI, it risks losing two benefits of the Java platform.
First, Java applications that depend on the JNI can no longer readily run on multiple host environments. Even though the part of an application written in the Java programming language is portable to multiple host environments, it will be necessary to recompile the part of the application written in native programming languages.
Second, while the Java programming language is type-safe and secure, native languages such as C or C++ are not. As a result, Java developers must use extra care when writing applications using the JNI. A misbehaving native method can corrupt the entire application. For this reason, Java applications are subject to security checks before invoking JNI features.
As a general rule, developers should architect the application so that native methods are defined in as few classes as possible. This entails a cleaner isolation between native code and the rest of the application.[10]
In HotSpot, the implementation of the JNI functions is relatively straightforward. It uses various VM internal primitives to perform activities such as object creation, method invocation, etc. In general, these are the same runtime primitives used by other subsystems such as the interpreter.
A command line option,-Xcheck:jni, is provided to aid in debugging problems in JNI usage by native methods. Specifying-Xcheck:jnicauses an alternate set of debugging interfaces to be used by JNI calls. The alternate interface verifies arguments to JNI calls more stringently, as well as performing additional internal consistency checks.
HotSpot must take special care to keep track of which threads are currently executing in native methods. During some VM activities, notably some phases of garbage collection, one or more threads must be halted at asafepointin order to guarantee that the Java memory heap is not modified during the sensitive activity. When we wish to bring a thread executing in native code to a safepoint, it is allowed to continue executing native code, but the thread will be stopped when it attempts to return into Java code or make a JNI call.
It is very important to provide easy ways to handle fatal errors for any software. Java Virtual Machine, i.e. JVM is not an exception. A typical fatal error would beOutOfMemoryError. Another common fatal error on Windows is calledAccess Violationerror which is equivalent toSegmentation Faulton Solaris/Linux platforms. It is critical to understand the cause of these kind of fatal errors in order to fix them either in your application or sometimes, in JVM itself.
Usually when JVM crashes on a fatal error, it will dump a hotspot error log file calledhs_err_pid<pid>.log, (where<pid>is replaced by the crashed java process id) to the Windows desktop or the current application directory on Solaris/Linux. Several enhancements have been made to improve the diagnosability of this file since JDK 6 and many of them have been back ported to the JDK-1.4.2_09 release. Here are some highlights of these improvements:
Another important feature is you can specify-XX:OnError="cmd1 args...;com2 ..."to the java command so that whenever VM crashes, it will execute a list of commands you specified within the quotes shown above. A typical usage of this feature is you can invoke the debugger such as dbx or Windbg to look into the crash when that happens. For the earlier releases, you can specify
-XX:+ShowMessageBoxOnErroras a runtime option so that when VM crashes, you can attach the running Java process to your favorite debugger.
Having said something about HotSpot error log files, here is a brief summary on how JVM internally handles fatal errors.
SinceOutOfMemoryErroris so common to some large scale applications, it is critical to provide useful diagnostic message to users so that they could quickly identify a solution, sometimes by just specifying a larger Java heap size. WhenOutOfMemoryErrorhappens, the error message will indicate which type of memory is problematic. For example, it could be Java heap space or PermGen space etc. Since JDK 6, a stack trace will be included in the error message. Also,
-XX:OnOutOfMemoryError="<cmd>"option was invented so that a command will be run when the first OutOfMemoryError is thrown. Another nice feature that is worth mentioning is a built-in heap dump at OutOfMemoryError. It is enabled by specifying-XX:+HeapDumpOnOutOfMemoryErroroption and you can also tell the VM where to put the heap dump file by specifying
-XX:HeapDumpPath=<pathname>.
Even though applications are carefully written to avoid deadlocks, sometimes it still happens. When deadlock occurs, you can type “Ctrl+Break” on Windows or grab the Java process id and send SIGQUIT to the hang process on Solaris/Linux. A Java level stack trace will be dumped out to the standard out so that you can analyze the reasons of deadlock. Since JDK 6, this feature has been built into jconsole which is a very useful tool in the JDK. So when the application hangs on a deadlock, use jconsole to attach the process and it will analyze which lock is problematic. Most of the time, the deadlock is caused by acquiring locks in the wrong order.
We strongly encourage you to check out the “Trouble-Shooting and Diagnostic Guide”[11]. It contains a lot of information which might be very useful to diagnose fatal errors.
“Resolving the Mysteries of Java SE Classloader”, Jeff Nisewanger, Karen Kinnear, JavaOne 2006.
[1] Java Language Specification, Third Edition. Gosling, Joy, Steele, Bracha.http://java.sun.com/docs/books/jls/third_edition/html/execution.html#12.2
[2] Java Virtual Machine Specification, Second Edition. Tim Lindholm, Frank Yellin.http://java.sun.com/docs/books/vmspec/2nd-edition/html/VMSpecTOC.doc.html
[3] Amendment to Java Virtual Machine Specification. Chapter 5: Loading, Linking and Initializing.http://java.sun.com/docs/books/vmspec/2nd-edition/ConstantPool.pdf
[4] Dynamic Class Loading in the Java Virtual Machine. Shen Liang, Gilad Bracha. Proc. of the ACM Conf. on Object-Oriented Programming, Systems, Languages and Applications, October 1998http://www.bracha.org/classloaders.ps
[5] “Safe Clsss and Data Evolution in Large and Long-Lived Java Applications”, Mikhail Dmitriev,http://research.sun.com/techrep/2001/smli_tr-2001-98.pdf
[6] Java Language Specification, Third Edition. Gosling, Joy, Steele, Bracha.http://java.sun.com/docs/books/jls/third_edition/html/exceptions.html
[7] “Biased Locking in HotSpot”.http://blogs.oracle.com/dave/entry/biased_locking_in_hotspot
[8] “Let’s say you’re interested in using HotSpot as a vehicle for synchronization research ...”.http://blogs.oracle.com/dave/entry/lets_say_you_re_interested
[9] “Java Native Interface Specifications”http://java.sun.com/javase/6/docs/technotes/guides/jni/spec/jniTOC.html
[10] “The Java Native Interface Programmer’s Guide and Specification”, Sheng Liang,http://java.sun.com/docs/books/jni/html/titlepage.html
[11] “Trouble-Shooting and Diagnostic Guide”http://java.sun.com/javase/6/webnotes/trouble/