禅与计算机 禅与计算机
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

sharkchili

计算机禅修者
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 基于Netty连接池泄露问题了解客户端启动源码
      • 连接池导致内存泄漏案例演示
        • 简介
        • 问题复现
        • 服务端代码
        • 服务端代码
        • 启动并重现问题
      • 连接池泄漏原因详解
      • 解决方案
      • 补充注意事项
      • 更进一步:客户端连接源码详解
        • Netty客户端创建原理
        • 图解
        • 源码验证
        • Netyy客户端注册源码详解
        • Netty注册源码核心异步执行流程
      • 小结
      • 参考
    • 来聊聊Netty使用不当导致的并发波动问题
    • 关于使用Netty业务处理器ChannelHanlder的一些注意事项
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析与调优策略
    • Linux下Netty实现高性能UDP服务
    • netty源码编译跑通简记
    • 基于Netty服务端快速了解核心组件
    • 用Netty快速落地一个客户端程序
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • 来聊聊Netty几个开箱即用的处理器框架
    • 聊聊Netty中几个重要的生命周期
    • Netty的几种IO模式的实现与切换
    • 聊聊Netty异常传播链与最佳实践
    • 从Netty的ByteBuf中学习高并发场景下的内存优化艺术
    • 聊聊Netty客户端断线重连的设计与实现
    • 基于Netty源码学习那些并发技巧
    • Netty连接可靠性Idle监测连环问
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解与编解码器最佳实践
    • Netty解码器源码解析
    • Netty Reactor模型常见知识点小结
  • 消息队列

  • Nginx

  • 中间件
  • Netty
sharkchili
2023-05-29
目录

基于Netty连接池泄露问题了解客户端启动源码

# 连接池导致内存泄漏案例演示

# 简介

我们生产环境常常会用Netty客户端作为连接工具,尽管Netty强大且方便,但是使用不当的话也可能造成严重的生成事故。笔者本文就以一个连接池使用不当导致内存泄漏的案例来展开探讨。

# 问题复现

# 服务端代码

我们先贴出服务端代码,代码非常简单,就是启动,然后处理客户端请求。

public class Server {

	public static void main(String[] args) {
		NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
		NioEventLoopGroup workerGroup = new NioEventLoopGroup();

		try {
			ServerBootstrap bootstrap = new ServerBootstrap();
			bootstrap.group(bossGroup, workerGroup)
					.channel(NioServerSocketChannel.class)
					.option(ChannelOption.SO_BACKLOG, 100)
					.handler(new LoggingHandler())
					.childHandler(new ChannelInitializer<SocketChannel>() {
						@Override
						protected void initChannel(SocketChannel ch) throws Exception {
							ChannelPipeline pipeline = ch.pipeline();
							pipeline.addLast(new LoggingHandler());
						}
					});
			ChannelFuture channelFuture = bootstrap.bind(9999).sync();
			//channelFuture.channel().closeFuture().sync();
			channelFuture.channel().closeFuture().addListener(new ChannelFutureListener() {
				@Override
				public void operationComplete(ChannelFuture future) throws Exception {
					System.out.println(">>>>>>>>>>>>>>>链路关闭!");
					bossGroup.shutdownGracefully();
					workerGroup.shutdownGracefully();
				}
			});
			/*TimeUnit.SECONDS.sleep(4);
			channelFuture.channel().close();*/
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			/*bossGroup.shutdownGracefully();
			workerGroup.shutdownGracefully();*/
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 服务端代码

服务端代码如下,这里笔者用poolSize来模拟客户端并发请求,通过传入的poolSize创建poolSize个客户端和服务端建立连接。

public class Client {

	static void initPool(int poolSize) {
        
        for (int i = 0; i < poolSize; i++) {
            NioEventLoopGroup bossGroup = new NioEventLoopGroup();

            try {
                Bootstrap bootstrap = new Bootstrap();
                bootstrap.group(bossGroup)
                        .channel(NioSocketChannel.class)
                        .option(ChannelOption.TCP_NODELAY, true)
                        //.handler(new LoggingHandler())
                        .handler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel ch) throws Exception {
                                ChannelPipeline pipeline = ch.pipeline();
                            }
                        });

                ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9999).sync();
                channelFuture.channel().closeFuture().addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture future) throws Exception {
                        System.out.println(">>>>>>>>>>>>>>>链路关闭!");
                        //bossGroup.shutdownGracefully();
                        channelFuture.channel().close();
                    }
                });

            } catch (Exception e) {
                e.printStackTrace();
            }
        }


	}

	
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

