Ошибка память не может быть java

If you keep on allocating & keeping references to object, you will fill up any amount of memory you have.

One option is to do a transparent file close & open when they switch tabs (you only keep a pointer to the file, and when the user switches tab, you close & clean all the objects… it’ll make the file change slower… but…), and maybe keep only 3 or 4 files on memory.

Other thing you should do is, when the user opens a file, load it, and intercept any OutOfMemoryError, then (as it is not possible to open the file) close that file, clean its objects and warn the user that he should close unused files.

Your idea of dynamically extending virtual memory doesn’t solve the issue, for the machine is limited on resources, so you should be carefull & handle memory issues (or at least, be carefull with them).

A couple of hints i’ve seen with memory leaks is:

—> Keep on mind that if you put something into a collection and afterwards forget about it, you still have a strong reference to it, so nullify the collection, clean it or do something with it… if not you will find a memory leak difficult to find.

—> Maybe, using collections with weak references (weakhashmap…) can help with memory issues, but you must be carefull with it, for you might find that the object you look for has been collected.

—> Another idea i’ve found is to develope a persistent collection that stored on database objects least used and transparently loaded. This would probably be the best approach…

In Java, all objects are stored in a heap. They are allocated using a new operator. The OutOfMemoryError Exception in Java looks like this: 

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

Usually, this error is thrown when the Java Virtual Machine cannot allocate an object because it is out of memory. No more memory could be made available by the garbage collector.

OutOfMemoryError usually means that you’re doing something wrong, either holding onto objects too long or trying to process too much data at a time. Sometimes, it indicates a problem that’s out of your control, such as a third-party library that caches strings or an application server that doesn’t clean up after deploys. And sometimes, it has nothing to do with objects on the heap.

The java.lang.OutOfMemoryError exception can also be thrown by native library code when a native allocation cannot be satisfied (for example, if swap space is low). Let us understand various cases when the OutOfMemory error might occur.

Symptom or Root cause?

To find the cause, the text of the exception includes a detailed message at the end. Let us examine all the errors. 

Error 1 – Java heap space: 

This error arises due to the applications that make excessive use of finalizers. If a class has a finalize method, objects of that type do not have their space reclaimed at garbage collection time. Instead, after garbage collection, the objects are queued for finalization, which occurs later. 

Implementation: 

  • finalizers are executed by a daemon thread that services the finalization queue.
  • If the finalizer thread cannot keep up with the finalization queue, the Java heap could fill up, and this type of OutOfMemoryError exception would be thrown.
  • The problem can also be as simple as a configuration issue, where the specified heap size (or the default size, if it is not specified) is insufficient for the application.

Java

import java.util.*;

public class Heap {

    static List<String> list = new ArrayList<String>();

    public static void main(String args[]) throws Exception

    {

        Integer[] array = new Integer[10000 * 10000];

    }

}

Output:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at Heap.main(Heap.java:11)

When you execute the above code above you might expect it to run forever without any problems. As a result, over time, with the leaking code constantly used, the “cached” results end up consuming a lot of Java heap space, and when the leaked memory fills all of the available memory in the heap region and Garbage Collection is not able to clean it, the java.lang.OutOfMemoryError:Java heap space is thrown.

Prevention: Check how to monitor objects for which finalization is pending in Monitor the Objects Pending Finalization.

Error 2 – GC Overhead limit exceeded: 

This error indicates that the garbage collector is running all the time and Java program is making very slow progress. After a garbage collection, if the Java process is spending more than approximately 98% of its time doing garbage collection and if it is recovering less than 2% of the heap and has been doing so far the last 5 (compile-time constant) consecutive garbage collections, then a java.lang.OutOfMemoryError is thrown. 

This exception is typically thrown because the amount of live data barely fits into the Java heap having little free space for new allocations. 

Java

import java.util.*;

public class Wrapper {

    public static void main(String args[]) throws Exception

    {

        Map m = new HashMap();

        m = System.getProperties();

        Random r = new Random();

        while (true) {

            m.put(r.nextInt(), "randomValue");

        }

    }

}

If you run this program with java -Xmx100m -XX:+UseParallelGC Wrapper, then the output will be something like this : 

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
    at java.lang.Integer.valueOf(Integer.java:832)
    at Wrapper.main(error.java:9)

Prevention: Increase the heap size and turn off it with the command line flag -XX:-UseGCOverheadLimit. 

Error 3 – Permgen space is thrown: 

Java memory is separated into different regions. The size of all those regions, including the permgen area, is set during the JVM launch. If you do not set the sizes yourself, platform-specific defaults will be used. 

The java.lang.OutOfMemoryError: PermGen space error indicates that the Permanent Generation’s area in memory is exhausted. 

Java

import javassist.ClassPool;

public class Permgen {

    static ClassPool classPool = ClassPool.getDefault();

    public static void main(String args[]) throws Exception

    {

        for (int i = 0; i < 1000000000; i++) {

            Class c = classPool.makeClass("com.saket.demo.Permgen" + i).toClass();

            System.out.println(c.getName());

        }

    }

}

