1. 项目概述:Java程序稳定性保障的黄金法则
在Java开发领域摸爬滚打十几年,我见过太多本可避免的生产事故。有一次凌晨三点被叫醒处理线上故障,发现只是因为某个服务没做简单的空指针判断,导致整个支付链路瘫痪。这种"低级错误引发重大事故"的案例,促使我总结出这套被团队戏称为"防崩溃十诫"的实战经验。
这十条规则不是教科书式的理论,而是用真实故障换来的血泪教训。它们涵盖了从编码规范到JVM调优,从异常处理到线程安全等关键领域,特别适合3-5年经验的Java开发者查漏补缺。遵循这些原则,至少能帮你避免80%的常见崩溃场景。
2. 核心原则解析
2.1 第一诫:永远不要信任外部输入
我刚入行时犯过的典型错误:
// 错误示范 public void processUserData(String input) { String[] parts = input.split(","); // 可能NPE int value = Integer.parseInt(parts[1]); // 可能NumberFormatException }正确处理方式应该像这样:
// 正确做法 public void processUserData(@NonNull String input) { if (StringUtils.isBlank(input)) { throw new IllegalInputException("输入不能为空"); } String[] parts = input.split(","); if (parts.length < 2) { throw new IllegalInputException("输入格式错误"); } try { int value = Integer.parseInt(parts[1]); } catch (NumberFormatException e) { log.error("数值转换失败", e); throw new BusinessException("数值格式异常", e); } }关键要点:
- 使用
@NonNull等注解配合静态检查工具 - 前置校验要覆盖null、空字符串、格式合法性等
- 数值转换必须try-catch
- 自定义异常区分系统异常和业务异常
2.2 第二诫:线程安全不是可选项
我遇到过最诡异的线上bug:
// 危险的单例实现 public class CacheManager { private static CacheManager instance; private Map<String, Object> cache = new HashMap<>(); public static CacheManager getInstance() { if (instance == null) { instance = new CacheManager(); // 多线程可能创建多个实例 } return instance; } public void put(String key, Object value) { cache.put(key, value); // HashMap非线程安全 } }修正方案:
// 线程安全版本 public class CacheManager { private static volatile CacheManager instance; private final ConcurrentMap<String, Object> cache = new ConcurrentHashMap<>(); public static CacheManager getInstance() { if (instance == null) { synchronized (CacheManager.class) { if (instance == null) { instance = new CacheManager(); } } } return instance; } public void put(String key, Object value) { cache.put(key, value); } }注意事项:
- 单例必须双重检查锁+volatile
- 优先使用ConcurrentHashMap等线程安全集合
- 状态变量必须考虑可见性问题
- 避免在方法内创建临时线程
3. 内存管理黄金法则
3.1 第三诫:像对待金钱一样管理内存
典型内存泄漏场景:
// 内存泄漏示例 public class OrderService { private static List<Order> completedOrders = new ArrayList<>(); public void completeOrder(Order order) { completedOrders.add(order); // 订单永远不会被移除 } }解决方案:
// 使用WeakHashMap自动清理 public class OrderService { private static Map<Order, Boolean> completedOrders = Collections.synchronizedMap(new WeakHashMap<>()); public void completeOrder(Order order) { completedOrders.put(order, true); } }关键技巧:
- 使用WeakReference/SoftReference管理缓存
- 静态集合要特别小心
- 第三方库的close()必须调用
- 用-XX:+HeapDumpOnOutOfMemoryError参数捕获内存快照
3.2 第四诫:JVM参数不是玄学
推荐的基础配置模板:
# 生产环境推荐配置 java -server -Xms4g -Xmx4g # 堆内存固定大小避免扩容开销 -XX:MaxMetaspaceSize=512m -XX:+UseG1GC # G1垃圾回收器 -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=4 -XX:ConcGCThreads=2 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs/heapdump.hprof调优经验:
- 新生代老年代比例根据对象生命周期调整
- G1的MaxGCPauseMillis不是越小越好
- metaspace要预留足够空间
- 避免频繁Full GC的关键是控制大对象分配
4. 异常处理实战技巧
4.1 第五诫:异常不是用来吞的
反面教材:
try { processOrder(); } catch (Exception e) { // 静默吞掉异常 }正确姿势:
try { processOrder(); } catch (BusinessException e) { log.warn("业务异常", e); throw e; } catch (Exception e) { log.error("系统异常", e); monitor.recordException(e); throw new SystemException("处理失败", e); }最佳实践:
- 区分业务异常和系统异常
- 记录完整异常链
- 添加业务上下文信息
- 使用统一异常处理器
4.2 第六诫:finally不是保险箱
常见陷阱:
Connection conn = null; try { conn = getConnection(); // ... } finally { if (conn != null) { conn.close(); // 可能再次抛出异常 } }更健壮的写法:
Connection conn = null; try { conn = getConnection(); // ... } finally { try { if (conn != null) conn.close(); } catch (SQLException e) { log.error("关闭连接失败", e); } }特别提醒:
- finally中的异常会覆盖try块异常
- 使用try-with-resources语法更安全
- 关闭资源要单独try-catch
- 记录资源释放失败的日志
5. 高可用设计要点
5.1 第七诫:超时机制是生命线
没有超时的灾难场景:
// 危险的HTTP调用 HttpResponse response = httpClient.execute(request);正确的超时设置:
RequestConfig config = RequestConfig.custom() .setConnectTimeout(3000) .setSocketTimeout(5000) .build(); HttpClient client = HttpClientBuilder.create() .setDefaultRequestConfig(config) .build();关键参数:
- 连接超时(connectTimeout)建议3秒
- 读写超时(socketTimeout)建议5-10秒
- 连接池获取超时建议1-3秒
- 分布式调用要设置全局超时
5.2 第八诫:降级方案不是可选项
典型降级实现:
@Slf4j public class PaymentService { @Degrade(key = "createPayment", fallback = "createPaymentFallback") public PaymentResult createPayment(Order order) { // 调用支付网关 } public PaymentResult createPaymentFallback(Order order) { log.warn("支付降级,转为线下处理"); return PaymentResult.of("DEGRADED"); } }降级策略:
- 读操作:返回缓存/默认值
- 写操作:队列缓冲/本地存储
- 关键路径要有旁路方案
- 降级状态要明确标识
6. 生产环境必备技能
6.1 第九诫:日志不是用来凑数的
好的日志示例:
// 有业务价值的日志 log.info("订单状态变更 [orderId={}, from={}, to={}, operator={}]", order.getId(), oldStatus, newStatus, UserContext.getCurrentUser());日志规范:
- 使用占位符{}代替字符串拼接
- 包含必要的业务上下文
- 错误日志要打印异常堆栈
- 敏感信息要脱敏
6.2 第十诫:监控不是运维的事
必备监控指标:
// 使用Micrometer暴露指标 @Bean public MeterRegistryCustomizer<MeterRegistry> metrics() { return registry -> { registry.config().commonTags("application", "order-service"); new JvmMemoryMetrics().bindTo(registry); new JvmGcMetrics().bindTo(registry); new ProcessorMetrics().bindTo(registry); }; }监控重点:
- JVM内存/线程/GC
- 关键接口RT/成功率
- 外部调用次数/异常
- 业务自定义指标
7. 终极生存指南
在真实生产环境中,我还建议:
- 启动时执行健康检查:
@Component public class DatabaseHealthChecker implements ApplicationRunner { @Override public void run(ApplicationArguments args) { if (!checkDatabase()) { throw new StartupException("DB连接失败"); } } }使用ChaosBlade定期演练故障场景
重要操作添加二次确认:
public void deleteUser(long userId) { if (operationConfirm.needConfirm("deleteUser")) { throw new ConfirmRequiredException("需要二次确认"); } // 实际删除逻辑 }- 建立线上问题复盘文化,每个事故都要有改进项
这套规则在多个百万级日活系统中验证过效果。记住,好的系统不是没有bug,而是在出现问题时能优雅降级、快速恢复。防崩溃的本质是控制影响范围,这需要我们在设计和编码阶段就考虑各种异常场景。