是否有可能追踪导致NPE的表达?

时间:2016-10-14 18:39:34

标签: java debugging jvm stack-trace bytecode

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

当然,这些信息必须在某个地方可用。 有没有办法解决这个问题? (如果不是java表达式,那么至少导致NPE的字节码指令也会有所帮助)

编辑#1:我已经看到一些评论暗示分手等等,这些都没有冒犯,实际上是非建设性和无关紧要的。如果我能做到这一点,我会的!我只想说修改源是不可能的。

编辑#2:apangin在下面发布了一个很好的答案,我接受了。但它的SOOO COOL我不得不将输出包含在这里,以供任何不想自己尝试的人使用! ;)

假设我有这个驱动程序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!只是这是怎么回事?

5 个答案:

答案 0 :(得分:7)

发生异常时,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;
}

将其编译为共享库并使用-agentpath选项运行java:

java -agentpath:/pato/to/libRichNPE.so Main

答案 1 :(得分:1)

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

我看到的一个选项是使用像字节码可视化器这样的字节码调试器来更紧密地本地化导致npe的字节码指令。前进直到异常发生,或为npe添加断点。

答案 2 :(得分:0)

堆栈跟踪机制依赖于可选地编译到每个类中的调试元数据(即SourceFile和LineNumberTable属性)。据我所知,字节码偏移不会保留在任何地方。但是,这些对于典型的Java程序没有用,因为您仍然知道每个字节码指令对应的代码。

但是,有一个明显的解决方法 - 只需将有问题的代码分成多行并重新编译即可!您几乎可以在Java中的任何位置插入空格。

答案 3 :(得分:0)

您可以将复杂的行分解为可以跟踪的许多较小的行,或者使用调试器查看异常发生时的值null

虽然您可以尝试查看发生这种情况的字节代码,但这只是复杂旅程的开始。我建议您使代码更易于理解,并且您可以确定哪些值可以是null(注意:它可能是null,除非您知道它是不可能的)

答案 4 :(得分:0)

JEP 358: 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开发的。