In the above sample code, code iterates over a loop and generates classes at run time. Class generation complexity is being taken care of by the Javassist library. 

Running the above code will keep generating new classes and loading their definitions into Permgen space until the space is fully utilized and the java.lang.OutOfMemoryError: Permgen space is thrown. 

Prevention : When the OutOfMemoryError due to PermGen exhaustion is caused during the application launch, the solution is simple. The application just needs more room to load all the classes to the PermGen area, so we need to increase its size. To do so, alter your application launch configuration and add (or increase if present) the -XX:MaxPermSize parameter similar to the following example: 

java -XX:MaxPermSize=512m com.saket.demo.Permgen

Error 4 – Metaspace: 

Java class metadata is allocated in native memory. Suppose metaspace for class metadata is exhausted, a java.lang.OutOfMemoryError exception with a detail MetaSpace is thrown. 

The amount of metaspace used for class metadata is limited by the parameter MaxMetaSpaceSize, which is specified on the command line. When the amount of native memory needed for a class metadata exceeds MaxMetaSpaceSize, a java.lang.OutOfMemoryError exception with a detail MetaSpace is thrown.

Java

import java.util.*;

public class Metaspace {

    static javassist.ClassPool cp

        = javassist.ClassPool.getDefault();

    public static void main(String args[]) throws Exception

    {

        for (int i = 0; i < 100000; i++) {

            Class c = cp.makeClass(

                            "com.saket.demo.Metaspace" + i)

                          .toClass();

        }

    }

}

This code will keep generating new classes and loading their definitions to Metaspace until the space is fully utilized and the java.lang.OutOfMemoryError: Metaspace is thrown. When launched with -XX:MaxMetaspaceSize=64m then on Mac OS X my Java 1.8.0_05 dies at around 70, 000 classes loaded.

Prevention: If MaxMetaSpaceSize, has been set on the command line, increase its value. MetaSpace is allocated from the same address spaces as the Java heap. Reducing the size of the Java heap will make more space available for MetaSpace. This is only a correct trade-off if there is an excess of free space in the Java heap. 

Error 5 – Requested array size exceeds VM limit: 

This error indicates that the application attempted to allocate an array that is larger than the heap size. For example, if an application attempts to allocate an array of 1024 MB but the maximum heap size is 512 MB then OutOfMemoryError will be thrown with “Requested array size exceeds VM limit”. 

Java

import java.util.*;

public class GFG {

    static List<String> list = new ArrayList<String>();

    public static void main(String args[]) throws Exception

    {

        Integer[] array = new Integer[10000 * 10000];

    }

}

Output:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at GFG.main(GFG.java:12)

The java.lang.OutOfMemoryError: Requested array size exceeds VM limit can appear as a result of either of the following situations: 

  • Your arrays grow too big and end up having a size between the platform limit and the Integer.MAX_INT
  • You deliberately try to allocate arrays larger than 2^31-1 elements to experiment with the limits.

Error 6 – Request size bytes for a reason. Out of swap space?: 

This apparent exception occurred when an allocation from the native heap failed and the native heap might be close to exhaustion. The error indicates the size (in bytes) of the request that failed and the reason for the memory request. Usually, the reason is the name of the source module reporting the allocation failure, although sometimes it is the actual reason. 

The java.lang.OutOfMemoryError: Out of swap space error is often caused by operating-system-level issues, such as: 

  • The operating system is configured with insufficient swap space.
  • Another process on the system is consuming all memory resources.

Prevention: When this error message is thrown, the VM invokes the fatal error handling mechanism (that is, it generates a deadly error log file, which contains helpful information about the thread, process, and system at the time of the crash). In the case of native heap exhaustion, the heap memory and memory map information in the log can be useful.

Error 7 – reason stack_trace_with_native_method: 

Whenever this error message(reason stack_trace_with_native_method) is thrown then a stack trace is printed in which the top frame is a native method, then this is an indication that a native method has encountered an allocation failure. The difference between this and the previous message is that the allocation failure was detected in a Java Native Interface (JNI) or native method rather than the JVM code. 

Java

import java.util.*;

public class GFG {

    public static void main(String args[]) throws Exception

    {

        while (true) {

            new Thread(new Runnable() {

                public void run()

                {

                    try {

                        Thread.sleep(1000000000);

                    }

                    catch (InterruptedException e) {

                    }

                }

            }).start();

        }

    }

}

The exact native thread limit is platform-dependent. For example, tests Mac OS X reveals that: 64-bit Mac OS X 10.9, Java 1.7.0_45 – JVM dies after #2031 threads have been created

Prevention: Use native utilities of the OS to diagnose the issue further. For more information about tools available for various operating systems, see Native Operating System tools. 

This article is contributed by Saket Kumar. If you like GeeksforGeeks and would like to contribute, you can also write an article using write.geeksforgeeks.org or mail your article to review-team@geeksforgeeks.org. See your article appearing on the GeeksforGeeks main page and help other Geeks. Please write comments if you find anything incorrect, or you want to share more information about the topic discussed above.

Overview

An out of memory error in Java formally known as java.lang.OutOfMemoryError is a runtime error that occurs when the Java Virtual Machine (JVM) cannot allocate an object in the Java heap memory. In this article, we will be discussing several reasons behind “out of memory” errors in Java and how you can avoid them.

