Type Loading, Linking, and Initialization
JVM makes types available to the running program through a process of loading, linking, and initialization (in that order). Linking is divided into three sub-steps: verification, preparation, and resolution.
All implementations must initialize each class or interface on its first active use. The following six situations qualify as active uses:
new
.invokestatic
.getstatic
andputstatic
.- The invocation of certain reflective methods, such as
forName
. - The initialization of a subclass of a class.
- Class with the
main
method.
The initialization of a class requires prior initialization of its superclass. Applied recursively, this rule means that all of a class’s superclasses must be initialized prior to the initialization of the class. The same is not true, however, of interfaces. An interface is initialized only because a non-constant field declared by the interface is used.
Loading
- Load the class file and produce a stream of binary data.
- Parse the binary data for a type into internal data structures in the method area and also instantiating a
Class
object on heap.
After that a type is created. Types are loaded either through the bootstrap class loader or through user-defined class loaders. Former loads classes and interfaces of the Java API in an implementation-dependent way; latter which are instances of subclasses of java.lang.ClassLoader
, load classes in custom ways.
Verification:
After a type is loaded, it is ready to be linked. The first step of the linking process is verification– ensuring that the type obeys the semantics of the Java language and that it won’t violate the integrity of the virtual machine. Here are some of the things that are good candidates for checking during the official verification phase:
- checking that final classes are not subclassed.
- checking that final methods are not overridden.
- making sure no incompatible method declarations appear between the type and its supertypes.
- checking that all special strings contained in the constant pool are well-formed
- verifying the integrity of the bytecodes
Preparation:
After a Java virtual machine has loaded a class and performed whatever verification it chooses to do up front, the class is ready for preparation. During the preparation phase, the Java virtual machine allocates memory for the class variables and sets them to default initial values.
Type | Initial Value |
---|---|
int | 0 |
long | 0L |
short | (short) 0 |
char | ‘\u0000’ |
byte | (byte) 0 |
boolean | false |
reference | null |
float | 0.0f |
double | 0.0d |
Resolution:
After a type has been through the first two phases of linking: verification and preparation, it is ready for the third and final phase of linking: resolution. Resolution is the process of locating classes, interfaces, fields, and methods referenced symbolically from a type’s constant pool, and replacing those symbolic references with direct references. As mentioned above, this phase of linking is optional until (and unless) each symbolic reference is first used by the program.
Initialization:
The final step required to ready a class or interface for its first active use is initialization, the process of setting class variables to their proper initial values. a proper initial value is specified via a class variable initializer or static initializer:
class A {
//variable initializer
static int a = 1;
// static initializer
static int b;
static {
b = 1;
}
}
All the class variable initializers and static initializers of a type are collected by the Java compiler and placed into one special method called clinit
which will later be invoked by JVM.
Initialization of a class consists of two steps:
- Initializing the class’s direct superclass (if any), if the direct superclass hasn’t already been initialized.
- Executing
clinit
, if it has one.
Initialization of an interface consists of only one step since it does not require initialization of its superinterfaces:
- Executing
clinit
, if it has one.
Note that the initialization process is properly synchronized. That’s why the inner static helper class
singleton is thread safe.
Thread
A thread
is a thread of execution in a program.
When a Java Virtual Machine starts up, there is usually a single non-daemon thread (which typically calls the method named main of some designated class). The Java Virtual Machine continues to execute threads until either of the following occurs:
- The
exit
method of classRuntime
has been called and the security manager has permitted the exit operation to take place. - All threads that are not daemon threads have died, either by returning from the call to the
run
method or by throwing an exception that propagates beyond therun
method.
There are two ways to create a new thread of execution. One is to declare a class to be a subclass of Thread
. This subclass should override the run
method of class Thread
. An instance of the subclass can then be allocated and started.
The other way to create a thread is to declare a class that implements the Runnable
interface. That class then implements the run
method. An instance of the class can then be allocated, passed as an argument when creating Thread, and started.
notify
, notifyAll
and wait
(all methods of Object
) should only be called by a thread that is the owner of this object’s monitor.
A thread becomes the owner of the object’s monitor in one of three ways:
- By executing a synchronized instance method of that object.
- By executing the body of a synchronized statement that synchronizes on the object.
- For objects of type Class, by executing a synchronized static method of that class.
Only one thread at a time can own an object’s monitor.
notify
wakes up a single thread that is waiting on this object’s monitor. If any threads are waiting on this object, one of them is chosen to be awakened. The choice is arbitrary and occurs at the discretion of the implementation. notifyAll
on the other hand wakes up all threads that are waiting on this object’s monitor. The awakened thread will not be able to proceed until the current thread relinquishes the lock on this object. The awakened thread will compete in the usual manner with any other threads that might be actively competing to synchronize on this object; for example, the awakened thread enjoys no reliable privilege or disadvantage in being the next thread to lock this object.
A thread waits on an object’s monitor by calling one of the wait
methods.
wait
causes the current thread (call it T
) to place itself in the wait set for this object and then to relinquish any and all synchronization claims on this object. Thread T becomes disabled for thread scheduling purposes and lies dormant until one of four things happens:
- Some other thread invokes the notify method for this object and thread
T
happens to be arbitrarily chosen as the thread to be awakened. - Some other thread invokes the notifyAll method for this object.
- Some other thread interrupts thread
T
. - The specified amount of real time has elapsed, more or less. If timeout is zero, however, then real time is not taken into consideration and the thread simply waits until notified.
The thread T
is then removed from the wait set for this object and re-enabled for thread scheduling. It then competes in the usual manner with other threads for the right to synchronize on the object; once it has gained control of the object, all its synchronization claims on the object are restored to the status quo ante - that is, to the situation as of the time that the wait
method was invoked. Thread T then returns from the invocation of the wait method. Thus, on return from the wait method, the synchronization state of the object and of thread T
is exactly as it was when the wait
method was invoked.
A thread can also wake up without being notified, interrupted, or timing out, a so-called spurious wakeup. While this will rarely occur in practice, applications must guard against it by testing for the condition that should have caused the thread to be awakened, and continuing to wait if the condition is not satisfied. In other words, waits should always occur in loops, like this one:
synchronized (obj) {
while (<condition does not hold>)
obj.wait(timeout);
... // Perform action appropriate to condition
}
If the current thread is interrupted by any thread before or while it is waiting, then an InterruptedException
is thrown. This exception is not thrown until the lock status of this object has been restored as described above.
Note that the wait
method, as it places the current thread into the wait set for this object, unlocks only this object; any other objects on which the current thread may be synchronized remain locked while the thread waits.
interrupt
interrupts current thread. An interrupt is an indication to a thread that it should stop what it is doing and do something else. It’s up to the programmer to decide exactly how a thread responds to an interrupt, but it is very common for the thread to terminate.
- If this thread is blocked in an invocation of the
wait
methods of the Object class, or of thejoin
,sleep
methods ofThread
class, then its interrupt status will be cleared and it will receive anInterruptedException
. - If this thread is blocked in an I/O operation upon an
InterruptibleChannel
then the channel will be closed, the thread’s interrupt status will be set, and the thread will receive aClosedByInterruptException
. - If this thread is blocked in a
Selector
then the thread’s interrupt status will be set and it will return immediately from the selection operation, possibly with a non-zero value, just as if the selector’s wakeup method were invoked. - If none of the previous conditions hold then this thread’s interrupt status will be set.
Interrupting a thread that is not alive need not have any effect. The interrupt mechanism is implemented using an internal flag known as the interrupt status. Invoking Thread.interrupt
sets this flag. When a thread checks for an interrupt by invoking the static method Thread.interrupted
, interrupt status is cleared. The non-static isInterrupted
method, which is used by one thread to query the interrupt status of another, does not change the interrupt status flag.
sleep
causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds, subject to the precision and accuracy of system timers and schedulers. The thread does not lose ownership of any monitors.
join
waits for the thread to die.
Methods such as sleep
, join
and wait
that could throw InterruptedException
are designed to cancel their current operation and return immediately when an interrupt is received. Also the interrupted status of the current thread is cleared when this exception is thrown.
Synchronization is built around an internal entity known as the intrinsic lock or monitor lock. Every object has an intrinsic lock associated with it. By convention, a thread that needs exclusive and consistent access to an object’s fields has to acquire the object’s intrinsic lock before accessing them, and then release the intrinsic lock when it’s done with them. A thread is said to own the intrinsic lock between the time it has acquired the lock and released the lock. As long as a thread owns an intrinsic lock, no other thread can acquire the same lock. The other thread will block when it attempts to acquire the lock. When a thread releases an intrinsic lock, a happens-before relationship is established between that action and any subsequent acquisition of the same lock.
Lock
Lock
objects work very much like the implicit locks used by synchronized code. As with implicit locks, only one thread can own a Lock object at a time. Lock objects also support a wait/notify
mechanism, through their associated Condition
objects.
The use of synchronized
methods or statements provides access to the implicit monitor lock associated with every object, but forces all lock acquisition and release to occur in a block-structured way: when multiple locks are acquired they must be released in the opposite order, and all locks must be released in the same lexical scope in which they were acquired. While the scoping mechanism for synchronized
methods and statements makes it much easier to program with monitor locks, and helps avoid many common programming errors involving locks, there are occasions where you need to work with locks in a more flexible way. For example, some algorithms for traversing concurrently accessed data structures require the use of “hand-over-hand” or “chain locking”: you acquire the lock of node A, then node B, then release A and acquire C, then release B and acquire D and so on. Implementations of the Lock
interface enable the use of such techniques by allowing a lock to be acquired and released in different scopes, and allowing multiple locks to be acquired and released in any order.
The biggest advantage of Lock objects over implicit locks is their ability to back out of an attempt to acquire a lock. The tryLock
method backs out if the lock is not available immediately or before a timeout expires (if specified). The lockInterruptibly
method backs out if another thread sends an interrupt before the lock is acquired.
The constructor for ReentrantLock
accepts an optional fairness parameter. When set true, under contention, locks favor granting access to the longest-waiting thread. Otherwise this lock does not guarantee any particular access order. Programs using fair locks accessed by many threads may display lower overall throughput (i.e., are slower; often much slower) than those using the default setting, but have smaller variances in times to obtain locks and guarantee lack of starvation. Note however, that fairness of locks does not guarantee fairness of thread scheduling. Thus, one of many threads using a fair lock may obtain it multiple times in succession while other active threads are not progressing and not currently holding the lock. Also note that the untimed tryLock
method does not honor the fairness setting. It will succeed if the lock is available even if other threads are waiting.
Interrupting a thread which is waiting “uninterruptedly” on a Lock
, ReentrantLock
or synchronized
block will merely result in the thread waking up and seeing if it’s allowed to take the lock yet, by whatever mechanism is in place in the defining lock, and if it cannot it parks again until it is interrupted again or told it can take the lock. When the thread can proceed it simply proceeds with its interrupted flag set.
Contrast to lockInterruptibly
where, actually, if you are interrupted, you do not ever get the lock, and instead you “abort” trying to get the lock and the lock request is cancelled.
ThreadPoolExecutor:
If the pool currently has more than corePoolSize
threads, excess threads will be terminated if they have been idle for more than the keepAliveTime. This provides a means of reducing resource consumption when the pool is not being actively used. By default, the keep-alive policy applies only when there are more than corePoolSize
threads. But method allowCoreThreadTimeOut(boolean)
can be used to apply this time-out policy to core threads as well, so long as the keepAliveTime
value is non-zero.
Queuing:
Any BlockingQueue
may be used to transfer and hold submitted tasks. The use of this queue interacts with pool sizing:
- If fewer than
corePoolSize
threads are running, the Executor always prefers adding a new thread rather than queuing. - If corePoolSize or more threads are running, the Executor always prefers queuing a request rather than adding a new thread.
- If a request cannot be queued, a new thread is created unless this would exceed
maximumPoolSize
, in which case, the task will be rejected.
There are three general strategies for queuing:
- Direct handoffs. A good default choice for a work queue is a
SynchronousQueue
that hands off tasks to threads without otherwise holding them. Here, an attempt to queue a task will fail if no threads are immediately available to run it, so a new thread will be constructed. This policy avoids lockups when handling sets of requests that might have internal dependencies. Direct handoffs generally require unbounded maximumPoolSizes to avoid rejection of new submitted tasks. This in turn admits the possibility of unbounded thread growth when commands continue to arrive on average faster than they can be processed. - Unbounded queues. Using an unbounded queue (for example a
LinkedBlockingQueue
without a predefined capacity) will cause new tasks to wait in the queue when allcorePoolSize
threads are busy. Thus, no more thancorePoolSize
threads will ever be created. (And the value of themaximumPoolSize
therefore doesn’t have any effect.) This may be appropriate when each task is completely independent of others, so tasks cannot affect each others execution; for example, in a web page server. While this style of queuing can be useful in smoothing out transient bursts of requests, it admits the possibility of unbounded work queue growth when commands continue to arrive on average faster than they can be processed. - Bounded queues. A bounded queue (for example, an
ArrayBlockingQueue
) helps prevent resource exhaustion when used with finite maximumPoolSizes, but can be more difficult to tune and control. Queue sizes and maximum pool sizes may be traded off for each other: Using large queues and small pools minimizes CPU usage, OS resources, and context-switching overhead, but can lead to artificially low throughput. If tasks frequently block (for example if they are I/O bound), a system may be able to schedule time for more threads than you otherwise allow. Use of small queues generally requires larger pool sizes, which keeps CPUs busier but may encounter unacceptable scheduling overhead, which also decreases throughput.
Rejected tasks:
New tasks submitted in method execute(Runnable) will be rejected when the Executor has been shut down, and also when the Executor uses finite bounds for both maximum threads and work queue capacity, and is saturated. In either case, the execute method invokes the RejectedExecutionHandler.rejectedExecution(Runnable, ThreadPoolExecutor)
method of its RejectedExecutionHandler. Four predefined handler policies are provided:
- In the default
ThreadPoolExecutor.AbortPolicy
, the handler throws a runtimeRejectedExecutionException
upon rejection. - In
ThreadPoolExecutor.CallerRunsPolicy
, the thread that invokes execute itself runs the task. This provides a simple feedback control mechanism that will slow down the rate that new tasks are submitted. - In
ThreadPoolExecutor.DiscardPolicy
, a task that cannot be executed is simply dropped. - In
ThreadPoolExecutor.DiscardOldestPolicy
, if the executor is not shut down, the task at the head of the work queue is dropped, and then execution is retried (which can fail again, causing this to be repeated.)
It is possible to define and use other kinds of RejectedExecutionHandler
classes. Doing so requires some care especially when policies are designed to work only under particular capacity or queuing policies.
GC
Heap consists of three parts – young
, old
and meta-space
, young
space is further divided into eden
, s0
and s1
:
| YOUNG | OLD | META |
| EDEN | SO | S1 |
- All new allocation happens in
eden
. - Minor GC (Young GC) when eden is full.
- One of
s0
ands1
is always empty. - Combine surviving objects from eden and the non-empty one of
s0
ors1
and copy those into the empty one. The age of those objects increment by 1. - Object in
s0
ors1
which age is bigger than-XX:MaxTenuringThreshold
got promoted intoold
. - If the count of the oldest objects in
s0
(let’s say it’s the empty one) is bigger than half ofs0
’s size then those objects got promoted intoold
. - Big object goes straight into
old
. - If the big object is bigger that average size which got promoted into
old
and there is not enough continues space inold
then start Full GC. - When using CMS, if
old
usage is bigger thanXX:CMSInitiatingOccupancyFraction
then start Old GC (exclusive to CMS). And Old GC doesn’t compact. - When using G1, if
old
is bigger than-XX:InitiatingHeapOccupancyPercen
then start Full GC. - When
-XX:MaxMetaspaceSize
is set and reached, JVM OOM.
GC OPTION | YOUNG | OLD | DESCRIPTION |
---|---|---|---|
-XX:+UseSerialGC | single | single | single core cpu/ small heap |
-XX:+UseParallelGC | single | parallel | |
-XX:+UseParallelOldGC | parallel | parallel | |
-XX:+UsePArNewGC | parallel | ||
-XX:+UseConsMarkSweepGC | parallel | concurrent | auto-enables ParNewGC |
-XX:+UseG1GC | g1 | g1 |
Use CMS when:
- There is more memory.
- There is high number of CPUS.
- Application demands short pauses.
Use parallel collector when :
- There is less memory.
- There is lesser number of CPUS.
- Application demands high throughput.
Throughput collectors can automatically tune themselves:
-XX:+UseAdaptiveSizePolicy
-XX:MaxGCPauseMillis
-XX:GCTimeRatio
Compressed Object Pointers
- Automatically used below 30GB of max heap.
- Usable below 32GB of max heap size.
- Pointers become 4 bytes long.
Uncompressed | Compressed | 32-bit | |
---|---|---|---|
Pointer | 8 | 4 | 4 |
Object header | 16 | 12* | 8 |
Array header | 24 | 14 | 12 |
Superclass pad | 8 | 4 | 4 |
(* Object can have 4 bytes of fields and still only take up 16 bytes)
(updating…)