Tuning the Garbage Collector

Garbage collection (GC) reclaims the heap space previously allocated
to objects no longer needed. The process of locating and removing the dead
objects can stall any application and consume as much as 25 percent throughput.

Almost all Java Runtime Environments come with a generational object memory system and sophisticated
GC algorithms. A generational memory system divides the heap into a few carefully
sized partitions called generations. The efficiency of
a generational memory system is based on the observation that most of the
objects are short lived. As these objects accumulate, a low memory condition
occurs forcing GC to take place.

The heap space is divided into the old and the new generation. The new
generation includes the new object space (eden), and two survivor spaces.
The JVM allocates new objects in the eden space, and moves longer lived objects
from the new generation to the old generation.

The young generation uses a fast copying garbage collector which employs
two semi-spaces (survivor spaces) in the eden, copying surviving objects from
one survivor space to the second. Objects that survive multiple young space
collections are tenured, meaning they are copied to the
tenured generation. The tenured generation is larger and fills up less quickly.
So, it is garbage collected less frequently; and each collection takes longer
than a young space only collection. Collecting the tenured space is also referred
to as doing a full generation collection.

The frequent young space collections are quick (a few milliseconds),
while the full generation collection takes a longer (tens of milliseconds
to a few seconds, depending upon the heap size).

Other GC algorithms, such as the Concurrent Mark Sweep (CMS) algorithm,
are incremental. They divide the full GC into several incremental pieces.
This provides a high probability of small pauses. This process comes with
an overhead and is not required for enterprise web applications.

When the new generation fills up, it triggers a minor collection in
which the surviving objects are moved to the old generation. When the old
generation fills up, it triggers a major collection which involves the entire
object heap.

Both HotSpot and Solaris JDK
use thread local object allocation pools for lock-free, fast, and scalable
object allocation. So, custom object pooling is not often required. Consider
pooling only if object construction cost is high and significantly affects
execution profiles.

Choosing
the Garbage Collection Algorithm

Pauses during a full GC of more than four seconds can cause intermittent
failures in persisting session data into HADB.

While GC is going on, the Application Server isn’t running. If
the pause is long enough, the HADB times out the existing connections. Then,
when the application server resumes its activities, the HADB generates errors
when the application server attempts to use those connections to persist session
data. It generates errors like, “Failed to store session data,” “Transaction
Aborted,” or “Failed to connect to HADB server.”

To prevent that problem, use the CMS collector as the GC algorithm. This collector can cause a
drop in throughput for heavily utilized systems, because it is running more
or less constantly. But it prevents the long pauses that can occur when the
garbage collector runs infrequently.

Tracing Garbage Collection

The two primary measures of garbage collection performance are throughput and pauses. Throughput
is the percentage of the total time spent on other activities apart from GC.
Pauses are times when an application appears unresponsive due to GC.

Two other considerations are footprint and promptness. Footprint
is the working size of the JVM process, measured in pages and cache lines. Promptness is the time between when
an object becomes dead, and when the memory becomes available. This is an
important consideration for distributed systems.

A particular generation size makes a trade-off between these four metrics.
For example, a large young generation likely maximizes throughput, but at
the cost of footprint and promptness. Conversely, using a small young generation
and incremental GC will minimize pauses, and thus increase promptness, but
decrease throughput.

JVM diagnostic output will display information on pauses due to garbage
collection. If you start the server in verbose mode (use the command asadmin
start-domain --verbosedomain), then the command
line argument -verbose:gc prints information for every
collection. Here is an example of output of the information generated with
this JVM flag:

On each line, the first number is the combined size of live objects
before GC, the second number is the size of live objects after GC, the number
in parenthesis is the total available space, which is the total heap minus
one of the survivor spaces. The final figure is the amount of time that the
GC took. This example shows three minor collections and one major collection.
In the first GC, 50650 KB of objects existed before collection and 21808 KB
of objects after collection. This means that 28842 KB of objects were dead
and collected. The total heap size is 76868 KB. The collection process required
0.0478645 seconds.

Other useful monitoring options include:

-XX:+PrintGCDetails for more detailed logging
information

-Xloggc:file to save the information in
a log file

Other Garbage Collector Settings

For applications that do not dynamically generate and load classes,
the size of the permanent generation has no effect on GC performance.
For applications that dynamically generate and load classes (for example,
JSP applications), the size of the permanent generation does affect GC performance,
since filling the permanent generation can trigger a Full GC. Tune the maximum
permanent generation with the -XX:MaxPermSize option.

Although applications can explicitly invoke GC with the System.gc() method,
doing so is a bad idea since this forces major collections, and inhibits scalability
on large systems. It is best to disable explicit GC by using the flag -XX:+DisableExplicitGC.

The Enterprise Server uses RMI in the Administration module for monitoring.
Garbage cannot be collected in RMI-based distributed applications without
occasional local collections, so RMI forces a periodic full collection. Control
the frequency of these collections with the property -sun.rmi.dgc.client.gcInterval. For
example, - java -Dsun.rmi.dgc.client.gcInterval=3600000 specifies
explicit collection once per hour instead of the default rate of once per
minute.

To specify the attributes for the Java virtual machine, use the Admin
Console and set the property under config-name > JVM
settings (JVM options).

Tuning the Java Heap

This section discusses topics related to tuning the Java Heap for performance.

Guidelines for Java Heap Sizing

Maximum heap size depends on maximum address space per process. The
following table shows the maximum per-process address values for various platforms:

Table 4–1 Maximum Address Space Per Process

Operating System

Maximum Address Space Per Process

Redhat Linux 32 bit

2 GB

Redhat Linux 64 bit

3 GB

Windows 98/2000/NT/Me/XP

2 GB

Solaris x86 (32 bit)

4 GB

Solaris 32 bit

4 GB

Solaris 64 bit

Terabytes

Maximum heap space is always smaller than maximum address space per
process, because the process also needs space for stack, libraries, and so
on. To determine the maximum heap space that can be allocated, use a profiling
tool to examine the way memory is used. Gauge the maximum stack space the
process uses and the amount of memory taken up libraries and other memory
structures. The difference between the maximum address space and the total
of those values is the amount of memory that can be allocated to the heap.

You can improve performance by increasing your heap size or using a
different garbage collector. In general, for long-running server applications,
use the J2SE throughput collector on machines with multiple processors (-XX:+AggressiveHeap) and as large a heap as you can fit in the free
memory of your machine.

Heap Tuning Parameters

You can control the heap size with the following JVM parameters:

-Xmsvalue

-Xmxvalue

-XX:MinHeapFreeRatio=minimum

-XX:MaxHeapFreeRatio=maximum

-XX:NewRatio=ratio

-XX:NewSize=size

-XX:MaxNewSize=size

-XX:+AggressiveHeap

The -Xms and -Xmx parameters define
the minimum and maximum heap sizes, respectively. Since GC occurs when the
generations fill up, throughput is inversely proportional to the amount of
the memory available. By default, the JVM grows or shrinks the heap at each
GC to try to keep the proportion of free space to the living objects at each
collection within a specific range. This range is set as a percentage by the
parameters -XX:MinHeapFreeRatio=minimum and -XX:MaxHeapFreeRatio=maximum; and the
total size bounded by -Xms and -Xmx.

Set the values of -Xms and -Xmx equal
to each other for a fixed heap size. When the heap grows or shrinks, the JVM
must recalculate the old and new generation sizes to maintain a predefined NewRatio.

The NewSize and MaxNewSize parameters
control the new generation’s minimum and maximum size. Regulate the
new generation size by setting these parameters equal. The bigger the younger
generation, the less often minor collections occur. The size of the young
generation relative to the old generation is controlled by NewRatio.
For example, setting -XX:NewRatio=3 means that the ratio
between the old and young generation is 1:3, the combined size of eden and
the survivor spaces will be fourth of the heap.

By default, the Enterprise Server is invoked with the Java HotSpot Server
JVM. The default NewRatio for the Server JVM is 2: the
old generation occupies 2/3 of the heap while the new generation occupies
1/3. The larger new generation can accommodate many more short-lived objects,
decreasing the need for slow major collections. The old generation is still
sufficiently large enough to hold many long-lived objects.

To size the Java heap:

Decide the total amount of memory you can afford for the JVM.
Accordingly, graph your own performance metric against young generation sizes
to find the best setting.

Make plenty of memory available to the young generation. The
default is calculated from NewRatio and the -Xmx setting.

Larger eden or younger generation spaces increase the spacing
between full GCs. But young space collections could take a proportionally
longer time. In general, keep the eden size between one fourth and one third
the maximum heap size. The old generation must be larger than the new generation.

Example 4–1 Heap Configuration on Solaris

Survivor Ratio Sizing

The SurvivorRatio parameter controls the size of
the two survivor spaces. For example, -XX:SurvivorRatio=6 sets
the ratio between each survivor space and eden to be 1:6, each survivor space
will be one eighth of the young generation. The default for Solaris is 32.
If survivor spaces are too small, copying collection overflows directly into
the old generation. If survivor spaces are too large, they will be empty.
At each GC, the JVM determines the number of times an object can be copied
before it is tenured, called the tenure threshold. This threshold is chosen
to keep the survivor space half full.

Use the option -XX:+PrintTenuringDistribution to
show the threshold and ages of the objects in the new generation. It is useful
for observing the lifetime distribution of an application.

Rebasing DLLs on Windows

When the JVM initializes, it tries to allocate its heap using the -Xms setting. The base addresses of Application Server DLLs can
restrict the amount of contiguous address space available, causing JVM initialization
to fail. The amount of contiguous address space available for Java memory
varies depending on the base addresses assigned to the DLLs. You can increase
the amount of contiguous address space available by rebasing the
Application Server DLLs.

To prevent load address collisions, set preferred base addresses with
the rebase utilty that comes with Visual Studio and the Platform SDK. Use
the rebase utility to reassign the base addresses of the Application Server
DLLs to prevent relocations at load time and increase the available process
memory for the Java heap.

There are a few Application Server DLLs that have non-default base addresses
that can cause collisions. For example:

The nspr libraries have a preferred address
of 0x30000000.

The icu libraries have the address of 0x4A?00000.

Move these libraries near the system DLLs (msvcrt.dll is
at 0x78000000) to increase the available maximum contiguous
address space substantially. Since rebasing can be done on any DLL, rebase
to the DLLs after installing the Application Server.

To rebase the Application Server’s DLLs

Before You Begin

To perform rebasing, you need:

Windows 2000

Visual Studio and the Microsoft Framework SDK rebase utility

Make install_dir\ bin the
default directory.

cd install_dir\bin

Enter this command:

rebase -b 0x6000000 *.dll

Use the dependencywalker utility to make sure
the DLLs were rebased correctly.