|
The Java Specialists' Newsletter
Issue 038a 2001-12-28
Category:
Performance
Java version: Counting Objects Clandestinelyby Dr. Heinz M. Kabutz
Welcome to the 38th edition of "The Java(tm) Specialists'
Newsletter", sent to 2226 Java experts in 70 countries, with
Egypt as latest addition. I'm missing a few African countries,
ok, it's worse than missing, I only have South Africa,
Ghana, Nigeria, Zimbabwe and Egypt, so if you are from another
African country, please send me an email.
Talking about Africa, I didn't include Mauritius in my list of
African countries, although politically I probably should have.
If you don't know what Mauritius is, it is a small island in the
Indian Ocean inbetween South Africa and India. Mauritius was
colonised by the French (who only drank and fought and never did
much for the country except introduce bureaucracy ;-) and later
properly colonised by the British. The result is that English is
the official language, whereas French (and derivatives thereof)
is the spoken language.
The people in Mauritius are amazing. Extremely friendly, and not
only to sell you things. The beaches are fantastic, the water
warm, lots of things to see and do, such as waterskiing, etc.
Oh, and the hotels! Such service you've never seen anywhere.
Thanks for reading this newsletter on our website. We also have a mailing list. That is where the real action takes place (webinars, free reports, etc.). Maybe subscribe today?
Counting Objects Clandestinely
A few months ago I received a frantic phone call from a friend
who had gotten himself seriously stuck. His program was running
out of memory and he didn't know where the objects were being
created. He ran his program in a commercial memory analyser,
which only had the effect that the memory analyser crashed. (If
you want to have a laugh, try running any big Java application
(e.g. Together/J or JBuilder 4) in a memory analyser). If
it runs, you'll see that the success of your product does not
depend on good programming but on good marketing.
The trick I'm going to show you in this newsletter essentially
saved my friend's project, and it is sooo easy and cheap, you
won't believe it. Please note however, that this is only an
initial implementation and that you should extend the
functionality if you want to use it a lot. I have myself used
this idea to effectively find memory problems in my own
applications.
Imagine for a second that there was some way of knowing when an
Object was created. Well, if you really want that functionality,
why don't you add it? Yes, why don't you change java/lang/Object?
Sure, it's intrusive, but very effective. The test code that
you add to your code to find the memory leaks would be removed
anyway (or commented out?) before the final build, so why not?
It took me a while to get all this right, which is why you didn't
see a newsletter for a while, but here is the code, which I tried
on Windows JDK 1.3.1. On other VMs it might reformat your
harddrive, sell your credit card details or promote you to sales
manager.
package java.lang;
import java.util.*;
public class Object {
// ... stick this at the bottom of Object.java:
private static HashMap countMap = new HashMap();
private static boolean counting = true;
private static int totalObjects = 0;
private class Counter {
int value = 1;
}
public Object() {
synchronized(Object.class) {
if (counting) {
counting = false;
totalObjects++;
Counter c = (Counter)countMap.get(getClass());
if (c == null) {
countMap.put(getClass(), new Counter());
} else {
c.value++;
}
counting = true;
}
}
}
public synchronized static void ___resetObjectCreationStats() {
counting = false;
totalObjects = 0;
countMap.clear();
counting = true;
}
public static void ___printObjectCreationStats() {
___printObjectCreationStats(System.out);
}
public synchronized static void ___printObjectCreationStats(java.io.PrintStream out) {
counting = false;
out.println("Total number of objects: " + totalObjects);
TreeSet sorted = new TreeSet(new Comparator() {
public int compare(Object o1, Object o2) {
int value1 = ((Counter)((Map.Entry)o1).getValue()).value;
int value2 = ((Counter)((Map.Entry)o2).getValue()).value;
int result = value2 - value1;
if (result == 0) {
String classname1 = ((Class)((Map.Entry)o1).getKey()).getName();
String classname2 = ((Class)((Map.Entry)o2).getKey()).getName();
return classname1.compareTo(classname2);
}
return result;
}
});
sorted.addAll(countMap.entrySet());
Iterator it = sorted.iterator();
while(it.hasNext()) {
Map.Entry entry = (Map.Entry)it.next();
out.println("\t" + ((Counter)entry.getValue()).value
+ "\t" + ((Class)entry.getKey()).getName());
}
out.println();
counting = true;
}
}
You can now know exactly how many objects were created since you
last reset the counters. [Actually, not really exactly. This
approach will not tell you when arrays are created, but you will
find out about any non-array objects that were made.] The way
you would use this in your code is as follows:
import java.util.*;
public class Test {
public static void testStringConcatenation() {
___resetObjectCreationStats();
int localHost = 0x7F000001;
String s =
((localHost>>24)&0xff) + "."
+ ((localHost>>16)&0xff) + "."
+ ((localHost>>8)&0xff) + "."
+ (localHost&0xff);
System.out.println("Objects created to make an IP address String");
System.out.println("--------------------------------------------");
___printObjectCreationStats();
}
public static void testLinkedListCreation() {
___resetObjectCreationStats();
LinkedList daysOfWeek = new LinkedList();
System.out.println("Objects created to make a LinkedList");
System.out.println("------------------------------------");
___printObjectCreationStats();
daysOfWeek.add("Sunday");
daysOfWeek.add("Monday");
daysOfWeek.add("Tuesday");
daysOfWeek.add("Wednesday");
daysOfWeek.add("Thursday");
daysOfWeek.add("Friday");
daysOfWeek.add("Saturday");
System.out.println("and after adding 7 elements to the LinkedList");
System.out.println("---------------------------------------------");
___printObjectCreationStats();
}
public static void testHashMapCreation() {
___resetObjectCreationStats();
HashMap monthsVsLength = new HashMap();
System.out.println("Objects created to make a HashMap");
System.out.println("---------------------------------");
___printObjectCreationStats();
monthsVsLength.put("January", new Integer(31));
monthsVsLength.put("February", new Integer(28));
monthsVsLength.put("March", new Integer(31));
monthsVsLength.put("April", new Integer(30));
monthsVsLength.put("May", new Integer(31));
monthsVsLength.put("June", new Integer(30));
monthsVsLength.put("July", new Integer(31));
monthsVsLength.put("August", new Integer(31));
monthsVsLength.put("September", new Integer(30));
monthsVsLength.put("October", new Integer(31));
monthsVsLength.put("November", new Integer(30));
monthsVsLength.put("December", new Integer(31));
System.out.println("and after adding 12 elements to the HashMap");
System.out.println("-------------------------------------------");
___printObjectCreationStats();
}
public static void main(String args[]) {
System.out.println("Objects created to get the VM started");
System.out.println("-------------------------------------");
___printObjectCreationStats();
testStringConcatenation();
testLinkedListCreation();
testHashMapCreation();
}
}
When you compile the test class you should also at the same time
point it to your Object.class otherwise you'll get a compiler
moan. You have to run Test by including the new Object.class
file in your bootclasspath, for example
java -Xbootclasspath/p:. Test, which will prepend
the current directory to the boot classpath. The output is
rather long and would be different for each VM version.
Objects created to get the VM started
-------------------------------------
Total number of objects: 784
514 java.util.Hashtable$Entry
104 java.lang.String
25 java.lang.StringBuffer
22 java.util.Locale
13 java.io.File
11 sun.security.action.GetPropertyAction
*snip - run it yourself to see the rest*
Objects created to make an IP address String
--------------------------------------------
Total number of objects: 6
5 java.lang.String
1 java.lang.StringBuffer
Objects created to make a LinkedList
------------------------------------
Total number of objects: 2
1 java.util.LinkedList
1 java.util.LinkedList$Entry
and after adding 7 elements to the LinkedList
---------------------------------------------
Total number of objects: 9
8 java.util.LinkedList$Entry
1 java.util.LinkedList
Objects created to make a HashMap
---------------------------------
Total number of objects: 1
1 java.util.HashMap
and after adding 12 elements to the HashMap
-------------------------------------------
Total number of objects: 25
12 java.lang.Integer
12 java.util.HashMap$Entry
1 java.util.HashMap
What are the disadvantages of this approach, and what are the
alternatives? The disadvantages that I can think of immediately
are that you don't see the arrays like this and that you cannot
see where objects were created from. I don't know how to solve
the problems with the arrays, but you could easily change the
code to also remember the stack trace of each object that gets
constructed. You could then analyse the stack traces and find
out exactly where the objects are constructed from. The
alternatives I can think of is to use
java -Xrunhprof (to perform JVMPI heap, cpu, or
monitor profiling) or to use an expensive commercial tool. The
disadvantage of some of the commercial tools is that if your
program quickly eats memory the memory tools tend to fall on
their back and play dead.
Have you ever wondered why Swing is so slow? Have a look at this
test code:
import javax.swing.*;
import java.awt.BorderLayout;
import java.awt.event.*;
public class TestGUI extends JFrame {
public TestGUI() {
super("TestGUI");
getContentPane().add(new JTextArea(), BorderLayout.CENTER);
JButton press = new JButton("Press Me!");
press.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
___printObjectCreationStats();
___resetObjectCreationStats();
}
});
getContentPane().add(press, BorderLayout.SOUTH);
setSize(500, 600);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
show();
}
public static void main(String[] args) {
new TestGUI();
}
}
I ran this code and moved my mouse in circles for 10 seconds in
the JTextArea. Here are the first couple of entries that were
shown when I pressed the "Press Me!" button:
Total number of objects: 15622
2867 java.lang.String
1545 java.lang.ref.Finalizer
1384 java.awt.event.MouseEvent
1326 java.awt.Point
1200 java.lang.StringBuffer
1047 java.util.Hashtable$Entry
769 java.util.WeakHashMap$WeakKey
750 java.awt.EventQueueItem
706 sun.awt.EventQueueItem
648 java.awt.Rectangle
316 sun.java2d.loops.GraphicsPrimitiveProxy
266 sun.awt.MostRecentKeyValue
253 java.awt.geom.AffineTransform
134 java.util.HashMap$Entry
Why so many Strings? I can only assume that that has to do with
the pluggable look & feel. The reason I say that is because if
I press the button again (after moving the mouse in circles a
few times), I get the following output:
Total number of objects: 5257
891 java.lang.ref.Finalizer
831 java.awt.event.MouseEvent
796 java.awt.Point
456 java.util.WeakHashMap$WeakKey
436 java.awt.EventQueueItem
It gets quite interesting when we look at different VMs, just
remember the warning at the beginning of this newsletter ;-)
That's all for this week and this year. I wish you all the best
for 2002, may you find much opportunity to tell others about this
newsletter ;-) [and make me prosper in the process ;]
Kind regards, and now I must get back to spending a few sunny
days with my wife & two kiddies.
Heinz
Performance Articles
Related Java Course
Discuss at The Java Specialist Club
|