Before you can optimize your program's footprint, it's important to understand how memory is used. There are many common misconceptions about how memory is consumed by Java programs, and it is easy to put a lot of effort into footprint control for relatively small gains. By understanding the factors that contribute to footprint, you can make informed decisions when coding.
This chapter shows you how to accurately measure the amount of memory used by your application and describes the factors that contribute to an application's RAM footprint. For tactics you can use to control some of these common problems, see Chapter 6, Controlling Class Loading.
java.lang.Runtime class can give you a general idea of how
much memory is being consumed by your program. These methods look at the
size of the virtual machine's object heap.
The object heap
Runtime.totalMemory-returns the size (in bytes) of the heap used to
allocate objects
Runtime.freeMemory-returns the amount of memory not being used in the
object heap
freeMemory from the totalMemory. Figure 5-1 shows a simplified
representation of the object heap. Note that the physical layout of the heap is totally
implementation-dependent and can vary widely.
These memory inspection methods are useful in a number of situations. For example, you can use them to display your program's heap usage at regular intervals. If the heap usage continues to increase over time, you might have a memory leak.
Although these memory inspection methods are useful, they only measure the object heap. Objects are not the only things that contribute to a program's RAM footprint. The only thing you can say for sure about the relationship between the memory usage calculated with the Runtime methods and the actual amount of memory your program requires is that the actual requirements are much larger.
Microsoft Windows NT 4.0 provides a tool called the Task Manager that displays memory usage information for the processes that are currently running. This information provides a more complete picture of your program's total RAM footprint.
The Windows NT Task Manager
To access the Task Manager on Windows NT, type ctrl-alt-del and click the Task Manager button in the dialog that's displayed. In the Task Manager window, look for a process called something like java.exe or javaw.exe. The program shown in Figure 5-2 is consuming about 9.8MB of RAM.
On Solaris, you can use the command line driven pmap utility to view your program's memory usage. This utility prints a map of the process's address space. You have to pass pmap a process ID with the -x option. To find the process ID for your JVM, use the ps command. Figure 5-3 shows how to find a running virtual machine and measure its memory usage. The key information reported by pmap is the total Kb entry in the Private column. The program shown in Figure 5-3 is using approximately 22MB of RAM.
Measuring footprint on Solaris
The relative memory consumption associated with each item varies across applications, runtime environments, and platforms. For a particular program, any one of these items might be the number one memory consumer.
Objects and classes typically make up the bulk of a program's RAM footprint. The ratio between the two, however, is application-specific. Figure 5-4 shows the breakdown for two theoretical applications.
In small to mid-sized client-programs, objects don't usually use the bulk of the memory consumed; the memory is used by classes. It takes several hundred classes to start a small GUI program, even one that creates only a few buttons and text fields. This is important when you evaluate how to optimize the RAM footprint of your program. Reducing the number of classes loaded is often more important than reducing the number of objects. (See Chapter 6, Controlling Class Loading, for more information.)
RAM consumption for two applications
The following section, Estimating the Size of an Object, provides a basic heuristic for quickly approximating the size of an object. The Measuring Object Size section shows a simple tool you can use to measure object size. Tactics for optimizing object size are described in the Optimizing Object Size section on page 61.
reference, primitive sizes are defined by the language specification and do not
change.1 The size of a reference isn't well defined, but it is typically 4 bytes on a
32-bit system and 8 bytes on a 64-bit system.
The heuristic for computing the approximate size of an object is
The sum of all fields + per object overhead
The amount of overhead associated with an object isn't defined, but 8 bytes is typical.
With this formula and the information in Table 5-1, it is possible to estimate an object's size. The exact amount of space used by the object depends on the virtual machine implementation, but this formula gives you something to work from. For example, the size of the object defined in Listing 5-1 can be estimated using the preceding formula.
class Cat { short lives = 9; double chanceToLandOnFeet = .998; Tail tail = null; }A simple classThe approximate size of a single instance of this class is
2 (short) + 8 (double) + 4 (reference) + 8 (overhead) = 22 bytes
Keep in mind that this is just an approximation of the object size-JVM implementations are free to structure their objects very differently. For example, a common optimization is to align all the fields on word boundaries (even if the field only requires a single byte of storage). This increases the size of many types of objects running on the JVM. The next section discusses a technique for measuring object size that makes some of these differences more apparent.
Runtime methods that provide information about the size of the JVM's object
heap.
You can also use these methods to determine the approximate size of an object by:
Although this general approach works, it has some flaws:
Runtime.freeSpace method might not be accurate down to the byte
level.
Listing 5-2 contains the source code for a utility that implements these techniques. To use the utility, you pass the name of the class you want to measure to the sizeOf method. Instances of the specified class are created using reflection and measured.
public class ObjectScale {
public static long sizeOf(Class clazz) {
long size= 0;
Object[] objects = new Object[100];
try {
Object primer = clazz.newInstance();
long startingMemoryUse = getUsedMemory();
for (int i = 0; i < objects.length; i++) {
objects[i] = clazz.newInstance();
}
long endingMemoryUse = getUsedMemory();
float approxSize = (endingMemoryUse -
startingMemory-Use) / 100f;
size = Math.round(approxSize);
} catch (Exception e) {
System.out.println("WARNING:couldn't instantiate"
+clazz);
e.printStackTrace();
}
return size;
}
private static long getUsedMemory() {
gc();
long totalMemory = Runtime.getRuntime().totalMemory();
gc();
long freeMemory = Runtime.getRuntime().freeMemory();
long usedMemory = totalMemory - freeMemory;
return usedMemory;
}
private static void gc() {
try {
System.gc();
Thread.currentThread().sleep(100);
System.runFinalization();
Thread.currentThread().sleep(100);
System.gc();
Thread.currentThread().sleep(100);
System.runFinalization();
Thread.currentThread().sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
Class clazz = Class.forName(args[0]);
System.out.println(sizeOf(clazz));
} catch (Exception e) {
e.printStackTrace();
}
System.exit(0);
}
}
Utility for measuring object size
Although this utility provides a reasonable estimate, it isn't 100 percent accurate. The size of a particular object can vary substantially under different JVM implementations.
Table 5-2 shows the sizes of several types of objects measured using the ObjectScale utility-note how the sizes vary across different versions of the
The change in sizes from one release to the next is caused by a number of things. For example, the change in Object is due to decreased overhead in the virtual machine. The decrease in the size of Hashtable is due to a reduction in the default capacity of the internal data structure. The reduction in size of an instance of JTable is a combination of several factors, including the delayed initialization of some large structures only used by a small percentage of applications.
class FinePoint {
double x;
double y;
double z;
}
High-precision point class
According to the heuristic described in the Estimating the Size of an Object section, an instance of this class consumes approximately 28 bytes. Since a 3D modeling program is likely to create a large number of these point objects, it might be tempting to sacrifice precision and switch from double to float. This would reduce the size of the object to about 16 bytes, a savings of 12 bytes per point. If a typical 3D model contains 5,000 points, you could save 60K with this optimization.
While this might seem like a pretty good improvement, 60K probably represents a very small percentage of the program's total footprint. If the program has a total RAM footprint of 12MB, the savings is only 0.5 percent of the total footprint. In this case, the loss in precision probably isn't worth the minor reduction in footprint-it would make more sense to look for more beneficial optimizations.
You should always do this type of calculation before you start trying to optimize the size of a particular object. Profiling tools can help you determine how many instances of a given class are in memory. By assessing the impact of the size of those objects on the overall footprint, you can make informed decisions about what to optimize.
javac it produces a class file. Each method in
the class is described by a set of bytecodes. This set of bytecodes is a processor
neutral version of the machine code that executes on your computer's processor.
When a class is loaded, the bytecodes for that class's methods must also be loaded
so that when a method is executed the instructions are available to the virtual
machine. These loaded bytecodes occupy space in RAM.
Because runtime implementations vary widely in how threads are handled, you might encounter situations where the impact threads have on footprint is significant. For example, some ports of the JRE create a heavyweight OS process for each running thread. In an application that uses many threads, this means that thread costs, rather than class or object costs, can become the dominant factor in the program's memory consumption.
You shouldn't avoid using threads-they're necessary in many cases, and generally don't have a large impact on footprint. However, you should be aware that the impact can be very different across runtimes. This is one of the reasons it's a good idea to measure performance characteristics under your program's different target environments.
java.awt.Button, require native peers. You should think
about whether the classes you use are likely to require large native structures and
consider the impact they could have on footprint.
It isn't always possible to tell that a class uses native structures just by looking at it, but one good indication is the presence of a finalizer method. If the class provides (or inherits) a finalize method, it is probably because the object needs to free native data structures before it is garbage collected. Finalizer methods also present other issues. See Appendix A, The Truth About Garbage Collection, for more information.
Before you start worrying about how to reduce the number of classes you load, though, you need to be able to measure what is being loaded.
-verbose flag when you launch
your program. For example:
A line is output to the console for each class that's loaded. Figure 5-5 shows a partial list of the classes loaded to run a simple spreadsheet application. One key piece of information this gives you is the number of classes required to start your program. By simply counting the number of lines produced by the verbose output, you can see precisely how many classes are loaded.java -verbose <MyMainClass>
Partial class loads for a sample program
By tracking the number of classes loaded as development progresses, you can catch potential problems early. For example, you might find that a particular change causes the number of classes loaded to increase sharply. It's much easier to track down the cause if you catch the increase when the change is introduced instead of trying to address class loading issues at the end of the development cycle.
Technically, the sizes aren't specified. The size of these structures is implied by the number of bits required to represent the required range, but a runtime could use more memory if it chose.
Copyright © 2001, Sun Microsystems,Inc.. All rights reserved.