谈谈Windows的内存分配

        作为一个程序员,内存分配是免不了的事情。像JAVA、Python等,底层支持内存管理,可以定时进行垃圾回收,防止内存泄露,但像C/C++,自己分配内存,还需要自己回收,否则就会造成内存泄露。今天我们不谈内存泄露相关问题,而是来谈谈内存分配的一些问题。

一、理解堆

        我们在进行程序开发的时候,经常会说到堆栈这个词,说得多了,有时候就会忽略它其实是指两种不同的数据结构,一个是堆,一个是栈。

        先说栈,程序每开启一个线程,就会自动创建一个栈,用于分配局部变量、存储函数调用参数和返回地址。对于C/C++语言来说,编译器在编译阶段就会生成合适的代码来从栈上分配和释放空间,不需要程序员进行干预。但是栈空间容量一般是比较少的,Windows程序默认栈大小是1M(当然在编译的时候可以调整栈空间大小),所以不适合分配特别大的内存区(记得曾经在写程序的时候,递归嵌套太深,导致栈溢出,不过这些问题也好发现,修改也比较容易) 。其次函数返回,栈空间就被回收了,所有栈上的变量生命期随着函数的返回而结束了。

        如果要分配大的内存空间,那么就可以使用堆来管理数据。在Windows内部,有一个堆管理器,堆管理器从内存中分配一块较大的内存进行管理,并将大块的内存分割成不同的小块来满足应该程序的需要,所以我们的C++程序new一块内存,并不是直接从内存中获得的,而是从堆管理器中拿到的。在NTDLL.dll中实现了一个通用的堆管理器,并且Windows SDK也公开了一组API来访问堆管理器,比如HeapAlloc、HeapFree等等。

        在NTDLL.dll中,有很多函数是用来管理堆,下表仅列出一部分

ntdll!RtlAllocateHeap从堆上分配内存
ntdll!RtlCreateHeap  创建堆
ntdll!RtlDestoryHeap销毁堆
ntdll!RtlFreeHeap释放堆块
ntdll!RtlSizeHeap获取堆块大小

        有些函数,在后面谈到分配函数的时候,会提及,所以先列在这儿。

        现在我们来看看,怎么去查找和分析系统中的堆

int main()
{
	return 0;
}

        上面的程序很简单,看起来没有任何操作,但实际上系统还是创建了一个堆,我们通过WinDbg来查看。

        将我们的程序通过WinDbg运行起来并加载上调试符号,然后使用!heap -h

0:000> !heap -h
Index   Address  Name      Debugging options enabled
  1:   25ee47f0000 
    Segment at 0000025ee47f0000 to 0000025ee48ef000 (00011000 bytes committed)
  2:   25ee46b0000 
    Segment at 0000025ee46b0000 to 0000025ee46c0000 (00001000 bytes committed)

         可以看到,系统已分配了两个堆,第一个堆起始位置是0x0000025ee47f0000,已提交大小为0x11000 bytes;第二个堆起始位置是0x0000025ee46b0000,已提交大小为0x1000 bytes。Windows各版本系统有所不同,我这里使用的是Windows 11呈现出来的结果

        如果要查看堆的详细信息,可以使用!heap 地址 -v来查看

0:000> !heap 0000025ee47f0000 -v
Index   Address  Name      Debugging options enabled
  1:   25ee47f0000 
    Segment at 0000025ee47f0000 to 0000025ee48ef000 (00011000 bytes committed)
    Flags:                00000002
    ForceFlags:           00000000
    Granularity:          16 bytes
    Segment Reserve:      00100000
    Segment Commit:       00002000
    DeCommit Block Thres: 00000400
    DeCommit Total Thres: 00001000
    Total Free Size:      00000120
    Max. Allocation Size: 00007ffffffdefff
    Lock Variable at:     0000025ee47f02c0
    Next TagIndex:        0000
    Maximum TagIndex:     0000
    Tag Entries:          00000000
    PsuedoTag Entries:    00000000
    Virtual Alloc List:   25ee47f0110
    Uncommitted ranges:   25ee47f00f0
    FreeList[ 00 ] at 0000025ee47f0150: 0000025ee47ff8d0 . 0000025ee47fa300   (4 blocks)

        里面的结构不详谈,要展开来说的话,有太多东西了。有兴趣的话,可以去找找相关文章,或者以后有时间,我们来慢慢聊。

