2024年Java最全【Nginx 源码学习】内存池 及 优秀案例赏析:Nginx内存池设计,字节跳动三场技术面+HR面

本文分享了作者的面试经验,强调了在面试中逻辑思维的重要性,同时详细讨论了内存碎片的成因、Java等语言的GC机制以及Nginx的内存池设计,包括大内存和小内存分配策略。
摘要由CSDN通过智能技术生成

最后总结我的面试经验

2021年的金三银四一眨眼就到了,对于很多人来说是跳槽的好机会,大厂面试远没有我们想的那么困难,摆好心态,做好准备,你也可以的。

另外,面试中遇到不会的问题不妨尝试讲讲自己的思路,因为有些问题不是考察我们的编程能力,而是逻辑思维表达能力;最后平时要进行自我分析与评价,做好职业规划,不断摸索,提高自己的编程能力和抽象思维能力。

BAT面试经验

实战系列:Spring全家桶+Redis等

其他相关的电子书:源码+调优

面试真题:

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

再大的内存,只要软件运行的时间足够久,都有可能产生大量的内存碎片,从而对性能和可用内存造成负面影响。

造成内存碎片的原因大致可以归为两类:

  1. 内存分配机制。拥有先进GC机制的语言(如Java、C#),在对抗内存碎片方面表现较好。它们的GC一般会有个Compact步骤,会移动对象在内存中的位置,将多个对象整齐无间隙地排列好,从而消除了不少内存碎片。

  2. 如果是使用传统malloc/free或者自己写内存分配的话,产生内存碎片的概率不小。这方面比较典型的例子就是Firefox,它以前代码里有不少自己写的allocator,内存碎片问题是非常严重的。后来Mozilla开始逐步采用jemalloc来帮助解决这个问题。

举2个例子:Firefox7的时候修改了一个内存分配行为,就一下子降低了不少内存碎片:Firefox 7 Might Solve Memory Fragmentation

IssuesFirefox15的时候对addon的机制做了改动,一下子解决了大量长期困扰的addon内存问题:Firefox 15 plugs the add-on leaks

取决于软件的具体类型,对抗内存碎片可能是个长期的战争,有兴趣的可以翻翻Mozilla的MemShrink项目:MemShrink | Nicholas Nethercote 看看别人是怎么用了2年功夫把Firefox从一个超级耗内存的浏览器变成一个最节约内存的浏览器。


malloc 底层原理


  1. malloc开始搜索空闲内存块,如果能找到一块大小合适的就分配出去

  2. 如果malloc找不到一块合适的空闲内存,那么调用brk等系统调用扩大堆区从而获得更多的空闲内存

  3. malloc调用brk后开始转入内核态,此时操作系统中的虚拟地址系统开始工作,扩大进程的堆区,操作系统并没有为此分配真正的物理内存

  4. brk执行结束后返回到malloc,从内核态切换到用户态,malloc找到一块合适的空闲内存后返回

  5. 进程拿到内存,继续干活。

  6. 当有代码读写新申请的内存时系统内部出现缺页中断,此时再次由用户态切换到内核态,操作系统此时真正的分配物理内存,之后再次由内核态切换回用户态,程序继续。

如果对堆和栈有所了解的朋友应该会知道,堆是像上伸展的,栈是向下延伸的,那什么向上向下啊?有点迷哈。看个图:

在这里插入图片描述

一切尽在不言中咯。

在这里插入图片描述


在这里插入图片描述


jemalloc && tcmalloc


jemalloc

tcmalloc

说实话啊,这俩我都没有用过呢,也是第一次听,先把概念放这儿,之后有时间了研究研究。


Nginx内存池设计


Nginx 使用内存池对内存进行管理,把内存分配归结为大内存分配和小内存分配,申请的内存大小比同页的内存池最大值 max 还 大,则是大内存分配,否则为小内存分配。

  1. 大块内存的分配请求不会直接在内存池上分配内存来满足请求,而是直接向系统申请一块内存(就像 直接使用 malloc 分配内存一样),然后将这块内存挂到内存池头部的 large 字段下。

  2. 小块内存分配,则是从已有的内存池数据区中分配出一部分内存。

Nginx 内存分配总流图如下:其中 size 是用户请求分配内存的大小,pool是现有内存池。

在这里插入图片描述


基础数据结构

数据块:

typedef struct {

u_char *last; // 当前内存池分配到此处,即下一次分配从此处开始

u_char *end; // 内存池结束位置

ngx_pool_t *next; // 内存池里面有很多块内存,这些内存块就是通过该指针连成链表的

ngx_uint_t failed; // 内存池分配失败次数

} ngx_pool_data_t;

池结构:

struct ngx_pool_s {

ngx_pool_data_t d; // 指向内存池的第一个数据块

size_t max; // 内存池数据块的最大值(数目)

ngx_pool_t *current; // 指向当前内存池

ngx_chain_t *chain; // 该指针挂接一个ngx_chain_t结构

ngx_pool_large_t *large; // 大块内存链表,即分配空间超过max的内存

ngx_pool_cleanup_t *cleanup; // 释放内存池的callback

ngx_log_t *log; // 主要用于记录日志信息

};

在这里插入图片描述


大块内存:

struct ngx_pool_large_s {

ngx_pool_large_t *next; // 指向下一个large内存

void *alloc; // 指向分配的large内存

};

在这里插入图片描述


回收站:

struct ngx_pool_cleanup_s {

ngx_pool_cleanup_pt handler; // 指向用于cleanup本cleanup内存

void *data; // 指向分配的cleanup内存

ngx_pool_cleanup_t *next; // 指向下一个cleanup内存

};

在这里插入图片描述


ngx_pool_t 的逻辑结构:

在这里插入图片描述


源码分析


ngx_create_pool 创建内存池

用于创建一个内存池,我们创建时,传入我们的初始大小:

#define ngx_memalign(alignment, size, log) ngx_alloc(size, log)

//ngx_alloc:对malloc进行了简单封装

ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log)