new java job roles

The JVM manages the memory by setting aside a specific size of the heap memory to store the newly allocated objects. All the referenced objects remain active in the heap and keep that memory occupied until their reference is closed. When an object is no longer referenced, it becomes eligible to be removed from the heap by the Garbage collector to free up the occupied heap memory. In certain cases, the Java Garbage Collector (GC) is unable to free up the space required for a new object and the available heap memory is insufficient to support the loading of a Java class, this is when an “out of memory” error occurs in Java.

What causes the out of memory error in Java?

An “out of memory” error in Java is not that common and is a direct indication that something is wrong in the application. For instance, the application code could be referencing large objects for too long that is not required or trying to process large amounts of data at a time. It is even possible that the error could have nothing to do with objects on the heap and the reason behind it like because of third-party libraries used within an application or due to an application server that does not clean up after deployment.

Following are some of the main causes behind the unavailability of heap memory that cause the out of memory error in Java.

· Java heap space error

It is the most common out of memory error in Java where the heap memory fills up while unable to remove any objects.

See the code snippet below where java.lang.OutOfMemoryError is thrown due to insufficient Java heap memory available:

public class OutOfMemoryError01 {
    public static void main(String[] args) {
        Integer[] arr = new Integer[1000 * 1000 * 1000];
    }
}

Output:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at OutOfMemoryErrorExample.main(OutOfMemoryErrorExample.java:8)

In the above code, an array of integers with a very large size is attempted to be initialized. As the Java heap is insufficient to allocate such a huge array, it will eventually throw a java.lang.OutOfMemoryError: Java heap space error. Initially, it might seem fine but over time, it will result in consuming a lot of Java heap space and when it fills all of the available memory in the heap, Garbage Collection will not be able to clean it as the code would still be in execution and the no memory can be freed.

Another reason for a Java heap space error is the excessive use of finalizers. If a class has a finalize() method, the GC will not clean up any objects of that class, instead, they all will be queued up for finalization at a later stage. If a finalizer thread cannot keep up with the finalization queue because of excessive usage of finalizers, the Java heap will eventually fill up resulting in an “out of memory” error in Java.

Prevention:

Developers need to use the finalize methods only when required and they must monitor all the objects for which finalization would be pending.

· GC Overhead limit exceeded:

This error indicates that the garbage collector is constantly running due to which the program will also be running very slowly. In a scenario where for minimum consecutive 5 garbage collection cycles, if a Java process utilizes almost 98% of its time for garbage collection and could recover less than 2% of the heap memory then a Java Out of Memory Error will be thrown.
This error typically occurs because the newly generated data could barely fit into the Java heap memory having very little free space for new object allocations.

Prevention:

Java developers have the option to set the heap size by themselves. To prevent this error, you must Increase the heap size using the -Xmx attribute when launching the JVM.

· PermGen space error:

JVM separates the memory into different sections. One of the sections is Permanent Generation (PermGen) space. It is used to load the definitions of new classes that are generated at the runtime. The size of all these sections, including the PermGen area, is set at the time of the JVM launch. If you do not set the sizes of every area yourself, platform-specific defaults sizes will be then set. If the Permanent Generation’s area is ever exhausted, it will throw the java.lang.OutOfMemoryError: PermGen space error.

Prevention:

The solution to this out of Memory Error in Java is fairly simple. The application just needs more memory to load all the classes to the PermGen area so just like the solution for GC overhead limit exceeding error, you have to increase the size of the PermGen region at the time of Java launch. To do so, you have to change the application launch configuration and increase or if not used, add the XX:MaxPermSize parameter to your code.

· Out of MetaSpace error:

All the Java class metadata is allocated in native memory (MetaSpace). The amount of MetaSpace memory to be used for class metadata is set by the parameter MaxMetaSpaceSize. When this amount exceeds, a java.lang.OutOfMemoryError exception with a detail MetaSpace is thrown.

Prevention:

If you have set the MaxMetaSpaceSize on the command line, increasing its size manually can solve the problem. Alternatively, MetaSpace is allocated from the same address spaces as the Java heap memory so by reducing the size of the Java heap, you can automatically make space available for MetaSpace. It should only be done when you have excess free space in the Java heap memory or else you can end up with some other Java out of memory error.

· Out of swap space error:

This error is often occurred due to certain operating system issues, like when the operating system has insufficient swap space or a different process running on the system is consuming a lot of memory resources.

Prevention:

There is no way to prevent this error as it has nothing to do with heap memory or objects allocation. When this error is thrown, the JVM invokes the error handling mechanism for fatal errors. it generates an error log file, which contains all the useful information related to the running threads, processes, and the system at the time of the crash. this log information can be very useful to minimize any loss of data.

How to Catch java.lang.OutOfMemoryError?

As the java.lang.OutOfMemoryError is part of the Throwable class, it can be caught and handled in the application code which is highly recommended. The handling process should include the clean up the resources, logging the last data to later identify the reason behind the failure, and lastly, exit the program properly.

See this code example below:

public class OutOfMemoryError02 {
    public void createArr (int size) {
        try {
            Integer[] myArr = new Integer[size];
        } catch (OutOfMemoryError ex) {
            //creating the Log
            System.err.println("Array size is too large");
            System.err.println("Maximum JVM memory: " + 
Runtime.getRuntime().maxMemory());
        }
    }
    public static void main(String[] args) {
        OutOfMemoryError02 oomee = new OutOfMemoryError02();
        ex.createArr (1000 * 1000 * 1000);
    }
}

In the above code, as the line of code that might cause an out of Memory Error is known, it is handled using a try-catch block. In case, if the error occurs, the reason for the error will be logged that is the large size of the array and the maximum size of the JVM, which will be later helpful for the caller of the method to take the action accordingly.

In case of an out of memory error, this code will exit with the following message:

Array size is too large
Maximum JVM memory: 9835679212

It is also a good option to handle an out of Memory Error in Java when the application needs to stay in a constant state in case of the error. This allows the application to keep running normally if any new objects are not required to be allocated.

See Also: CompletableFuture In Java With Examples

Conclusion

In this article, we have extensively covered everything related to the “out of memory” error in Java. In most cases, you can now easily prevent the error or at least will be able to retrieve the required information after the crashing of the program to identify the reason behind it. Managing errors and exceptions in your code is always challenging but being able to understand and avoid these errors can help you in making your applications stable and robust.

new Java jobs

OutOfMemoryError может быть OutOfMemoryError при возникновении одного из следующих обстоятельств:

  • JVM не хватает родной памяти
  • Кучи Java не хватает памяти
  • PermGen или Metaspace не хватает памяти
  • JVM потратила слишком много времени, пытаясь собрать мусор

Основная причина OutOfMemoryError обычно может быть вычтена из сообщения об ошибке. Давайте посмотрим на детали каждого из обстоятельств.

JVM не хватает родной памяти

В основном это означает, что объем памяти, выделенной для JVM, исчерпан. Максимальный размер процесса для 32-разрядной JVM составляет примерно 3,5–4 ГБ. Если он превышен, OutOfMemoryError будет брошен. Даже в 64-разрядной JVM, когда JVM запрашивает больше памяти, операционной системе может просто не хватать ее. Посмотрите на следующий фрагмент:

01

02

03

04

05

06

07

08

09

10

11

for (int i = 0; true; ++i) {

  new Thread() {

    public void run() {

      try {

         Thread.sleep(1000000);

      } catch(InterruptedException e) { }

    }

   }.start();

   System.out.println("Thread"; + i + "created");

}

На моем ноутбуке (64-битная Mac OS X 10.11.6 с Java 1.8.0_112) JVM аварийно завершает работу после создания 2023 потоков:

1

2

3

4

Thread 2021 created

Thread 2022 created

Thread 2023 created

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread

Кучи Java не хватает памяти

Это довольно очевидно. Слишком много объектов выделено, поэтому они не помещаются в пространство кучи, настроенное для JVM. Увеличение размера кучи звучит как решение, но если оно вызвано утечкой памяти, оно просто OutOfMemoryError . Сообщение об ошибке довольно ясно:

1

Exception in thread “main” java.lang.OutOfMemoryError: Java heap space

PermGen или Metaspace не хватает памяти

PermGen (Java 7 и более ранние версии) имеет ограниченный максимальный размер . Это означает, что если загружено слишком много классов, PermGen может заполниться, и OutOfMemoryError будет брошен. Увеличение максимального размера PermGen должно помочь. Java 8 не имеет PermGen, но вместо этого Metaspace. По умолчанию он имеет неограниченный максимальный размер, поэтому до тех пор, пока вы не установите ограничение с MaxMetaspaceSize флага MaxMetaspaceSize , ошибка не должна возникать. Чтобы диагностировать OutOfMemoryError вызванную PermGen или Metaspace, следует изучить сообщение об ошибке:

1

2

Exception in thread “main” java.lang.OutOfMemoryError: PermGen space

Exception in thread “main” java.lang.OutOfMemoryError: Metaspace

JVM потратила слишком много времени, пытаясь собрать мусор

Это самый OutOfMemoryErrorOutOfMemoryError когда GC тратит слишком много времени на сбор мусора с слишком маленьким результатом, а дальнейшее выполнение приложения не имеет смысла. Другими словами, должны быть выполнены все следующие условия :

  • Более 98% времени уходит в GC (98% является значением по умолчанию, оно может быть переопределено GCTimeLimit=N )
  • Менее 2% кучи восстанавливается во время полного GC (опять же, 2% является значением по умолчанию, оно может быть переопределено GCHeapFreeLimit=N )
  • Оба условия, упомянутые выше, выполняются в течение пяти последовательных полных циклов ГХ
  • Флаг UseGCOverheadLimit не отключен (значение по умолчанию – true)

Запуск полного GC означает, что JVM не хватает памяти в любом случае. Если 98% времени тратится на освобождение только 2% кучи, то это означает, что ЦП почти полностью занят GC и практически никакая логика приложения не может быть выполнена. Вот почему имеет смысл отказаться и выбросить OutOfMemoryError со следующим сообщением:

1

Exception in thread “main” java.lang.OutOfMemoryError: GC overhead limit exceeded

Тема данного поста — довольно часто встречающаяся проблема переполнения памяти в Java. Постараюсь рассказать,
почему этой ошибке стоит уделить особое внимание, почему так сложно искать причину и какие утилиты в этом помогают.

Ошибка OutOfMemoryError в java неизбежна. Если в нагруженном приложении такая ошибка не возникает, значит она была
раньше и её починили. Я немного утрирую, но я не помню проекта, на котором бы такая ошибка рано или поздно не проявлялась.
На собеседованиях вопрос про OutOfMemory не очень часто задают; честно говоря, редко кто отвечает правильно и
подробно.

Даже несмотря на то, что такая ошибка может приводить к реальным убыткам, так как чаще всего возникает под нагрузкой,
на production instance (продакшне, по-народному) и приводит к параличу системы на
часы и более, всё равно в 90% случаев вместо поиска причины ошибки люди увеличивают объём памяти JVM (хоть до -Xmx64g) и
настраивают еженедельные, а потом и ежедневные рестарты сервера.
Чтобы система не повисала, настраивают мониторинги и когда память приближается к пороговому значению, проводят
небольшой анализ, который, как правило, ничего не даёт, а уже потом перезагружают сервер.

Но что делать если рестарт или увеличение памяти не помогают?
Тогда вся команда бросит свои текущие задачи и будет искать причину, воспроизводить, изучать логи, дампы памяти и
потоков. Тогда, конечно, причину найдут и починят. Конечно, не всегда всё так серьезно, но бывает и так.

Как вообще происходит управление памятью в Java? Тот, кто знаком с языками С или C++,
знает каково это явно выделять память под каждый массив или объект и потом так же явно освобождать её.
Пример работы с массивом в C++:

    int * p;
    p = new (nothrow) int[1000]; //выделяем память под массив
    if (p == nullptr)            //обрабатываем ситуацию, если память выделить не удалось
      cout << "Error: memory could not be allocated";
    delete[] p;                  //освобождаем память после использования    

Пример создания массива в Java:

    int[] array = new int[1000];
    

В С++ легко было определить термин «утечка памяти» — память была выделена, но не была освобождена, при этом в программе она больше никак использоваться не будет.
Для этого достаточно «потерять» указатель («int * p» в нашем примере, например, после выхода из функции, если он был локальной переменной этой функции.)
Когда память заканчивается, это событие можно сразу же явно обработать.

В Java же мы не управляем ни выделением памяти, ни его освобождением, ни моментом, когда память заканчивается. Попытка перехватить и обработать
исключение OutOfMemoryError бессмысленна — после её возникновения дальнейшее поведение программы не определено.
Вроде как память освобождает специальный сборщик мусора, но им тоже никак нельзя управлять.
Его можно вежливо попросить освободить ненужную занятую память с помощью команды System.gc(), но этот вызов ничего не гарантирует.
Под «утечкой памяти» применительно к Java подразумевают то, что объект, под который она выделена,
никогда больше не будет использоваться в соответствии с потоком выполнения и логикой программы, но память, занимаемая им не может быть освобождена сборщиком мусора.

Можно выделить две основных причины OutOfMemoryError: первая — программе действительно нужно больше памяти, вторая — в программе присутствует утечка.
Первый случай решается либо переработкой кода с целью эффективнее расходовать память, либо простым увеличением объёма памяти выделенной под процесс.
Во втором же случае что бы мы не делали, память рано или поздно закончится, если причина утечки не будет исправлена.
Почему же помогают рестарты сервера? Дело в том, что утечка расходует память не мгновенно. Например, после каждого вызова сервиса теряется один мегабайт.
Если за сутки сервис вызывается 1000 раз, а для процесса выделено 2Гб, то ежедневные рестарты спасут от OutOfMemory, пока дневная нагрузка не поднимется сильно выше средней.

Я не привожу строгого формального описания процесса управления памятью в Java и работы сборщика мусора.
Я считаю, что на практике в первую очередь нужно разобраться с основным принципом.
Выделением и освобождением памяти занимается JVM.
Так или иначе если объект становится неиспользуемым (недостижимым по графу ссылок), сборщик мусора его освободит до того, как память полностью закончится.
Если объект всё ещё достижим, то память, занимаемая этим объектом, не будет освобождена вне зависимости от настроек сборщика мусора и конкретной его реализации.
Появление достижимых объектов, которые более никогда не будут использоваться в программе — это утечка памяти, которую следует исправлять.

Достижимость — это существование пути в графе ссылок от корневых объектов (GC Roots) до целевого объекта.
Вот основные из них:

  1. Классы, их статические поля
  2. Все запущенные потоки
  3. Стек вызовов
  4. Локальные переменные и параметры функций

Упрощённо достижимость можно объяснить так: если в программе есть способ обратиться к объекту, значит он достижим (из GC Roots).
Мы можем обратиться к статическим полям загруженных классов, к локальным переменным и параметрам, к текущему потоку.
Утечек памяти, связанных с тонкостями определения корневых объектов, я не встречал. Возможно, они имеют место в случае использования
собственных загрузчиков классов (custom classloaders). В такие дебри лезть не будем.

