In this blog post, I want
to discuss optimization of java memory usage. The Sun JDK has two
simple-but-powerful tools for memory profiling -- jmap and jhat.
jmap has two important capabilities for memory profiling. It can:
• create a heap dump file for any live java process
• show a heap distribution histogram
Neither of these capabilities requires any special parameters for the Java virtual machine (JVM). Below is a heap distribution histogram produced by jmap.
jmap has two important capabilities for memory profiling. It can:
• create a heap dump file for any live java process
• show a heap distribution histogram
Neither of these capabilities requires any special parameters for the Java virtual machine (JVM). Below is a heap distribution histogram produced by jmap.
gyadav@gyadav-ubuntu:jmap -histo:live 5241
num #instances #bytes class name
----------------------------------------------
1: 123406 19940016 <constMethodKlass>
2: 198852 16388720 [C
3: 123406 15831360 <methodKlass>
4: 11531 14892200 <constantPoolKlass>
5: 11531 8938112 <instanceKlassKlass>
6: 9596 8641280 <constantPoolCacheKlass>
7: 91819 4703840 [B
8: 176758 4242192 java.lang.String
9: 88520 2832640 java.util.HashMap$Entry
10: 27069 1948968 java.lang.reflect.Field
11: 12444 1577032 java.lang.Class
12: 10453 1383296 [Ljava.util.HashMap$Entry;
13: 17116 1321848 [S
14: 29401 1318704 [Ljava.lang.Object;
15: 24378 1143144 [I
16: 1734 999016 <methodDataKlass>
17: 18074 922552 [[I
18: 15235 680328 [Ljava.lang.String;
19: 896 480256 <objArrayKlassKlass>
20: 9605 461040 org.eclipse.core.internal.registry.ReferenceMap$SoftRef
21: 9394 450912 java.util.HashMap
22: 10832 433280 java.util.LinkedHashMap$Entry
23: 17229 413496 java.util.ArrayList
----------------------------------------------
1: 123406 19940016 <constMethodKlass>
2: 198852 16388720 [C
3: 123406 15831360 <methodKlass>
4: 11531 14892200 <constantPoolKlass>
5: 11531 8938112 <instanceKlassKlass>
6: 9596 8641280 <constantPoolCacheKlass>
7: 91819 4703840 [B
8: 176758 4242192 java.lang.String
9: 88520 2832640 java.util.HashMap$Entry
10: 27069 1948968 java.lang.reflect.Field
11: 12444 1577032 java.lang.Class
12: 10453 1383296 [Ljava.util.HashMap$Entry;
13: 17116 1321848 [S
14: 29401 1318704 [Ljava.lang.Object;
15: 24378 1143144 [I
16: 1734 999016 <methodDataKlass>
17: 18074 922552 [[I
18: 15235 680328 [Ljava.lang.String;
19: 896 480256 <objArrayKlassKlass>
20: 9605 461040 org.eclipse.core.internal.registry.ReferenceMap$SoftRef
21: 9394 450912 java.util.HashMap
22: 10832 433280 java.util.LinkedHashMap$Entry
23: 17229 413496 java.util.ArrayList
Total 1317091 118936448
For each class we can see the class name, the number of instances of the class in the heap, and the number of bytes used in the heap by all instances of the class. The table is sorted by consumed heap space.
You can also try jhat. jhat can read and explore a heap dump. It has a web interface and you can click though your dumped object graph.
gyadav@gyadav-ubuntu:~$ jmap -dump:file=eclipse.heap 5241
Dumping heap to /home/gyadav/eclipse.heap ...
Heap dump file created
Reading from eclipse.heap...
Dump file created Thu May 01 12:43:48 IST 2014
Snapshot read, resolving...
Resolving 1365807 objects...
Chasing references, expect 273 dots.................................................................................................................................................................................................................................................................................
Eliminating duplicate references.................................................................................................................................................................................................................................................................................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.
Now open http://localhost:7000 and you can see a summary of all classes. You can use standard queries via links or go straight to the “Execute Object Query Language (OQL) query” link at the bottom and type your own query. Query language is quite awkward (it is based on the java script engine) and may not work well for large numbers of objects, but it is a very powerful tool.
Strings
Let's look how the JVM is storing strings. String objects are semantically immutable. Each instance has four fields (all except hash are marked final):
• Reference to char array
• Integer offset
• Integer count of character
• Integer string hash (lazily evaluated, and once evaluated never changes)
How much memory does one String instance consume?
Here and below are size calculations for the Sun JVM (32bit). This should be similar for other vendors.
Object header (8 bytes) + 3 refs (12 bytes) + int (4 bytes) = 24 bytes. But the String instance is only a header. Actual text is stored in char array (2 bytes each character + 12 bytes array header).
If you are creating a String object with a constructor, a new char array is always allocated. To copy content of a substring you can use the following construct (it looks a bit strange, but it works):
new String(a.substring(…))
String.intern() – think twice
String class has a method intern() that can guarantee the following:
a.equals(b) => a.intern() == b.intern()
There is a table inside of the JVM used to store normal forms of strings. If some text is found in a table then value from a table will be returned from intern(), else the string will be added to table. So a reference returned by intern() is always an object from JVM intern table.
String intern table keep weak references to its objects, so unused strings can be collected as garbage when no other references exist except from intern table itself. It looks like a great idea to use intern(), and eliminate all duplicated strings in an application. Many have tried … and many have regretted such decision. I cannot say this is true for every JVM vendor, but if you are using Sun’s JVM, you should never do this. Why?
JVM string intern tables are stored in PermGen -- a separate region of the heap (not included in -Xmx size) used for the JVM’s internal needs. Garbage collection in this area is expensive and size is limited (though configurable).- You would have to insert a new string into the table which has O(n) complexity, where n is table size.
String
intern tables in JVMs work perfectly for the JVM’s needs (new entries are
added only while loading new classes so insertion time is not a big issue) and
it is very compact. But it is completely unsuitable for storing millions of
application strings. It just was not designed for such a use case.
Removing of duplicates
Removing of duplicates
Maps and sets
Good old java.util.HashMap is used everywhere. Standard implementation in JDKs is to use the open hash table data structure.
References
to key and value are stored in the Entry object (which also keeps the hash for
the key for faster access). If several keys are mapped to same hash slot, they
are stored as a list of interlinked entries. The size of each entry structure:
object header + 3 references + int = 24 bytes. java.util.HashSet is using java.util.HashMap
under the hood so your overhead will be the same. Can we store map/set in more
compact form? Sure.
Sorted array map
If the keys are comparable, we can store the map or set as sorted arrays of interleaved key/value pairs (array of keys in the case of a set). Binary search can be used for fast lookups in an array, but insertion and deletion of entries will have to shift elements. Due to the high cost of updates in such a structure, it will be effective only for smaller collection sizes, and for operation patterns that are mostly read. Fortunately, this is usually what we have -- map/set of a few dozen objects that are read more often than modified.
Closed hash table
If we have a collection of a larger size, we should use a hashtable. Can we make the hashtable more compact? Again, the answer is yes. There are data structures called closed hashtables that do not require entry objects.
In
closed hashtables, references from the hashtable point directly to an object
(key). What if we want to put in a reference to a key but the hash slot is
occupied already? In such a case, we should find another slot (e.g., next one).
How do you lookup a key? Search through all adjacent slots until key or null
reference is found. As you can see from algorithm, it is very important to have
enough empty slots in your table. Density of closed hash tables should be kept
below 0.5 to avoid performance degradation.
Good old java.util.HashMap is used everywhere. Standard implementation in JDKs is to use the open hash table data structure.
Sorted array map
If the keys are comparable, we can store the map or set as sorted arrays of interleaved key/value pairs (array of keys in the case of a set). Binary search can be used for fast lookups in an array, but insertion and deletion of entries will have to shift elements. Due to the high cost of updates in such a structure, it will be effective only for smaller collection sizes, and for operation patterns that are mostly read. Fortunately, this is usually what we have -- map/set of a few dozen objects that are read more often than modified.
Closed hash table
If we have a collection of a larger size, we should use a hashtable. Can we make the hashtable more compact? Again, the answer is yes. There are data structures called closed hashtables that do not require entry objects.
Optimization is an art. There are no magical data structures capable of solving every problem. As you can see, you have to fight for every byte. Memory optimization is a complex process. Remember that you should design your data so each object can be referenced from different collections (instead of having to copy data). It is usually better to use semantically immutable objects because you can easily share them instead of copying them. And from my experience, in a well-designed application, optimization and tuning can reduce memory usage by 30-50%.
No comments:
Post a Comment