CodeOverflow

567
votes

How to debug intermittent NullPointerException in production?

We're experiencing an intermittent NullPointerException in our production Spring Boot application that only occurs under high load (about 1-2 times per day). The stack trace shows:

java.lang.NullPointerException: Cannot invoke "String.length()" because "userEmail" is null
    at com.example.service.UserService.processEmail(UserService.java:145)
    at com.example.service.UserService$$FastClassBySpringCGLIB.invoke(...)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(...)
    ...

The problematic code:

@Service
public class UserService {
    
    public void processEmail(User user) {
        String userEmail = user.getEmail();
        // Line 145 - NPE happens here sporadically
        if (userEmail.length() > 0) {
            sendNotification(userEmail);
        }
    }
}

We can't reproduce this in our test environment. What are the best techniques to debug this in production without restarting the service?

6 Answers

823
votes

Great question! Here are powerful techniques for debugging production issues without downtime:

1. Dynamic Logging with Bytecode Instrumentation

Use BTrace or Arthas to add logging dynamically:

// BTrace script to log method parameters
import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;

@BTrace
public class UserServiceTracer {
    
    @OnMethod(
        clazz="com.example.service.UserService",
        method="processEmail"
    )
    public static void onProcessEmail(@Self Object self, AnyType user) {
        println("=== processEmail called ===");
        println(strcat("User object: ", str(user)));
        println(strcat("User.getEmail(): ", str(get(field(classOf(user), "email"), user))));
        println("===========================");
    }
}

Or use Arthas (easier and more powerful):

# Attach to running JVM
java -jar arthas-boot.jar

# Watch method invocations with parameters and return values
watch com.example.service.UserService processEmail "{params,returnObj,throwExp}" -x 3

# Monitor when user.email is null
watch com.example.service.UserService processEmail "{params[0].email}" "params[0].email==null" -x 3

# Decompile to see actual runtime code
jad com.example.service.UserService processEmail

2. Add Conditional Breakpoints with Remote Debugging

# Enable remote debugging in production (use SSH tunnel for security)
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 \
     -jar application.jar

# In IntelliJ, set conditional breakpoint:
# Condition: user == null || user.getEmail() == null

3. Enhanced Logging Strategy

@Service
public class UserService {
    
    private static final Logger log = LoggerFactory.getLogger(UserService.class);
    
    public void processEmail(User user) {
        // Defensive logging
        if (user == null) {
            log.error("processEmail called with null user - this should never happen!");
            return;
        }
        
        String userEmail = user.getEmail();
        
        if (userEmail == null) {
            log.error("User {} has null email. User details: {}", 
                     user.getId(), 
                     user.toString(),
                     new Exception("Stack trace for null email"));
            return;
        }
        
        if (userEmail.length() > 0) {
            log.debug("Sending notification to {}", userEmail);
            sendNotification(userEmail);
        }
    }
}

4. Thread Dump Analysis

# Capture thread dump when issue occurs
jstack -l [PID] > thread_dump_$(date +%Y%m%d_%H%M%S).txt

# Or use jcmd (recommended)
jcmd [PID] Thread.print > thread_dump.txt

# Automated thread dump on high CPU
while true; do
    CPU=$(top -b -n 1 | grep java | awk '{print $9}')
    if (( $(echo "$CPU > 80" | bc -l) )); then
        jstack [PID] > "dump_$(date +%s).txt"
    fi
    sleep 10
done

5. Flight Recorder for Detailed Events

# Start recording when issue is suspected
jcmd [PID] JFR.start name=debug duration=60s filename=/tmp/recording.jfr

# Dump current recording
jcmd [PID] JFR.dump name=debug filename=/tmp/dump.jfr

# Analyze with JDK Mission Control or IntelliJ

6. Custom Exception Handler with Context

