In the Java Virtual Machine Specification, outofMemoryErrors are possible in all areas of the virtual machine except program counters. All of the code in this chapter is “bound to fail” : Our goal is to be able to analyze OOM exceptions from error exceptions so that we can take specific tuning actions.

The whole part of the code is executed in Oracle JDK, because OOM exception is closely related to the implementation of virtual machine, so different JDK publishers, different JDK versions and virtual machines may lead to a little deviation in the results of program operation. The startup parameters covered in this article are VM options, not the startup parameters of the main program.

Stack overflow

A heap overflow is the most common overflow situation: either a large number of objects are created, or objects take up too much space. Here’s a code block to illustrate:

public class HeapOOM {
    static class OOMObject {}

    /**
     * VM args :  -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=C:\Users\ljh\Desktop\dumping
     * @param args args
     */
    
    public static void main(String[] args) {

        / / strong reference
        List<OOMObject> list = new ArrayList<>();

        while (true){
            list.add(new OOMObject());
            // Avoid GC collectionSystem.out.println(list.size()); }}}Copy the code

To save native resources, it is best to limit the heap size again with -xms and -xmx VM options before running this program. In addition, – XX: XX: + HeapDumpOnOutOfMemoryError and HeapDumpPath allows you to store heap snapshot files, so that in the case of heap overflow we can pass this file to analysis program space occupancy of each object.

This program will throw an exception shortly after it runs. The console will also prompt you that the heap snapshot file has been saved to the specified path.

java.lang.OutOfMemoryError: Java heap space Dumping heap to C:\Users\ljh\Desktop\dumping\java_pid292.hprof ... Heap dump file created [28126913 bytes in 0.112 secs] the Exception in the thread "is the main" Java. Lang. OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:2245) at java.util.Arrays.copyOf(Arrays.java:2219) at java.util.ArrayList.grow(ArrayList.java:242) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208) at java.util.ArrayList.add(ArrayList.java:440) at heapOOM.HeapOOM.main(HeapOOM.java:18)Copy the code

For Windows platforms, you can find a program called JVisualvm. exe in the /bin directory of the JDK that can be used to analyze heap snapshot files with the.hprof suffix.

As shown, the snapshot file indicates that the internal class instances of OOMObject take up most of the heap space, which they do. When running out of heap memory in real development, we need to consider whether objects that occupy a large amount of memory need to be around forever. If so, we either set the -xms and -xmx parameters to “borrow” more memory from the native machine, or we try to solve the problem using singleton pattern or object reuse.

Mentality breakdown GC

Heap overflow has another exception related to GC:

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
Copy the code

This exception indicates that the program has exhausted all available heap memory, and the GC is running inefficiently — according to Oracle, the JVM spends 98% of its time using GC for garbage collection, but only gets 2% of its memory. This tiny amount of memory is quickly filled up again as the program runs, causing the virtual machine to recycle inefficiently using GC again, creating a vicious cycle.

In the worst-case scenario, where an application spends 100% of its resources on meaningless GC collection, gridlock occurs. Therefore, after several of these inefficient GC collections, the virtual machine will simply throw an OOM exception and prompt the above information.

The usual appeasement strategy is to make the heap bigger, but this only delays OOM. We still need to analyze which objects are taking up a lot of space through the heap snapshot file and try to optimize them.

Stack overflow

The HotSpot virtual machine does not distinguish between virtual machine stacks and local method stacks, so the -xoSS parameter does not make sense to it. In this article, stack refers to the virtual machine stack.

Total vm stack space Size = stack space allocated per thread M x number of threads N. If Size is constant, an increase in either M or N will result in a decrease in the other variable. This corresponds to two cases:

  1. To create as many threads N as possible, M is too small so that at some point a thread cannot create any more stack frames, at which point the thread should throw a StackOverFlowError exception.
  2. The space M allocated to each stack is too large, causing N to be too small, so at some point the virtual machine cannot create a new thread, and an OutOfMemoryError should be thrown.

Here we write code for each of these cases and see if they throw the corresponding exception.

An exception occurs when the stack capacity is too small

For case 1, we use the -xss argument to reduce the stack size per thread, and then set up an infinite recursive function that forces the main thread to keep adding new stack frames. This recursion is performed on only one main thread. Here is the code block:

public class JavaVMStackSOF {
    /**
     * VM args : -Xss 128k
     *
     * @param args
     */
    public static void main(String[] args) {

        StackSof stackSof = new StackSof();

        try {
            stackSof.suicide();
        } catch (StackOverflowError e) {
            e.printStackTrace();
            System.out.println("depth of stack :"+ stackSof.stackDepth); }}}class StackSof {

    public int stackDepth = 0;

    public void suicide(a) { stackDepth++; suicide(); }}Copy the code

The main program will throw the following error:

java.lang.StackOverflowError
	at stackOOM.StackSof.suicide(JavaVMStackSOF.java:27)
	at stackOOM.StackSof.suicide(JavaVMStackSOF.java:28)
	at stackOOM.StackSof.suicide(JavaVMStackSOF.java:28)
	at stackOOM.StackSof.suicide(JavaVMStackSOF.java:28)
	at stackOOM.StackSof.suicide(JavaVMStackSOF.java:28)
	...
Copy the code

In fact, the Java Virtual Machine Specification allows stack memory to be dynamically expanded, but the HotSpot VIRTUAL machine has not chosen to do so. Therefore, unless an OOM exception is raised due to insufficient stack space when the virtual machine creates a new thread, the thread does not raise an OOM due to expanding stack space, but a StackOverFlowError due to not being able to introduce a new stack frame.

Effect of local variable table on stack depth

Given that the amount of stack space allocated by a thread is fixed, the more local variables it executes, the more local variables it will inflate, and the more stack frames it will need to execute the function. If the -xSS parameter constraint remains unchanged, the maximum stack depth corresponding to this thread will be reduced accordingly. To prove this, we first declare a recursive function with 100 local variables of type Long:

public class JavaStackSOF1 {

    private static int stackDepth = 0;

    private static void suicide(a) {

        // Fill the stack frame, local variables account for 800 KB
        longunused1, unused2, unused3, unused4, unused5, unused6, unused7, unused8, unused9, unused10, unused11, unused12, unused13, unused14, unused15, unused16, unused17, unused18, unused19, unused20, unused21, unused22, unused23, unused24, unused25, unused26, unused27, unused28, unused29, unused30, unused31, unused32, unused33, unused34, unused35, unused36, unused37, unused38, unused39, unused40, unused41, unused42, unused43, unused44, unused45, unused46, unused47, unused48, unused49, unused50, unused51, unused52, unused53, unused54, unused55, unused56, unused57, unused58, unused59, unused60, unused61, unused62, unused63, unused64, unused65, unused66, unused67, unused68, unused69, unused70, unused71, unused72, unused73, unused74, unused75, unused76, unused77, unused78, unused79, unused80, unused81, unused82, unused83, unused84, unused85, unused86, unused87, unused88, unused89, unused90, unused91, unused92, unused93, unused94, unused95, unused96, unused97, unused98, unused99, unused100; unused1 = unused2 = unused3 = unused4 = unused5 = unused6 = unused7 = unused8 = unused9 = unused10 = unused11 = unused12  = unused13 = unused14 = unused15 = unused16 = unused17 = unused18 = unused19 = unused20 = unused21 = unused22 = unused23 = unused24 = unused25 = unused26 = unused27 = unused28 = unused29 = unused30 = unused31 = unused32 = unused33 =  unused34 = unused35 = unused36 = unused37 = unused38 = unused39 = unused40 = unused41 = unused42 = unused43 = unused44 = unused45 = unused46 = unused47 = unused48 = unused49 = unused50 = unused51 = unused52 = unused53 = unused54 = unused55  = unused56 = unused57 = unused58 = unused59 = unused60 = unused61 = unused62 = unused63 = unused64 = unused65 = unused66 = unused67 = unused68 = unused69 = unused70 = unused71 = unused72 = unused73 = unused74 = unused75 = unused76 =  unused77 = unused78 = unused79 = unused80 = unused81 = unused82 = unused83 = unused84 = unused85 = unused86 = unused87 = unused88 = unused89 = unused90 = unused91 = unused92 = unused93 = unused94 = unused95 = unused96 = unused97 = unused98  = unused99 = unused100 =0;

        stackDepth++;

        suicide();

    }