二、分配和释放堆块

        在C/C++程序中,我们一般使用malloc/new来分配内存,使用free/delete来释放内存。在应用层方面,new 和 malloc确实有很多不同,这里我们不再详谈。那么一步步进入到分配的底层,是什么样子的呢?我们来看看

int main()
{
	char* c = (char*)malloc(1000);
	char* b = new char[1000];
	free(c);
	delete[] b;
	return 0;
}

        首先,我们来看看malloc的调用过程

00007ff7`75a41000 4883ec58        sub     rsp,58h
00007ff7`75a41004 b9e8030000      mov     ecx,3E8h
00007ff7`75a41009 ff15b1100000    call    qword ptr [Test!_imp_malloc (00007ff7`75a420c0)]

从上面的汇编代码可以看到,malloc走到了_imp_malloc里面去了,我们进一步去看看

00007ff8`2db00060 e90b000000      jmp     ucrtbase!_malloc_base (00007ff8`2db00070)
00007ff8`2db00065 cc              int     3
00007ff8`2db00066 cc              int     3
00007ff8`2db00067 cc              int     3
00007ff8`2db00068 71a8            jno     ucrtbase!_realloc_base+0x22 (00007ff8`2db00012)
00007ff8`2db0006a 53              push    rbx
00007ff8`2db0006b 14d3            adc     al,0D3h
00007ff8`2db0006d 8f              ???
00007ff8`2db0006e f4              hlt
00007ff8`2db0006f a7              cmps    dword ptr [rsi],dword ptr [rdi]
ucrtbase!_malloc_base:
00007ff8`2db00070 4883ec28        sub     rsp,28h
00007ff8`2db00074 4883f9e0        cmp     rcx,0FFFFFFFFFFFFFFE0h
00007ff8`2db00078 0f872cb00400    ja      ucrtbase!_malloc_base+0x4b03a (00007ff8`2db4b0aa)
00007ff8`2db0007e 48895c2430      mov     qword ptr [rsp+30h],rbx
00007ff8`2db00083 4885c9          test    rcx,rcx
00007ff8`2db00086 bb01000000      mov     ebx,1
00007ff8`2db0008b 48897c2420      mov     qword ptr [rsp+20h],rdi
00007ff8`2db00090 480f45d9        cmovne  rbx,rcx
00007ff8`2db00094 488b0d15070f00  mov     rcx,qword ptr [ucrtbase!_acrt_heap (00007ff8`2dbf07b0)]
00007ff8`2db0009b 4c8bc3          mov     r8,rbx
00007ff8`2db0009e 33d2            xor     edx,edx
00007ff8`2db000a0 ff1542870b00    call    qword ptr [ucrtbase!_imp_HeapAlloc (00007ff8`2dbb87e8)]

看最后一行,调用到了ucrtbase!_imp_HeapAlloc

我们执行到call那,再进去看一看

ntdll!RtlAllocateHeap:
00007ff8`30268e70 48895c2408      mov     qword ptr [rsp+8],rbx ss:00000068`f0effb70=0000000000000000
00007ff8`30268e75 4889742410      mov     qword ptr [rsp+10h],rsi
00007ff8`30268e7a 57              push    rdi
00007ff8`30268e7b 4883ec30        sub     rsp,30h
00007ff8`30268e7f 498bf8          mov     rdi,r8
00007ff8`30268e82 8bf2            mov     esi,edx
00007ff8`30268e84 488bd9          mov     rbx,rcx
00007ff8`30268e87 4885c9          test    rcx,rcx
00007ff8`30268e8a 0f841c460900    je      ntdll!RtlAllocateHeap+0x9463c 

看,最终调到了ntdll!RtlAllocateHeap。这就是我们前面提到的从堆上分配内存的底层函数。

我们再来看看new的执行过程

0007ff7`75a41014 b9e8030000      mov     ecx,3E8h
00007ff7`75a41019 e89a000000      call    Test!operator new[] (00007ff7`75a410b8)

 首先是调用的operator new[],进去看看

Test!operator new:
00007ff7`75a414b8 4053            push    rbx
00007ff7`75a414ba 4883ec20        sub     rsp,20h
00007ff7`75a414be 488bd9          mov     rbx,rcx
00007ff7`75a414c1 eb0f            jmp     Test!operator new+0x1a (00007ff7`75a414d2)
00007ff7`75a414c3 488bcb          mov     rcx,rbx
00007ff7`75a414c6 e8480a0000      call    Test!callnewh (00007ff7`75a41f13)
00007ff7`75a414cb 85c0            test    eax,eax
00007ff7`75a414cd 7413            je      Test!operator new+0x2a (00007ff7`75a414e2)
00007ff7`75a414cf 488bcb          mov     rcx,rbx
00007ff7`75a414d2 e8c4090000      call    Test!malloc (00007ff7`75a41e9b)
00007ff7`75a414d7 4885c0          test    rax,rax

看倒数第二行,走到了malloc这个函数里面去了,说明new分配内存还是使用的malloc,只是分配内存后再实现与malloc不一样的东西。

接下来的过程应该也猜到了,最后肯定也是调用的ntdll!RtlAllocateHeap

ucrtbase!malloc:
00007ff8`2db00060 e90b000000      jmp     ucrtbase!_malloc_base (00007ff8`2db00070)
00007ff8`2db00065 cc              int     3
00007ff8`2db00066 cc              int     3
00007ff8`2db00067 cc              int     3
00007ff8`2db00068 71a8            jno     ucrtbase!_realloc_base+0x22 (00007ff8`2db00012)
00007ff8`2db0006a 53              push    rbx
00007ff8`2db0006b 14d3            adc     al,0D3h
00007ff8`2db0006d 8f              ???
00007ff8`2db0006e f4              hlt
00007ff8`2db0006f a7              cmps    dword ptr [rsi],dword ptr [rdi]
ucrtbase!_malloc_base:
00007ff8`2db00070 4883ec28        sub     rsp,28h
00007ff8`2db00074 4883f9e0        cmp     rcx,0FFFFFFFFFFFFFFE0h
00007ff8`2db00078 0f872cb00400    ja      ucrtbase!_malloc_base+0x4b03a (00007ff8`2db4b0aa)
00007ff8`2db0007e 48895c2430      mov     qword ptr [rsp+30h],rbx
00007ff8`2db00083 4885c9          test    rcx,rcx
00007ff8`2db00086 bb01000000      mov     ebx,1
00007ff8`2db0008b 48897c2420      mov     qword ptr [rsp+20h],rdi
00007ff8`2db00090 480f45d9        cmovne  rbx,rcx
00007ff8`2db00094 488b0d15070f00  mov     rcx,qword ptr [ucrtbase!_acrt_heap (00007ff8`2dbf07b0)]
00007ff8`2db0009b 4c8bc3          mov     r8,rbx
00007ff8`2db0009e 33d2            xor     edx,edx
00007ff8`2db000a0 ff1542870b00    call    qword ptr [ucrtbase!_imp_HeapAlloc (00007ff8`2dbb87e8)]