{

ngx_pool_t *p;

p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);

if (p == NULL) {

return NULL;

}

// 可以看到 last 指向 pool 之后的位置,即下一个pool块分配的位置

p->d.last = (u_char *) p + sizeof(ngx_pool_t);

// end 指向pool的size的最后,即当前pool可容纳的最大尺寸的结束位置

p->d.end = (u_char *) p + size;

p->d.next = NULL;

p->d.failed = 0;

size = size - sizeof(ngx_pool_t);

p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;

/*

nginx对内存的管理分为大内存与小内存,

当某一个申请的内存大于某一个值时,就需要从大内存中分配空间,否则从小内存中分配空间。

nginx中的内存池是在创建的时候就设定好了大小,

在以后分配小块内存的时候,如果内存不够,则是重新创建一块内存串到内存池中,而不是将原有的内存池进行扩张。

当要分配大块内存时,则是在内存池外面再分配空间进行管理的,称为大块内存池。

*/

p->current = p;

p->chain = NULL;

p->large = NULL;

p->cleanup = NULL;

p->log = log;

return p;

}


ngx_destroy_pool 销毁内存池

void ngx_destroy_pool(ngx_pool_t *pool)

{

ngx_pool_t *p, *n;

ngx_pool_large_t *l;

ngx_pool_cleanup_t *c;

for (c = pool->cleanup; c; c = c->next) {

if (c->handler) {

ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,

“run cleanup: %p”, c);

c->handler(c->data);

}

}

for (l = pool->large; l; l = l->next) {

ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, “free: %p”, l->alloc);

if (l->alloc) {

ngx_free(l->alloc);

}

}

#if (NGX_DEBUG)

… …

#endif

for (p = pool, n = pool->d.next; ; p = n, n = n->d.next) {

ngx_free§;

if (n == NULL) {

break;

}

}

}

遍历内存池链表,释放所有内存,包括pool,large,cleanup链表,如果指定了cleanup回调来释放,则调用cleanup的handler来释放cleanup链表中的内存。

先依次释放pool中cleanup,large类型的链表,最后释放pool本身的链表。


ngx_reset_pool 重置内存池

void ngx_reset_pool(ngx_pool_t *pool)

{

ngx_pool_t *p;

ngx_pool_large_t *l;

// 先遍历large链表,释放large内存

for (l = pool->large; l; l = l->next) {

if (l->alloc) {

ngx_free(l->alloc);

}

}

pool->large = NULL;

//重置所有小块内存区

for (p = pool; p; p = p->d.next) {

p->d.last = (u_char *) p + sizeof(ngx_pool_t);

}

}


