This is the 30th day of my participation in the August Challenge
Phase to recommend
- Java Basics
- Java concurrent programming
1, an overview of the
The JVM’s classloading mechanism is that the VIRTUAL machine loads the data describing the Class from the Class file into memory, verifies, converts, and initializes the data, and eventually forms Java types that can be used directly by the JVM.
In the Java language, classes are loaded, connected, and initialized during program execution.
2. Class loading timing
- When do you need to start the first phase of the class loading process: loading? There are no constraints in this Java virtual machine specification, and the implementation can be left to the virtual machine.
- However, forInitialize thePhase, the virtual machine specification is strictly specifiedOne and only
5 kinds of circumstances
Classes must be immediately”Initialize the“(loading
,validation
,To prepare
It has been done before) :
The above five scenarios are called actions against a classActive reference. In addition, all other ways of referring to a class do not trigger initialization, calledPassive reference. Examples of common passive quotes include:
(1) Using a subclass to refer to a static variable of the parent class does not result in subclass initialization. System.out.println(SubClass.staticValue);
(2) Referencing a class through an array definition does not trigger initialization of the class. SuperClass[] arr = new SuperClass[10];
(3) Static constants are stored in the constant pool of the calling class at compile time, and are not directly referenced to define constants per se. System.out.println(ConstClass.finalStaticValue);
3. Class loading process
The entire life cycle of a class from when it is loaded into virtual machine memory to when it is unloaded from memory includes: 2, Loading, Verification, Preparation, Resolution, Initialization, Using and Unloading are two stages. Preparation, validation and parsing are collectively referred to as Linking.
As shown in the figure:
loading
,validation
,To prepare
andInitialize the
The sequence in which these four stages occur is determined, whileparsing
Phases are not necessarily: they can be started after the initialization phase in some cases to support runtime binding (also known as dynamic binding or late binding) for the Java language
Also note that the phases here start in sequence, not proceed or complete sequentially, as these phases are often intermixed with each other, often invoking or activating one phase while the other is executing.
3.1 loading
Loading is the first stage of the class loading process, during which the virtual machine needs to do three things:
-
Get the binary byte stream that defines this Class by the fully qualified name of a Class (not from a Class file, but from other sources such as networks, dynamic generation, databases, etc.);
-
Convert the static storage structure represented by this byte stream to the runtime data structure of the method area;
-
Generate an in-memory java.lang.Class object representing the Class as an access point to the Class’s various data in the method area.
Compared to other stages of class loading,The load phase (more precisely, the action of the load phase to retrieve the binary stream of the class) is the most controllable phaseBecause developers can either use the class loader provided by the system to complete the loading, or they can customize their own class loader to complete the loading.
After the loading phase is complete, the binary byte streams outside the Java virtual machine are stored in the method area in the format required by the virtual machine, and an object of java.lang.Class Class is created in the Java heap through which the data in the method area can be accessed.
Instead of waiting for a class to be “first actively used” before loading it, the JVM specification allows the classloader to preload a class in anticipation of it being used, and if it encounters a missing.class file or an error during preloading, The class loader must not report a LinkageError until the program first actively uses the class. If the class is never actively used by the program, the class loader will not report an error.
How to load a. Class file
-
Load directly from the local system
-
Download the. Class file from the network
- Typical scenario: A Web Applet, our small application
-
Load. Class files from zip, JAR, etc archives
- Typical scenario: The jar and WAR formats are subsequently changed
-
Advance. Class files from a proprietary database
- Typical scenario: JSP applications extract. Class files from a proprietary database, which is rare.
-
Dynamically compile Java source files into.class files, which are computed at runtime
- Typical scenario: Dynamic proxy technology
-
Obtained from an encrypted file
- Typical scenario: Typical protection measures against Class files being decompiled
3.2 validation
Validation is the first step in the connection phase, which ensures that the byte stream in the Class file meets the requirements of the current virtual machine and does not compromise the security of the virtual machine itself. On the whole, the verification phase will roughly complete the following four stages of verification: file format verification, metadata verification, bytecode verification, symbol reference verification.
3.2.1 File format validation
It mainly verifies that the byte stream complies with the Class file format specification and can be processed by the current version of the virtual machine.
Main verification points:
- Whether to magic number
0xCAFEBABE
At the beginning - Check whether the major and minor versions are within the processing range of the current VM
- Whether constants in the constant pool have unsupported types (check the constant tag flag)
- Is there any index value that points to a constant that does not exist or that does not conform to a type
- CONSTANT_Utf8_info whether there is data in the constant that does not conform to UTF8 encoding
- Whether parts of the Class file and the file itself have been deleted or additional information…
Verification at this stage is based on binary byte streams, and only after file format validation is passed will the byte streams be stored in the method area of memory.Copy the code
3.2.2 Metadata validation
The information described by bytecode is mainly analyzed to ensure that the information provided meets the requirements of Java language specification.
Main verification points:
-
Whether the class has a parent class (only Object objects do not have a parent class, all others do)
-
Whether the class inherits classes that are not allowed to be inherited (classes modified by final)
-
If the class is not abstract, does it implement all the methods required by its parent or interface
-
Does a field or method ina class conflict with the parent class (e.g., overwriting a final field in the parent class, or overloading a method that does not conform to rules, e.g., method parameters are identical but return value types are different)
.
3.2.3 Bytecode verification
Mainly for data flow and control flow analysis, to determine the program semantics is legal, logical. The method body of a class is verified and analyzed to ensure that the methods of the verified class do not harm vm security during running.
Main verification points:
-
Ensure that the data type of the operand stack and the sequence of instructions work together at any time, such that an int in the operand stack is not loaded into a local variable as long when used
-
Ensure that the jump does not jump to bytecode instructions outside the method body
-
Ensure that type conversions within the method body are legal. For example, it is legal for a subclass to assign to a parent class, but not legal for a parent class to assign to a subclass or any other type that has no inheritance relationship.
3.2.4 Symbolic reference verification
The final phase of validation occurs when the virtual machine converts symbolic references to direct references, which occurs during the third phase of connection parsing. Symbolic references match information outside the class itself (the various symbolic references in the constant pool).
Main verification points:
-
Whether a class is found for a fully qualified name described by a string in a symbol reference
-
Whether a field descriptor that matches a method exists in the specified class and the methods and fields described by the simple name
-
The purpose of symbolic reference validation is to ensure that the parse action can be performed properly. If symbolic reference validation fails, the class, method, and field are accessible (private,public,protected, default). It will throw a Java. Lang. A subclass of IncompatibleClassChangeError anomalies, Such as Java. Lang. IllegalAccessError, Java. Lang. NoSuchFieldError, Java. Lang. NoSuchMethodError, etc.
The validation phase is important, but not required, and has no effect on program runtime. If the referenced classes are repeatedly validated, consider using the -xVerifyNone parameter to turn off most of the class validation to shorten the virtual machine class load time. *
3.3 to prepare
The preparation phase is the formal allocation of memory for class variables (static member variables) and the setting of their initial values (zero). The memory used by these variables will be allocated in the method area. Two things to note here:
- Member variables are not allocated here; they are allocated in the heap when the class instantiates the object.
- Setting the initial value here refers to the zero value of the type (such as 0, NULL, false, etc.), not the assigned value displayed in the code.
Such as:
public class Test {
public int number = 111;
public static int sNumber = 111;
}
Copy the code
The member variable number is not allocated and initialized at this stage. The class variable sNunber allocates memory in the method area and sets the value of 0 to int instead of 111, which is only executed during initialization.
Java base and reference data types have zero values
But what if the class variable if it’s beingfinal
In the preparation phase, memory is also allocated in the method area and its value is set to the value assigned by the display.
Such as:
public class Test {
public static final int NUMBER = 111; } Duplicate codeCopy the code
At this point, the value of NUMBER is set to 111 in the preparation phase.
3.4 analytical
The parsing phase is the process of replacing symbolic references in the constant pool with direct references.
Symbolic References: Symbolic References describe the referenced target as a set of symbols, which can be any literal, as long as the target can be uniquely located. Symbol references are independent of memory layout, so the referenced object does not necessarily have to have been loaded into memory. The memory layout can vary among virtual machine implementations, but the accepted symbolic references must be consistent, because the literal form of symbolic references is clearly defined in the Class file format. (CONSTANT_Class_info, CONSTANT_Fieldref_info, CONSTANT_Methodref_info, etc.)
Direct References: A pointer to a target directly, a relative offset, or a handle that can be indirectly located to the target. A direct reference is related to the memory layout implemented by the VIRTUAL machine. The translation of a symbolic reference on different virtual machines may not be the same. If there is a direct reference, it must already be in memory.
- The parse action is aimed at
Class or interface
,field
,Class method
,Interface methods
,Method type
,Method handles
andCall qualifier
Class 7 symbolic references are carried out.
Class/interface resolution: Assuming that the Java VIRTUAL machine references either class N or interface C in the method body of class D, the following steps are performed:
- If C is not an array type, D’s defining classloader is used to create either class N or interface C. Any exceptions that occur during loading can be considered class and interface resolution failures.
- If C is an array type and its element type is a reference type. Symbolic references to classes or interfaces representing element types are resolved by recursive calls.
- Check C’s access permission. If D does not have access to C, throw
java.lang.IllegalAccessError
The exception.
Field resolution: To resolve an unparsed field symbol reference, the CONSTANT_Class_info symbol reference of the index in the class_index entry in the field table is first resolved. If any exception occurs during the resolution of this class or interface symbol reference, the field resolution will fail. If parsing is complete, the class or interface to which the field belongs is represented by C. The VM specification requires the following steps to search for subsequent fields in C.
1. If C contains a field whose simple name and field descriptor match that of the target, a direct reference to the field is returned. The search ends.
2. Otherwise, if an interface is implemented in C, the interface and its parent interface are recursively searched from bottom to top based on inheritance relationship. If the interface contains a field whose simple name and field descriptor match the target, the direct reference of the field is returned, and the search ends.
3. Otherwise, if C is not java.lang.Object, it will recursively search its parent class from bottom to top based on inheritance. If the class contains a field whose simple name and field descriptor match the target, it will return a direct reference to that field, and the search ends.
4. If no, find out failure, throw Java. Lang. NoSuchFieldError anomalies. If returned to the reference, you also need to check the access, if there is no access, will throw out the Java. Lang. IllegalAccessError anomalies.
In a practical implementation, the requirements might be stricter, and the compiler might refuse to compile if the same field name appears in both the parent class and interface of C.
Class method resolution Class method resolution also begins by resolving symbolic references to the class or interface to which the indexed method belongs in the class_index entry in the class method table. We still use C to represent the parsed class, and the virtual machine will perform the following steps to search C for the class methods.
1. First of all check the C method reference for the class or interface, if is the interface, then the method reference will be thrown IncompatibleClassChangeError exception 2. The Method reference checks whether C and its parent class contain the Method. If there is indeed a Method in C that has the same name as the specified Method reference and claims to be a Signature Polymorphic Method, the Method lookup process is considered successful. Classes mentioned by all method descriptors also need to be resolved. For C, it is not necessary to declare methods using the descriptor specified by method references.
3. Otherwise, if C declares a method with the same name and descriptor as a method reference, the method lookup is successful.
4. If C has a parent class, recursively find C’s immediate parent class as described in Step 2.
5. Otherwise, the class C list of implementation of the interface and their parent interface of recursive search whether there is a simple name and descriptor with the target matching method, if there is a match, the method of class C as an abstract class, to find the end, and throw out the Java. Lang. AbstractMethodError anomalies. Otherwise, declaring the method fails, and throw out the Java. Lang. NoSuchMethodError.
Finally, if the lookup process successfully returned to the reference directly, will the method for authentication, if it is found that do not have access to this method, then throws the Java. Lang. IllegalAccessError anomalies.
Interface method parsing
The interface method also needs to resolve the symbolic reference of the class or interface to which the indexed method belongs in the class_index entry of the interface method table. If the resolution succeeds, the interface is still represented by C. The VM will perform the following steps to search for interface methods.
1. With the class method resolution is different, if found in the interface method table class_index corresponding index C is classes instead of interfaces, direct selling Java. Lang. IncompatibleClassChangeError anomalies.
2. Otherwise, search interface C for a method whose simple name and descriptor match the target. If there is a method whose simple name and descriptor match the target, return a direct reference to the method.
3. Otherwise, recursively search the parent interface of interface C up to java.lang.Object to see if there is a method whose simple name and descriptor match the target. If there is, return a direct reference to the method.
4. Otherwise, declaring the method fails, throwing Java lang. NoSuchMethodError anomalies.
Due to the method of the interface are public by default, so there is no access problem, also can’t throw Java. The basic lang. IllegalAccessError anomalies.
3.5 the initialization
Initialization is the last step in class loading. In the previous phase, except for the loading phase, which can be loaded through user-defined class loaders, the rest of the virtual machine is basically in charge. But it is during the initialization phase that the user-written Java code is actually executed.
In the preparation phase, variables are assigned initial values, but in the initialization phase, all variables are reinitialized according to user-written code. To put it another way, the initialization phase is the execution of the class constructor
() method.
The < Clinit >() method is generated by combining the compiler’s automatic collection of assignment actions for all class variables in a class with statements in the static block, which can only access variables defined before the static block, based on the order in which the statements appear in the source file. Variables defined after it can be assigned in the previous static block, but cannot be accessed.
public class Test {
static {
i=0; // Can be assigned
System.out.print(i); // The compiler will say "illegal forward reference"
}
static int i=1;
}
Copy the code
The
() method, unlike the class’s constructor
() method, does not explicitly call the parent constructor. The virtual advantage is that the parent
() has already executed before the subclass’s
() method executes. Therefore, the first
() to be executed in the virtual machine must be java.lang.object.
Also due to the order in which
() is executed, the static statement block in the parent class is superior to the variable assignment operation in the subclass, so in the following code snippet, B will have a value of 2.
static class Parent {
public static int A=1;
static {
A=2; }}static class Sub extends Parent{
public static int B=A;
}
public static void main(String[] args) {
System.out.println(Sub.B);
}
Copy the code
The
() method is not required for a class, and the compiler will not generate a
() method for a class that has neither a static block nor a static variable assignment action.
An interface cannot use static blocks, but it does allow variable initialization assignments, so the interface generates < Clinit >() just as the class does, but the < Clinit >() in the interface does not need to execute the parent class first. The parent interface is initialized only when a variable defined in the parent class is used. In addition, the interface’s implementation class does not execute the interface’s < Clinit >() method when initialized.
The virtual machine ensures that a class’s
() methods are properly chained and synchronized in a multithreaded environment. If multiple threads initialize a class, only one thread will execute the class’s < clinit > () method, and all the other threads will block until the active thread completes executing the < clinit > () method. Lengthy operations in a class’s < Clinit > () method can cause multiple process blocks that are often hidden in practice.
3.6 Exiting the Java VM
The general conditions for logging out of a Java VM are as follows:
- Some thread callsThe Runtime classorThe System classthe
exit
Methods; - The Runtime classthe
halt
Method, and the Java Security Manager also allows thisexit
orhalt
Operation; - In addition, inJNI(
Java Native Interface
The specification also describes when to useJNI APITo load and unload (Load & Unload
) Exit the Java VM.
Class loaders
-
Class loaders: Code modules that implement the class-loading action of getting the binary stream describing a class by its fully qualified name are called classloaders. The virtual machine design team implemented this action outside of the Java virtual machine to give the application a sense of how to get the required classes.
-
For any class, the uniqueness of the Java virtual machine needs to be established both by the class loader that loads it and by the class itself. Each class loader has a separate class namespace. Comparing two classes to be “equal” only makes sense if the two classes are loaded by the same Class loader. Otherwise, even if the two classes are from the same Class file and loaded by the same virtual machine, as long as they are loaded by different Class loaders, the two classes must be different.
From a Java virtual machine perspective, there are only two different class loaders:
- One is to start the Bootstrap ClassLoader, which is implemented in C++ as part of the virtual machine itself.
- The other is all the other classloaders, which are implemented in the Java language, independent of the virtual machine, and all inherit from the abstract java.lang.classloader class.
From a Java developer’s perspective, most Java programs use one of three system-provided classloaders:
Bootstrap ClassLoader
: this class loader is responsible for storing it in the <JAVA_HOME>\lib directory, or by-Xbootclasspath
In the path specified by the parameter, and is recognized by the virtual machine (only recognized by the file name, such as rt.jar; libraries with incorrect names will not be loaded even if they are placed in the lib directory).Extension ClassLoader
: This loader is created bysun.misc.Launcher$ExtClassLoader
Implementation, which is responsible for loading the <JAVA_HOME>\lib\ext directory, or byjava.ext.dirs
For all libraries in the path specified by the system variable, the developer can use the extended class loader directly.Application ClassLoader
The class loader is created bysun.misc.Launcher$AppClassLoader
The implementation. Because this class loader isClassLoaderIn thegetSystemClassLoader()
Method, so it is also commonly called the system classloader. It is responsible for loading the user classpath (ClassPath). Developers can use this class loader directly. If the application does not have its own custom class loader, this is usually the default class loader.
The relationship between class loaders is generally as follows:
5. Parental delegation model
This hierarchical relationship between class loaders, shown in the figure above, is called the Parents Delegation Model of class loaders.
- The parent delegate model requires that all class loaders have their own parent class loaders, except for the top-level start class loaders.
- The parent-delegate model for classloaders was introduced in JDK 1.2, when it was not a mandatory constraint model, but rather a kind of loader implementation recommended by Java designers to developers.
The working process of the parental delegation model is:
- If a classloader receives a class-loading request, it first does not attempt to load the class itself, but delegates the request to the parent classloader.
- This is true at each level of class loaders, so all classloading requests should ultimately be routed to the top class loaders.
- The child loader will try to load it only if the parent loader reports that it cannot complete the load request.
ClassLoader
Source:
protected Class<? > loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// Check whether the requested class has already been loadedClass<? > c = findLoadedClass(name);if (c == null) {
long t0 = System.nanoTime();
try {
// delegate the class loading request to the parent class loader
if(parent ! =null) {
// If the parent loader is not empty, delegate to the parent loader for loading
c = parent.loadClass(name, false);
} else {
// If the parent class loader is empty, it means that the class is loaded from Bootstrapc = findBootstrapClassOrNull(name); }}catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
// If the parent class loader throws a ClassNotFoundException
// Indicates that the parent class loader cannot complete the load request
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
// If the parent class loader fails to load, call findClass to load the class
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); }}if (resolve) {
resolveClass(c);
}
returnc; }}Copy the code
Check to see if it has already been loaded. If not, call the loadClass() method of the parent class loader and recurse up. If the parent class loader is empty, the recursion is to the start class loader. If all loaders from the parent class loader up to the starting class loader fail to load, you call your own findClass() method to load.
Using the parent delegate model enables Java classes to have a hierarchy of priorities along with the loader, ensuring that the same class is loaded only once, avoiding reloading, and preventing malicious replacement of system classes.
5.1 Destruction of parental delegation model
The parental delegation model is not a mandatory constraint model, but rather a recommended implementation of class loaders by Java designers to developers.
First break: Forward compatibility
The first “break” of the parent delegation model actually happened before the parent delegation model appeared – before JDK1.2 was released. Because the parent delegate model was introduced after JDK1.2, and the class loaders and abstract java.lang.ClassLoader were introduced in JDK1.0, Java designers had to make some compromises when introducing the parent delegate model in the face of the existing implementation code for user-defined class loaders. For forward compatibility, a new proceted method, findClass(), was added in java.lang.classLoader after JDK1.2. Prior to this, users could inherit java.lang.classLoader only to override the loadClass() method. This is because the virtual class calls the loader’s private method loadClassInternal() during class loading, and the only logic for this method is to call its own loadClass(). After JDK1.2, users are no longer encouraged to override the loadClass() method. Instead, they should write their own class loading logic into the findClass() method. In the loadClass() logic, if the parent class loader fails to load, the user will call its own findClass() method to complete the load. This ensures that newly written classloaders conform to the parental delegation model.
Second failure: loading the SPI interface implementation class
The second time the parent delegate model was “broken” was because of a flaw in the model itself, which could not solve the problem of the underlying class calling user code.
Java Naming and Directory Interface (JNDI), the JNDI code is loaded by the startup class loader, but the purpose of JNDI is centralized management and lookup of resources, It requires invoking the code of the JNDI interface provider implemented by an independent vendor and deployed in the application’s ClassPath, but that code may not be known by the startup class loader. So the Java design team introduced the Thread Context ClassLoader. The JNDI service uses the thread-context class loader to load the required SPI code, that is, the parent class loader requests the subclass loader to complete the class loading action, which breaks the parent delegate model.
Third disruption: hot deployment
The third “break” of the parental delegation model is due to the user’s desire for the dynamic nature of the program.
In order to hot-plug, hot-deploy, modularize, which means add a feature or subtract a feature without rebooting, you just take that module and replace it with the same kind of loader and you get hot replacement of the code. Such as the advent of OSGi. In the OSGi environment, class loaders are no longer a tree structure in the parent delegate model, but a network structure.
Each program module (called a Bundle in OSGi) has its own class loader. When a Bundle needs to be replaced, the Bundle is replaced with the same kind of loader to achieve the hot replacement of code. In an OSGi environment, class loaders move away from the tree structure recommended by the parent delegate model and evolve into a more complex network structure. When a class load request is received, OSGi searches for classes in the following order:
-
Delegate classes that begin with Java. To the parent class loader.
-
Otherwise, delegate the classes in the list to the parent class loader.
-
Otherwise, delegate the classes from the Import list to the class loader of the Export class’s Bundle.
-
Otherwise, look up the current Bundle’s ClassPath and load it using your own class loader.
-
Otherwise, it checks whether the class is in its own Fragment Bundle. If so, it delegates the load to the Fragment Bundle’s classloader.
-
Otherwise, look for the Bundle in the Dynamic Import list and delegate the load to the corresponding Bundle’s classloader.
-
Otherwise, class lookup fails.
Only the first two points of the lookup order above still conform to the principles of the parent delegate model; the rest of the class lookup is done in the flat classloader.
5.2 What are the scenarios that break the parental delegation model
- Thread-context classloaders, typically: JDBC is loaded using thread-context classloaders
Driver
The implementation class Tomcat
The moreWeb
The applicationOSGI
Implement modular hot deployment
5.3 Customizing class loaders
Normally, we use the system class loader directly. However, there are times when custom class loaders are also needed. For example, an application transmits Java class bytecodes over the network. To ensure security, the bytecodes are encrypted. In this case, the system class loader cannot load the bytecodes. Custom classloaders are generally inherited from classloaders, so we just need to override the findClass method.
package com.akiang.jvm.classloader;
import java.io.*;
public class MyClassLoader extends ClassLoader {
private Stringroot; protected Class<? > findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) {
String fileName = root + File.separatorChar
+ className.replace('. ', File.separatorChar) + ".class";
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while((length = ins.read(buffer)) ! = -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public String getRoot() {
return root;
}
public void setRoot(String root) {
this.root = root;
}
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("D:\\temp"); Class<? > testClass =null;
try {
testClass = classLoader.loadClass("com.pdai.jvm.classloader.Test2");
Object object = testClass.newInstance();
System.out.println(object.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch(IllegalAccessException e) { e.printStackTrace(); }}} The core of a custom classloader is the retrieval of bytecode filesCopy the code
6. Reference and thanks
1. The Tomcat class loader breaks the parent delegate
2. Parent delegation model, class loading mechanism, and solve high frequency interview questions
3. In Depth Java Virtual Machine Version 3