Рассмотрим несколько примеров. В первом создаём бесконечный список, OutOfMemoryError вылетает мгновенно.

    IntStream.generate(() -> 1).boxed().collect(Collectors.toList());

    //Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    //  at java.base/java.util.Arrays.copyOf(Arrays.java:3689)
    //  at java.base/java.util.ArrayList.grow(ArrayList.java:237)

Во втором бесконечно вызываем String.intern, сохраняющий строку во внутреннем пуле.
Потребление оперативной памяти не растёт со временем, ошибка переполнения не происходит.

    for(;;) {
        UUID.randomUUID().toString().intern();
    }

Начиная с версии java 1.8 PermGen space был удалён и данный код стал работать немного иначе: раньше все строки,
добавленные во внутренний пул, хранились там вечно, и злоупотребление методом intern вместо экономии памяти могло наоборот привести к её переполнению.
Теперь же возможно не задумываясь вставлять вызовы .intern() хоть для каждой переменной типа String. Делать этого, конечно же, не нужно.
Вообще не так просто придумать приложение, в котором при разумном проектировании в памяти одновременно находится очень много одинаковых строковых значений.

Теперь рассмотрим неочевидный случай, анализ которого потребует специальных утилит.
Код тут., хотя в нём буквально 10 строчек:

    @RestController
    @RequestMapping("/api")
    public class OomRestService {
         private final Logger log = LoggerFactory.getLogger(OomRestService.class);

         @GetMapping("/ping")
         public String ping(HttpServletRequest request) {
             log.trace("req from {}, session id {}", request.getRemoteHost(), request.getSession().getId());
             return "pong";
         }
    }

Тривиальный сервис, который на HTTP GET-запрос по адресу «/api/ping» отвечает «pong».
Вызывать я его буду в бесконечном цикле из теста:

    @Test
    public void testPingInfiniteOom() {
        for(;;) {
            ResponseEntity<String> ping  =
               testRestTemplate.getForEntity("http://localhost:" + port + "/api/ping", String.class);
        }
    }

Кстати, в Junit5 появилась очень удобная аннотация @RepeatedTest, но она замедляет исполнение,
поэтому вместо неё здесь использую некрасивый бесконечный цикл.

Тест запускается с дополнительным аргументом виртуальной машины «-Xmx50m»,
то есть всего 50 мегабайт, но для старта приложению достаточно и 20 мегабайт, а OutOfMemory мы получим и при гигабайте, только ждать дольше.
Ошибка случается через пару минут и около 80 000 вызовов сервиса.

    Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "http-nio-auto-1-ClientPoller"
    Exception in thread "RMI TCP Connection(idle)" java.lang.OutOfMemoryError: Java heap space
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

Как обычно бывает в таких случаях, никакой информации о том, что привело к переполнению памяти, нет.
Иногда стректрейс указывает упавшую операцию, как в первом примере. Но это не означает, что именно она является причиной утечки,
так как когда памяти остаётся совсем мало, любая рядовая операция может завершиться с ошибкой, при этом вся память забита совершено другими объектами.
В данном случае у нас нет стектрейса и придётся проводить полноценное расследование.

Первое, что нужно получить — дамп памяти процесса JVM перед падением. Для этого тест следует запустить с дополнительным параметром:

    -XX:+HeapDumpOnOutOfMemoryError  

Перед тем, как перезапускать тест, установим две утилиты, которые помогут нам в анализе:

Первая — Visual VM. До Java 9 эта утилита входила в состав Oracle JDK.
В более поздних версиях устанавливается отдельно. Позволяет подключиться к процессу с помощью технологии JMX и осуществлять разнообразную диагностику и даже управление.
Например, можно изучать состояние потоков, загрузку процессора и памяти.
Для данного примера я установил плагин VisualGC (Tools->Plugins). С его помощью мы в динамике сможем понаблюдать
за заполнением памяти и состоянием программы в момент возникновения OutOfMemory.
Если процесс запущен локально, то подключение не вызывает проблем, никаких портов специально открывать не нужно.

Вторая утилита — Eclipse Memory Analyzer Tool.
С её помощью мы будем анализировать дамп памяти, чтобы определить вероятные причины проблемы.

Итак, запускаем тест с параметрами «-Xmx50m -XX:+HeapDumpOnOutOfMemoryError», после чего сразу запускаем VisualVM.
В левой панели видим список всех Java процессов, запущенных на локальной машине. Среди них будет IDE, VisualVM и прочие.
Нам нужен com.intellij.rt.JUnitStarter, он добавится в список через пять-десять секунд, выбираем его и подключаемся двойным щелчком левой кнопки мыши.
Теперь открываем вкладку Monitor или VisualGC и ждём, пока программа не упадёт. Очень познавательно наблюдать за работой сборщика мусора вот так наглядно.
Видно, как меняются графики, когда памяти остаётся мало. Таким образом можно исследовать приложение: совершать пользовательские операции
и сразу же визуально оценивать их влияние на потребление ресурсов. В нашем случае никаких операций совершать не нужно, просто ждём две-три минуты.
Процесс завершается с ошибкой, а в корне проекта создаётся файл .hprof (в моем случае, java_pid11712.hprof, 85мб),
размер которого сопоставим с объёмом выделенной памяти. Вот что получилось у меня:

картинки нет, но вы держитесь

картинки нет, но вы держитесь

Следующий шаг — запустить Eclipse MAT, открыть дамп памяти и произвести поиск утечек (File->Open Heap Dump->Leak Suspect).
Логика определения утечек следующая — если в нормальном состоянии работы приложения занято не больше 50-60 процентов от всей выделенной памяти,
то переполнение происходит от того, что какой-то объект или однотипные объекты, накапливающиеся в результате утечки памяти, занимают всё оставшееся место.
По дампу памяти несложно определить что, скажем, вся память забита объектами типа String, но это не очень полезно.
Eclipse MAT же строит дерево на основе ссылок и показывает подозрительные узлы, суммируя память по всем поддеревьям.
Результат анализа дампа нашего теста:

картинки нет, но вы держитесь

картинки нет, но вы держитесь

Видно, что 71% занятой памяти как-то связан с классом org.apache.catalina.session.StandardManager.
Это-ещё не ответ, а лишь подсказка и предположение, которое следует проверить. Но оно очень ценно, без него мы были практически в тупике.

Следующие шаги поиска проблемы будут напрямую зависеть от того, чем занята память, здесь нет общего рецепта.
Может быть, это будет какой-то кэш, который не очищается,
может нагрузка слишком велика и столько одновременных запросов система не способна обработать.

Конечно, в данном случае я точно знал причину заранее. Она следует из принципов работы технологии сервлетов.
На каждый новый запрос, поступающий к приложению, создаётся объект HttpSession, а пользователю в ответе добавляется заголовок, устанавливающий куку
JSESSIONID. За счёт этого работает аутентификация и всевозможное кэширование.
Это особенно характерно для корпоративных приложений, в которых обычно небольшое количество пользователей,
но приличный объём данных и очень сложная бизнес логика. Главная страница открывается долго, потому что загружаются и обрабатываются сложные структуры данных, права, настройки текущего пользователя.
Всё это сохраняется в сессии (наподобие session.setAttribute(«settings», settings)).

Потребовалось 80 000 пустых сессий, чтобы заполнить 50мб памяти. При использовании фреймворка Vaadin одна сессия по слухам требует уже 0.1мб,
ну а в реальном приложении можно и всю память забить данными, сохранёнными в атрибутах одной сессии.
Важно здесь то, что сессия создаётся на каждый запрос и остаётся активной до истечения таймаута, который по умолчанию составляет 30 минут.

Если установить малое значение, то сессии начнут становиться неактивными быстрее, чем переполнится память.
Проверим это предположение и установим таймаут в одну минуту:

    server:
       servlet:
         session:
            timeout: 1

В документации сказано, что значение в секундах, но Tomcat в текущей реализации умножает на 60, поэтому 1 — одна минута.
Время в секундах можно выставить прямо на объекте сессии методом setMaxInactiveInterval.
Перезапускаем тест, память больше не переполняется. Мне ещё потребовалось увеличить память до 150мб (-Xmx150m),
потому что иначе ошибка выпадала быстрее, чем за минуту.
Наглядно:

картинки нет, но вы держитесь

Сессия не создавалась бы вовсе, если бы не вот эта строчка кода, которая имитирует неаккуратное логирование, приводящее к побочным эффектам:

    log.trace("req from {}, session id {}", request.getRemoteHost(), request.getSession().getId());
 

Если её удалить или использовать дополнительный параметр при вызове getSession (request.getSession(false)), то сессия не будет создаваться вообще.
Такое поведение — скорее исключение, потому что как минимум аутентификации хранится в сессии, а уж аутентификация есть практически в любом корпоративном приложении.
Чтобы сервисы не имели состояния (stateless), нужно заранее проектировать систему таким образом, использовать явные настройки и тестировать их.
Например, в Spring Security есть разные конфигурации создания сессии: always, ifRequired, never, stateless. Это отдельная тема.
Здесь же я хотел показать общую схему поиска ошибки:

  1. Воспроизведение OutOfMemoryError
  2. Получение дампа памяти
  3. Диагностика приложения в динамике
  4. Анализ дампа памяти, гипотеза
  5. Проверка гипотезы
  6. Hotfix: Быстрое решение, чтобы починить приложение прямо сейчас
  7. Long term solution: Правильное решение, исправление кода или даже изменение дизайна.

У нас быстрое решение — уменьшить таймаут сессии и, по возможности дать больше памяти процессу. Правильное решение — перейти
на stateless сервис, удалить ошибочное создание сессии без необходимости либо уменьшить размер данных, хранящихся в сессии,
перейдя на специализированные библиотеки кэширования.

