是否有可能追踪到哪个表达式导致了 NPE?
Is it possible to track down which expression caused an NPE?
当我得到一个 NPE 时,我将得到一个带有行号的堆栈跟踪。
这很有用,但如果行非常密集 and/or 包含嵌套表达式,仍然无法找出哪个引用为空。
当然,此信息一定在某处可用。
有办法解决这个问题吗? (如果不是 java 表达式,那么至少导致 NPE 的字节码指令也会有所帮助)
编辑#1:我看到一些评论建议打断这条线等等,这没有冒犯,实际上是非建设性和无关紧要的。如果我能那样做,我会的!只是说这种修改源代码是不可能的。
编辑 #2:apangin 在下面发布了一个很好的答案,我接受了。但是对于不想自己尝试的人来说,我不得不在这里包含输出太酷了! ;)
假设我有这个驱动程序TestNPE.java
1 public class TestNPE {
2 public static void main(String[] args) {
3 int n = 0;
4 String st = null;
5
6 System.out.println("about to throw NPE");
7 if (n >= 0 && st.isEmpty()){
8 System.out.println("empty");
9 }
10 else {
11 System.out.println("othereise");
12 }
13 }
14
15 }
字节码如下所示(仅显示 main() 方法并省略其他不相关的部分)
Code:
stack=2, locals=3, args_size=1
0: iconst_0
1: istore_1
2: aconst_null
3: astore_2
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String about to throw NPE
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: iload_1
13: iflt 34
16: aload_2
17: invokevirtual #5 // Method java/lang/String.isEmpty:()Z
20: ifeq 34
23: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
26: ldc #6 // String empty
28: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
31: goto 42
34: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
37: ldc #7 // String othereise
39: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
42: return
现在当你 运行 带有代理的 TestNPE 驱动程序时,你会得到这个
$ java -agentpath:libRichNPE.o TestNPE
about to throw NPE
Exception in thread "main" java.lang.NullPointerException: location=17
at TestNPE.main(TestNPE.java:7)
因此指向偏移量 17 处的 invokevirtual #5!只是 那有多酷?
异常本身没有足够的信息来提供超过行号。
我看到的一个选择是使用字节码调试器(如字节码可视化工具)来更接近地定位导致 npe 的字节码指令。一直往前走,直到出现异常,或者给npe加个断点。
堆栈跟踪机制依赖于选择性编译到每个 class 中的调试元数据(即 SourceFile 和 LineNumberTable 属性)。据我所知,字节码偏移量不会在任何地方保留。但是,这些对于典型的 Java 程序没有用,因为您仍然知道每个字节码指令对应的代码。
但是,有一个明显的解决方法 - 只需将有问题的代码分成多行并重新编译!您几乎可以在 Java.
中的任何位置插入空格
当异常发生时,JVM 知道导致异常的原始字节码。但是,StackTraceElement
不跟踪字节码索引。
解决方案是每当发生异常时使用JVMTI捕获字节码索引。
以下示例 JVMTI 代理将拦截所有异常,如果异常类型为 NullPointerException
,代理将用字节码位置信息替换其 detailMessage
。
#include <jvmti.h>
#include <stdio.h>
static jclass NullPointerException;
static jfieldID detailMessage;
void JNICALL VMInit(jvmtiEnv* jvmti, JNIEnv* env, jthread thread) {
jclass localNPE = env->FindClass("java/lang/NullPointerException");
NullPointerException = (jclass) env->NewGlobalRef(localNPE);
jclass Throwable = env->FindClass("java/lang/Throwable");
detailMessage = env->GetFieldID(Throwable, "detailMessage", "Ljava/lang/String;");
}
void JNICALL ExceptionCallback(jvmtiEnv* jvmti, JNIEnv* env, jthread thread,
jmethodID method, jlocation location, jobject exception,
jmethodID catch_method, jlocation catch_location) {
if (env->IsInstanceOf(exception, NullPointerException)) {
char buf[32];
sprintf(buf, "location=%ld", (long)location);
env->SetObjectField(exception, detailMessage, env->NewStringUTF(buf));
}
}
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* vm, char* options, void* reserved) {
jvmtiEnv* jvmti;
vm->GetEnv((void**)&jvmti, JVMTI_VERSION_1_0);
jvmtiCapabilities capabilities = {0};
capabilities.can_generate_exception_events = 1;
jvmti->AddCapabilities(&capabilities);
jvmtiEventCallbacks callbacks = {0};
callbacks.VMInit = VMInit;
callbacks.Exception = ExceptionCallback;
jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks));
jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_INIT, NULL);
jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION, NULL);
return 0;
}
将其编译到共享库中并 运行 java 使用 -agentpath
选项:
java -agentpath:/pato/to/libRichNPE.so Main
您可以将复杂的行分解成许多您可以跟踪的更小的行,或者使用调试器查看异常发生时 null
的值。
虽然您可以尝试查看发生这种情况的字节码,但这只是复杂旅程的开始。我建议让你的代码更容易理解,你可能会计算出哪些值可以是 null
(注意:它可能是 null
除非你知道这是不可能的)
JEP 358: Helpful NullPointerExceptions在OpenJDK 14中加入了这样的特性,默认是禁用的;您必须指定 -XX:+ShowCodeDetailsInExceptionMessages
才能启用它。有了它,您的示例结果为:
Exception in thread "main"
java.lang.NullPointerException: Cannot invoke "String.isEmpty()" because "st" is null
at TestNPE.main(TestNPE.java:7)
类 无需重新编译即可利用此功能。它最初是为 SAP JVM.
开发的
当我得到一个 NPE 时,我将得到一个带有行号的堆栈跟踪。 这很有用,但如果行非常密集 and/or 包含嵌套表达式,仍然无法找出哪个引用为空。
当然,此信息一定在某处可用。 有办法解决这个问题吗? (如果不是 java 表达式,那么至少导致 NPE 的字节码指令也会有所帮助)
编辑#1:我看到一些评论建议打断这条线等等,这没有冒犯,实际上是非建设性和无关紧要的。如果我能那样做,我会的!只是说这种修改源代码是不可能的。
编辑 #2:apangin 在下面发布了一个很好的答案,我接受了。但是对于不想自己尝试的人来说,我不得不在这里包含输出太酷了! ;)
假设我有这个驱动程序TestNPE.java
1 public class TestNPE {
2 public static void main(String[] args) {
3 int n = 0;
4 String st = null;
5
6 System.out.println("about to throw NPE");
7 if (n >= 0 && st.isEmpty()){
8 System.out.println("empty");
9 }
10 else {
11 System.out.println("othereise");
12 }
13 }
14
15 }
字节码如下所示(仅显示 main() 方法并省略其他不相关的部分)
Code:
stack=2, locals=3, args_size=1
0: iconst_0
1: istore_1
2: aconst_null
3: astore_2
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String about to throw NPE
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: iload_1
13: iflt 34
16: aload_2
17: invokevirtual #5 // Method java/lang/String.isEmpty:()Z
20: ifeq 34
23: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
26: ldc #6 // String empty
28: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
31: goto 42
34: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
37: ldc #7 // String othereise
39: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
42: return
现在当你 运行 带有代理的 TestNPE 驱动程序时,你会得到这个
$ java -agentpath:libRichNPE.o TestNPE
about to throw NPE
Exception in thread "main" java.lang.NullPointerException: location=17
at TestNPE.main(TestNPE.java:7)
因此指向偏移量 17 处的 invokevirtual #5!只是 那有多酷?
异常本身没有足够的信息来提供超过行号。
我看到的一个选择是使用字节码调试器(如字节码可视化工具)来更接近地定位导致 npe 的字节码指令。一直往前走,直到出现异常,或者给npe加个断点。
堆栈跟踪机制依赖于选择性编译到每个 class 中的调试元数据(即 SourceFile 和 LineNumberTable 属性)。据我所知,字节码偏移量不会在任何地方保留。但是,这些对于典型的 Java 程序没有用,因为您仍然知道每个字节码指令对应的代码。
但是,有一个明显的解决方法 - 只需将有问题的代码分成多行并重新编译!您几乎可以在 Java.
中的任何位置插入空格当异常发生时,JVM 知道导致异常的原始字节码。但是,StackTraceElement
不跟踪字节码索引。
解决方案是每当发生异常时使用JVMTI捕获字节码索引。
以下示例 JVMTI 代理将拦截所有异常,如果异常类型为 NullPointerException
,代理将用字节码位置信息替换其 detailMessage
。
#include <jvmti.h>
#include <stdio.h>
static jclass NullPointerException;
static jfieldID detailMessage;
void JNICALL VMInit(jvmtiEnv* jvmti, JNIEnv* env, jthread thread) {
jclass localNPE = env->FindClass("java/lang/NullPointerException");
NullPointerException = (jclass) env->NewGlobalRef(localNPE);
jclass Throwable = env->FindClass("java/lang/Throwable");
detailMessage = env->GetFieldID(Throwable, "detailMessage", "Ljava/lang/String;");
}
void JNICALL ExceptionCallback(jvmtiEnv* jvmti, JNIEnv* env, jthread thread,
jmethodID method, jlocation location, jobject exception,
jmethodID catch_method, jlocation catch_location) {
if (env->IsInstanceOf(exception, NullPointerException)) {
char buf[32];
sprintf(buf, "location=%ld", (long)location);
env->SetObjectField(exception, detailMessage, env->NewStringUTF(buf));
}
}
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* vm, char* options, void* reserved) {
jvmtiEnv* jvmti;
vm->GetEnv((void**)&jvmti, JVMTI_VERSION_1_0);
jvmtiCapabilities capabilities = {0};
capabilities.can_generate_exception_events = 1;
jvmti->AddCapabilities(&capabilities);
jvmtiEventCallbacks callbacks = {0};
callbacks.VMInit = VMInit;
callbacks.Exception = ExceptionCallback;
jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks));
jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_INIT, NULL);
jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION, NULL);
return 0;
}
将其编译到共享库中并 运行 java 使用 -agentpath
选项:
java -agentpath:/pato/to/libRichNPE.so Main
您可以将复杂的行分解成许多您可以跟踪的更小的行,或者使用调试器查看异常发生时 null
的值。
虽然您可以尝试查看发生这种情况的字节码,但这只是复杂旅程的开始。我建议让你的代码更容易理解,你可能会计算出哪些值可以是 null
(注意:它可能是 null
除非你知道这是不可能的)
JEP 358: Helpful NullPointerExceptions在OpenJDK 14中加入了这样的特性,默认是禁用的;您必须指定 -XX:+ShowCodeDetailsInExceptionMessages
才能启用它。有了它,您的示例结果为:
Exception in thread "main"
java.lang.NullPointerException: Cannot invoke "String.isEmpty()" because "st" is null
at TestNPE.main(TestNPE.java:7)
类 无需重新编译即可利用此功能。它最初是为 SAP JVM.
开发的