完成上述编码之后,我们编写启动代码,可以看到笔者这里启动了200个并发请求。

public static void main(String[] args) {

		try {
			TimeUnit.SECONDS.sleep(20);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
			initPool(200);
		System.out.println(">>>>>>>>>>>>连接池创建成功!");
	}
1
2
3
4
5
6
7
8
9
10

# 启动并重现问题

完成编码工作之后,我们先把服务端启动,然后为了更快的重现问题,我们在启动客户端之前,需对客户端配置如下参数调整堆区

-Xmn32m -Xmx32m
1

启动后不久,我们就发现客户端并发请求服务端时,抛出内存泄漏的错误

Exception in thread "nioEventLoopGroup-23-1" Exception in thread "nioEventLoopGroup-28-1" Exception in thread "nioEventLoopGroup-28-16" java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: GC overhead limit exceeded
Exception in thread "RMI TCP Connection(idle)" Exception in thread "nioEventLoopGroup-28-8" java.lang.OutOfMemoryError: GC overhead limit exceeded
java.lang.OutOfMemoryError: GC overhead limit exceeded
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at io.netty.channel.nio.SelectedSelectionKeySet.<init>(SelectedSelectionKeySet.java:29)
	at io.netty.channel.nio.NioEventLoop.openSelector(NioEventLoop.java:207)
	at io.netty.channel.nio.NioEventLoop.<init>(NioEventLoop.java:149)
	at io.netty.channel.nio.NioEventLoopGroup.newChild(NioEventLoopGroup.java:127)
	at io.netty.channel.nio.NioEventLoopGroup.newChild(NioEventLoopGroup.java:36)
	at io.netty.util.concurrent.MultithreadEventExecutorGroup.<init>(MultithreadEventExecutorGroup.java:84)
	at io.netty.util.concurrent.MultithreadEventExecutorGroup.<init>(MultithreadEventExecutorGroup.java:58)
	at io.netty.channel.MultithreadEventLoopGroup.<init>(MultithreadEventLoopGroup.java:52)
	at io.netty.channel.nio.NioEventLoopGroup.<init>(NioEventLoopGroup.java:87)
	at io.netty.channel.nio.NioEventLoopGroup.<init>(NioEventLoopGroup.java:82)
	at io.netty.channel.nio.NioEventLoopGroup.<init>(NioEventLoopGroup.java:63)
	at io.netty.channel.nio.NioEventLoopGroup.<init>(NioEventLoopGroup.java:51)
	at io.netty.channel.nio.NioEventLoopGroup.<init>(NioEventLoopGroup.java:43)
	at com.mx.tuning.case2.Client.initPool(Client.java:29)
	at com.mx.tuning.case2.Client.main(Client.java:70)
Exception in thread "nioEventLoopGroup-26-1" java.lang.OutOfMemoryError: GC overhead limit exceeded
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 连接池泄漏原因详解

我们打开jvisualvm查看客户端线程信息,可以看到客户端创建无数个独立的NioEventLoopGroup。

在这里插入图片描述

查看server却发现,都是使用同一个NioEventLoopGroup,每个请求都是通过NioEventLoopGroup中的一个线程去处理的。

在这里插入图片描述

很明显造成客户端连接池泄漏的原因,就是我们错用的线程池,我们每个客户端发起请求时用的都是各自的NioEventLoopGroup,这不仅使得连接池没有复用,更使得nio模型用起来和bio没有区别。

在这里插入图片描述

对此,我们不妨看看NioEventLoopGroup的源码,先从构造方法入手,可以看到默认情况下会调用一个 this(0);

 public NioEventLoopGroup() {
        this(0);
    }
1
2
3

经过我们的不断步入,即可发现默认情况下EventLoopGroup会创建DEFAULT_EVENT_LOOP_THREADS 个线程

protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
        super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
    }
1
2
3

我们不妨通过源码看看DEFAULT_EVENT_LOOP_THREADS 的线程数。如下图,默认情况下,线程池数是CPU核心数的2倍。

在这里插入图片描述

以笔者为例,笔者的CPU核心数为16,那么最终结果则是32。

