这是一篇关于内存管理算法的文章,对于Java开发者而言这个话题比较遥远。 虽然咱们平常开发中一直在跟内存打交道,但不多关注过内存管理的具体细节,毕竟JVM已经作得很好了。 然而在高并发场景下,程序运行过程当中产生的大量内存对象,会形成必定的GC负担,这直接影响着程序运行性能。若是能缓解一部分GC压力,节省下来的系统资源便会对性能有显著的提高,由此便衍生出了池技术。java
本次咱们分享的内存池技术主要用于提高网络通讯的I/O能力,固然该技术也可用于本地磁盘I/O。比较常见的内存管理算法有如下几种:算法
首次适应算法(First-Fit)数组
从空闲分区表的第一个表目起查找该表,把最早可以知足要求的空闲区分配给做业,这种方法目的在于减小查找时间。为适应这种算法,空闲分区表(空闲区链)中的空闲分区要按地址由低到高进行排序。该算法优先使用低址部分空闲区,在低址空间形成许多小的空闲区,在高地址空间保留大的空闲区。网络
优势多线程
该算法倾向于优先利用内存中低址部分的空闲分区,从而保留了高址部分的大空闲区,这为之后到达的大做业分配大的内存空间创造了条件。并发
缺点socket
低址部分不断被划分,会留下许多难以利用的,很小的空闲分区,称为碎片。而每次查找又都是从低址部分开始的,这无疑又会增长查找可用空闲分区时的开销。高并发
最佳适应算法(Best-Fit)性能
从所有空闲区中找出能知足做业要求的、且大小最小的空闲分区,这种方法能使碎片尽可能小。为适应此算法,空闲分区表(空闲区链)中的空闲分区要按从小到大进行排序,自表头开始查找到第一个知足要求的自由分区分配。该算法保留大的空闲区,但形成许多小的空闲区。this
最差适应算法(Worst-Fit)
它从所有空闲区中找出能知足做业要求的、且大小最大的空闲分区,从而使链表中的结点大小趋于均匀,适用于请求分配的内存大小范围较窄的系统。为适应此算法,空闲分区表(空闲区链)中的空闲分区要按大小从大到小进行排序,自表头开始查找到第一个知足要求的自由分区分配。该算法保留小的空闲区,尽可能减小小的碎片产生。
这些算法各有优劣,本次咱们只分享首次适应算法,smart-socket中正是应用了该算法实现的高性能通讯。
接下来咱们经过几个步骤来演示内存申请、释放的过程,以及在此过程当中如何致使内存碎片化的产生。
初始状态内存容量为15。
ABCDE前后申请特定大小的内存块:一、二、三、四、5,此时内存池中已无可用空间。
B、D释放内存,内存池中出现两块不相邻的内存块。后续再次申请内存即可从这两块不相邻的内存块中挑选可用空间进行分配。
F申请1字节,G申请2字节。按First-Fit算法,会优先从低位查找可用内存块。当F申请到第2位内存块后,紧邻的3号内存块便再也不知足G所需的2字节,因此只能从7~10号内存块中申请2字节。若是内存块小到没法知足应用所需,便成了内存碎片。
A、C、E回收内存,内存池中还原出了大片可用区域。如若F、G也释放内存,则次内存池便恢复如初。
availableBuffers有序存储了内存池申请/释放过程当中产生的内存块。低地址内存块存储于队列头部,高地址存于队列尾部。
申请内存时遍历内存块队列,查找容量足够的内存块。
若是内存块容量恰好符合申请所需大小,则从队列中移除该内存块并返回。
若是内存容量大于申请所需大小,则对该内存块进行拆分。只返回所需大小的内存块,剩余部分存留于队列中。
若无可用内存块,则申请失败,此时只能建立临时内存块。
public VirtualBuffer allocate(final int size) { lock.lock(); try { Iterator<VirtualBuffer> iterator = availableBuffers.iterator(); VirtualBuffer bufferChunk; while (iterator.hasNext()) { VirtualBuffer freeChunk = iterator.next(); final int remaining = freeChunk.getParentLimit() - freeChunk.getParentPosition(); if (remaining < size) { continue; } if (remaining == size) { iterator.remove(); buffer.limit(freeChunk.getParentLimit()); buffer.position(freeChunk.getParentPosition()); freeChunk.buffer(buffer.slice()); bufferChunk = freeChunk; } else { buffer.limit(freeChunk.getParentPosition() + size); buffer.position(freeChunk.getParentPosition()); bufferChunk = new VirtualBuffer(this, buffer.slice(), buffer.position(), buffer.limit()); freeChunk.setParentPosition(buffer.limit()); } return bufferChunk; } } finally { lock.unlock(); } return new VirtualBuffer(null, allocate0(size, false), 0, 0); }
使用完毕的内存块须要主动释放回收,以供下次继续使用。释放的过程主要作到两点:
找到被释放内存块在内存队列中的正确点位。
被释放内存块所处的点位若能与先后相邻内存块造成连续内存块,则合并内存块;反之,则直接放入队列中便可。
private void clean0(VirtualBuffer cleanBuffer) { int index = 0; Iterator<VirtualBuffer> iterator = availableBuffers.iterator(); while (iterator.hasNext()) { VirtualBuffer freeBuffer = iterator.next(); //cleanBuffer在freeBuffer以前而且造成连续块 if (freeBuffer.getParentPosition() == cleanBuffer.getParentLimit()) { freeBuffer.setParentPosition(cleanBuffer.getParentPosition()); return; } //cleanBuffer与freeBuffer以后并造成连续块 if (freeBuffer.getParentLimit() == cleanBuffer.getParentPosition()) { freeBuffer.setParentLimit(cleanBuffer.getParentLimit()); //判断后一个是否连续 if (iterator.hasNext()) { VirtualBuffer next = iterator.next(); if (next.getParentPosition() == freeBuffer.getParentLimit()) { freeBuffer.setParentLimit(next.getParentLimit()); iterator.remove(); } else if (next.getParentPosition() < freeBuffer.getParentLimit()) { throw new IllegalStateException(""); } } return; } if (freeBuffer.getParentPosition() > cleanBuffer.getParentLimit()) { availableBuffers.add(index, cleanBuffer); return; } index++; } availableBuffers.add(cleanBuffer); }
完整代码参阅smart-socket项目中的BufferPage.java
内存申请/释放在实际应用中还有一个没法回避的问题,那就是并发。如何才能在高并发场景下保证内存池依旧能高效稳定的提供申请与释放服务? 为了不多线程并发申请致使某块内存区域被屡次分配,必需要对申请的过程加同步锁控制,内存释放的过程亦是如此。
可一旦加上同步锁,内存的申请、释放性能必然受到影响。最为理想的状态是每个CPU绑定着独立的内存池对象, 运行时便不存在多个CPU对同一个内存池对象进行申请/释放操做,这样即可实现无锁化。
惋惜CPU绑定内存池的想法没法实现,只能作到线程级的隔离,采用ThreadLocal即可。只不过此方式如若使用不当可能出现内存泄露,以及内存池资源利用率不高等状况。 为此,推荐的作法是采用数组的方式来维护多个内存池对象,使用时经过某种均衡策略将内存池对象分配给任务做业。 虽然不能杜绝锁竞争的状况发生,但在必定程度上仍是能够下降锁机率的。