    /** * VM : args : Xss128k */
    public static void main(String[] args) {

        try {
            suicide();
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println("depth of stack:"+ stackDepth); }}}Copy the code

Put this recursive function into the main function and see that StackOverFlowError occurs when the stack depth is around 50. When the main function is re-executed after commenting out the declared 100 Long variables, the stack depth is up to about 1000.

A large number of threads throw exceptions

This exception depends on the memory usage of the host operating system itself. On 32-bit Windows systems, the maximum memory available per process is 2 GB, which is the maximum amount of Java stack space after removing the space occupied by the Java heap and method area. Creating threads without limit (each additional thread means a bit more stack space is allocated) theoretically causes the virtual machine to be unable to allocate additional stack space for new threads, resulting in an OOM exception.

/ * * *!!!!! Please do not run this code directly on 64-bit Windows!! * /
public class JavaStackOOM{

    private static void continuing(a){
        while (true){}
    }

    /** * The stack size that a single thread can operate is 2M. * VM args: -xss2m *@param args
     */
    public static void main(String[] args) {

        while(true){
            Thread thread = new Thread(JavaStackOOM::continuing);
            System.out.println(thread.getName() +" is running..."); thread.start(); }}}Copy the code

This code is extremely risky to run on Windows because the virtual machine threads map to the system’s kernel threads, and creating threads without limit is highly likely to cause the system to freeze before an exception is thrown (my machine has 8 cores + 8 GB of ram, Basically crashed after 6000 threads were created). Running this code on a 32-bit operating system displays:

Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread
Copy the code

Similarly, the more stack space each thread can allocate, the fewer threads the virtual machine can create.

Method area overflow

First, review the internal structure of the method area. The method area can be subdivided into a string constant pool (moved to the heap after JDK 7) and a runtime constant pool (moved to meta-space after JDK 8).

The compiled.class files contain descriptions of the class version, fields, methods, interfaces, and so on, as well as a constant pool of class files used to record compiler-generated literal and symbolic references. When the.class file is loaded by the virtual machine, all of its information is loaded into the virtual machine’s runtime constant pool. In other words, each loaded class has a pool of runtime constants.

String literals defined by.class files are stored in the string constant pool. There is only one string constant pool globally, but it is up to each vendor to implement it. For the HotSpot virtual machine, the String constant pool is implemented as String Table.

In JDK 6 or earlier, the string constant pool and runtime constant pool were in the permanent generation space in the method area, so the program will prompt the PermGen space error when an OOM error occurs on them. At this point, we’ll use our own JDK version to figure out what went wrong. In this section, we debug code in JDK 6, 7, and 8 environments.

Oracle JDK 6 download address | Oracle JDK 7 download address

Fill the string constant pool with the intern method

String:: Intern is a native method, and it’s worth explaining how it’s implemented in different JDK versions.

Prior to JDK 6, if this string was recorded in the string constant pool, a reference to it was returned. Otherwise, the string value is added to the pool and its reference is returned. (Current string constant pools only store values)

In JDK 6 and later, if this string is logged in the string constant pool, a reference to it is returned. Otherwise, if the string is in the heap, the string reference is stored in the pool and its reference is returned. If not, the string value is added to the pool and its reference is returned. (The string constant pool can store values as well as references.)

We use this method in the JDK 6 environment to run the following code to populate the string constant pool, and then plan to “detonate” an error. PermSize and MaxPermSize are used to limit the space of the permanent generation. If the two VM parameters are set to the same value, the permanent generation space will not expand automatically.

public class RuntimeConstantOOM {
    /** * VM args: -xx :PermSize=6M -xx :MaxPermSize=6M (before JDK 1.7) * VM args: -xms20m -xmx20m (after JDK 1.7) */
    public static void main(String[] args) {

	    List<String> list = new ArrayList<String>();
        
        for (int i = 0; i < Integer.MAX_VALUE; i++) { String str = base + base; base = str; list.add(str.intern()); }}}Copy the code

An OOM error occurred due to filling the string constant pool and causing the permanent generation, so it can be proved that in JDK 6 the string constant pool is part of the method area permanent generation.

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
	at java.lang.String.intern(Native Method)
	at RuntimeConstantOOM.main(RuntimeConstantPoolOOM.java:17)
Copy the code

However, when you compile and execute the same code in JDK 7, you get different exceptions.

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.lang.Integer.toString(Integer.java:333)
	at java.lang.String.valueOf(String.java:2954)
	at RuntimeConstantOOM.main(RuntimeConstantOOM.java:17)
Copy the code

The root cause is that string constant pools were migrated to the heap in JDK 7 and later. At this point the string constant pool is affected by the heapspace arguments -xmx, -xms, and it makes no sense to use the VM arguments -xx :PermSize and -xx :MaxPermSize, especially after JDK 8 when these two parameters are completely useless.

Extension: the memory usage of a String

The source code for the String class has three members:

public final class String
    implements java.io.Serializable.Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;
    / /...
}
Copy the code

