关于如何最好地处理这个问题,我听到了非常矛盾的事情,并且陷入了以下困境:
- OOME 会关闭线程,但不会关闭整个应用程序
- 我需要关闭整个应用程序但不能因为线程没有任何内存
我一直理解最佳实践是让它们离开,这样 JVM 就会死掉,因为此时 JVM 处于不一致的状态,但这似乎在这里不起作用。
关于如何最好地处理这个问题,我听到了非常矛盾的事情,并且陷入了以下困境:
我一直理解最佳实践是让它们离开,这样 JVM 就会死掉,因为此时 JVM 处于不一致的状态,但这似乎在这里不起作用。
OutOfMemoryError
就像任何其他错误一样。如果它从中逃脱,Thread.run()
将导致线程死亡。而已。此外,当线程死亡时,它不再是 GC 根,因此仅由该线程保留的所有引用都符合垃圾收集的条件。这意味着 JVM 很可能会从 OOME 中恢复。
如果你想杀死你的JVM,因为你怀疑它可能处于不一致的状态,把它添加到你的java
选项中:
-XX:OnOutOfMemoryError="kill -9 %p"
%p
是当前的 Java 进程 PID 占位符。其余的不言自明。
当然你也可以尝试OutOfMemoryError
以某种方式捕捉和处理它。但这很棘手。
在 Java 版本 8u92 中,VM 参数
-XX:+ExitOnOutOfMemoryError
-XX:+CrashOnOutOfMemoryError
已添加,请参阅发行说明。
ExitOnOutOfMemoryError
启用此选项后,JVM 将在第一次出现内存不足错误时退出。如果您更喜欢重新启动 JVM 实例而不是处理内存不足错误,则可以使用它。CrashOnOutOfMemoryError
如果启用此选项,当发生内存不足错误时,JVM 会崩溃并生成文本和二进制崩溃文件。
增强请求:JDK-8138745(参数命名错误,虽然 JDK-8154713,ExitOnOutOfMemoryError
而不是ExitOnOutOfMemory
)
在 8u92 版本中,Oracle JDK 中现在有一个 JVM 选项,可以在发生 OutOfMemoryError 时使 JVM 退出:
从发行说明:
ExitOnOutOfMemoryError - 启用此选项时,JVM 在第一次出现内存不足错误时退出。如果您更喜欢重新启动 JVM 实例而不是处理内存不足错误,则可以使用它。
一旦发生错误,您可以通过多种方式强制您的程序终止。就像其他人建议的那样,如果需要,您可以捕获错误并在之后执行 System.exit。但我建议你也使用 -XX:+HeapDumpOnOutOfMemoryError,这样一旦事件产生,JVM 就会创建一个包含应用程序内容的内存转储文件。您将使用配置文件,我建议您使用 Eclipse MAT 来研究图像。这样,您将很快找到问题的原因,并做出适当的反应。如果您不使用 Eclipse,您可以将 Eclipse MAT 用作独立产品,请参阅:http ://wiki.eclipse.org/index.php/MemoryAnalyzer 。
如果您想关闭您的程序,请查看命令行上的-XX:OnOutOfMemoryError="<cmd args>;<cmd args>"
(在此处记录)选项。只需将其指向您的应用程序的终止脚本。
一般来说,我从来没有运气在不重新启动应用程序的情况下优雅地处理此错误。总会有某种特殊情况发生,所以我个人建议确实停止您的应用程序,但要调查问题的根源。
我建议从应用程序中处理所有未捕获的异常,以确保它在终止之前尝试为您提供最佳数据。然后有一个外部脚本,当它崩溃时重新启动你的进程。
public class ExitProcessOnUncaughtException implements UncaughtExceptionHandler
{
static public void register()
{
Thread.setDefaultUncaughtExceptionHandler(new ExitProcessOnUncaughtException());
}
private ExitProcessOnUncaughtException() {}
@Override
public void uncaughtException(Thread t, Throwable e)
{
try {
StringWriter writer = new StringWriter();
e.printStackTrace(new PrintWriter(writer));
System.out.println("Uncaught exception caught"+ " in thread: "+t);
System.out.flush();
System.out.println();
System.err.println(writer.getBuffer().toString());
System.err.flush();
printFullCoreDump();
} finally {
Runtime.getRuntime().halt(1);
}
}
public static void printFullCoreDump()
{
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("\n"+
sdf.format(System.currentTimeMillis())+"\n"+
"All Stack Trace:\n"+
getAllStackTraces()+
"\nHeap\n"+
getHeapInfo()+
"\n");
}
public static String getAllStackTraces()
{
String ret="";
Map<Thread, StackTraceElement[]> allStackTraces = Thread.getAllStackTraces();
for (Entry<Thread, StackTraceElement[]> entry : allStackTraces.entrySet())
ret+=getThreadInfo(entry.getKey(),entry.getValue())+"\n";
return ret;
}
public static String getHeapInfo()
{
String ret="";
List<MemoryPoolMXBean> memBeans = ManagementFactory.getMemoryPoolMXBeans();
for (MemoryPoolMXBean mpool : memBeans) {
MemoryUsage usage = mpool.getUsage();
String name = mpool.getName();
long used = usage.getUsed();
long max = usage.getMax();
int pctUsed = (int) (used * 100 / max);
ret+=" "+name+" total: "+(max/1000)+"K, "+pctUsed+"% used\n";
}
return ret;
}
public static String getThreadInfo(Thread thread, StackTraceElement[] stack)
{
String ret="";
ret+="\n\""+thread.getName()+"\"";
if (thread.isDaemon())
ret+=" daemon";
ret+=
" prio="+thread.getPriority()+
" tid="+String.format("0x%08x", thread.getId());
if (stack.length>0)
ret+=" in "+stack[0].getClassName()+"."+stack[0].getMethodName()+"()";
ret+="\n java.lang.Thread.State: "+thread.getState()+"\n";
ret+=getStackTrace(stack);
return ret;
}
public static String getStackTrace(StackTraceElement[] stack)
{
String ret="";
for (StackTraceElement element : stack)
ret+="\tat "+element+"\n";
return ret;
}
}
一般来说,您永远不应该编写一个 catch 块来捕获java.lang.Error
或其任何子类,包括OutOfMemoryError
. 唯一的例外是,如果您使用的是第三方库,该库会抛出一个自定义子类,说明Error
他们应该何时拥有 subclassed RuntimeException
。不过,这实际上只是解决他们代码中的错误的方法。
来自JavaDoc的java.lang.Error
:
Error 是 Throwable 的子类,表示合理的应用程序不应尝试捕获的严重问题。
如果您的应用程序在其中一个线程因 OOME 而死后仍继续运行时遇到问题,您有几个选择。
首先,您可能想检查是否可以将剩余线程标记为守护线程。如果在某个时刻只有守护线程保留在 JVM 中,它将运行所有关闭挂钩并尽可能有序地终止。为此,您需要setDaemon(true)
在线程对象启动之前调用它。如果线程实际上是由框架或其他代码创建的,您可能必须使用不同的方法来设置该标志。
另一种选择是为有问题的线程分配一个未捕获的异常处理程序,并在System.exit()
绝对必要时调用Runtime.getRuntime().halt()
。调用停止是非常危险的,因为关闭挂钩甚至不会尝试运行,但在某些情况下,停止可能会在 System.exit 失败的情况下工作,如果已经抛出 OOME。
由于 JVM 选项
-XX:+ExitOnOutOfMemoryError
-XX:+CrashOnOutOfMemoryError
-XX:OnOutOfMemoryError=...
OutOfMemoryError
如果由于线程耗尽而发生这种情况(请参阅相应的 JDK 错误报告),则不工作,可能值得尝试工具jkill。它通过JVMTI注册并在内存或可用线程耗尽时退出 VM。
在我的测试中,它按预期工作(以及我期望 JVM 选项如何工作)。
您可以使用 OOME 的 try catch 包围您的线程代码,并在发生此类事件时进行一些手动清理。一个技巧是让你的线程函数只是对另一个函数的尝试捕获。发生内存错误时,它应该在堆栈上释放一些空间,以便您进行一些快速删除。如果您在捕获和/或设置垂死标志以告诉其他线程退出后立即对某些资源执行垃圾收集请求,这应该可以工作。
一旦带有 OOME 的线程死亡并且您对其元素进行了一些收集,您应该有足够的可用空间让其他线程以有序的方式退出。这是一个更优雅的退出,也有机会在死前记录问题。