This issue presents tips, techniques, and sample code for the
following topics:
This issue of the JDC Tech Tips is written by Stuart Halloway, a Java specialist at DevelopMentor.
These tips were developed using JavaTM 2 SDK, Standard Edition,
v 1.2.2, and are not guaranteed to work with other versions.
USING
FINALLY VERSUS FINALIZE TO GUARANTEE QUICK RESOURCE
CLEANUP
The JavaTM programming
language includes a finalize method that
allows an object to free system resources, in other words, to
clean up after itself. However using finalize doesn't
guarantee
that a class will clean up resources expediently. A better
approach for cleaning up resources involves the finally method
and an explicit close statement.
To compare the two approaches, let's look at an example.
public class Finalize1 {
private static final int testIter = 100;
static public void main(String[] args) {
int n;
//Initialize a batch of objects
//that use Finalize to clean up
for (n=testIter; --n>=0;) {
UsesFinalize uf =
new UsesFinalize();
}
//Initialize a batch of objects
//that use an explicit close
//to clean up. Note that the
//code is more complex. This
//is a necessary evil.
for (n=testIter; --n>=0;) {
UsesClose uf = null;
try {
uf = new UsesClose();
}
finally {
if (uf != null)
uf.close();
}
}
System.out.println("This demo" +
"demonstrates the danger of relying" +
"on finally to expediently close" +
"resources.");
System.out.println("Testing" +
"with 100 resources:");
//Each of the classes tracking
//the maximum number of "open" resources
//at any given time.
System.out.println("Using Finalize" +
"to close resources required "
+ UsesFinalize.maxActive +
" open resources.");
System.out.println("Using
explicit close required "
+ UsesClose.maxActive +
" open resource.");
}
static public class UsesFinalize {
static int active;
static int maxActive;
UsesFinalize() {
active++;
maxActive =
Math.max(active,
maxActive);
}
public void finalize() {
active--;
}
}
static public class UsesClose {
static int active;
static int maxActive;
public UsesClose() {
active++;
maxActive =
Math.max(active,
maxActive);
}
public void close() {
active--;
}
}
}
The Finalize1
program takes two alternative
approaches to cleaning
up resources. In the first approach it creates 100 objects,
incrementing a counter for each object. It then uses the
finalize
method to clean up each object. Each time it cleans up an
object,
it decrements the counter.
In the second approach, 100 objects are also created. However
here the finally
statement and an explicit close
method are used
to clean up and decrement the counter.
If you run Finalize1
, you'll see that the
Finalize approach does
not decrement the counter. None of the objects are closed.
However
the Finally-plus-close approach does the job it's intended to
do.
It decrements the counter for each object. It closes all the
objects.
The purpose of the Finalize method is often misunderstood by
programmers. The Javadoc comment for Finalize
states that it's
called by the garbage collector on an object when the garbage
collector determines that there are no more references to the
object. Presumably the garbage collector will, like its civil
servant namesake, visit the heap on a regular basis to clean
up
resources that are no longer in use.
As reasonable as it may seem, this interpretation of
finalization
relies on assumptions about garbage collection that are not
supported by the Java language specification. The primary
purpose
of Java garbage collection is not to run finalizers. Garbage
collection exists to prevent programmers from calling delete.
This
is a wonderful feature. For example, if you can't call delete,
then you can't accidentally call delete twice on the same
object.
However, removing delete from the language is not the same
thing
as automatically cleaning up. The name "garbage
collection"
promises too much. Confusion might have been saved by using
the
name "delete prevention" instead. In fact, the Java
garbage
collection specification imposes only minimal requirements for
the
behavior of garbage collection, which include:
- Garbage collection might not ever run. If garbage
collection
runs at all, and an object is no longer referenced, then
that
object's finalize will run.
- Across multiple objects, finalize order is not
predictable.
Either of these rules, taken alone, would be enough to make
finalize a risky way to clean up resources. So why do
developers
leap to the wrong conclusion and rely on finalize? There are
two
reasons: they are swayed by the analogy to the C++ destructor,
and
they often get away with it in the short run. Combine a simple
project with a better-than-average VM implementation, and
finalize
will appear almost as reliable as a C++ destructor. Don't be
fooled by this temporary good luck. If you rely on finalize,
your
code will not scale to larger projects, and it will not run
consistently on different virtual machines.
The correct approach to resource cleanup in Java language
programs
does not rely on finalize. Instead, you simply write explicit
close
methods for objects that wrap native resources. If you take
this
approach, you must document that the close method exists and
when
it should be called. Callers of the object must then remember
to
call close when they are finished with a resource.
This probably does not live up to your hopes for garbage
collection, since you are back to the manual labor of freeing
resources yourself. Moreover, this code still has a problem:
if an
exception is thrown from somewhere inside the method that
calls
close, the close method will never be reached. This calls for
a way
to force a code block to be executed, regardless of
exceptions.
Java's finally clause fits the bill perfectly. After any try
block
in Java, you can specify a finally block which will execute,
no
matter how the try block exits--either normally or
exceptionally.
Click to view Source code for
this tip, or right-click to download.
USING HPROF
TO TUNE PERFORMANCE
How fast is the JavaTM
platform? For many applications, the
answer is "fast enough"--if you make careful choices
in your
design and make good use of the language. But while design
documents and coding standards might encourage efficient code,
the only way to know for sure is by profiling, that is,
obtaining
method timing and other information pertinent to performance.
Fortunately, the tools that you need to do profiling are part
of
the JavaTM 2 SDK. This tip
will get you started with HPROF,
the Java Profiler Agent, and present an example where a simple
code
snippet is improved to run 100 times faster.
To see HPROF's options, enter the following at a command
prompt:
java -Xrunhprof:help
One specification you can make for HPROF is
cpu=samples
. This
setting enables you to profile by sampling. With sampling, the
JavaTM virtual machine
1 regularly pauses execution
and checks to see which
method call is active. With enough samples (and a decent
sampling rate), you can pinpoint where your code spends its
time.
For example, consider the following example:
package com.develop.demos;
import java.io.IOException;
public class TestHprof {
public static String cat = null;
public final static int loop=5000;
public static void makeString() {
cat = new String();
for (int n=0; n<loop; n++) {
addToCat("more");
}
}
public static void addToCat(String more) {
cat = cat + more;
}
public static void makeStringInline() {
cat = new String();
for (int n=0; n<loop; n++) {
cat = cat + "more";
}
}
public static void makeStringWithLocal() {
String tmp = new String();
for (int n=0; n<loop; n++) {
tmp = tmp + "more";
}
cat = tmp;
}
public static void makeStringWithBuffer() {
StringBuffer sb = new StringBuffer();
for (int n=0; n<loop; n++) {
sb.append("more");
}
cat = sb.toString();
}
public static void main(String[] args) {
long begin = System.currentTimeMillis();
if (null !=
System.getProperty("WaitForProfiler")) {
System.out.println(
"Start your profiler, then
press any key to begin...");
try {
System.in.read();
}
catch (IOException ioe) {
}
}
makeString();
makeStringInline();
makeStringWithLocal();
makeStringWithBuffer();
long end =
System.currentTimeMillis();
System.out.println("Total run time of "
+ (end - begin) + " milliseconds");
}
}
A call to makeString
simply builds up a long
string by repeated
concatenation. This is definitely a slow way to build the
string,
but how can it be made faster? One possibility is to
eliminate the
overhead of a function call by putting the
addToCat
method inline,
as in makeStringInLine
.
Another possible optimization is illustrated in
makeStringWithLocal
.
This method uses a temporary local variable; a local variable
might
be accessed more quickly than the static cat
.
Still another possibility is to use the
StringBuffer
class
instead of String
, since the intermediate results
don't have
to be stored in Strings
. This is demonstrated in
makeStringWithBuffer
.
Which of these four implementations is fastest? Let's run
HPROF
against the program. Running HPROF will help determine which
optimizations are worthwhile.
java -Xrunhprof:cpu=samples,depth=6
com.develop.demos.TestHprof
Notice the depth=6 specification. This indicates a stack trace
depth of 6. Note too that by default the profiler output goes
to java.hprof.txt
. The interesting part of this
file is the table
at the bottom which lists the percentage of time spent in each
different stack trace:
CPU SAMPLES BEGIN (total = 7131) Wed Jan 12 13:12:40 2000
rank self accum count trace method
1 20.57% 20.57% 1467 47
demos/TestHprof.makeStringInline
2 20.40% 40.98% 1455 39 demos/TestHprof.addToCat
3 20.28% 61.25% 1446 53
demos/TestHprof.makeStringWithLocal
4 11.85% 73.10% 845 55 java/lang/String.getChars
5 11.75% 84.85% 838 42 java/lang/String.getChars
6 11.72% 96.58% 836 50 java/lang/String.getChars
(remaining entries less than 1% each, omitted for brevity)
The self column is an estimate of the percentage of time
a particular stack trace is active. In this case, you want to
time four methods (makeString, makeStringInline,
makeStringWithLocal, and makeStringWithBuffer
); let's
call these
top-level methods. You cannot simply add the times for these
methods, because a sample that was not in a top-level method
might still have that top-level method somewhere in its call
stack.
So, for each entry in the table, you need to crawl back up the
stack to find the associated top-level method. The trace
column
is a pointer to the needed information, which is higher up in
the HPROF output file. For example, the 4th ranked sample is
trace 55:
TRACE 55:
java/lang/String.getChars(:Compiled method)
java/lang/StringBuffer.append(:Compiled method)
com/develop/demos/TestHprof.makeStringWithLocal \
(TestHprof.java:Compiled method)
com/develop/demos/TestHprof.main(TestHprof.java:57)
Ahah! Trace 55 leads back to makeStringWithLocal
,
so its time
should be added to the time for 3rd-ranked Trace 53, which is
a
direct invocation of makeStringWithLocal
. If
necessary, you could
continue this process, and cross-reference all the call stacks
and the CPU samples by hand. Alternately, you could use a tool
that interprets the profiling output. In this simple example,
the top six samples are enough. Three of the top-level methods
(makeString, makeStringInline
, and
makeStringWithLocal
) each have
two entries in the top six. Adding up each method's entries
leads
to a tie. Each of the three contributed over 30% of the total
running time. On the other hand, makeStringBuffer's stack
traces
are way down the list, and total less than 0.3% of the runtime
of
the application. In other words, putting the function inline
and
using a local variable didn't help, but switching from String
to
StringBuffer
caused the code to execute over 100
times faster.
Vive la HPROF!
This example only scratches the surface of Java profiling.
Profiling data can often be used not only to time methods, but
also to explain why one implementation is faster than another.
With HPROF's cpu=timings flag, you can profile by explicitly
timing methods. This gives more accurate results than
sampling,
but is slower and more intrusive. The heap options can be used
to
track memory problems.
Profiling is an important arrow in any Java developer's
quiver,
and HPROF is a free way to get started. Whatever tools you
choose
to use, make sure that you profile your code to find and prove
performance gains.
Click to view Source code for
this tip, or right-click to download.
Note
The names on the JDCSM
mailing list
are used for internal Sun MicrosystemsTM
purposes only. To remove your name from the list, see
Subscribe/Unsubscribe
below.
Feedback
Comments? Send your feedback on the JDC Tech Tips to: jdc-webmaster
Subscribe/Unsubscribe
The JDC Tech Tips are sent to you because you elected to
subscribe when you
registered as a JDC member. To unsubscribe from JDC email, go
to the following
address and enter the email address you wish to remove from
the mailing list:
http://developer.java.sun.com/unsubscribe.html
To become a JDC member and subscribe to this newsletter go to:
http://java.sun.com/jdc/
_______
1 As used on this web site, the
terms "Java
virtual machine" or "JVM" mean a virtual
machine for the Java
platform.