优秀的编程知识分享平台

网站首页 > 技术文章 正文

Java中常见OutOfMemoryError问题原因和处理方式

nanyue 2024-10-12 05:45:36 技术文章 8 ℃

在 Java 中,内存管理对于程序的高效执行至关重要。有两种主要类型的内存:栈内存和堆内存。

栈内存:栈内存用于管理方法调用和存储具有固定生存期的局部变量。它遵循后进先出 (LIFO) 结构,内存分配非常高效。可以在这里分配静态变量,例如整数和对象引用。方法调用和局部变量也在栈内存中管理。

堆内存:堆内存是用于动态内存分配的独立区域。这是具有不同生命周期的对象和数组所在的位置。堆中的内存由垃圾收集器管理,它会自动从不再使用的对象中回收内存,从而防止内存泄漏。字符串和数组等对象通常分配在堆内存中。例如,当我们使用“new”关键字创建对象或数组时,它会在堆内存中分配。

什么是“内存泄漏”

内存泄漏指的是JVM中某些不再需要使用的对象,仍然存活于JVM中而不能及时释放而导致内存空间的浪费。Java中内存泄漏的原因有多种,这些众多的因素会导致Java程序产生不同类型的内存泄漏,随着时间的推移,内存泄漏会使程序增加额外的内存资源占用,从而导致程序性能下降甚至崩溃。

什么是“OutOfMemoryError”

OutOfMemoryError 是当堆空间已满时发生的运行时错误。当 Java 虚拟机 (JVM) 无法在堆上分配更多内存来容纳新对象时,就会发生该错误。此错误通常由以下原因引起内存消耗过多,可能是由于应用程序内存使用效率低下,或者是堆空间配置不足。

示例代码:

Bash
public class App  {


    public static void main(String[] args) throws Exception {
           App app = new App();
           app.generateOOM();
    }
    public void generateOOM() throws Exception {
        int iteratorValue = 20;
        System.out.println("\n=================> OOM test started..\n");
        for (int outerIterator = 1; outerIterator < 20; outerIterator++) {
            System.out.println("Iteration " + outerIterator + " Free Mem: " + Runtime.getRuntime().freeMemory());
            int loop1 = 2;
            int[] memoryFillIntVar = new int[iteratorValue];
            // feel memoryFillIntVar array in loop..
            do {
                memoryFillIntVar[loop1] = 0;
                loop1--;
            } while (loop1 > 0);
            iteratorValue = iteratorValue * 5;
            System.out.println("\nRequired Memory for next loop: " + iteratorValue);
            Thread.sleep(1000);
        }
    }
}

运行结果:

Bash
=================> OOM test started..

Iteration 1 Free Mem: 130023424

Required Memory for next loop: 100
Iteration 2 Free Mem: 130023424

Required Memory for next loop: 500
Iteration 3 Free Mem: 130023424

Required Memory for next loop: 2500
Iteration 4 Free Mem: 130023424

Required Memory for next loop: 12500
Iteration 5 Free Mem: 130023424

Required Memory for next loop: 62500
Iteration 6 Free Mem: 130023424

Required Memory for next loop: 312500
Iteration 7 Free Mem: 129773408

Required Memory for next loop: 1562500
Iteration 8 Free Mem: 127676256

Required Memory for next loop: 7812500
Iteration 9 Free Mem: 121384800

Required Memory for next loop: 39062500
Iteration 10 Free Mem: 89927520

Required Memory for next loop: 195312500
Iteration 11 Free Mem: 127849360

Required Memory for next loop: 976562500
Iteration 12 Free Mem: 125833224
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at com.xuetangwan.App.generateOOM(App.java:49)
        at com.xuetangwan.App.main(App.java:39)

如何捕获 java.lang.OutOfMemoryError 异常

要捕获OutOfMemoryError,我们只需要用try-catch块包含会导致内存问题的代码,如下所示:

public class JavaHeapSpace {
  public static void main(String[] args) throws Exception {
    try {
      String[] array = new String[100000 * 100000];
    } catch (OutOfMemoryError oom) {
      System.out.println("OutOfMemory Error appeared");
    }
  }
}

执行上述代码不会导致OutOfMemoryError ,而是会打印以下内容:

OutOfMemory Error appeared

OutOfMemoryError 的原因及解决方案

