作为新人 Java 开发者,从校园步入企业,编写新代码往往不是最大挑战,快速定位并解决程序问题才是拉开差距的关键。本文聚焦新人最常遇到的 10 类问题场景,提供 “现象识别 - 工具使用 - 代码修复” 的全流程实战方案,帮你从 “遇错慌” 成长为 “排错快” 的团队能手。
目录
问题一:空指针异常(NullPointerException - NPE)
问题二:内存溢出(OutOfMemoryError: Java heap space)

问题一:空指针异常(NullPointerException - NPE)
场景描述
调用对象方法 / 访问属性时,对象为null,是新人遇到频次最高的 “入门级 bug”,常见于对象未初始化、外部数据返回空等场景。
常见触发点
- 调用
null对象的实例方法(如str.length(),str为null) - 访问或修改
null对象的字段(如user.getName(),user为null) - 获取
null数组的长度(如arr.length,arr为null) - 将
null作为Throwable对象抛出(如throw null)
实战排查指南
1. 定位问题行
控制台会打印异常堆栈,找到at开头且属于你代码的行,这就是问题根源。示例堆
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "str" is null
at com.example.MyClass.myMethod(MyClass.java:10) // 问题行:MyClass.java第10行
解读:在第 10 行,试图对null的str变量调用length()方法。
2. 追溯对象来源
找到问题行后,分析变量str的生成逻辑,判断空值来源:
- 是方法参数?可能调用方传入了
null - 是数据库 / API 查询结果?可能查询无数据返回
- 是自己初始化的变量?可能初始化逻辑漏写(如未
new对象)
解决方案与最佳实践
- 防御性判空:使用前先检查对象是否为
nullif (str != null && !str.isEmpty()) { // 先判空再用,避免NPE int length = str.length(); } - 用 Java 8 Optional:明确标记 “值可能为空”,强制处理空场景
Optional<String> optionalStr = Optional.ofNullable(getStrFromDB()); String result = optionalStr.orElse("默认值"); // 空值时返回默认值,避免NPE - 参数校验快速失败:方法入口用
Objects.requireNonNull校验参数,提前暴露问题public void myMethod(String str) { // 若str为null,直接抛异常并提示,避免后续逻辑出问题 this.str = Objects.requireNonNull(str, "参数str不能为null"); }
问题二:内存溢出(OutOfMemoryError: Java heap space)
场景描述
JVM 堆内存不足,无法创建新对象,垃圾回收(GC)也无法释放足够空间,常见于内存泄漏、处理超大数据集(如加载 10 万条数据到内存)场景,最终会导致应用崩溃。
实战排查指南
1. 先获取内存快照
启动应用时添加 JVM 参数,让 OOM 发生时自动生成堆转储文件(记录内存中所有对象状态):
# -XX:+HeapDumpOnOutOfMemoryError:OOM时生成快照
# -XX:HeapDumpPath:指定快照保存路径
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.dump -jar your-app.jar
2. 分析快照定位问题
用工具打开heap.dump文件,推荐工具:Eclipse MAT(免费)、JVisualVM(JDK 自带)。关键操作步骤:
- 查看「Dominator Tree」:找到占用内存最大的对象(如一个超大
List) - 运行「Leak Suspects Report」:工具自动分析可能的内存泄漏点
- 追溯「GC Roots」:看哪个对象在持有大对象引用(如静态
Map、未关闭的连接),导致 GC 无法回收
3. 代码层面排查
- 检查静态集合:是否有
static List/Map持续添加数据却不清理(如缓存未设置过期时间) - 检查资源关闭:数据库连接、文件流、网络连接是否用
try-with-resources自动关闭// 正确做法:try-with-resources自动关闭资源,避免连接泄漏 try (Connection conn = dataSource.getConnection(); PreparedStatement stmt = conn.prepareStatement(sql)) { ResultSet rs = stmt.executeQuery(); // 处理结果 } catch (SQLException e) { log.error("数据库操作异常", e); }
问题三:高 CPU 占用
场景描述
Java 进程 CPU 使用率持续 100%,导致服务响应变慢、超时,常见于死循环、密集计算、锁竞争场景。
实战排查指南
1. 定位高 CPU 线程
用 Linux 命令逐步缩小范围,找到 “罪魁祸首” 线程:
- 找 Java 进程 PID:
top -c # 查看所有进程,找到CPU高的Java进程,记PID(如1234) # 或用JDK自带命令 jps -l # 直接列出Java进程PID和主类 - 找进程内高 CPU 线程:
top -H -p 1234 # 查看PID=1234的进程下所有线程,记高CPU线程ID(如1235) - 线程 ID 转 16 进制(jstack 日志用 16 进制标识线程):
printf "%x\n" 1235 # 输出结果如4d3(后续搜索用)
2. 分析线程堆栈
用jstack获取线程快照,查看高 CPU 线程在执行什么代码:
jstack 1234 > thread_dump.log # 将PID=1234的线程快照输出到文件
打开thread_dump.log,搜索 16 进制线程 ID(如4d3),查看对应线程的堆栈:
- 若看到
RUNNABLE状态且循环调用某方法:可能是死循环 - 若看到复杂算法 / 正则表达式:可能是密集计算导致 CPU 高
- 若看到
BLOCKED状态且等待锁:可能是锁竞争
常见解决方案
- 死循环:检查
while(true)是否有退出条件(如while(flag),确保flag能被修改) - 密集计算:拆分任务、加缓存(如用 Redis 缓存计算结果)、异步处理
- 锁竞争:减少锁粒度(如用
ConcurrentHashMap代替HashMap+同步锁)
问题四:线程阻塞与死锁
场景描述
程序不报错但停止响应,或日志频繁出现 “线程超时” 警告,死锁时线程互相等待资源,永远无法执行。
实战排查指南
1. 获取线程快照
同 “高 CPU 占用” 步骤,用jstack 1234 > thread_dump.log生成快照。
2. 分析死锁
jstack日志末尾会明确标注死锁信息,直接指出互相等待的线程和锁:示例死锁日志:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor ... (object 0x000000071a234a58) # 要等的锁
which is held by "Thread-0" # 锁被Thread-0持有
"Thread-0":
waiting to lock monitor ... (object 0x000000071a234a88) # 要等的锁
which is held by "Thread-1" # 锁被Thread-1持有
解决方案:确保所有线程按相同顺序获取锁(如先锁 A 再锁 B,避免 Thread-1 锁 A 等 B、Thread-0 锁 B 等 A)。
3. 分析线程状态
查看线程STATE,判断阻塞原因:
BLOCKED:等待进入同步块,可能是锁竞争激烈(如大量线程抢同一把锁)WAITING/TIMED_WAITING:等待资源(如Object.wait()、Thread.sleep()),若大量线程处于此状态,可能是数据库 / Redis 响应慢
问题五:数据库连接池耗尽
场景描述
日志报错Cannot get connection from datasource,应用无法执行数据库操作,常见于连接泄漏(未关闭连接)、连接池配置过小。
实战排查指南
1. 先检查连接池配置
查看application.yml(以 Spring Boot 为例),确认最大连接数是否足够:
spring:
datasource:
hikari:
maximum-pool-size: 20 # 最大连接数,根据业务调整(如高并发场景设30-50)
idle-timeout: 300000 # 连接空闲5分钟后回收,避免闲置连接占用资源
2. 排查连接泄漏(核心)
连接池耗尽的根本原因通常是 “连接用后未关闭”,排查方式:
- 代码审查:检查所有数据库操作,确保
Connection、PreparedStatement、ResultSet用try-with-resources关闭 - 启用连接池监控:HikariCP 支持 JMX 监控,或通过日志查看连接状态:
若日志出现 “leak detection”,根据提示定位未关闭连接的代码。spring: datasource: hikari: leak-detection-threshold: 60000 # 超过60秒未归还连接,打印泄漏日志
3. 临时解决方案
重启应用可释放所有未关闭的连接,恢复服务,但需尽快修复代码,避免问题复发。
问题六:Spring 依赖注入失败
场景描述
Spring Boot 启动时报BeanCreationException或NoSuchBeanDefinitionException,无法创建 Bean,导致应用启动失败。
实战排查指南
1. 解读异常类型
NoSuchBeanDefinitionException:找不到指定类型的 Bean- 原因 1:类未加
@Component/@Service/@Repository/@Controller注解 - 原因 2:Bean 所在包未被
@ComponentScan扫描到(如 Bean 在主应用类的父包)
- 原因 1:类未加
BeanCreationException:创建 Bean 时出错- 原因 1:Bean 的构造方法 /
@PostConstruct方法抛异常 - 原因 2:
@Autowired注入的依赖不存在(且未设required=false)
- 原因 1:Bean 的构造方法 /
2. 核心检查步骤
- 检查注解:确保 Bean 类加了正确注解(如服务类加
@Service,DAO 类加@Repository) - 检查包扫描:Spring Boot 默认扫描 “主应用类所在包及子包”,若 Bean 在其他包,需手动指定:
// 主应用类:用@ComponentScan指定额外扫描的包 @SpringBootApplication @ComponentScan(basePackages = {"com.example.service", "com.example.dao"}) public class YourApp { public static void main(String[] args) { SpringApplication.run(YourApp.class, args); } } - 检查依赖注入:若依赖是可选的,用
@Autowired(required = false)避免启动失败:@Service public class UserService { // 若OrderService不存在,也不会抛异常 @Autowired(required = false) private OrderService orderService; }
问题七:HTTP 客户端 / 服务端超时
场景描述
调用外部 HTTP API 时,长时间无响应,最终抛ConnectTimeoutException(连接超时)或ReadTimeoutException(读取超时)。
实战排查指南
1. 先区分超时类型
- 连接超时:无法建立 TCP 连接,可能原因:
- 对方服务宕机、端口未开放
- 网络不通(如防火墙拦截)
- 客户端 IP 被对方拉黑
- 读取超时:TCP 连接已建立,但对方未在指定时间内返回数据,可能原因:
- 对方服务处理慢(如复杂查询耗时久)
- 传输数据量大,网络带宽不足
2. 客户端配置超时(关键)
无论用RestTemplate还是Feign,必须设置超时时间,避免无限等待:
- RestTemplate 示例(用 HttpClient 配置):
@Bean public RestTemplate restTemplate() { RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(5000) // 连接超时:5秒(超过则抛异常) .setSocketTimeout(10000) // 读取超时:10秒(超过则抛异常) .build(); CloseableHttpClient httpClient = HttpClientBuilder.create() .setDefaultRequestConfig(requestConfig) .build(); return new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient)); } - Feign 示例(配置文件):
feign: client: config: default: # 所有Feign客户端生效 connectTimeout: 5000 readTimeout: 10000
问题八:序列化 / 反序列化错误
场景描述
对象与 JSON 转换时(如 Redis 缓存、HTTP API 交互),抛JsonParseException(JSON 格式错)或JsonMappingException(字段不匹配)。
实战排查指南
1. 先检查 JSON 格式
用在线工具(如JSON.cn)验证 JSON 字符串是否合法,排除 “格式错误” 问题(如少逗号、引号不闭合)。
2. 解决字段匹配问题
若格式合法仍报错,大概率是 “JSON 字段与 Java 对象属性不匹配”:
- 字段名不一致:用
@JsonProperty指定映射关系public class User { // JSON中的"user_name"映射到Java的"userName" @JsonProperty("user_name") private String userName; } - Java 对象无无参构造器:添加无参构造器(Jackson 反序列化需要)
public class User { // 必须有无参构造器 public User() {} public User(String userName) { this.userName = userName; } } - JSON 有多余字段:用
@JsonIgnoreProperties忽略未知字段// 忽略JSON中Java对象没有的字段 @JsonIgnoreProperties(ignoreUnknown = true) public class User { private String userName; }
3. 处理日期格式
日期是序列化常见坑,用@JsonFormat明确指定格式:
public class Order {
// JSON日期格式:2024-05-20 14:30:00
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
}
问题九:配置文件(application.yml)错误
场景描述
应用启动失败,报ConfigurationProperties绑定错误,或配置值未生效,常见于 YAML 语法错、配置键拼写错。
实战排查指南
1. 检查 YAML 语法
YAML 对缩进敏感,常见错误:
- 用制表符(Tab)缩进(必须用空格)
- 缩进层级不一致(如同一级配置缩进不同)
- 冒号后没加空格(如
spring:datasource,正确是spring: datasource)
建议:用在线工具(如YAML Lint)校验语法,或在 IDE 中开启 YAML 语法提示(如 IntelliJ IDEA、Eclipse 的 YAML 插件)。
2. 检查配置键拼写
配置键拼写错会导致 “配置不生效”,例如:
- 错写
spring.datasource.url为spring.datasource.uri - 错写
server.port为server.portt
解决方案:善用 IDE 自动补全,或引入spring-boot-actuator查看最终生效的配置:
- 加依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> - 配置暴露端点:
management: endpoints: web: exposure: include: configprops,env # 暴露配置查看端点 - 访问端点:浏览器打开
http://localhost:8080/actuator/configprops,查看所有配置的最终绑定结果。
问题十:日志混乱,无法定位问题
场景描述
出问题时日志要么无关键信息,要么信息太多 “大海捞针”,无法快速追溯问题链路。
实战排查指南
1. 正确记录异常堆栈
错误做法:只打印异常信息,丢失堆栈(无法定位问题行)
// 错误:仅打印消息,无堆栈
log.error("处理请求失败:" + e.getMessage());
正确做法:将异常对象作为最后一个参数传入,保留完整堆栈
// 正确:打印消息+完整堆栈,便于定位问题
log.error("处理用户{}的请求失败", userId, e);
2. 合理配置日志级别
日志级别从低到高:TRACE < DEBUG < INFO < WARN < ERROR,根据环境调整:
- 开发环境:用
DEBUG,打印详细调试信息 - 生产环境:用
INFO或WARN,减少日志量(避免磁盘占满)
配置示例:
logging:
level:
com.example.yourapp: DEBUG # 自己的应用包用DEBUG
org.springframework.web: INFO # Spring Web用INFO(减少冗余)
org.hibernate: WARN # Hibernate用WARN(只看警告和错误)
file:
name: /var/log/yourapp/yourapp.log # 日志输出路径
3. 用 MDC 实现链路追踪
同一请求的日志分散在不同地方,用 MDC 添加 “链路 ID”,将所有日志串联:
- 实现 MDC 过滤器,在请求入口添加 Trace ID:
@Component public class MdcFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { // 生成唯一Trace ID,放入MDC String traceId = UUID.randomUUID().toString(); MDC.put("traceId", traceId); chain.doFilter(request, response); } finally { // 请求结束,清除MDC(避免线程复用导致数据污染) MDC.clear(); } } } - 配置日志格式,显示 Trace ID:
logging: pattern: console: "%d{yyyy-MM-dd HH:mm:ss} [%X{traceId}] %-5level %logger{36} - %msg%n" file: "%d{yyyy-MM-dd HH:mm:ss} [%X{traceId}] %-5level %logger{36} - %msg%n"
效果:同一请求的所有日志都会带相同traceId,搜索traceId即可找到完整链路。
总结:新人必备的 5 个排查心法
- 先看日志,再查代码:90% 的问题能从日志中找到线索(异常堆栈、错误消息),不要上来就 debug。
- 工具先行,效率翻倍:熟练用
jstack(线程)、jmap(内存)、MAT(堆分析)、Actuator(配置监控),比硬看代码快 10 倍。 - 由表及里,缩小范围:先确定 “哪个模块 / 接口出问题”,再定位 “哪行代码”,最后追溯 “数据来源”。
- 大胆假设,小心求证:根据经验猜可能原因(如 NPE 先看对象是否初始化),再用日志 / 工具验证。
- 复盘预防,避免重复踩坑:解决问题后记录 “原因 - 方案”,并通过代码规范(如强制 try-with-resources)、Code Review 避免同类问题。
这份指南覆盖了新人 80% 的日常排错场景,建议收藏备用。遇到新问题时,按 “现象 - 排查 - 解决” 的流程拆解,你会发现 “排错” 比 “写代码” 更能提升技术能力。
1万+

被折叠的 条评论
为什么被折叠?