Obviously, an int and long member is already 12 bytes long, and the value variable is a char[] object (arrays are also objects in Java). Any object contains an object Header (8 bytes), a Reference (4 bytes), and a total of 16 bytes after padding.

The internal members of a String take up 12 + 16 = 28 bytes, or 12 + 28 = 40 bytes including the headers and references. The length is a multiple of 8 bytes, so no padding is required. Considering that a char takes up two bytes, the actual footprint of a String is calculated as 40 + 2n. N is the length of the content of the string.

Beware of CGLIB dynamic proxies causing run-time constant pool overflow

The main function of the method area is to save type information, and this time we want to cause the runtime constant pool to overflow by having the VIRTUAL machine constantly load type information. The idea is to use JDK dynamic proxy, cglib dynamic proxy and other technologies to dynamically generate a large number of classes to fill it when the program runs, the way used in this paper is Cglib.

This example is not purely for experimental purposes. Nowadays, various mainstream frameworks such as Spring and Hibernate make use of bytecode technology like Cglib when enhancing classes. The more complex a class is, the more space its corresponding run-time constant pool will obviously take up. In addition, languages that run on virtual machines, such as Groovy, might dynamically generate types to make the language dynamic. As the above techniques become more popular, OOM exceptions for runtime constant pools will become more common.

The code shown below will continuously use Enhancer to dynamically generate classes until an OOM error occurs. (To import cglib and ASM dependencies, Maven is recommended.) First run it in JDK 7:

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

class NonSubject {}

class MyMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        methodProxy.invokeSuper(o, args);
        return null; }}public class RuntimeConstantPoolOOM {
    /** * VM args : -XX:PermSize=10M -XX:MaxPermSize=10M */
    public static void main(String[] args) throws OutOfMemoryError {
        for(; ;) { Enhancer enhancer =new Enhancer();

            enhancer.setSuperclass(NonSubject.class);
            enhancer.setUseCache(false);
            
            // The callback function here is equivalent to the method interceptor.
            enhancer.setCallback(newMyMethodInterceptor()); enhancer.create(); }}}Copy the code

It should throw a PermSize exception (including one described in some sources), and this is the exception I encountered. However, by modifying the -xx :PermSize parameter, we found that dynamically generating a large number of classes does have an impact on the permanent generation space.

Exception in thread "main" 
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
Copy the code

However, since JDK 8, the permanent generation has been completely phased out, and changing the -xx :PermSize parameter no longer forces the method area to throw errors. This time, we change the MetaspaceSize size to 20M (if it is too small, the vm will not start) : -xx :MetaspaceSize=20M, -xx :MaxMetaspaceSize=20M. Obviously, the exception log is completely different from JDK 7:

