《Windows核心编程系列》十四谈谈默认堆和自定义堆

本文详细介绍了堆内存管理的原理与应用,包括堆的基本概念、进程默认堆的创建与使用、额外堆的创建时机及方法、内存分配与释放过程、以及C++中堆的使用。重点阐述了如何通过堆函数实现高效、安全的内存管理,适用于需要大量小型数据的场景。

                           堆

     前面我们说过堆非常适合分配大量的小型数据。使用堆可以让程序员专心解决手头的问题,而不必理会分配粒度和页面边界之类的事情。因此堆是管理链表和数的最佳方式。但是堆进行内存分配和释放时的速度比其他方式都慢,而且无法对物理存储器的调拨和撤销调拨进行控制。

 

     什么是堆?

 

     在系统内部堆就是一块预定的地址空间区域。刚开始堆的大部分页面都没有调拨物理存储器。随着我们不断的从堆中分配内存,堆管理器会给堆调拨越来越多的物理存储器。这些物理存储器始终是从页交换文件中分配的。释放堆中的内存时,堆管理器会撤销已调拨的物理存储器。

 

     进程默认堆。

 

     进程初始化时,系统会在进程的地址空间中创建一个堆。这个堆被称为进程的默认堆。默认情况下,这个堆的地址空间区域大小是1MB。程序员可以控制这个大小。我们可以在创建应用程序时用/HEAP连接器开关来改变默认堆的大小。由于DLL没有与之关联的堆,因此在创建DLL时,不应该使用/HEAP开关。

 

  

   /HEAP:reserve[,commit]


 

     由于许多Windows函数会用到进程默认堆,因此对默认堆的访问必须一次进行。系统会保证在任何情况下只让一个线程从默认堆中分配或释放内存块。如果应用程序只有一个线程,而我们又希望以最快的方式访问堆,我们应该创建自己的堆,而不要使用默认堆。

 

     默认堆由系统创建和释放,我们无法销毁默认堆。每个堆都有一个标识自己的句柄,所有分配和释放内存块的堆函数都会在参数中使用到这个堆句柄。我们可以调用GetProcessHeap来得到默认堆的句柄。

 

 

HANDLE GetProcessHeap();


 

     创建额外堆的时机:

 

     一:对数据保护。创建两个或多个独立的堆,每个堆保存不同的结构,对两个堆分别操作,可以使问题局部化。

 

     二:更有效的内存管理。创建额外的堆,管理同样大小的对象。这样在释放一个空间后可以刚好容纳另一个对象。

 

     三:内存访问局部化。将需要同时访问的数据放在相邻的区域,可以减少缺页中断的次数。

 

     四:避免线程同步开销。默认堆的访问是依次进行的。堆函数必须执行额外的代码来保证线程安全性。通过创建额外的堆可以避免同步开销。

 

     五:快速释放。我们可以直接释放整个堆而不需要手动的释放每个内存块。这不但极其方便,而且还可以更快的运行。

 

     创建额外的堆

 

     我们可以调用HeapCreate来创建额外的堆:

 

 

HANDLE HeapCreate(

     DWORD fdwOptions,

     SIZE_T dwInitilialize,

     SIZE_T dwMaximumSize);


 

     fdwOptions表示对堆的操作该如何进行。可以传入0,   HEAP_NO_SERIALIZE

HEAP_GENERATE_EXCEPTIONSHEAP_CREATE_ENABLE_EXECUTE或这些标志的组合。

HEAP_NO_SERIALIZE告诉堆管理器堆管理器不负责堆的线程安全性。对堆的线程安全性的控制由程序员控制。

 

     HEAP_GENERATE_EXCEPTIONS标志告诉系统,每当在堆中分配或重新分配内存块失败时抛出一个异常。用来通知应用程序有错误发生。

 

     dwInitialSize表示一开始要调拨给堆的字节数。如果需要HeapCreate会把这个值向上取整到cpu页面大小的整数倍。

 

     dwMaximumSize表示堆所能增长到的最大大小。即系统为堆所预定的地址空间的最大大小。如果试图分配的内存块超过最大大小,分配操作会失败。如果dwMaximumSize0,则表明创建的堆是可增长的,没有一个指定上限。

 

     函数执行成功HeapCreate会返回一个句柄,标识了新创建的堆

 

堆创建后,需要从堆中分配内存时,要调用HeapAlloc函数:

 

 