过程也与malloc一模一样了。

        所以,不管上层是怎么实现,什么语法,到了底层,肯定是殊途同归。这也很好理解,都是实现同样的功能,底层都有一套东西了,为啥还要另起锅灶,从新开始呢。

        最后,再来简单看看free的过程

ucrtbase!free:
00007ff8`2db02150 c744241000000000 mov     dword ptr [rsp+10h],0
00007ff8`2db02158 8b442410        mov     eax,dword ptr [rsp+10h]
00007ff8`2db0215c e90f000000      jmp     ucrtbase!_free_base (00007ff8`2db02170)
00007ff8`2db02161 cc              int     3
00007ff8`2db02162 cc              int     3
00007ff8`2db02163 cc              int     3
00007ff8`2db02164 cc              int     3
00007ff8`2db02165 cc              int     3
00007ff8`2db02166 cc              int     3
00007ff8`2db02167 cc              int     3
00007ff8`2db02168 7190            jno     ucrtbase!__stdio_common_vswprintf+0x1ea (00007ff8`2db020fa)
00007ff8`2db0216a 5b              pop     rbx
00007ff8`2db0216b 12e7            adc     ah,bh
00007ff8`2db0216d 9e              sahf
00007ff8`2db0216e 70ce            jo      ucrtbase!__stdio_common_vswprintf+0x22e (00007ff8`2db0213e)
ucrtbase!_free_base:
00007ff8`2db02170 4883ec28        sub     rsp,28h
00007ff8`2db02174 4885c9          test    rcx,rcx
00007ff8`2db02177 741a            je      ucrtbase!_free_base+0x23 (00007ff8`2db02193)
00007ff8`2db02179 4c8bc1          mov     r8,rcx
00007ff8`2db0217c 33d2            xor     edx,edx
00007ff8`2db0217e 488b0d2be60e00  mov     rcx,qword ptr [ucrtbase!_acrt_heap (00007ff8`2dbf07b0)]
00007ff8`2db02185 ff1535660b00    call    qword ptr [ucrtbase!_imp_HeapFree (00007ff8`2dbb87c0)]