@ControllerAdvice
public class GlobalExceptionHandler {
    
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
    @ExceptionHandler(NullPointerException.class)
    public ResponseEntity handleNPE(
            NullPointerException ex, 
            HttpServletRequest request) {
        
        // Capture full context
        Map context = new HashMap<>();
        context.put("url", request.getRequestURL().toString());
        context.put("method", request.getMethod());
        context.put("headers", Collections.list(request.getHeaderNames()));
        context.put("parameters", request.getParameterMap());
        context.put("sessionId", request.getSession(false) != null ? 
                                request.getSession().getId() : "none");
        context.put("user", SecurityContextHolder.getContext()
                           .getAuthentication()?.getName());
        
        log.error("NullPointerException in production. Context: {}", 
                 context, ex);
        
        // Send to monitoring system
        sendToDatadog(ex, context);
        
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                           .body(new ErrorResponse("Internal error"));
    }
}

7. Aspect-Based Debugging

@Aspect
@Component
public class DebugAspect {
    
    private static final Logger log = LoggerFactory.getLogger(DebugAspect.class);
    
    @Around("execution(* com.example.service.*.*(..))")
    public Object logMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        
        log.debug("Entering: {} with args: {}", methodName, args);
        
        try {
            Object result = joinPoint.proceed();
            log.debug("Exiting: {} with result: {}", methodName, result);
            return result;
        } catch (Exception ex) {
            log.error("Exception in: {} with args: {}", methodName, args, ex);
            throw ex;
        }
    }
}

8. Memory Dump Analysis

# Capture heap dump
jmap -dump:live,format=b,file=/tmp/heap_dump.hprof [PID]

# Or use jcmd
jcmd [PID] GC.heap_dump /tmp/heap_dump.hprof

# Analyze with Eclipse MAT or VisualVM to find:
# - Objects with null fields
# - Memory leaks
# - Object retention patterns

Prevention Strategy:

// Use Optional to make nullability explicit
public void processEmail(User user) {
    Optional.ofNullable(user)
            .map(User::getEmail)
            .filter(email -> !email.isEmpty())
            .ifPresent(this::sendNotification);
}

// Or use Objects.requireNonNull for fail-fast
public void processEmail(User user) {
    Objects.requireNonNull(user, "User cannot be null");
    String email = Objects.requireNonNull(user.getEmail(), 
                                         "User email cannot be null");
    if (!email.isEmpty()) {
        sendNotification(email);
    }
}

These techniques helped me debug and fix over 50 production issues without a single restart!

234
votes

Arthas is absolutely incredible for this! I use it daily in production. Here's my go-to debugging workflow:

# 1. Attach Arthas
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

# 2. Find the exact issue
watch com.example.service.UserService processEmail \
  '{params, returnObj, throwExp}' \
  'params[0].email==null' \
  -x 3

# 3. See call stack when condition met
stack com.example.service.UserService processEmail \
  'params[0].email==null'

# 4. Monitor method timing
monitor -c 5 com.example.service.UserService processEmail

# 5. Trace method call path
trace com.example.service.UserService processEmail -n 1

# 6. Even modify return values temporarily!
ognl '@com.example.service.UserService@someStaticField'

The best part? Zero downtime, zero restarts! Just attach, debug, detach. It's saved me countless times.

178
votes

Don't forget about OpenTelemetry for distributed tracing! It helps track the full request flow:

@Service
public class UserService {
    
    private final Tracer tracer;
    
    public void processEmail(User user) {
        Span span = tracer.spanBuilder("processEmail")
                         .setAttribute("user.id", user.getId())
                         .setAttribute("user.email", user.getEmail())
                         .startSpan();
        
        try (Scope scope = span.makeCurrent()) {
            // Your logic here
            if (user.getEmail() == null) {
                span.recordException(new IllegalStateException("Email is null"));
                span.setStatus(StatusCode.ERROR, "Null email detected");
            }
        } finally {
            span.end();
        }
    }
}

Combined with Jaeger or Zipkin, you can see exactly which requests are causing NPEs and their full context!