Take a look at Beginners.co.uk Take a look at Beginners.co.uk

Java: Performance Tuning and Memory Management Part 4 - Memory Utilization
Submitted By: Wrox Books
Published Date: 9th March 2001 
Viewed: 34,269 times
Full computer related training courses are available in our membership area. To view over 400 Beginners.co.uk courses, click here. An annual training membership costs only £99 OR $149.
Although Java's performance problems have often been exaggerated, it is true that Java programs sometimes use a large amount of memory.
 
As mentioned earlier, the initial heap size varies from one JVM implementation to the next, as does the maximum heap size. However, you can set one or both values using the -Xms and -Xmx options when starting the JVM, where the -Xms option allows you to specify the initial heap size and -Xmx the maximum heap size. For example, you can limit the heap's size to 5 megabytes by executing a command similar to the following:
java -Xmx5mb HeapTest

Since the heap size can only grow to 5MB, executing that command will cause the HeapTest application to terminate after only a few iterations as shown below:

Total memory = 1048568, free memory = 754944
Total memory = 2097144, free memory = 1139800
Total memory = 3403768, free memory = 1306672
Total memory = 4407288, free memory = 1003504
Exception in thread "main" java.lang.OutOfMemoryError
        at HeapTest.main(HeapTest.java, Compiled Code)

Note that during the last successful iteration of the loop that the total memory was approximately 4.4 megabytes. Since the heap size could not be increased to satisfy the next request for space, the application again terminated with an OutOfMemoryError.

If the documentation associated with the JVM you're using does not identify its default initial and maximum heap sizes, you may be able to learn what they are by causing it to generate an error message. For example, you might use the following option to indicate that the maximum heap size should be zero megabytes:

java -Xmx0m HeapTest

