所有你要执行的应用程序都需要内存。如果应用程序是用汇编语言开发的,这并不重要。或者如果你使用的是像C这样的低级编程语言或像Java这样被编译成字节码的语言。运行应用程序需要为代码本身、变量和代码处理的数据提供内存。根据你的使用情况,内存要求会有所不同。有些程序需要的内存非常少--例如,一个旨在处理小型文本文件的简单应用;其他程序则需要数千兆字节的内存,因为它们在内存中处理的数据量很大。
在Java中,至少在最初,你可以忘记内存的问题。你创建对象,使用它们,然后不去管它们。最终,Java的垃圾收集器(GC)会释放未使用的对象所占用的内存,并释放已使用的内存。然而,你能同时保留在内存中的数据量总是有限的,这就是堆的大小。
堆是Java虚拟机保存应用程序所需数据的地方。堆不是无限的--你在应用程序启动时控制它,你不能在内存中保留超过它允许的对象。如果堆已经满了,而你又多创建了一个对象,你可能会收到OutOfMemory错误。
在这篇博文中,我将告诉你什么是Java OutOfMemory错误,是什么原因造成的,以及如何处理它们。
什么是Java中的OutOfMemoryError:原因和解决方法
java.lang.OutOfMemoryError意味着应用程序出了问题--准确地说,是应用程序内存的某个部分出了问题。要比这更具体,我们需要深入了解Java虚拟机会出现内存不足的原因,而每一个原因都是不同的。
Java堆空间
当涉及到Java虚拟机世界中的内存处理时,Java Heap空间是最常见的错误之一。这个错误意味着你试图在JVM进程的堆上保留太多的数据,没有足够的内存来创建新的对象,而且垃圾收集器不能收集足够的垃圾。这种情况可能由于各种原因而发生--例如,你可能试图将过大的文件加载到应用程序的内存中,或者你保留了对对象的引用,尽管你并不需要这些数据。
基本上,java.lang.OutOfMemoryError Java堆空间告诉你,你的应用程序的堆不够大,或者你做错了什么,或者你有一个旧的、好的Java内存泄漏。
这里有一个例子,说明了Java堆空间的问题:
public class JavaHeapSpace {
public static void main(String[] args) throws Exception {
String[] array = new String[100000 * 100000];
}
}
代码试图创建一个字符串对象的数组,这个数组相当大。而这就是了。在内存大小的默认设置下,执行上述代码时你应该看到以下结果:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at memory.JavaHeapSpace.main(JavaHeapSpace.java:5)
结果很简单--堆上没有足够的内存来分配数组,因此JVM抛出一个错误,告知我们这一点。
如何解决这个问题:在某些情况下,为了缓解这个问题,只要在JVM应用程序的启动设置中加入-Xmx,并将其设置为更大的数值,就可以增加最大堆的大小。例如,为了将最大堆大小增加到8GB,你可以在你的JVM应用程序启动参数中添加-Xmx8g参数。然而,如果你有一个内存泄漏,你最终会看到错误再次出现。这意味着你需要翻阅应用程序代码,寻找可能发生内存问题的地方。像Java剖析器这样的工具和好的、老的堆转储将帮助你做到这一点。
GC开销限制
GC Overhead Limit就像它的名字一样--Java虚拟机的垃圾收集器无法回收内存的问题。你会看到 java.lang.OutOfMemoryError。如果Java虚拟机在垃圾收集中花费超过98%的时间,连续进行5次垃圾收集,并且可以回收不到2%的堆,就超过了GC开销限制。
当使用旧的Java版本,即使用旧的垃圾收集实现(如Java 8)时,你可以通过运行类似于下面的代码来轻松模拟GC Overhead异常:
public class GCOverhead {
public static void main(String[] args) throws Exception {
Map<Long, Long> map = new HashMap<>();
for (long i = 0l; i < Long.MAX_VALUE; i++) {
map.put(i, i);
}
}
}
当运行一个小的堆时,比如25MB,你会得到这样的一个异常:
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.base/java.lang.Long.valueOf(Long.java:1211)
at memory.GCOverhead.main(GCOverhead.java:10)
这意味着堆几乎满了,而且垃圾收集器至少连续花了5次垃圾收集,删除了不到2%的分配对象。
如何解决这个问题:对于这样的错误,可能的解决办法是通过在JVM应用程序的启动设置中添加-Xmx来增加堆,并将其设置为比目前使用的更大的值。
阵列大小的限制
你可能遇到的错误之一是 java.lang.OutOfMemoryError: Requested array size exceeds VM limit
,它指出你试图保存在内存中的数组的大小大于Integer.MAX_INT
,或者你试图拥有一个大于堆大小的数组。
如何解决这个问题:如果你的数组大于你的堆大小,你可以尝试增加堆的大小。如果你试图把超过2^31-1的条目放入一个数组,你需要修改你的代码以避免这种情况。
线程的数量问题
当涉及到你可以在一个应用程序内运行的线程数量时,操作系统有限制。当你看到运行你的代码的Java虚拟机抛出一个java.lang.OutOfMemoryError: unable to create native thread
的错误时,你可以肯定,你碰到了限制,或者你的操作系统没有资源来创建一个新的线程了。基本上,一个新的线程没有在操作系统层面上创建,错误发生在Java本地接口或本地方法本身。
为了说明创建线程的问题,让我们创建一个持续创建线程并使其进入睡眠的代码。比如说像这样:
public class ThreadsLimits {
public static void main(String[] args) throws Exception {
while (true) {
new Thread(
new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000 * 60 * 60 * 24);
} catch (Exception ex) {}
}
}
).start();
}
}
}
就在你运行上述代码后,你可以期待一个异常被抛出:
[0.420s][warning][os,thread] Failed to start thread - pthread_create failed (EAGAIN) for attributes: stacksize: 1024k, guardsize: 4k, detached.
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached
at java.base/java.lang.Thread.start0(Native Method)
at java.base/java.lang.Thread.start(Thread.java:802)
at memory.ThreadsLimits.main(ThreadsLimits.java:15)
我们可以清楚地看到,我们的Java代码用尽了操作系统的限制,无法创建更多线程。
为了诊断这个问题,我们建议参考Java文档的相应部分。例如,Java 17文档包括一个名为 "基于操作系统的故障排除工具"的部分,其中提到了可以帮助你发现问题的工具。
PermGen问题
PermGen或永久生成是Java堆中的一个特殊位置,Java虚拟机用它来跟踪所有加载的类元数据、静态方法、对静态对象的引用和原始变量。PermGen随着Java 8的发布而被移除,所以在这一点上,你可能永远不会碰到它的问题。
PermGen的问题在于其有限的默认大小--在32位Java虚拟机版本中为64MB,在64位JVM版本中高达82MB。这是有问题的,因为如果你的应用程序包含大量的类、静态方法和对静态对象的引用,你很容易陷入PermGen空间太小的问题。
如何解决这个问题。如果你曾经遇到过java.lang.OutOfMemoryError: PermGen space
,你可以通过包括-XX:PermSize和**-XX:MaxPermSize**JVM参数来增加PermGen空间的大小。
元空间问题
随着PermGen空间的移除,类的元数据现在住在本地空间中。保存类元数据的空间现在被称为Metaspace,是Java虚拟机堆的一部分。然而,这个区域仍然是有限的,如果你有很多类,就会被耗尽。
如何解决这个问题。 Metaspace区域的问题是由Java虚拟机在抛出java.lang.OutOfMemoryError: Metaspace
错误时发出的信号。为了缓解这个问题,你可以通过在你的Java应用程序的启动参数中添加-XX:MaxMetaspaceSize
标志来增加Metaspace的大小。例如,要将Metaspace区域大小设置为128M,你可以添加以下参数:-XX:MaxMetaspaceSize=128m
。
交换空间不足
你的操作系统使用交换空间作为辅助内存来处理内存管理方案的 分页过程。当本地内存--包括RAM和交换空间--接近耗尽时,Java虚拟机可能没有足够的空间来创建新对象。这可能由于各种原因而发生--你的系统可能过载,其他应用程序可能是内存的重度使用者,并且正在耗尽资源。在这种情况下,JVM会抛出java.lang.OutOfMemoryError: Out of swap space
错误,这意味着原因是操作系统方面的问题。
**如何解决这个问题。**确切的异常堆栈通常有助于缓解该错误,因为它将包括JVM试图分配的内存量以及这样做的代码。当这个错误发生时,你可以期待你的Java虚拟机创建一个文件,详细描述发生了什么。你可能还想检查一下你的操作系统交换设置,如果太低的话就增加它。同时,你需要验证是否有其他重度内存消耗者与你的应用程序在同一台机器上运行。
如何捕捉java.lang.OutOfMemoryError 异常
Java可以选择捕捉异常并优雅地处理它们。例如,你可以捕获FileNotFoundException,当你试图处理一个不存在的文件时,它可能会被抛出。对于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的地方。一定要避免在刚开始执行的主方法中捕捉到上述错误。如果你不了解Java中的异常处理,请阅读我们的博文,了解更多关于如何处理OutOfMemoryError和其他类型的Java错误。
用Sematext监控和分析Java OutOfMemoryError
为了确保你的业务流程有一个健康的环境,你需要确保在运行你的Java应用程序时不会错过任何可能由内存问题引起的错误。这意味着你需要密切关注你的Java应用程序产生的日志,并设置相关事件的警报--OutOfMemoryError的事件。你可以通过使用Sematext Logs来实现这一切--一个智能且易于使用的日志集中化解决方案,让你在一个地方获得所有需要的信息,创建警报并在处理内存问题时主动出击。
你可以在我们关于当今最好的日志管理软件、日志分析工具和云日志服务的博客文章中,了解更多关于Sematext Logs以及它与类似解决方案的比较。
总结
Java中每个与内存有关的错误都是不同的,我们需要采取的解决方法也是不同的。首先,最重要的是理解。要知道什么需要修复,我们需要了解发生了什么样的错误,什么时候发生的,以及最后为什么发生。这些信息对于采取适当的反应和修复作为错误根源的基本问题至关重要。
这就是日志管理工具,如Sematext Logs发挥作用的地方。有一个地方,你可以看到所有的异常情况并对其进行分析,这是无价的。Sematext Logs是Sematext Cloud的一部分,是一个多合一的可观察性解决方案,具有Java监控集成和JVM垃圾收集器日志功能。所有这些结合起来给你一个单一的平台,让你可以把所有必要的指标联系起来。