Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ


Choose your language

InfoQ Homepage Articles Where Has the Java PermGen Gone?

Where Has the Java PermGen Gone?

Leia em Português

Lire ce contenu en français

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
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>] 

     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 

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
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

Rate this Article