最终调用的是ntdll!RtlFreeHeap。delete过程也是如此,就不列出来了。

        其实不管Windows还是Linux,都有一套堆管理API,上层使用malloc/new,底层肯定都是归到同一个分配函数里面去,但是我没有Linux的内核符号,所以跟踪不进去,也无法向大家展示过程。

        这里我使用的是Release版本的程序,为什么呢?因为Debug版在分配函数的时候,会加入很多检查函数,影响调试,所以,还是用Release干净一点。后续有时间,可以谈谈Debug中的检查函数都做了些什么。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
在Eclipse中,内存分配是指设置Eclipse运行时的内存使用情况。Eclipse在运行时使用Java虚拟机(JVM)来执行代码和管理内存。根据引用,JVM主要管理两种类型的内存:堆和非堆。堆是Java代码可及的内存,是开发人员可以使用的;非堆是JVM自身使用的内存,包括方法区、JVM内部处理或优化所需的内存以及类结构和代码等内容。 根据引用,JVM的最大内存取决于实际的物理内存和操作系统。因此,可以通过设置JVM的参数来改变Eclipse的内存分配。如果设置的内存参数过大,可能导致程序无法启动。 关于Eclipse的内存分配问题,引用提到了一个具体的情况。当Eclipse检测到非堆内存(Perm Gen)的空闲空间少于5%时,会提示该警告信息。这意味着当前可用的非堆内存很少。为了解决这个问题,可以尝试增加非堆内存的大小。 因此,要设置Eclipse的内存分配,可以通过调整JVM的参数来改变堆和非堆内存的大小。具体的设置方式取决于使用的Eclipse版本和操作系统。可以通过编辑Eclipse的启动配置文件或命令行参数来进行设置。确保根据实际需求和系统资源情况来进行合理的内存分配。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [由MyEclipse内存不足谈谈JVM内存(转)](https://blog.csdn.net/sunxboy/article/details/83198768)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cantaloupe77

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值