PVOID HeapAlloc(

     HANDLE hHeap,

     DWORD fdwFlags,

     SIZE_T dwBytes);


 

     hHeap是一个堆句柄,表示要从哪个堆分配内存。

 

     fdwFlags用来执行一些标志。这些标志会对分配产生一些影响。总共有三个标志:

 

          HEAP_ZERO_MEMORYHEAP_GENERATE_EXCEPTIONSHEAP_NO_SERIALIZE

 

     HEAP_ZERO_MEMORY会让HeapAlloc返回之前把内存块的内容都清

 

     HEAP_GENERATE_EXCEPTIONS用来告诉系统如果堆中没有足够的内存来满足分配请求,此次调用的

HeapAlloc应抛出异常。可以在创建堆时指定这个标志,只要在这个堆上分配内存,如果内存不足都抛出异常。

 

     如果分配成功HeapAlloc会返回内存块地址。否则将会返回NULL

 

     默认情况下,对堆的访问会依次进行。当任何程序试图从堆中分配一块内存时,HeapAlloc会执行以下操作:

 

     1:遍历已分配的内存的链表和闲置内存的链表。

 

     2:找到一块足够大的闲置内存块。

 

     3:分配一块新的内存,将2找到的内存块标记为已分配。

 

     4:将新分配的内存块添加到已分配的链表中。

 

     注意:在分配大于1MB的内存时应该避免使用堆函数,而应该使用VirtualAlloc函数。

 

     HeapReAlloc可以改变堆中某一块内存的大小:

 

 

PVOID HeapReAlloc(

     HANDLE hHeap,

     DWORD fdwFlags,

     PVOID pvMem,

     SIZE_T dwBytes);


 

hHeap用来标识一个堆。

 

fdwFlags用来在调整内存块大小时用到这些标志。可以有以下标志:HEAP_GENERATE_EXCEPTIONSHEAP_NO_SERIALIZEHEAP_ZERO_MEMORYHEAP_REALLOC_IN_PLACE_ONLY

 

前两个标志与前面介绍的一样。只有当增大内存块时HEAP_ZERO_MEMORY才有用。额外的字节会被清0

 

在增大内存块时HeapReAlloc可能会移动内存块,HEAP_REALLOC_IN_PLACE_ONLY标志告诉HeapReAlloc尽量不要移动内存块。如果不移动不能增大内存块,则HeapReAlloc返回新地址。

 

pvMem指定要调整大小的内存块。

 

dwBytes指定内存块的新大小。

 

分配一块内存后,调用HeapSize可以获得这块内存的实际大小:

 

 

SIZE_T HeapSize(

    HANDLE hHeap,

    DWORD fdwFlags,

    LPCVOID pvMem);


 

hHeap用来标识堆。

 

pvMem表示内存地址。

 

dwFlags可以是0HEAP_NO_SERIALIZE

 

当不要使用一块内存时可以调用HeapFree来释放它:

 

 

BOOL HeapFree(

    HANDLE hHeap,

    DWORD fdwFlags,

    PVOID pvMem);


 

如果操作成功则返回TRUE。调用这个函数可能会使堆管理器撤销一些已经调拨的物理存储器。

 

如果应用程序不再需要自己创建的堆,可以调用HeapDestroy来销毁它:

 

 

BOOL HeapDestroy(HANDLE hHeap);


 

此时系统会收回堆所占用的物理存储器和地址空间区域。执行成功则返回TRUE。如果我们不调用此函数主动销毁自己创建的堆,在进程结束时,系统会替我们销毁。我们不能调用此函数销毁默认堆,默认堆由系统管理。

 

C++中使用堆

 

C++中我们可以调用new操作符来分配类对象。不需要时可以调用delete来释放它。如

 

 

CA *pCA=new CA;


 

在编译此段代码时,编译器会首先检查类CA是否重载了new操作符成员函数。如果找到编译器会调用这个函数。否额,会调用C++标准的new操作符。

 

 

deleted pCA;


 

对此句代码C++编译器会执行与上面类似的步骤,只有CA类没有重载delete操作符成员函数时,才会调用标准的C++delete运算符。

 

通过对C++类的newdelete操作符进行重载,我们可以非常容易的将堆函数加以运用:

 

 

class CA

{

  public:

       CA();

       ~CA();

  public:

       void *operator new(size_t size);

       void*operator delete(void*p);

};


 

上述代码调用operator newoperator delete是从默认堆中分配的内存。我们可以让其在自己创建的堆中分配内存,一般让所有对象共享同一个堆,每个对象都创建一个堆为导致额外的性能开销。可以采用计数法来对堆的生存期进行控制。

 

ToolHelp函数允许我们枚举进程的堆以及分配的内存块。它包括一下函数:Heap32First,Heap32Next,Heap32ListFirstHeap32ListNext

 