在这里插入图片描述

这一点,我们完全可以将Netty源码的结果输出打印出来

System.out.println("thread num="+Math.max(1, SystemPropertyUtil.getInt(
				"io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)));
1
2

可以看到结果确实是32

在这里插入图片描述

完成线程数初始化的工作之后,源码就会为ThreadPerTaskExecutor初始化对应个数的执行器处理后续的各种异步任务。

在这里插入图片描述

# 解决方案

可以想到一共200个请求都会创建32个线程,在32m的内存空间是多么可怕的一件事,所以我们不妨对客户端做一个调整,使得每一个请求都可以复用一个NioEventLoopGroup。

改造后的代码如下所示,可以看到笔者将循环建立到连接操作上,这样一来就确保的所有的客户端请求都复用一个NioEventLoopGroup。

在这里插入图片描述

再次启动我们就发现客户端所有连接都成功了,使用监控工具查看线程池也没有问题,线程数确实是CPU核心数的2倍。

在这里插入图片描述

控制台也输出连接成功了。

在这里插入图片描述

# 补充注意事项

这里我们补充一个注意事项,可以看到笔者对每一个请求结束后的关闭方法并不是将bossGroup关闭,而是将每个客户端对应的管道即channelFuture.channel()关闭。这样一来,我们也确保了一个连接报错之后,不会将其他连接对应的NioEventLoopGroup关闭了。

在这里插入图片描述

# 更进一步:客户端连接源码详解

# Netty客户端创建原理

# 图解

了解了客户端连接池的错误使用案例之后,我们不妨对客户端创建进行进一步的了解,先来看看下面这张经典的时序图,可以看到Netty客户端建立连接的方式大抵是以下几个步骤:

  1. 创建客户端
  2. 构建NIO线程组,这也就是我们说的创建NioEventLoopGroup
  3. 反射创建频道
  4. 创建频道流水线管理要处理的管道
  5. 上述初始化完成之后,通过异步的方式发起TCP连接
  6. 然后异步处理连接
  7. 连接操作成功后发起连接操作结果事件
  8. 调用我们编写的业务代码的handler

在这里插入图片描述

# 源码验证

这里我们不妨对几个比较核心的步骤通过源码的方式进行一下分析,创建NioEventLoopGroup这一步我们就不多说了,上文已经说明了,感兴趣的读者可以自行进一步查看源码。

然后就是反射创建频道,这一步在源码中的这个位置,可以看到这个代码我们会将管道的类传入

在这里插入图片描述

步入源码,我们也能发现这块代码会通过一个反射工厂完成频道的创建。

在这里插入图片描述

后续频道流水线初始化这里不是重点,我们这里简单查看一下即可

在这里插入图片描述

服务端流水线操作也是同理,通过addLast方法来编排频道的排序。

在这里插入图片描述

自此我们将客户端创建的流程整体有了整体的了解,下面我们不妨进一步了解一下客户端连接的工作流程。

# Netyy客户端注册源码详解

上文我们大概的过了初始化配置的源码,接下来我们就来了解一下bootstrap连接服务器的原理。我们首先将服务端代码启动。

然后调整一下客户端启动代码,再将其启动

 public static void main(String[] args) {


        initPool(1);
        System.out.println(">>>>>>>>>>>>连接池创建成功!");
    }
1
2
3
4
5
6

然后我们在connect处插入断点,并将客户端代码启动

在这里插入图片描述

此时,不断步入,我们的代码走到了Bootstrap的connect方法,启动有一个doResolveAndConnect,我们不妨步入查看详情。

 public ChannelFuture connect(SocketAddress remoteAddress) {
      ......
        return doResolveAndConnect(remoteAddress, config.localAddress());
    }
1
2
3
4

然后代码会走到一个initAndRegister方法,该方法完成之后会生成一个ChannelFuture 的异步任务,任务发起后,后续代码会注册一个监听器,监听注册结果以及根据注册结果发起真正的远程连接代码,我们先不妨看看异步任务生成的逻辑方法initAndRegister为我们做了些什么。

 private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
        final ChannelFuture regFuture = initAndRegister();
....

if (regFuture.isDone()) {
          ..........
        } else {
        ......
        //上述异步注册任务发起后,设置一个监听器,一旦上述注册任务完成就会执行监听器中的doResolveAndConnect0方法发起连接
            regFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    // Directly obtain the cause and do a null check so we only need one volatile read in case of a
                    // failure.
                    Throwable cause = future.cause();
                    if (cause != null) {
                     ......
                    } else {
                     //发起客户端连接服务端doResolveAndConnect0
                        promise.registered();
                        doResolveAndConnect0(channel, remoteAddress, localAddress, promise);
                    }
                }
            });
            return promise;
        }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

