一次由于八股文引起的内存泄漏

导读:本文记录两次报错系统监控现象以及作者针对性的排查过程和分析,最终解决了问题的全过程。

文章开头,先分享一张大部分Java开发同学都记在心里的一张图。

图片

没错,就是Spring Bean生命周期图。就因为这张图不熟悉,导致线上环境出现内存泄漏问题,系统频繁FullGC,服务无法响应。

一、第一次报错系统监控现象

图片

图片

关键时间节点:

14:16 机器发布新代码

15:35 机器开始出现fullGC

15:50 机器fullGC耗时上升

17:48 对JVM进行dump操作,然后进行机器置换

由图可知,在14:16发布完成后,系统正常运行了一段时间,期间内存、线程等均未出现异常,不过当系统运行了一段时间后,通过监控可以明显发现内存使用量和线程数都在持续上升,那这样问题就很明确了:

1.有大量阻塞线程

2.存在内存泄露问题

1.1 排查过程

分析线程Dump文件

图片

Dump文件记录

通过截图中Dump文件内容可知,HSFBizProcessor-DEFAULT-9-thread-792 这个线程已经阻塞了116s,并且的阻塞线程共有682个。

1.2 分析原因

根据线程堆栈信息,查到了线程是阻塞在下面这段代码:

@Componentpublic class OssClient implements BeanPostProcessor {private OSS ossClient = null;/**      * 初始化OSS客户端     **/@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {// 省略代码……// 以下是阻塞代码行        ossClient = new OSSClientBuilder().build(ossProperty.getString("endpoint"),                        ossProperty.getString("accessKeyId"),                        ossProperty.getString("accessKeySecret"),                        configuration);// 省略代码……return bean;    }}

这段代码本意是在应用启动时,通过动态配置文件来配置OSS客户端。

但是线程阻塞在了这行,首先我想到可能是由于OSS客户端初始化需要发起网络请求,因为饿了么有张北和南通机房且一般情况下跨机房无法访问,所以第一时间检查了一下配置,果不其然,南通机房配置了张北的OSS。

登录上南通机房的机器,尝试PING张北的OSS域名,发现无法PING通,验证了我的猜测。

图片

1.3 第一次问题解决

Get到了报错原因,就方便解决了;通过修改配置,将OSS机房配置正确后,重启机器即可。

二、第二次报错系统监控现象

本来以为万事大吉,在观察了30分钟,确认系统无BLOCKED线程后,就认为该问题已经解决。

图片

图片

关键时间节点:

19:48 机器发布新代码

22:30 机器开始出现fullGC

23:30 机器fullGC耗时上升

00:30 对JVM进行dump操作,然后进行机器置换

然而,在发布后3个小时以后,系统又开始报错,同样是fullGC,只不过这次fullGC耗时没有之前那么长了。

2.1 排查过程

分析线程Dump文件

因为有了前车之鉴,所以第一步想到的就是上一步的问题没有解决,线程仍然阻塞在刚才的代码处。

图片

不过,这次并没有查询到阻塞线程。这至少证明:

1.阻塞线程确实是由于OSS跨单元拒绝访问导致的

2.还有其他问题导致了内存泄漏

分析GC Dump文件

首先,通过集团Grace工具,发现有严重的内存泄漏问题。

图片

这里显示有11万个org.apache.http.impl.conn.PoolingHttpClientConnectionManager实例,占用了80.42%的堆内存,但是这个类并不是我直接引入的,那么一定是有间接依赖,生成了大量该类对象。

另外,通过类名,能判断这个对象是和网络请求有关系,而我这个应用上需要网络请求的地方有几处:

1.访问DB

2.访问Redis

3.访问OSS

4.进行HSF调用

继续通过对对象依赖进行分析,发现了一个重要信息:

图片

org.apache.http.impl.conn.PoolingHttpClientConnectionManager这个类由OSS间接依赖进来的,确定了引起内存泄漏的罪魁祸首。

2.2 分析原因

虽然定位到了是由于OSS建议依赖进来,但是看代码仍然不能解释为什么会产生内存泄漏。​​​​​​​

@Componentpublic class OssClient implements BeanPostProcessor {private OSS ossClient = null;/**      * 初始化OSS客户端     **/@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {// 省略代码……// 一下是阻塞代码行        ossClient = new OSSClientBuilder().build(ossProperty.getString("endpoint"),                        ossProperty.getString("accessKeyId"),                        ossProperty.getString("accessKeySecret"),                        configuration);// 省略代码……return bean;    }}

排查原因过程中,有一篇文章给了我答案,下面是这篇文章给的OOM原因的解释:

每次new OSSClient的时候,都会往List中放入HttpClientConnectionManager,但是没有主动调用OSSClient的shutdown的方法,所以List只会增大不会变小。反观我们的代码,每次接口调用都会创建一个OSSClient对象,但却在使用完之后,没有调用OSSClient的shutdown方法,导致未调用IdleConnectionReaper的removeConnectionManager方法,使得IdleConnectionReaper中静态列表存储的PoolingHttpClientConnectionManager实例数据一直会增长,一直都不会被回收,最终带来的结果就是OOM。

其实通过代码能够看出,我的初衷是在OssClient这个Bean初始化的时候执行一下初始化逻辑,在我查到导致内存泄漏的原因后,我仍然对一个问题很是不解:为什么OSS初始化的代码会被多次执行?

回到文章标题和开头,为什么这篇文章标题叫“一次由于八股文引起的内存泄漏”,以及为什么文章开头会引入下面这张图?

图片

实际上,是由于实现错了接口导致的OSS初始化代码被重复调用,最终导致系统OOM。

2.3 最终问题解决

改变一下实现接口,使代码逻辑符合我预期效果即可,当然这个解决方式有多种多样,下面只是我的一种解决方案。​​​​​​​

@Component
public class OssClient implements InitializingBean {