Exception in thread "main" net.sf.cglib.core.CodeGenerationException: java.lang.reflect.InvocationTargetException-->null at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:237) at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:377) at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:285) at createProxy.JavaMethodAreaOOM.main(JavaMethodAreaOOM.java:24) Caused by: java.lang.reflect.InvocationTargetException at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:384) at  net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:219) ... 3 more Caused by: java.lang.OutOfMemoryError: MetaspaceCopy the code

The above error message indicates that the essential error occurs in the meta space, because the program uses the cglib tool to dynamically load a large amount of type information, so that the meta space throws the OOM error.

Direct memory overflow

The Unsafe class was originally open only to classes in the VM standard library, but the following code attempts to allocate memory directly by overriding the DirectByteBuffer class and getting its instance directly through reflection. Here we use VM options to set the local memory to 10 M. By default, the size of direct memory matches the Maximum Java heap size (-xmx).

public class DirectMemoryOOM {

    private static final int _1MB = 1024 * 1024;

    /**
     *  VM args : -Xmx20M -XX:MaxDirectMemorySie=10M
     * @throws IllegalAccessException from Unsafe
     */
    public static void main(String[] args) throws IllegalAccessException {

        Field field = Unsafe.class.getDeclaredFields()[0];

        field.setAccessible(true);

        Unsafe unsafe =(Unsafe)field.get(null);

        for(;;) { unsafe.allocateMemory(_1MB); }}}Copy the code

When a direct memory overflow occurs, exceptions printed by the console do not prompt additional information. One more detail to note here is that the program does not actually ask the system to allocate memory before running out of local memory. Instead, it calculates that it will not allocate the specified size of memory next time, and then throws an OOM exception.

Exception in thread "main" java.lang.OutOfMemoryError
	at sun.misc.Unsafe.allocateMemory(Native Method)
	at directMemoryOOM.DirectMemoryOOM.main(DirectMemoryOOM.java:25)
Copy the code

When this problem occurs, the.hprof file is usually checked as well. At this point, you need to consider whether direct memory is abnormal due to the use of tools such as NIO.

summary

In view of OOM involved in this chapter code and its causes, the author here uses thinking export to sort out.

Vm parameters involved in this chapter

Tip: VM options comes after the Java command and before the class name. Such as:

./java -XX:PermSize=10M HelloWorld
Copy the code
VM options role
-Xms{size} Sets the minimum size of the heap.
-Xmx{size} Sets the maximum size of the heap.
-XX:HeapDumpOnOutOfMemoryError The heap snapshot file is printed when a stack overflow error occurs.hprof.
-XX:HeapDumpPath={path} The output path of the heap snapshot file.
-XX:Xss Sets the amount of stack space that can be allocated per thread.
-XX:PermSize (deprecated since JDK 8) Sets the initial space for the permanent generation.
-XX:MaxPermSize` (deprecated after JDK 8) Set the maximum space for the permanent generation.
-XX:MetaspaceSize Sets the initial size of the meta-space.
-XX:MaxMetaspaceSize Set the maximum size of the metacase.
-XX:MinMetaspaceFreeRatio Setting a minimum percentage of free capacity for a meta-space reduces frequent GC due to insufficient space. There are other similar functions-XX:MaxMetaspaceFreeRatio.
-XX:MaxDirectMemorySize Sets the maximum space for direct memory, default and-XmxKeep parameter Settings consistent.

Refer to the link

  • In-depth Understanding of the Java Virtual Machine Third Edition
  • For details about how to use Cglib, see Cglib Dynamic Proxy
  • Maven packages jars with its own dependent Jars using plug-in compression as a JAR package [CSDN]
  • For this experiment, I packaged the code along with cglib dependencies and sent it to a virtual machine environment to run. See: Package dependencies into jars and specify JDK versions using maven-assembly-plugin
  • For a refresher on dynamic proxies, see the Understanding of Java dynamic proxies section
  • String constant pool, class constant pool, and runtime constant pool
  • [CSDN] Analysis of the memory size occupied by Java String
  • Java. Lang. OutOfMemoryError GC overhead limit exceeded the reason analysis and solution