由于进程在自己的地址空间可以有多个堆,GetProcessHeaps可以让我们得到这些堆的句柄。

 

 

DWORD GetProcessHeaps(

     DWROD dwNumHeaps,

     PHANDLE phHeaps);


 

phHeaps是一个数组指针。用以存储返回的堆句柄。

 

dwNumHeaps是数组大小。

 

函数返回句柄数组个数。

 

函数所返回的句柄数组中也包括进程的默认堆的句柄。

 

 

HANDLE hHeaps[20];

DWORD dwHeaps=GetProcessHeaps(20,hHeaps);


 

HeapValidate可以验证堆的完整性。

 

 

BOOL HeapValidate(

    HANDLE hHeap,

    DWORD fdwFlags,

    LPCVOID pvMem);


 

通常在调用这个函数时,我们会传一个堆句柄和一个标志0,并传入NULLpvMem。该函数会遍历堆中的各个内存块,确保没有任何一块内存被破坏。如果给pvMem制定一块内存地址,那么函数就只检查这一块内存的完整性。

 

为了让堆中闲置的内存块能重新结合在一起,并撤销调拨给堆中闲置内存块的存储器,可以调用HeapCompact

 

UINT HeapCompact(

    HANDLE hHeap,

    DWORD fdwFlags);


 

一般来说会传0fdwFlags

 

下面两个函数要配对使用,用于线程同步:

BOOL HeapLock(HANDLE hHeap);

BOOL HeapUnlock(HANDLE hHeap);




 

 

当第一个线程调用HeapLock时,它就占有了堆。其他线程在调用堆函数时,系统就会暂停其他线程。只有当第一个线程调用HeapUnlock之后才会唤醒被暂停的进程。

 

HeapAllocHeapSizeHeapFree这些函数会在内部调用HeapLockHeapUnlock,一般来说不需要自己去调用HeapLockHeapUnlock

 

最后一个函数是HeapWalk,它允许我们遍历堆的内容。只用于调试。具体不再介绍。

            以上参考自《Windows核心编程》第五版第三部分,如有纰漏,请不吝指正!!
                                                                     2012.12.28于山西大同
