是否有可能追踪哪个表达式导致了 NPE?

2022-09-04 22:18:24

当我获得 NPE 时,我将获得带有行号的堆栈跟踪。这很有帮助,但是如果行非常密集和/或包含嵌套表达式,仍然无法确定哪个引用为 null。

当然,这些信息一定在某个地方可用。有没有办法解决这个问题?(如果不是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 处的调用虚拟 #5!这有多酷?


答案 1

当异常发生时,JVM 知道导致异常的原始字节码。但是,不跟踪字节码索引。StackTraceElement

解决方案是在发生异常时使用 JVMTI 捕获字节码索引。

以下示例 JVMTI 代理程序将截获所有异常,如果异常类型为 ,则代理程序会将其替换为字节码位置信息。NullPointerExceptiondetailMessage

#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

答案 2

异常本身没有足够的信息来提供行号以外的信息。

我看到的一个选项是使用字节码调试器(如字节码可视化器)来更紧密地本地化导致npe的字节码指令。向前单步执行,直到发生异常,或为 npe 添加断点。