ngx_palloc 分配内存

ngx_align_ptr:一个用来内存地址取整的宏。取整可以降低CPU读取内存的次数,提高性能。这里并没有真正意义调用malloc等函数申请内存,而是移动指针标记而已,所以内存对齐的活,得自己动手。

#define ngx_align_ptr(p, a) \

(u_char *) (((uintptr_t) § + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))

void *ngx_palloc(ngx_pool_t *pool, size_t size)

{

u_char *m;

ngx_pool_t *p;

if (size <= pool->max) { // 如果需要分配的size大于max,则使用palloc_large来分配

p = pool->current; // 小于max,则从current开始遍历pool链表

do {

// 每次从last处开始分配aligned内存

m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);

if ((size_t) (p->d.end - m) >= size) {

// 如果分配的内存够用,则就从此处分配,并调整last

p->d.last = m + size;

return m;

}

p = p->d.next;

} while §;

// 表示链表里没有能够分配size大小的内存节点

// 则生成一个新的节点,并在其中分配内存

return ngx_palloc_block(pool, size);

}

// 大于max的,就在large中进行分配

return ngx_palloc_large(pool, size);

}

static void *ngx_palloc_block(ngx_pool_t *pool, size_t size)

{

u_char *m;

size_t psize;

ngx_pool_t *p,*new,*current;

psize = (size_t) (pool->d.end - (u_char *) pool);//计算内存池第一个内存块的大小

m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);//分配和第一个内存块同样大小的内存块

if (m == NULL) {

return NULL;

}

new = (ngx_pool_t *) m;

new->d.end = m + psize;//设置新内存块的end

new->d.next = NULL;

new->d.failed = 0;

m += sizeof(ngx_pool_data_t);//将指针m移动到d后面的一个位置,作为起始位置

m = ngx_align_ptr(m, NGX_ALIGNMENT);//对m指针按4字节对齐处理

new->d.last = m + size;//设置新内存块的last,即申请使用size大小的内存

current = pool->current;

//这里的循环用来找最后一个链表节点,这里failed用来控制循环的长度,如果分配失败次数达到5次,

//就忽略,不需要每次都从头找起

for (p = current; p->d.next; p = p->d.next) {

if(p->d.failed++ > 4) { //这儿咋总觉得哪里怪怪的

current = p->d.next;

}

}

p->d.next = new;

pool->current = current ? current : new;

return m;

}


ngx_palloc_large:开辟一个大内存检查后交给大内存链表管理。所以开辟的内存必定在大内存链表上。

/*

  • 1)判断pool->large链表上查询是否有NULL的,只在链表上往下查询3次,主要判断大数据块是否有被释放的,有就给它赋值,如果没有则只能跳出。

  • 2)然后往下新创建一个pool->large结构体,将刚开辟的空间赋给该新结构体管理。

*/

static void *ngx_palloc_large(ngx_pool_t *pool, size_t size)

{

void *p;

ngx_uint_t n;

ngx_pool_large_t *large;

p = ngx_alloc(size, pool->log);//注意该函数是单独调用malloc,所以它的内存与内存池链表的内存是不连续的或者叫无关。

if (p == NULL) {

最后

由于篇幅原因,就不多做展示了

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

*/

static void *ngx_palloc_large(ngx_pool_t *pool, size_t size)

{

void *p;

ngx_uint_t n;

ngx_pool_large_t *large;

p = ngx_alloc(size, pool->log);//注意该函数是单独调用malloc,所以它的内存与内存池链表的内存是不连续的或者叫无关。

if (p == NULL) {

最后

[外链图片转存中…(img-MbdTdZhJ-1714876058382)]

[外链图片转存中…(img-liTIJGnp-1714876058383)]

[外链图片转存中…(img-SntyeVMv-1714876058383)]

[外链图片转存中…(img-LWHSTcZ6-1714876058383)]

[外链图片转存中…(img-hWbDSpTO-1714876058384)]

[外链图片转存中…(img-IoBvRjuu-1714876058384)]

由于篇幅原因,就不多做展示了

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 10
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值