Если Вы сами повторяли шаги анализа, то могли заметить, насколько приложение замедляется перед тем, как случается ошибка OutOfMemoryError.
Плата за удобство управления памятью — время, которое требуется сборщику мусора на каждый цикл его работы.
Реализация сборщика мусора G1, которая используется по умолчанию, периодически полностью останавливает выполнение команд.
Это называется Stop-the-world. Другие стандартные реализации поступают так же: Serial, Parallel, CMS.
В обычном состоянии эти паузы малы и практически не влияют на производительность, хотя и не позволяют использовать Java для «real time» приложений.
Но когда существует утечка и свободной памяти остаётся всё меньше, работа сборщика мусора превращается в сизифов труд.
Такое состояние ещё хуже, чем если бы ошибка выпадала сразу.
Иногда JVM удаётся определить подобное состояние и выбрасывается исключение:

    java.lang.OutOfMemoryError: GC overhead limit exceeded

Чаще всего это означает утечку памяти, заполняющую её не очень быстро.
Отдельной темой будет анализ логов сборщика мусора, поиск там событий «Full GC» и так далее.
Приведу лишь ссылку на статью на эту тему.
По моему опыту, эффективный анализ производится по дампам памяти, а логи GC используются, чтобы исключить из рассмотрения
проблемы утечек памяти при анализе проблем с производительностью приложения. Как правило, при достижении верхней границы памяти события
Full GC начинают происходить одно за другим.

В заключении приведу несколько примеров реальных и выдуманных ситуаций, приводящих к переполнению памяти и варианты решения.

Кэширование

Если применяется библиотека кэширования, то следует ограничивать не только максимальное время жизни объектов и их количество,
но и максимальный объём памяти. Тогда переполнения точно не случится, в худшем случае мы получим слишком быстрое удаление объектов из кэша.
Пример конфигурации для Hazelcast:

    <max-size policy="USED_HEAP_SIZE">4096</max-size>

А вот так красиво можно настраивать Apache Geode:

    <gfe:partitioned-region-template id="PartitionRegionTemplate" template="ExtendedRegionTemplate"
                                     copies="1" load-factor="0.70" local-max-memory="1024"
                                     total-max-memory="16384" value-constraint="java.lang.Object">
        

С другой стороны, библиотека JCS, например, не позволяет явно указывать максимальный объём памяти (по крайней мере, я в своё время не нашёл такой конфигурации).
В общем, идея в том, что ограничивать количество объектов имеет смысл, если их размер стабилен, но и тогда придётся считать и исследовать.
А ограничив общее потребление памяти, легко подобрать конфигурацию так, чтобы суммарно весь кэш занимал предсказуемый объём памяти и не приводил к переполнению.

Неожиданно большие коллекции в базе данных

При использовании в JPA отношений OneToMany или ManyToMany, когда загрузка одного объекта из базы влечет за собой загрузку списка связанных объектов,
существует два варианта загрузки: ленивый и жадный (Lazy, Eager). При разумном подходе все коллекции либо, по крайней мере, большую часть помечают как Lazy.
Это означает, что они не будут инициализироваться без необходимости.
Но для удобства часть коллекций можно оставить как Eager. Подобные коллекции могут привести к переполнению памяти,
если их размер со временем разрастется в связи со спецификой нагрузки на продакшне.
Другой случай — коллекция действительно нужна в конкретном сценарии использования.
Скажем, мы показываем администратору неудачные попытки ввода пароля за сутки на одной странице.
Обычно их 10 — 100. Код может работать отлично, пока это предположение не нарушено, но в один прекрасный день в результате какой-нибудь DDOS атаки или просто ошибки в коде,
коллекция вырастает до 500 000 элементов, и приложение закономерно падает с OutOfMemoryError.
На практике лучше избегать предположений по размеру коллекций.
Возможно, для этого придётся поменять бизнес требования, реализовывать постраничное отображение информации или ограничения (constraints) на уровне базы данных.

Неожиданно большие файлы

Если входные данные от сторонних систем поступают в виде файлов, нужно избегать полной загрузки файла в память.
Скорее даже не самого файла, а объектной модели, соответствующей его содержимому.
Для большинства форматов данных (например, JSON, XML, CSV) существуют способы потоковой обработки информации
без отказа от удобной объектной модели.

Утечки в сторонних библиотеках

Они — возможны, но намного реже утечек в коде приложения. Такие проблемы исследовать очень тяжело.
Если на основе анализа в Eclipse MAT появляется гипотеза о некорректном поведении библиотеки, то чтобы её проверить может потребоваться разработка
синтетического приложения, которое отражает только один сценарий работы приложения, связанный с этой библиотекой, а всё остальное из него исключено.
Впрочем, этот подход справедлив для исследования любых подозрений на ошибки в сторонних библиотеках, не только об утечках памяти.

Заключение

Причины ошибки переполнения памяти слишком разнообразны, чтобы можно было предложить универсальное решение.
Однако разумный подход и знание типовых ошибок во многом спасает.

Понравилась статья? Поделить с друзьями:
  • Ошибка память не может reading
  • Ошибка память переполнена как исправить
  • Ошибка памяти черного картриджа hp laserjet m1120 mfp
  • Ошибка память не может быть реад что это
  • Ошибка память не может быть прочитана что делать