The Java Virtual Machine (JVM) uses an internal representation of its classes containing per-class metadata such as class hierarchy information, method data and information (such as bytecodes, stack and variable sizes), the runtime constant pool and resolved symbolic reference and Vtables.
In the past (when custom class loaders weren’t that common), the classes were mostly “static” and were infrequently unloaded or collected, and hence were labeled “permanent”. Also, since the classes are a part of the JVM implementation and not created by the application they are considered “non-heap” memory.
For HotSpot JVM prior to JDK8, these “permanent” representations would live in an area called the “permanent generation”. This permanent generation was contiguous with the Java heap and was limited to -XX:MaxPermSize that had to be set on the command line before starting the JVM or would default to 64M (85M for 64bit scaled pointers). The collection of the permanent generation would be tied to the collection of the old generation, so whenever either gets full, both the permanent generation and the old generation would be collected. One of the obvious problems that you may be able to call out right away is the dependency on the ‑XX:MaxPermSize. If the classes metadata size is beyond the bounds of ‑XX:MaxPermSize, your application will run out of memory and you will encounter an OOM (Out of Memory) error.
Trivia: Before JDK7, for HotSpot JVM, interned-strings were also held in the permanent generation aka PermGen, causing a vast number of performance issues and OOM errors. For more information on the removal of interned strings from the PermGen please see here.
Bye, Bye PermGen, Hello Metaspace!
With the advent of JDK8, we no longer have the PermGen. No, the metadata information is not gone, just that the space where it was held is no longer contiguous to the Java heap. The metadata has now moved to native memory to an area known as the “Metaspace”.
The move to Metaspace was necessary since the PermGen was really hard to tune. There was a possibility that the metadata could move with every full garbage collection. Also, it was difficult to size the PermGen since the size depended on a lot of factors such as the total number of classes, the size of the constant pools, size of methods, etc.
Additionally, each garbage collector in HotSpot needed specialized code for dealing with metadata in the PermGen. Detaching metadata from PermGen not only allows the seamless management of Metaspace, but also allows for improvements such as simplification of full garbage collections and future concurrent de-allocation of class metadata.
What Does The Removal Of Permanent Space Mean To The End Users?
Since the class metadata is allocated out of native memory, the max available space is the total available system memory. Thus, you will no longer encounter OOM errors and could end up spilling into the swap space. The end user can either choose to cap the max available native space for class metadata, or the user can let the JVM grow the native memory in-order to accommodate the class metadata.
Note: The removal of PermGen doesn’t mean that your class loader leak issues are gone. So, yes, you will still have to monitor your consumption and plan accordingly, since a leak would end up consuming your entire native memory causing swapping that would only get worse.
Moving On To Metaspace And Its Allocation:
The Metaspace VM now employs memory management techniques to manage Metaspace. Thus moving the work from the different garbage collectors to just the one Metaspace VM that performs all of its work in C++ in the Metaspace. A theme behind the Metaspace is simply that the lifetime of classes and their metadata matches the lifetime of the class loaders’. That is, as long as the classloader is alive, the metadata remains alive in the Metaspace and can’t be freed.
We have been using the term “Metaspace” loosely. More formally, per classloader storage area is called “a metaspace”. And these metaspaces are collectively called “the Metaspace”. The reclamation of metaspace per classloader can happen only after its classloader is no longer alive and is reported dead by the garbage collector. There is no relocation or compaction in these metaspaces. But metadata is scanned for Java references.
The Metaspace VM manages the Metaspace allocation by employing a chunking allocator. The chunking size depends on the type of the classloader. There is a global free list of chunks. Whenever a classloader needs a chunk, it gets it out of this global list and maintains its own chunk list. When any classloader dies, its chunks are freed, and returned back to the global free list. The chunks are further divided into blocks and each block holds a unit of metadata. The allocation of blocks from chunks is linear (pointer bump). The chunks are allocated out of memory mapped (mmapped) spaces. There is a linked list of such global virtual mmapped spaces and whenever any virtual space is emptied, its returned back to the operating system.
The figure above shows Metaspace allocation with metachunks in mmapped virtual spaces. Classloaders 1 and 3 depict reflection or anonymous classloaders and they employ “specialized” chunk size. Classloaders 2 and 4 can employ small or medium chunk size based on the number of item in those loaders.
Tuning And Tools For Metaspace:
As previously mentioned, a Metaspace VM will manage the growth of the Metaspace. But there may be scenarios where you may want to limit the growth by explicitly setting the -XX:MaxMetaspaceSize on the command line. By default, the –XX:MaxMetaspaceSize doesn’t have a limit, so technically the Metaspace size could grow into swap space and you would start getting native allocation failures.
For a 64-bit server class JVM, the default/initial value of –XX:MetaspaceSize is 21MB. This is the initial high watermark. Once this watermark is hit, a full garbage collection is triggered to unload classes (when their classloaders are no longer alive) and the high watermark is reset. The new value of the high watermark depends on the amount of freed Metaspace. If insufficient space is freed up, the high watermark goes up; if too much space is freed, the high watermark goes down. This will be repeated multiple times if the initial watermark is too low. And you will be able to visualize the repeated full garbage collections in your garbage collector logs. In such a scenario, you are advised to set the –XX:MetaspaceSize to a higher value on the command line in order to avoid the initial garbage collections.
After subsequent collections, the Metaspace VM will automatically adjust your high watermark, so as to push the next Metaspace garbage collection further out.
There are also two options: ‑XX:MinMetaspaceFreeRatio and ‑XX:MaxMetaspaceFreeRatio. These are analogous to GC FreeRatio parameters and they can be set on the command line as well.
A few tools have been modified to help get more information regarding the Metaspace and they are listed here:
- jmap –clstats <PID>: prints class loader statistics. (This now supersedes –permstat that used to print class loader stats for JVMs prior to JDK8). An example output while running DaCapo’s Avrora benchmark:
$ jmap -clstats <PID> Attaching to process ID 6476, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.5-b02 finding class loader instances ..done. computing per loader stat ..done. please wait.. computing liveness.liveness analysis may be inaccurate ... class_loader classes bytes parent_loader alive? type <bootstrap> 655 1222734 null live <internal> 0x000000074004a6c0 0 0 0x000000074004a708 dead java/util/ResourceBundle$RBClassLoader@0x00000007c0053e20 0x000000074004a760 0 0 null dead sun/misc/Launcher$ExtClassLoader@0x00000007c002d248 0x00000007401189c8 1 1471 0x00000007400752f8 dead sun/reflect/DelegatingClassLoader@0x00000007c0009870 0x000000074004a708 116 316053 0x000000074004a760 dead sun/misc/Launcher$AppClassLoader@0x00000007c0038190 0x00000007400752f8 538 773854 0x000000074004a708 dead org/dacapo/harness/DacapoClassLoader@0x00000007c00638b0 total = 6 1310 2314112 N/A alive=1, dead=5 N/A
- jstat –gc <LVMID>: now prints Metaspace information as shown in the following example:
- jcmd <PID> GC.class_stats: This is a new diagnostic command that enables the end user to connect to a live JVM and dump a detailed histogram of Java class metadata.
Note: With JDK8 build 13, you have to start Java with ‑XX:+UnlockDiagnosticVMOptions.
$ jcmd <PID> help GC.class_stats 9522: GC.class_stats Provide statistics about Java class meta data. Requires -XX:+UnlockDiagnosticVMOptions. Impact: High: Depends on Java heap size and content. Syntax : GC.class_stats [options] [<columns>] Arguments: columns : [optional] Comma-separated list of all the columns to show. If not specified, the following columns are shown: InstBytes,KlassBytes,CpAll,annotations,MethodCount,Bytecodes,MethodAll,ROAll,RWAll,Total (STRING, no default value) Options: (options must be specified using the <key> or <key>=<value> syntax) -all : [optional] Show all columns (BOOLEAN, false) -csv : [optional] Print in CSV (comma-separated values) format for spreadsheets (BOOLEAN, false) -help : [optional] Show meaning of all the columns (BOOLEAN, false)
Note: For more information on the columns, please see here.
An example output:
$ jcmd <PID> GC.class_stats 7140: Index Super InstBytes KlassBytes annotations CpAll MethodCount Bytecodes MethodAll ROAll RWAll Total ClassName 1 -1 426416 480 0 0 0 0 0 24 576 600 [C 2 -1 290136 480 0 0 0 0 0 40 576 616 [Lavrora.arch.legacy.LegacyInstr; 3 -1 269840 480 0 0 0 0 0 24 576 600 [B 4 43 137856 648 0 19248 129 4886 25288 16368 30568 46936 java.lang.Class 5 43 136968 624 0 8760 94 4570 33616 12072 32000 44072 java.lang.String 6 43 75872 560 0 1296 7 149 1400 880 2680 3560 java.util.HashMap$Node 7 836 57408 608 0 720 3 69 1480 528 2488 3016 avrora.sim.util.MulticastFSMProbe 8 43 55488 504 0 680 1 31 440 280 1536 1816 avrora.sim.FiniteStateMachine$State 9 -1 53712 480 0 0 0 0 0 24 576 600 [Ljava.lang.Object; 10 -1 49424 480 0 0 0 0 0 24 576 600 [I 11 -1 49248 480 0 0 0 0 0 24 576 600 [Lavrora.sim.platform.ExternalFlash$Page; 12 -1 24400 480 0 0 0 0 0 32 576 608 [Ljava.util.HashMap$Node; 13 394 21408 520 0 600 3 33 1216 432 2080 2512 avrora.sim.AtmelInterpreter$IORegBehavior 14 727 19800 672 0 968 4 71 1240 664 2472 3136 avrora.arch.legacy.LegacyInstr$MOVW …<snipped> …<snipped> 1299 1300 0 608 0 256 1 5 152 104 1024 1128 sun.util.resources.LocaleNamesBundle 1300 1098 0 608 0 1744 10 290 1808 1176 3208 4384 sun.util.resources.OpenListResourceBundle 1301 1098 0 616 0 2184 12 395 2200 1480 3800 5280 sun.util.resources.ParallelListResourceBundle 2244312 794288 2024 2260976 12801 561882 3135144 1906688 4684704 6591392 Total 34.0% 12.1% 0.0% 34.3% - 8.5% 47.6% 28.9% 71.1% 100.0% Index Super InstBytes KlassBytes annotations CpAll MethodCount Bytecodes MethodAll ROAll RWAll Total ClassName
Current Issues:
As mentioned earlier, the Metaspace VM employs a chunking allocator. There are multiple chunk sizes depending on the type of classloader. Also, the class items themselves are not of a fixed size, thus there are chances that free chunks may not be of the same size as the chunk needed for a class item. All this could lead to fragmentation. The Metaspace VM doesn’t (yet) employ compaction hence fragmentation is a major concern at this moment.
About the Author
Monica Beckwith is a Java Performance Consultant. Her past experiences include working with Oracle/Sun and AMD; optimizing the JVM for server class systems. Monica was voted a Rock Star speaker @JavaOne 2013 and was the performance lead for Garbage First Garbage Collector (G1 GC). You can follow Monica on twitter @mon_beck