    private OSS ossClient = null;

    /** 
     * 初始化OSS客户端
     **/
    @Override
    public void afterPropertiesSet() throws Exception {
        // 省略代码……
        // 以下是阻塞代码行
        ossClient = new OSSClientBuilder().build(ossProperty.getString("endpoint"),
                        ossProperty.getString("accessKeyId"),
                        ossProperty.getString("accessKeySecret"),
                        configuration);
        // 省略代码……
    }

}

总结

圈内常有声音抱怨,“面试好比是造火箭,而工作不过是拧螺丝”,尤其对于Java开发岗位面试中的常规知识题目持有轻蔑态度。然而,这些被称作“八股文”的知识,实际上是每位开发工程师技术根基的核心。坚实的基础才能确保构建在其之上的高楼大厦能够屹立不倒,历经岁月的洗礼。

嵌入式开发作为一门综合性强、涉及面广的技术方向,其面试题通常涵盖硬件基础、操作系统、编程语言、驱动开发、系统架构等多个方面。以下是嵌入式开发领域中常见的经典面试题与知识点总结: ### 嵌入式基础知识 1. **什么是嵌入式系统?** 嵌入式系统是以应用为中心,以计算机技术为基础,并且软硬件可裁剪,适用于对功能、可靠性、成本、体积及功耗有严格要求的专用计算机系统。 2. **ARM处理器有哪些特点?** ARM处理器具有低功耗、高性能、精简指令集(RISC)、支持多种操作系统等优点,广泛应用于移动设备和嵌入式平台。 3. **什么是交叉编译?为什么需要交叉编译?** 交叉编译是指在一个平台上生成另一个平台上的可执行代码。由于嵌入式系统的资源有限,通常在主机上进行程序的开发和编译,再将编译好的程序下载到目标设备上运行。 4. **Bootloader的作用是什么?** Bootloader是系统启动时运行的第一段小程序,主要负责初始化硬件设备、建立内存空间映射图,从而为操作系统内核的启动做好准备。 5. **RTOS与通用操作系统的区别是什么?** RTOS(实时操作系统)强调任务调度的确定性和响应时间的可预测性,适用于对时间敏感的应用场景;而通用操作系统更注重多任务处理和资源管理的灵活性。 6. **中断与异常的区别是什么?** 中断是由外部设备触发的异步事件,用于通知CPU某个外部事件的发生;异常则是由CPU内部执行指令过程中检测到的错误或特殊条件引起的同步事件。 7. **DMA的工作原理是什么?** DMA(直接内存访问)允许某些硬件子系统在不经过CPU的情况下直接读写系统内存,从而提高数据传输效率,减轻CPU负担。 8. **什么是看门狗定时器(Watchdog Timer)?** 看门狗定时器是一种硬件计时器,用于监控系统是否正常运行。如果系统出现故障导致无法定期重置看门狗,则它会自动复位系统以恢复运行。 9. **GPIO的基本概念是什么?** GPIO(通用输入输出)引脚可以被配置为数字输入或输出端口,常用于控制外设或读取外部信号状态。 10. **SPI、I²C、UART三种通信协议的特点是什么?** - SPI:高速同步串行接口,全双工通信,需要四根线。 - I²C:半双工同步串行总线,使用两根线(SCL和SDA),支持多主多从设备。 - UART:异步串行通信接口,通常使用两根线(TXD和RXD),适合远距离通信。 ### 操作系统相关问题 1. **进程与线程的区别是什么?** 进程是资源分配的基本单位,拥有独立的地址空间;线程是调度执行的基本单位,共享所属进程的资源。 2. **死锁产生的四个必要条件是什么?如何预防?** 必要条件包括互斥、请求与保持、不可抢占和循环等待。预防策略包括破坏其中一个或多个条件。 3. **虚拟内存的概念及其作用是什么?** 虚拟内存通过将磁盘空间作为内存扩展来提供比实际物理内存更大的地址空间,使得程序能够访问超过实际RAM大小的数据量。 4. **文件系统的类型有哪些?它们各自的特点是什么?** 包括FAT、NTFS、ext系列、YAFFS、JFFS等,不同类型的文件系统针对不同的应用场景优化了性能、可靠性和兼容性。 5. **系统调用的过程是怎样的?** 用户态程序通过特定的中断或陷阱机制切换到内核态,由内核完成相应的服务后返回结果给用户态程序。 ### 编程语言与调试技巧 1. **C语言中指针与数组的关系是什么?** 数组名在大多数情况下会被视为指向数组首元素的指针,但两者本质上有所不同,例如sizeof运算符对它们的影响就不同。 2. **volatile关键字的作用是什么?** volatile告诉编译器不要对该变量做任何优化,因为它的值可能会在程序之外被改变,比如由硬件或其他线程修改。 3. **static关键字在函数内部和全局变量中的意义有何不同?** 在函数内部声明的static变量具有持久生命周期,仅限于该函数访问;而在全局作用域下声明的static变量则限制了其可见性至当前文件。 4. **assert()宏的用途是什么?** assert用于调试阶段检查某些条件是否成立,如果不成立则终止程序运行并打印错误信息。 5. **gdb调试工具常用命令有哪些?** 如break设置断点,run启动程序,step单步执行,continue继续执行,print查看变量值等。 6. **Makefile的基本结构是什么?** Makefile包含规则定义,每个规则由目标、依赖项以及更新目标所需的命令组成,make工具依据这些规则自动化构建项目。 7. **版本控制系统Git的基本工作流程是什么?** 初始化仓库、添加文件到暂存区、提交更改、推送至远程仓库,同时支持分支管理和合并操作。 8. **内存泄漏的检测方法有哪些?** 使用Valgrind等工具可以帮助发现未释放的内存块,或者手动审查代码逻辑确保每次malloc/new都有对应的free/delete配对。 9. **如何避免野指针?** 初始化所有指针为NULL,在释放后将其置为NULL,并且总是检查指针有效性后再解引用。 10. **堆栈溢出的原因及防范措施有哪些?** 原因可能包括递归过深、局部变量过大等,可通过增加堆栈大小、改用动态分配或重构算法等方式缓解。 ### 驱动开发与系统移植 1. **Linux设备驱动模型的核心组件有哪些?** 包括sysfs虚拟文件系统、kobject对象管理系统、platform总线和设备模型等。 2. **字符设备与块设备的主要差异是什么?** 字符设备按字节流方式处理数据,没有缓存机制;块设备则以固定大小的数据块为单位进行读写,并利用缓冲区提高效率。 3. **设备树(Device Tree)的作用是什么?** 设备树描述了目标平台上具体的硬件配置信息,使得同一个内核镜像可以适配多种不同的硬件平台。 4. **U-Boot的启动过程大致分为哪几个阶段?** 第一阶段通常是汇编代码实现的底层初始化,第二阶段则是C语言实现的功能丰富阶段,最终加载并跳转到操作系统内核。 5. **如何向Linux内核添加一个新的驱动模块?** 编写驱动源码,创建对应的Kconfig和Makefile条目,配置内核选项启用该模块,最后编译安装并测试加载模块。 6. **交叉编译环境搭建的关键步骤是什么?** 获取或构建适用于目标平台的工具链,配置环境变量PATH指向交叉编译工具的位置,验证工具链能否正确生成目标平台的可执行文件。 7. **Linux内核裁剪的目的和方法是什么?** 目的是减少内核体积,提升启动速度和运行效率;方法是在内核源码目录下运行make menuconfig选择所需功能模块去除不必要的部分。 8. **嵌入式Linux系统的启动流程是怎样的?** 一般包括引导加载程序(如U-Boot)、Linux内核、initramfs/initrd、根文件系统挂载、init进程启动等一系列步骤。 9. **如何编写一个简单的字符设备驱动?** 实现file_operations结构体中的open、read、write、release等成员函数,注册cdev结构体并与设备号关联,创建设备节点供用户空间访问。 10. **设备驱动中并发访问的处理方式有哪些?** 利用自旋锁、信号量、原子操作、completion机制等同步原语保护共享资源免受竞争条件影响。 ```c // 示例:简单字符设备驱动框架 #include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> MODULE_LICENSE("GPL"); static dev_t my_dev; static struct cdev *my_cdev; ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { // Read implementation here return 0; } ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { // Write implementation here return count; } struct file_operations fops = { .owner = THIS_MODULE, .read = my_read, .write = my_write, }; int init_module(void) { alloc_chrdev_region(&my_dev, 0, 1, "my_device"); my_cdev = cdev_alloc(); cdev_init(my_cdev, &fops); cdev_add(my_cdev, my_dev, 1); return 0; } void cleanup_module(void) { cdev_del(my_cdev); unregister_chrdev_region(my_dev, 1); } ```
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值