On some JVM implementations (though not Sun's JDK 1.3 "HotSpot" JVM), the error message that is produced will identify the default initial and maximum heap sizes for that JVM as shown below:

Incompatible initial and maximum heap sizes specified:

    initial size: 1048576 bytes, maximum size: 0 bytes

The initial heap size must be less than or equal to the maximum heap size.
The default initial and maximum heap sizes are 1048576 and 67108864 bytes.
Could not create the Java virtual machine.

In this case, the default initial size of the heap is 1 megabyte, while the default maximum size is 64 megabytes (64 * 1024 * 1024 = 67108864).

We'll now make a minor change to the HeapTest application and see what effect it has upon the program's execution. Specifically, we'll comment out the portions of the code that cause a reference to each byte array to be stored in a Vector:

public class HeapTest {

  public static void main(String[] args) {
    Runtime rt = Runtime.getRuntime();
    // java.util.Vector v = new java.util.Vector();
    while (true) {
      long size = rt.freeMemory();
      System.out.println("Total memory = " + rt.totalMemory() 
                         + ", free memory = " + size);
      byte[] buffer = new byte[(int)size];
      // v.addElement(buffer);
    }
  }

}

If you execute this modified version of the application with the JDK 1.2.2 virtual machine, the heap size will again gradually rise to its maximum length, but the application runs indefinitely and never throws an OutOfMemoryError. In addition, it takes a much larger number of iterations for the heap to grow to its maximum size:

Total memory = 1048568, free memory = 756080
Total memory = 2097144, free memory = 1138976
Total memory = 3403768, free memory = 1306672
Total memory = 4452344, free memory = 1804656
Total memory = 5500920, free memory = 2187552
...
Total memory = 52559864, free memory = 17758576
Total memory = 53452792, free memory = 18034272
Total memory = 53899256, free memory = 17903072
Total memory = 53899256, free memory = 17903072

The explanation for this is very simple, and can easily be illustrated by specifying the -verbosegc option when executing the application. That option produces a large number of messages from the garbage collector, and the following listing provides a sample of the messages produced. Notice that the messages are sent to standard output, so they are intermixed with the messages generated by the application itself that identify the amounts of total and free memory. Those produced by the garbage collector are prefixed with <GC:

<GC: need to expand mark bits to cover 16384 bytes>
Total memory = 1048568, free memory = 756080
<GC: managing allocation failure: need 756088 bytes, type=1, action=1>
<GC: 0 milliseconds since last GC>
<GC: freed 1195 objects, 82792 bytes in 7 ms, 78% free (658328/838856)>
  <GC: init&scan: 1 ms, scan handles: 3 ms, sweep: 1 ms, compact: 2 ms>
  <GC: 0 register-marked objects, 6 stack-marked objects>
  <GC: 1 register-marked handles, 39 stack-marked handles>
  <GC: refs: soft 0 (age >= 32), weak 0, final 2, phantom 0>
  <GC: compactHeap: blocks_moved=1106>
  <GC: 0 explicitly pinned objects, 2 conservatively pinned objects>
  <GC: last free block at 0x0196BD54 of length 578216, is at end>
<GC: managing allocation failure: need 756088 bytes, type=1, action=2>
<GC: 10 milliseconds since last GC>
<GC: expanded object space by 1048576 to 1887432 bytes, 90% free>
<GC: need to expand mark bits to cover 16384 bytes>
Total memory = 2097144, free memory = 1138976
<GC: managing allocation failure: need 1138984 bytes, type=1, action=1>
<GC: 10 milliseconds since last GC>
<GC: freed 17 objects, 1064 bytes in 6 ms, 50% free (950848/1887432)>
  <GC: init&scan: 1 ms, scan handles: 3 ms, sweep: 1 ms, compact: 1 ms>
  <GC: 0 register-marked objects, 6 stack-marked objects>
  <GC: 1 register-marked handles, 40 stack-marked handles>
  <GC: refs: soft 0 (age >= 6), weak 0, final 0, phantom 0>
  <GC: compactHeap: blocks_moved=14>
  <GC: 0 explicitly pinned objects, 2 conservatively pinned objects>
  <GC: last free block at 0x01A246CC of length 870704, is at end>
<GC: managing allocation failure: need 1138984 bytes, type=1, action=2>
<GC: 0 milliseconds since last GC>
<GC: expanded object space by 1306624 to 3194056 bytes, 70% free>
<GC: need to expand mark bits to cover 20416 bytes>
Total memory = 3403768, free memory = 1306672

The second message in this listing is generated by the application code and it indicates that the heap size is 1M, while approximately 750K bytes are available for use. However, when the application attempts to create an array of that size, the garbage collector generates a message indicating that it's handling an "allocation failure":

<GC: managing allocation failure: need 756088 bytes, type=1, action=1>

Since there is memory overhead associated with each object (and array) allocation, allocating a 1000 element array of byte values will cause the JVM to allocate slightly more than 1000 bytes. Therefore, when the HeapTest application attempts to create a byte array with as many elements as there are available bytes of memory, an allocation failure occurs. In other words, the allocation request initially fails because there is not enough memory available in the heap.

The garbage collector's first response to the allocation failure is to reclaim all space that's available in the heap, which it does very quickly (7 milliseconds) as indicated by the following message:

<GC: freed 1195 objects, 82792 bytes in 7 ms, 78% free (658328/838856)>

Once it has completed its sweep, the garbage collector has managed to reclaim approximately 82K of space that was occupied by 1195 objects, which results in roughly 650K of free space being available on the heap. However, that is not sufficient to satisfy the request that was made for roughly 750K, so the garbage collector increases the heap size by 1 megabyte as indicated by the message shown below:

<GC: expanded object space by 1048576 to 1887432 bytes, 90% free>

At that point, sufficient space has been made available on the heap for the allocation request to be satisfied, and the HeapTest application is allowed to continue execution. This is why the modified code never throws an OutOfMemoryError: the garbage collector reclaims unreferenced objects and/or increases the heap size to prevent that from happening.

With the HotSpot Client JVM supplied with JDK 1.3, the results of our program are somewhat different; the total memory rapidly reaches a constant value, while free memory alternates between two values:

Total memory = 2031616, free memory = 1453992
Total memory = 3485696, free memory = 1681792
Total memory = 3342336, free memory = 1310632
Total memory = 2818048, free memory = 1157504
Total memory = 3190784, free memory = 1684024
Total memory = 3719168, free memory = 1685888
Total memory = 3719168, free memory = 1684024
Total memory = 3719168, free memory = 1685888
Total memory = 3719168, free memory = 1684024

Running the program with the -verbosegc option also produces output somewhat different to that from the JDK 1.2.2 version:

[GC 509K->285K(1984K), 0.0111699 secs]
Total memory = 2031616, free memory = 1453992
[GC 565K->349K(1984K), 0.0046168 secs]
[Full GC 349K->341K(1984K), 0.0305008 secs]
Total memory = 3485696, free memory = 1681792
[GC 1762K->1761K(3404K), 0.0019486 secs]
[Full GC 1761K->341K(3264K), 0.0304134 secs]
Total memory = 3342336, free memory = 1310632
[GC 1984K->1984K(3264K), 0.0021654 secs]
[Full GC 1984K->341K(2752K), 0.0301376 secs]
Total memory = 2818048, free memory = 1157504
[GC 1622K->1621K(2752K), 0.0019380 secs]
[Full GC 1621K->341K(1984K), 0.0382456 secs]
Total memory = 3190784, free memory = 1684024
[GC 1472K->1471K(3116K), 0.0019315 secs]
[Full GC 1471K->341K(1984K), 0.0298365 secs]
Total memory = 3719168, free memory = 1685888
[GC 1986K->1985K(3632K), 0.0020628 secs]
[Full GC 1985K->341K(1984K), 0.0302888 secs]
Total memory = 3719168, free memory = 1684024
[GC 1988K->1987K(3632K), 0.0019581 secs]
[Full GC 1987K->341K(1984K), 0.0304005 secs]

The HotSpot virtual machine is clearly being much more efficient at garbage collection than the earlier version. To better understand how to control memory utilization in your Java programs, it's helpful to closely examine how garbage collection works.

Understanding Garbage Collection

Garbage collection is used to prevent you from having to assume responsibility for explicitly allocating and releasing storage. When you create an instance of an object or an array, the necessary storage space is obtained automatically from the heap. Once the object or array is no longer reachable from any live thread, the storage associated with the item is eligible to be reclaimed by the garbage collector.

For an item to be "reachable" simply means that it's possible to access that item directly or directly from a given thread through some reference or series of references. For example, suppose you run the following application:

public class GarbageTest {

  protected Object objectRef;

  public static void main(String[] args) {
    GarbageTest gt = new GarbageTest();
  } 

  public GarbageTest() {
    objectRef = new TestClass(this);
  }

  class TestClass {

    Object testref;

    public TestClass(Object objref) {
      testref = objref;
    }

  }

}

The static main() method is called by a thread created by the Java Virtual Machine; its first line creates an instance of the GarbageTest class and creates a reference to that object in a local variable named myRef. The GarbageTest object's constructor in turn creates an instance of inner class TestClass, storing a reference to that instance in the objectRef field, and the TestClass object maintains a reference back to the GarbageTest instance. The figure below illustrates the chain of reachable objects and the references they maintain to one another:

Once the thread exits the main() method, the GarbageTest object that was referenced by myRef becomes unreachable, because myRef was a local variable defined inside the main() method. In addition, since the TestClass instance was only reachable through the GarbageTest object, it too becomes unreachable when execution of the main() method completes:

The factors that determine when the garbage collector runs are implementation-specific and the garbage collector's behavior can even vary across different releases of the same vendor's JVM implementations. For example, JavaSoft's garbage collector behavior changed significantly in Java 1.3 with the introduction of the HotSpot Client VM. Since garbage collection behavior is only loosely defined and it varies across JVM implementations, you should not make any assumptions about when unreachable objects will be reclaimed. However, as we saw earlier, the garbage collector will usually run when a space allocation cannot be satisfied by the available heap space.

Not only shouldn't any assumptions be made when an object will be reclaimed, but you also can't even assume that it will ever be collected. It's entirely possible that the JVM will terminate before the garbage collector ever gets around to reclaiming an object. In fact, the behavior of some garbage collectors makes it likely that unreachable objects will not be collected unless your application uses most or all of the available memory in the heap space. However, you can explicitly request that the garbage collector run by calling the static gc() method defined in System as shown below:

System.gc();

The API documentation states that once this method returns, the garbage collector will have "made a best effort to reclaim space from all discarded objects". However, there is in fact no guarantee that garbage will have been collected.

Continued...



Previous Page

Next Page



RELATED PAGES