<think>嗯,用户之前让我介绍了JUC并发工具包,现在又问线程池,显然想更深入了解线程池的部分。首先,我需要回顾之前提到的线程池内容,确保这次回答更详细全面。 用户可能对线程池的基本概念有一定了解,但需要深入原理、参数配置、使用场景以及注意事项。他们可能在实际开发中遇到了性能问题,或者想优化现有的线程池配置。 我需要分结构讲解,比如核心参数、工作流程、常见类型、配置建议注意事项。要避免使用过多专业术语,用更易懂的例子场景说明。例如,解释corePoolSizemaxPoolSize时,可以用银行柜台的例子,让读者更容易理解。 另外,用户可能对如何选择线程池类型配置参数有疑问,比如为什么推荐自定义ThreadPoolExecutor而不是Executors的工厂方法。需要强调资源耗尽的风险,比如OOM,并举出newFixedThreadPoolnewCachedThreadPool的问题。 还要提到线程池的工作流程,任务队列的不同类型及其影响,比如LinkedBlockingQueueSynchronousQueue的区别。可能用户在处理高并发任务时,对任务排队拒绝策略不太清楚,需要详细说明四种拒绝策略及其适用场景。 在注意事项部分,需要提醒线程池的资源隔离、异常处理以及监控的重要性。特别是异常处理,如果不当可能导致任务静默失败,影响排查。可以给出使用UncaughtExceptionHandler或者Future.get()捕获异常的方法。 最后,总结部分要简明扼要,强调线程池的优势正确使用的重要性,帮助用户形成系统化的理解。确保回答结构清晰,层次分明,让用户能逐步掌握线程池的核心知识点。</think>### 线程池深度解析:Java并发编程的发动机 线程池是 JUC 中资源管理的核心工具,通过复用线程降低创建/销毁开销,提升系统吞吐量。以下从底层原理到实践场景全面剖析: --- #### 一、线程池核心参数(以 `ThreadPoolExecutor` 为例) ```java new ThreadPoolExecutor( int corePoolSize, // 核心线程数(常驻线程) int maximumPoolSize, // 最大线程数(应急线程) long keepAliveTime, // 非核心线程空闲存活时间 TimeUnit unit, // 时间单位 BlockingQueue<Runnable> workQueue, // 任务队列 ThreadFactory threadFactory, // 线程创建工厂 RejectedExecutionHandler handler // 拒绝策略 ) ``` **参数比喻**: - 核心线程 = 银行固定柜台 - 最大线程 = 临时加开的流动柜台 - 任务队列 = 客户等候区 - 拒绝策略 = 客满时的处理方式 --- #### 二、线程池工作流程(关键执行逻辑) 1. 提交任务时优先使用核心线程 2. 核心线程满 → 任务进入队列 3. 队列满 → 创建非核心线程(不超过 maximumPoolSize) 4. 线程队列均满 → 触发拒绝策略 **流程图**: ``` 新任务提交 → 核心线程是否满? ├─ 否 → 创建核心线程执行 └─ 是 → 队列是否满? ├─ 否 → 加入队列等待 └─ 是 → 能否创建新线程? ├─ 能 → 创建临时线程执行 └─ 不能 → 执行拒绝策略 ``` --- #### 三、四大拒绝策略对比 | 策略类 | 行为 | 适用场景 | |---------------------------------|--------------------------------------|-----------------------| | `AbortPolicy`(默认) | 抛出 `RejectedExecutionException` | 严格要求任务完整性 | | `CallerRunsPolicy` | 由提交任务的线程直接执行 | 流量削峰,降低提交速度 | | `DiscardPolicy` | 静默丢弃新任务 | 允许丢任务(如日志监控)| | `DiscardOldestPolicy` | 丢弃队列最旧任务,重试提交新任务 | 优先处理最新任务 | --- #### 四、常见线程池类型(通过 `Executors` 创建) 1. **固定线程池** ```java ExecutorService fixedPool = Executors.newFixedThreadPool(5); ``` - 特点:corePoolSize = maximumPoolSize,队列无界 - 风险:任务积可能导致 OOM 2. **缓存线程池** ```java ExecutorService cachedPool = Executors.newCachedThreadPool(); ``` - 特点:corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,队列为 `SynchronousQueue` - 风险:线程数量失控 3. **单线程池** ```java ExecutorService singleThreadPool = Executors.newSingleThreadExecutor(); ``` - 特点:保证任务顺序执行,队列无界 - 风险:同固定线程池 4. **定时线程池** ```java ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3); scheduledPool.scheduleAtFixedRate(task, initialDelay, period, unit); ``` - 支持定时/周期性任务 --- #### 五、线程池配置黄金法则 1. **CPU 密集型任务** ```math corePoolSize = CPU核心数 + 1 ``` (如 8 核 CPU → 9 线程) 2. **IO 密集型任务** ```math corePoolSize = CPU核心数 × 2 ``` (如 8 核 CPU → 16 线程) 3. **混合型任务** - 拆分为 CPU 密集 IO 密集两部分 - 分别使用不同线程池 --- #### 六、生产环境最佳实践 1. **禁止使用 `Executors` 创建线程池** - 推荐手动配置 `ThreadPoolExecutor` 参数 ```java // 标准安全配置示例 new ThreadPoolExecutor( Runtime.getRuntime().availableProcessors(), // 核心线程数 Runtime.getRuntime().availableProcessors() * 2, // 最大线程数 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000), // 有界队列 new CustomThreadFactory(), // 自定义线程命名 new ThreadPoolExecutor.CallerRunsPolicy() // 降级策略 ); ``` 2. **监控关键指标** ```java ThreadPoolExecutor pool = (ThreadPoolExecutor) executor; System.out.println("活跃线程数:" + pool.getActiveCount()); System.out.println("已完成任务数:" + pool.getCompletedTaskCount()); System.out.println("队列大小:" + pool.getQueue().size()); ``` 3. **优雅关闭** ```java executor.shutdown(); // 停止接收新任务 if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { executor.shutdownNow(); // 强制终止 } ``` --- #### 七、常见问题与解决方案 1. **线程池打满导致服务不可用** - 增加队列容量 - 优化任务处理速度 - 使用熔断机制 2. **线程上下文切换开销大** - 降低线程数量 - 使用协程(需第三方库,如 Quasar) 3. **任务执行异常丢失** - 在任务内加 try-catch - 使用 `Future` 获取异常 ```java Future<?> future = executor.submit(task); try { future.get(); } catch (ExecutionException e) { Throwable cause = e.getCause(); // 处理异常 } ``` --- ### 总结:线程池的三层认知 1. **基础层**:理解核心参数与工作流程 2. **实践层**:根据业务特点选择配置策略 3. **高级层**:监控调优与异常处理体系 **特别提示**:在高并发场景下,建议为不同业务类型使用独立线程池,避免相互影响(如订单处理与日志记录分开)。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值