可以看到该方法内部为调用一个register方法,我们不妨看看这个register做了些什么。

 final ChannelFuture initAndRegister() {
        Channel channel = null;
      .....

        ChannelFuture regFuture = config().group().register(channel);
      ......
        }
1
2
3
4
5
6
7

我们不断这深入这段代码,可以看到AbstractChannel中的register会通过eventLoop提交一个register0的任务,我们不妨看看eventLoop的execute方法做了些什么。

 @Override
        public final void register(EventLoop eventLoop, final ChannelPromise promise) {
           

            AbstractChannel.this.eventLoop = eventLoop;

            if (eventLoop.inEventLoop()) {
                .....
            } else {
                try {
                    eventLoop.execute(new Runnable() {
                        @Override
                        public void run() {
                            register0(promise);
                        }
                    });
                } catch (Throwable t) {
                   ......
                }
            }
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

可以看到execute方法,执行了两段核心代码:

  1. addTask,即将我们register0这个任务提交到队列中
  2. 调用startThread启动NioEventLoop获取并运行队列中的任务。

所以我们不妨看看startThread是如何启动线程去执行我们的register这个任务的。

@Override
    public void execute(Runnable task) {
      ....
      //添加任务到队列中
        addTask(task);
        if (!inEventLoop) {
        // 如果NioEventLoop没启动,则启动让其跑任务
            startThread();
            if (isShutdown() && removeTask(task)) {
                reject();
            }
        }

      ......
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

可以看到startThread方法调用了doStartThread去启动线程,我们不妨看看它做了些什么。

private void startThread() {
        if (state == ST_NOT_STARTED) {
            if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
                try {
                    doStartThread();
                } catch (Throwable cause) {
                   ......
                }
            }
        }
    }
1
2
3
4
5
6
7
8
9
10
11

由于代码比较长,笔者这里便使用图片的形式来展示一下代码,可以看到这段代码中会通过threadPerTaskExecutor提交一个任务,该任务便是获取任务中的线程然后,调用 SingleThreadEventExecutor.this.run();方法,读者如果查看一下this对象即可发现,这个this就是我们的NioEventLoop,我们不妨看看run方法内部做了些什么。

在这里插入图片描述

步入源码后,我们会发现该run方法是一个无限的for循环,会获取本次通到事件的key

在这里插入图片描述

因为我们是初次建立连接,所以代码走到了这里,内部没有做任务事情,所以笔者直接略过这段代码。然后就走到了下图的runAllTasks

在这里插入图片描述

最终代码就会从队列中取出我们的register0的任务,并执行。

在这里插入图片描述

由此走向了一个闭环。

在这里插入图片描述

自此,我们在这里做一个小结,整个任务的流程:

  1. 调用connect方法。
  2. 调用initAndRegister封装并提交一个register0的异步任务。
  3. 添加上述任务到队列中。
  4. 启动NioEventLoop线程去处理提交到队列中的任务。
  5. NioEventLoop获取通到事件,得到上述的register0。
  6. 执行register0。

# Netty注册源码核心异步执行流程

上文中我们代码最终走到了register0,我们不妨看看register0做了些什么。经过笔者的整理,可以看到该方法大抵做了3个步骤:

  1. 调用doRegister
  2. 流水线注册新的handler即调用invokeHandlerAddedIfNeeded
  3. 对promise发起连接注册成功后的通知,即调用safeSetSuccess方法。

所以我们不妨看看doRegister做了些什么

private void register0(ChannelPromise promise) {
            try {
        .......
                doRegister();
.......
                // user may already fire events through the pipeline in the ChannelFutureListener.
                pipeline.invokeHandlerAddedIfNeeded();

                safeSetSuccess(promise);
                pipeline.fireChannelRegistered();
             .........
                }
            } catch (Throwable t) {
               ......
            }
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

可以看到doRegister会调用register,最终生成一个通到事件的key,赋值给selectionKey ,我们不妨看看生成key之前register方法做了些什么?

@Override
    protected void doRegister() throws Exception {
        boolean selected = false;
        for (;;) {
            try {
                selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
                return;
            } catch (CancelledKeyException e) {
               ........
            }
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12

然后我们就来到了register方法,因为是第一次注册,所以k的值为null,代码走到register执行事件注册一个连接事件,然后返回这个key。

public final SelectionKey register(Selector sel, int ops,
                                       Object att)
        throws ClosedChannelException
    {
        synchronized (regLock) {
           .......
            SelectionKey k = findKey(sel);
            if (k != null) {
              .....
            }
            if (k == null) {
              .....
                synchronized (keyLock) {
                    if (!isOpen())
                        throw new ClosedChannelException();
                    k = ((AbstractSelector)sel).register(this, ops, att);
                    addKey(k);
                }
            }
            return k;
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

自此我们的doRegister即注册都完成了,继续进行执行后续步骤,通知客户端可以真正开始连接了。

在这里插入图片描述

可以看到,代码走到了promise的trySuccess方法,我们不妨步入看看这个方法为我们做了些什么

protected final void safeSetSuccess(ChannelPromise promise) {
            if (!(promise instanceof VoidChannelPromise) && !promise.trySuccess()) {
                logger.warn("Failed to mark a promise as success because it is done already: {}", promise);
            }
        }
1
2
3
4
5

可以看到该段代码会调用一个名为notifyListeners区通知上文注册register监听器。

public boolean trySuccess(V result) {
        if (setSuccess0(result)) {
            notifyListeners();
            return true;
        }
        return false;
    }
1
2
3
4
5
6
7

查看该通知方法,我们可以看到调用了notifyListener0方法。

在这里插入图片描述

然后代码就走到了DefaultPromise的notifyListener0方法,它将会调用operationComplete方法,他将会将注册状态设置为true,并让客户端执行连接方法,我们不妨步入看看

private static void notifyListener0(Future future, GenericFutureListener l) {
        try {
            l.operationComplete(future);
        } catch (Throwable t) {
          .....
            }
        }
    }
1
2
3
4
5
6
7
8

可以看到,代码最终走到了我们一开始所说的哪个异步任务的后续注册监听的内部逻辑代码中,Bootstrap的doResolveAndConnect ,它做了以下两件事:

  1. 将注册结果设置为true,即调用 promise.registered();
  2. 执行连接逻辑doResolveAndConnect0,我们不妨看看内部执行细节。

在这里插入图片描述

可以看到其内部核心步骤就是调用doConnect方法。

在这里插入图片描述

同理,它也是向eventLoopGroup提交一个连接的异步任务,如下图,这里的execute仍然会执行我们上述步骤中的addTask,然后NioEventLoop轮询通到事件的过程。

在这里插入图片描述

之后代码就执行到上图的channel.connect(remoteAddress, connectPromise);

在这里插入图片描述

然后这个连接方法就会执行doConnect将OP_CONNECT注册到通到事件中。

 @Override
    protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
        if (localAddress != null) {
            doBind0(localAddress);
        }

        boolean success = false;
        try {
            boolean connected = SocketUtils.connect(javaChannel(), remoteAddress);
            if (!connected) {
                selectionKey().interestOps(SelectionKey.OP_CONNECT);
            }
            success = true;
            return connected;
        } finally {
            if (!success) {
                doClose();
            }
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

最终NioEventLoop轮询到上述所说的注册的感兴趣的连接事件,完成finishConnect工作,自此所有连接工作完成。

在这里插入图片描述

# 小结

自此我们的整个客户端连接工作就完成了,我们不妨小结整个流程:

在这里插入图片描述

# 参考

Java性能调优 6步实现项目性能升级 (opens new window)

编辑 (opens new window)
上次更新: 2026/03/26, 01:05:31
来聊聊Netty消息发送的那些事
来聊聊Netty使用不当导致的并发波动问题

← 来聊聊Netty消息发送的那些事 来聊聊Netty使用不当导致的并发波动问题→

最近更新
01
基于EasyExcel实现高效导出
03-25
02
从开源框架中学习那些实用的位运算技巧
03-25
03
浅谈分布式架构设计思想和常见优化手段
03-25
更多文章>
Theme by Vdoing | Copyright © 2025-2026 Evan Xu | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×
×