Understanding dynamic memory (heap) in Java applications
Let's start with a simple question. Does every Java developer really understand how memory works in Java? One of the responsibilities of any Java developer is to ensure that by fine-tuning a Java application, it can extract as much performance as possible. It takes time to learn how to manage memory in Java and understand the process, this applies to everyone who deals with Java. In this article I will try to explain how to master these skills.
The point is to properly organize in the Java language the allocation of new objects and the removal of unused ones; the latter process is more often called “garbage collection”. By default, Java and automatically handle memory management quite well. The fact is that the garbage collector constantly works in the background and cleans up unused objects, as well as those that are not worth references, in order to free up some memory.
A developer simply cannot do without sufficient experience and knowledge of how memory is used in Java, how exactly the Java virtual machine (JVM) and the garbage collector work. Without knowing this, it is impossible to find and eliminate bottlenecks, in search of which we execute the code and test its performance.
All my experience in the theory and practice of programming, studying the structure of this language, the desire to understand technical problems and many years of experience hosting numerous Minecraft servers gradually led me to understand how the Java virtual machine works in various environments. When this adventure first began, I wasn't even sure how or where exactly a Java object was created. I also didn't fully understand how the various garbage collectors clean up various objects that are not worth reference, as they do in various areas of the Java heap.
When I first tried to optimize several Minecraft servers (increase their performance), I encountered several memory errors, in particular java.lang.OutOfMemoryError
. It was then that I began to understand much better what roles the stack and heap play when optimizing memory in Java.
Here's an important thing to keep in mind when thinking about memory in Java: you should always start the Java Virtual Machine first, and then tune it for the best possible performance. Creating any class, method, object or variable – it's working with memory. Thus, all information is stored in the Java dynamic memory (heap).
Java Dynamic Memory
I think most of you have seen ten to twenty examples of diagrams explaining how dynamic memory works in Java. Any Java developer needs to understand what is the difference between
PermGen
And
Metaspace
in the latest releases of the Java Development Kit (JDK). We will turn to these diagrams below.
Dynamic memory is divided into two generations. The first one is called
young
and the second
old
. The first large part is called space
eden
and the second – space
survivor
. Space
survivor
consists of subspaces
survivor0
And
survivor1
.
Now let's explain why each of the spaces in a generation is needed young
. All objects we create are first stored in space eden
. The Java Virtual Machine has automatic memory management enabled by default.
In cases where there are a lot of objects in the application – say, they are created in the thousands – memory eden
will be completely filled with objects. Once the garbage collector notices this, it will remove any unused objects or objects that are not referenced. This mechanism is called a “Minor Garbage Collector”. The small garbage collector will carry all surviving objects into space survivor
. From this we draw the following conclusion: generation young
automatically falls under garbage collection to free up memory as needed. This operation will be performed quickly and in a short time.
Considering that so many objects can be created in different classes in the application code, the space allocated in memory for eden
. Here we can assume that they will work GC1
, GC2
And GC3
, and perhaps many others. When garbage collection operations are called in such a huge number in the Java virtual machine, the small garbage collector has no choice but to check all the objects that are ready to be moved to survivor
. Finally, the Java Virtual Machine will be ready to move all remaining objects into memory space survivor
.
Surviving objects from the generation young
move into generations old
and when space old
is filled with objects, the main garbage collector is turned on.
Main Garbage Collector
So, the job of the master garbage collector begins when the generation
old
memory is completely filled with objects. Please note that it takes some time for the main garbage collector to start. When a Java developer (or team of developers) writes an application from scratch or automates this work using some framework, one must be extremely careful in handling generations
young
And
old
.
A big thing to think about when developing in Java is to try not to create unnecessary objects that would be used infrequently in your application code anyway. After all, if you create any objects, the garbage collector will dispose of them as soon as they complete their assigned tasks. To make it easier to understand the work of the garbage collector, you can formulate its purpose as follows:
Small garbage collector aims to recycle generation
young
and generationold
falls under the actions of the main garbage collector.
If we consider as an example how the Amazon or Walmart sites are structured, it turns out that a huge number of requests are received from them to the web server. When there is active traffic, delays will begin to appear as requests are processed. The fact is that the main garbage collector takes up quite a lot of memory space – this is necessary to destroy unused objects. A side effect in this case is that there is a large load on the CPU and RAM on those nodes that serve the site(s).
This picture means that you are creating too many instances of a particular class within the system. In this case, the main garbage collector will try to continuously and in large quantities destroy all unused objects. The work of the main garbage collector takes much more time than the work of the small one.
Memory related errors
I recommend that a Java developer learn to read and understand the output of error messages – after all, they can be used to clearly understand what is happening with the Java virtual machine; these are not errors in the application code itself that runs on it. The actual cause of the code error you are seeing is, for example, a memory leak, a garbage collector failure, it could be related to resource allocation and even synchronization problems. First we need to learn how to solve such problems, because this way we can quickly limit the affected area of resources.
Please note that we will also need to track how resources are used. Performance testing of Java applications is highly recommended. To do this, you can, for example, profile each category, take multiple heap dumps, inspect and debug application code, and much more. If none of these measures work, then it is usually recommended to simply allocate more resources to your application. Below are common errors and what happens when they occur.
java.lang.StackOverFlowError
— stack memory is full.java.lang.OutOfMemoryError
– Heap memory is full.java.lang.OutOfMemoryError: GC Overhead limit exceeded
— the garbage collector has used up its allowable overhead limitjava.lang.OutOfMemoryError: Permgen space
— the space of “permanent generation” is filledjava.lang.OutOfMemoryError: Metaspace
– metaspace is filled (Metaspace, used in Java JDK 8 and higher)java.lang.OutOfMemoryError: Unable to create new native thread
– This means that the native JVM code is no longer able to create new native threads in the underlying operating system because too many threads have already been created and they are consuming all the available memory allocated to the Java Virtual Machine.java.lang.OutOfMemoryError: request size bytes for reason
– this error means that the application has completely used up the space allocated for swappingjava.lang.OutOfMemoryError: Requested array size exceeds VM limit
– this means that the array used by your application exceeds the maximum size allowed on this platform.
Setting the initial and maximum heap size
Initial heap size – XMS
This is the initial heap size. It usually allocates 1/64th of the total RAM available on the node you are using. Usually in this case I set the value 128MB
. This value can be overridden via the command line using the option java -Xms128M
.
Maximum heap size is XMX
This is the maximum memory that can be allocated to the heap. The smaller the total RAM power on the node you are using, the smaller it is. Typically this is where I set the default to 8192MB
. This value can also be overridden via the command line using the option java -Xmx8192M
.
Here is a complete example of a command that runs a Java application.
java -Xms128M -Xmx8192M app.jar
The initial and maximum values can be changed based on the needs of the application. I usually select default values that I assign to all the Java applications I work with. I typically have to deal with Minecraft servers, and Minecraft users typically need no more than 8GB of memory on their Minecraft server. I give them an additional
1GB
memory for performing background tasks, such as garbage collection.
Additional options
-XX:+UseG1GC
: activates the primary garbage collector(G1)
which is designed to work with applications where the heap is large and a relatively large delay is allowed during garbage collection.-XX:+UseZGC
: Enables the Z-garbage collector, designed to work with applications that need to reduce latency without sacrificing throughput.-XX:+UseShenandoahGC
: Enables the Shenandoah Garbage Collector, whose goal is to reduce garbage collection pauses by parallelizing the work across different application threads if possible.-XX:+UseParallelGC
: Activates the parallel garbage collector for a generationyoung
.-XX:NewRatio=
: establishes the relationship between the sizes of the “old” and “young” generations. For example, with the value-XX:NewRatio=3
the old generation will be three times larger than the young.-XX:SurvivorRatio=
: establishes the relationship between spaceseden/survivor
. By lowering this ratio, we increase the amount of space allocated for survivors.-XX:MaxGCPauseMillis
: Sets the target value for maximum GC pause. This is a soft goal, and the Java Virtual Machine will strive to achieve it as best it can.-XX:+UseSerialGCx
: Enables a sequential garbage collector that does all the work in a single thread and is typically suitable for small applications that don't occupy much memory space.-XX:ParallelGCThreads
: sets how many threads will be used when garbage collectors run in parallel. The default value varies depending on the platform on which the Java virtual machine is running.-XX:ConcGCThreads
: The number of threads that concurrent garbage collectors will use. The default value varies depending on the platform on which the Java virtual machine is running.-XX:InitiatingHeapOccupancyPercentx
: The percentage by which the heap must be full to trigger concurrent garbage collection cycles.-XX:+HeapDumpOnOutOfMemoryError
: Tells the Java Virtual Machine to generate a heap dump when an exception is thrownOutOfMemoryError
.-XX:+PrintGCDetails
: Displays a detailed report of each garbage collection operation. Used to fine-tune the garbage collector.-XX:+PrintGCDateStamps
: Adds a timestamp to each garbage collection event, and then these timestamps are displayed in the logs along with the events themselves.-XX:+PrintHeapAtGC
: Prints detailed information about the heap before and after garbage collection.-XX:+PrintGCApplicationStoppedTime
: Displays how much time was spent pausing the garbage collector, thus helping to determine when exactly such pauses occur.-XX:+PrintGCApplicationConcurrentTime
: Reports how much of the time spent was not garbage collected (that is, how long the application was running).-XX:+UseCodeCacheFlushing
: Allows the Java Virtual Machine to clear the code cache when it is full. This prevents the Java virtual machine from crashing due to such an overflow.
Conclusion
There are easily 500+ arguments that could be passed to the Java Virtual Machine to fine-tune Java garbage collection and memory to run applications. If you want to control other aspects, the number of such arguments will easily exceed 1000. Here I have explained only a very few arguments that help solve the most pressing issues related to managing Java applications and other programs.