Java Memory Model Overview
The JVM uses its own memory model and as soon as it is launch the system allocates memory for the process, and this memory is shared by:
- Heap Memory: Java objects are stored in one of the heap areas.
- Metaspace: since Java 8 it has replaced the older PermGen memory space to provide a more flexible and reliable memory usage.
- Stack Memory: a JVM stack is created for each thread and used to store local variables and partial results, perform dynamic linking, return values from methods and dispatch exceptions.
- Codecache: it is mostly used by the just-in-time (JIT) to store bytecode compiled into native code.
The heap memory is divided into two areas or generations, young (also called nursery) and old generation. The heap is where your object data is stored and it is managed by the Garbage Collector.
Garbage collection: is the process of looking at the heap memory to clean up the unused objects. An Used object is a referenced object, which means that somewhere in your program there is something pointing to that object. In other languages like C this is a manual process whereas in Java is handled automatically.
If the young generation fills up, this causes a minor garbage collection and it triggers a Stop the World. Surviving objects are moved to the old generation.
Stop the World: All the application threads are stoped until a particular operation finishes.
Inside the Young Generation there are three areas: Eden, Survivor Space 0, and Survivor Space 1.
The Old Generation is used to store long surviving objects. To clasify an object as a candidate to be moved from the young generation to the old generation a threshold is set. Eventually the old generation needs to be collected and this event is called a major garbage collection.
A mayor garbage collection is also a Stop the World event. However, this type of Stop the World is much slower since it involves live objects, so this kind of events should be minimized.
Generational Garbage Collection Process
The Garbage Collector is an automatic process reponsible for identifying and delete unsed objects. Steps:
- New objects are put in the eden and survivor spaces start empty.
- When the eden space is full a minor garbage collection is triggered. Referenced objects are moved to the first survivor space (S0) and unreferenced objects are removed.
- Next time the eden fills up it will happen the same, but this time referenced objects are moved to the second survivor space (S1), and referenced objects from S0 are also moved to S1 and age is incremented. Unreferenced object in S0 are removed.
- During the next minor GC, the same process occurs, but this time survivor spaces switch. Referenced objects are moved to S0, survivors are aged and unreferenced objects from eden and S1 are removed.
- When objects reach a particular age they are promoted from the young generation to the old generation.
- The previous 5 steps repeat until a major GC is performed on the old generation which cleans up and compact the space.
Garbage Collector Selection
When no Garbage Collector is selected by CLI (e.g.
-XX:+UseSerialGC), the JVM selects a GC base on the platform is running on, heap size and runtime compiler. The process of selecting a particular GC is called Ergonomics.
- Young Generation Collection
- Serial (S GC) is a stop-the-world. It uses a single thread for garbage collection and freezes all the threads while doing the garbage collection.
- Parallel Scavenge is a stop-the-world, copying collector that uses multiple GC threads
- ParNew (P GC) is a stop-the-world. It uses multiple threads and also freezes all the threads during garbabe collection. It differs from Parallel Scavenge in that it has enhancements that make it usable with CMS. For example, ParNew does the synchronization needed so that it can run during the concurrent phases of CMS.
- Old Generation Collection
- Serial Old is a stop-the-world, mark-sweep-compact collector that uses a single GC thread
- CMS (Concurrent Mark Sweep, CMS GC) is a mostly concurrent, low-pause collector. It uses multiple threads and only freezes the threads while marking the referenced objects in the tenured generation space, and if there is a change in heap memory in parallel while doing the garbage collection. It uses more CPU to improve throughput.
- Parallel Old is a compacting collector that uses multiple GC threads
- G1 (G1 GC) is the Garbage First collector for large heaps and provides reliable short GC pauses
- Has generations but uses different memory layout
- Default collector in JDK 9
- It supposed to replace CMS in the long term and the main differences with CMS are:
- It uses regions to simplify the collection and avoid fragmentation issues.
- G1 offers more predictable garbage collection pauses and allows users to specify desired pause targets.
- The Z Garbage Collector - ZGC: Is designed to offer very low pause times on large heaps and it does this through the use of coloured pointers and load barriers.
The GC selected by default in your machine can be checked with the following JVM option:
java -XX:+PrintCommandLineFlags -version
All options are set or retrieve with
-XX. Options that have true/false values are specified using + for true and - for false. e.g.
-XX:+PrintCommandLineFlagssets this option to true. To see all you default JVM options you can run
If a parallel GC is selected we can tune a couple of parameters:
- Maximum Pause Time Goal (
-XX:MaxGCPauseMillis=<milliseconds>): The JVM will ajust the heap size and other GC parameters to reduce the pause time during garbage collector stops. This might cause more frequent GC and reduce the application throughput.
- Throughput Goal (
-XX:GCTimeRatio=<milliseconds>): The ratio is
1 / (1 + <milliseconds>). For instance, if it is set to 19, it will be 1/20th or 5% of the total time for the garbage collection. This ratio affects both the young and old generation time. The JVM might increase the size of the generations, so the application will run for longer without having to execute a garbage collection.
Tuning Heap Memory
System performance is greatly influenced by the size of the Java heap available to the JVM.
- Heap Size < Avialable Physical RAM.
- Young Generation Size < Total Heap Size/2
- It’s recommended to use same values for minimum and maximum heap size to prevent wasting VM resources used to constantly grow and shrink the heap.
|Initial java heap size||
|Maximum Heap Size||
|Initial and maximum size of the young generation||
Oracle docummentation advices not to choose a maximum value for the heap unless you know that you need a heap greater than the default maximum heap size. Instead choose a throughput goal that is sufficient for your application.
Metaspace is the memory allocated for metadata. In JDK 8, the permanent generation was replace with metaspace, and now class metadata is stored in native memory.
The memory is allocated in the same as C does (Metadata uses space allocated by mmap, not by malloc).The amount of native memory that can be used by Metaspace area is by default unlimited, however you can use the option
MaxMetaspaceSize to set an upper limit. The GC will also run on metaspace when is getting full.
Java Class Metadata
Java Class Metadata is JVM’s internal model of everything in the bytecode. The JVM gets all the bytecode, creates the model and throughs away the bytecode (bycode is not used because of complexity reasons).
It also contains:
- Resolution state (linkage to other classes/interfaces, fields, methods and constants).
- Interpretation state (quick acces resources for resolved references and basic profile counters)
- Compilation state (conde entry addresses, linking stubs and sophisticated profile counters).
Why Java Class Metadata? It’s needed to model the code base at runtime; for the interpreter and JIT, so they know about the class organization; for reflection; and for the JVM Tool Interface to query or update classes at runtime.
Java Stack memory is used for static memory allocation and during the execution of threads, it stores method specific values that are short-lived and references to other objects in the heap that are getting referred from the method.
Whenever a method is invoked, a new block is created in the stack memory for the method to hold local primitive values and reference to other objects in the method. As soon as method ends, the block becomes unused and become available for next method.
- Access to this memory is in LIFO (last-in-first-out).
- Size depends on the OS and is relatively small compared to Heap memory.
- This memory exists as long as the method is running.
- When it’s full it throws a java.langStackOverFlowException.
- Access to this memory is faster than heap memory.
Code Cache is the memory area where the JVM stores the compiled code generated by the Just-In-Time (JIT) compiler. Code Cache uses native memory and is managed by the Code Cache Sweeper.
Code Cache Tuning
We can use JVM option to tweak the codecache comsumption by the JIT.
Some common Codecache options:
|InitialCodeCacheSize||160K (varies)||Initial code cache size (in bytes)|
|ReservedCodeCacheSize||32M/48M||Reserved code cache size (in bytes) - maximum code cache size|
|CodeCacheExpansionSize||32K/64K||Code cache expansion size (in bytes)|
|UseCodeCacheFlushing||false||Attempt to sweep the codecache before shutting off compiler|
|CodeCacheMinimumFreeSpace||500K||When less than the specified amount of space remains, stop compiling. This space is reserved for code that is not compiled methods, for example, native adapters|
|PrintCodeCache||false||Print the code cache memory usage when exiting|
You can benefit from codecache restriction when a lot of code is stored during startup, but very little of this compiled code is needed afterward. By doing this codecache flushing will be likely triggered and make room for the code needed during runtime.