[Contents] [Prev] [Next] [Index]

CHAPTER 5 - RAM Footprint

A program's RAM footprint can be critical to its success. A program that uses too much memory can force the operating system to rely on virtual memory. Because virtual memory is many times slower than physical RAM, relying on virtual memory can result in slow performance and a poor user experience. While developing your system, be aware of your target deployment environment and consider how much RAM will be available on machines running your software.

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.

5.1 Computing RAM Footprint

Figuring out how much RAM your program is consuming can be tricky. While the Java platform provides some facilities that help, they're not as useful as they first appear. To fully evaluate your program's footprint, you need to use platform- specific mechanisms. This section discusses the APIs that relate to memory usage and a few of the platform-specific utilities that you can use to measure footprint.

5.1.1 Assessing Memory Usage

Two methods in the 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

You can figure out approximately how much space objects are consuming by subtracting the 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.

5.1.2 Measuring a Program's True Footprint

There are no methods in the standard API to help you assess the true footprint of your program. To accurately measure your program's total footprint, you need to use platform-specific utilities.

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

5.2 What Contributes to Footprint?

Given that the object heap is much smaller than the total memory consumed by most programs, where is the memory being used? Several factors contribute to the overall footprint of a program:

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

5.2.1 Objects

In many ways, the number of objects used is the part of your program's footprint over which you have the most control. Often, however, the number of objects used isn't the biggest contributing factor to your program's footprint. You need to be able to estimate the size of an object and understand the impact it has on your overall footprint before you start trying to optimize.

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.

Estimating the Size of an Object

The exact size of any particular object isn't specifically defined in any of the Java platform specifications. However, it's fairly easy to roughly approximate the size of an object based on information such as the sizes of primitives in the Java programming language, which are shown in Table 5-1. With the exception of 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.

Primitive Sizes

Data Type


Size


byte


1 byte


char


2 bytes


short


2 bytes


int


4 bytes


float


4 bytes


long


8 bytes


double


8 bytes


"reference"


4 bytes (typically)

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 class
The 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.

Measuring Object Size

While the Java platform doesn't provide a way to measure the size of an object, it is possible to measure the approximate size indirectly. Section 5.1.1 discusses the 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:

  1. Requesting garbage collection to get the heap into a known state.
  2. Measuring the free heap space.
  3. Creating an instance of the class you want to measure and keeping a reference to it.
  4. Requesting garbage collection again.
  5. Measuring the free heap space.
  6. Subtracting the second measurement from the first.

Although this general approach works, it has some flaws:

Fortunately, you can get around most of these by:

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

Common Object Sizes

Class


Java 2 SDK 1.2.2


Java 2 SDK 1.3


java.lang.Object


16 bytes


8 bytes


java.util.Hashtable


472 bytes


96 bytes


java.awt.Point


24 bytes


16 bytes


javax.swing.JTextField


3,082 bytes


3,109 bytes


javax.swing.JTable


26,836 bytes


4,086 bytes

Java 2 SDK. In this case, these differences are due to performance tuning in 1.3, but object sizes can vary from release to release for a number of reasons.

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.

Optimizing Object Size

Developers sometimes try to reduce a program's RAM footprint by reducing the size of the objects that are instantiated. This can help, but it might not result in the savings you expect. For example, consider the class in Listing 5-3, a high- precision point class that might be used in a 3D modeling program to represent an exact location in space.

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.

5.2.2 Classes

Several entities associated with loaded classes contribute to RAM footprint:

Bytecodes

When a source file is compiled with 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.

Reflective Data Structures

When a class is loaded by a virtual machine, two things happen:

  1. The class file is loaded into RAM.
  2. The contents of the file are parsed and reflective data structures are created inside the virtual machine to represent the class's methods and fields.
Typically, the JVM creates structures to represent each method and field within the class. The size of these structures is totally dependent on the JVM implementation. However, inspection of a variety of JVM implementations shows that the size of the reflective structures can be substantial.

Constant Pool Entries

The constants defined by a class also contribute to the program's footprint. For example, any string literal that appears in your code is stored in a special table. The names of all the classes, methods, and fields are also stored.

JIT Compiled Code

If the JIT stores compiled code for a large number of methods, it can lead to a major increase in footprint. Early JIT code generators converted each method into native code the first time it was called. While this drastically improved performance in some situations, it led to major problems in others. In particular, this approach leads to a major increase in footprint. It turns out that many program methods are executed very few times, while others are executed thousands of times. Converting all of the bytecodes resulted in native code being stored for a large number of methods that were never executed again. Newer JIT compilers use better rules to determine when to compile methods. Seldom-used methods are never compiled, which helps reduce the impact to the program's footprint.

5.2.3 Threads

The impact that threads have on RAM footprint isn't a problem for most programs, but running threads do need space to store their stack state, and the system- specific data structures do consume memory.

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.

5.2.4 Native Data Structures

Some classes in the Java libraries create native, OS-specific resources. In particular , AWT uses large numbers of native data structures. For example, most AWT components, such as 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.

5.2.5 Native Libraries

Runtimes always depend on some native code, which often resides in shared libraries. For example, a particular runtime implementation might have shared libraries for the virtual machine, AWT, and networking. These libraries often have dependencies on standard libraries such as the C runtime library. Although these costs typically are out of your control, you should be aware that they exist so you have a complete picture of what's going on.

5.3 Class Loading

The key problem with classes, from a footprint perspective, is that you often load more than you need. Class loading is like a web-loading a single class often causes several others to be loaded, which in turn load more classes, and so on. The key is to try to load only what you use, and defer loading rarely-used features until they are needed. For more information about how to do this, see Chapter 6, Controlling Class Loading.

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.

5.3.1 Measuring Class Loads

To list each class as it's loaded, you can use the -verbose flag when you launch your program. For example:

java -verbose <MyMainClass>
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.
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.

Key Points

  • RAM footprint can be the key, limiting factor on performance, especially in environments with limited memory.
  • You need to understand where memory is being consumed before you can optimize effectively.
  • You can use the Runtime methods to measure object heap, but platform-specific tools are needed to measure true footprint.
  • Objects, loaded classes, threads, and native data structures can all contribute to RAM footprint.



[Contents] [Prev] [Next] [Index]

1

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.