原因1:加载大数据集:

  • 场景:当我们的应用程序将大型数据集加载到内存中时,例如读取大型文件或处理大量数据库。
  • 解决方案:使用流式处理或分页来处理较小块的数据,而不是一次加载所有内容。流式处理允许我们在处理大型数据集时,不需要立即将整个数据集加载到内存中。这对于正在处理非常大的数据集或者在内存资源有限的计算机上运行应用程序特别有用。
  • 示例代码:
// Example: Reading a large file using streaming
try (BufferedReader reader = new BufferedReader(new FileReader("large_file.txt"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        // Process the line
    }
} catch (IOException e) {
    e.printStackTrace();
}

原因 2:创建大对象

创建大型数组可能导致效率低下,并且可能进一步导致内存问题。这是因为 Java 虚拟机 (JVM) 必须为整个数组分配连续的内存块。如果数组非常大,JVM 可能没有足够的连续可用内存。这可能会导致 JVM 抛出 OutOfMemoryError 异常。

解决方案:以下是优化对象创建的一些技巧:

尽可能使用不可变对象:

  • 不可变对象在创建后无法修改,这使得它们本质上是线程安全且高效的。
  • 不可变对象减少了同步的需要,并且可以在线程之间安全地共享。
  • 例如,Java中的String类是不可变的。

使用高效的数据结构:

  • 选择适合的数据结构,可以最大限度地减少内存使用并提高性能。
  • 例如,当需要动态数组时用ArrayList、HashMap用于键值对以及作为唯一元素的集合时使用HashSet。
// Example of using efficient data structures
List<Integer> numbers = new ArrayList<>(); // Use ArrayList for a dynamic list
Map<String, Integer> scoreMap = new HashMap<>(); // Use HashMap for key-value pairs
Set<String> uniqueNames = new HashSet<>(); // Use HashSet for unique elements

Java中,字符串是不可变的,这意味着字符串一旦创建就无法修改。任何看似修改字符串的操作(例如串联)实际上都会创建一个新字符串。因此,在循环中使用 “+”重复连接字符串时,实际上会在每次迭代中创建一个新的字符串对象。

相反StringBuilder 是可变的。当我们在循环中追加或修改 StringBuilder的内容时,实际上正在使用相同的底层对象,从而避免创建不必要的中间字符串。

循环中使用StringBuilder代替字符串连接“+”是一种节省内存和提高性能的做法。它通过避免创建多个中间字符串对象来减少内存开销,从而允许就地修改字符串。

当在循环内连接字符串或处理大量字符串操作时,这种方法特别有用。

// Inefficient: Creates multiple intermediate string objects
/*As a result, you end up with 1000 intermediate string objects in memory, 
each representing the concatenation of "value" with a different integer i*/
String result = "";
for (int i = 0; i < 1000; i++) {
    result += "value" + i;
}

// Efficient: Uses a single StringBuilder, minimizing object creation
/**/
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    builder.append("value").append(i);
}
String result = builder.toString();

原因3:内存泄漏

  • 场景:当对象没有正确释放时,就会发生内存泄漏,导致堆逐渐填满。
  • 解决方案:通过确保不再需要对象时不再引用它们来识别和修复内存泄漏。分析器等工具可以帮助识别内存泄漏。

原因4:堆空间不足

  • 场景:当 JVM 的堆空间没有使用-Xmx选项配置足够的内存时。
  • 解决方案:如果我们的应用程序确实需要更多内存,可以增加堆空间分配。但是,请注意不要将其设置得太高,因为这可能会导致性能问题。
java -Xmx512m YourApp

原因 4:垃圾收集 (GC) 开销

  • 场景:频繁的垃圾收集会消耗 CPU 时间并降低应用程序的速度,尤其是当堆接近满时。当垃圾收集器频繁运行时,它可能会暂停应用程序线程,从而影响程序的响应能力和性能。
  • 解决方案: 调整 JVM 的垃圾收集设置(例如-XX:MaxGCPauseMillis)以平衡内存使用和收集频率。
java -XX:MaxGCPauseMillis=50 -jar YourApp.jar

在本例中,我们将最大 GC 暂停时间设置为 50 毫秒。垃圾收集器将尝试通过调整其策略将暂停时间保持在此限制内。

此外,我们还可以选择适合应用程序需求的垃圾收集算法。例如,G1(垃圾优先)收集器旨在更好地控制暂停时间,对于低延迟至关重要的应用程序来说通常是一个不错的选择。

java -XX:+UseG1GC -jar YourApp.jar
最近发表
标签列表