作者简介
路路内存池源码,热爱技术、乐于分享内存池源码的技术人内存池源码,目前主要从事数据库相关技术内存池源码的研究。
前言今天给大家带来 DBLE 的内存管理模块源码解析文章。
本文主要分为两部分,一是内存管理模块结构,二是内存管理模块源码解析。
内存管理模块结构DBLE 管理的内存的主要目的有以下两点内存池源码:
1. 网络读写
2. 查询结果集处理
简单说明一下这两点:
第一点比较好理解,网络读的时候需要从通道中读取字节流到内存中,以便进一步处理,网络写的时候也需要将字节流从内存中写入到通道中,以便发送信息。
第二点就比较复杂一些了,DBLE 获取到 MySQL 端的数据后,会经过 DBLE 然后再发送给客户端,如果是简单查询的话,DBLE 端只是起到一个中转的作用,会利用少量内存用于将结果转发给客户端。如果是复杂查询的话,DBLE 会先对结果集进行处理,比如连接、分组排序等操作,然后再将结果发送给客户端,这里涉及到的内存使用就复杂了,对于复杂查询 DBLE 会先使用堆内存,如果使用的堆内存超过限制,则会写内存映射文件,如果写内存映射文件个数再次达到上限,则会写硬盘。是不是很复杂?放心,这一块本文不会介绍,原因我就不说了(太复杂了)……
我们看一下 DBLE 官方内存结构图:
个人觉得上图有一点理解上的疑惑, DirectByteBufferPool 占用的内存应该位于物理内存中,JVM 中只是有着堆外内存的引用对象而已。
下图个人觉得可能更好理解。
结合 DBLE 管理内存的主要两个作用,网络读写主要用到了堆外内存,即 DirectByteBufferPool 管理的部分。结果集处理根据情况有所不同,本文只讨论简单情况,对于简单情况来讲,会使用到堆外内存或堆内存,下文源码中会提到具体情况。
内存管理模块源码解析来看下内存管理模块涉及到的类:
从上图可以看出内存管理模块涉及到的类不多,只有两个:
1. DirectByteBufferPool 类即为管理堆外内存的内存池类,可以看到它创建了 ByteBuffePage 类,并且与该类有着一对多的关系;
2. ByteBufferPage 类为 DBLE 抽象出的堆外内存页的概念,内存页中又有内存块的概念,内存块是内存分配的最小单元。采用此种内存概念主要是为了减少内存碎片问题。
DBLE 官方对于这两个类的内部结构以图的形式表示的很清楚了:
上图中的参数 bufferPoolPageNumber、bufferPoolPageSize 和 bufferPoolChunkSize 可在 Server.xml 中配置 ,bufferPoolPageSize 默认为 2M,bufferPoolPageNumber 默认为 Java 虚拟机的可用的处理器数量 * 20, bufferPoolChunkSize 的参数默认为 4k,该参数最好设为 bufferPoolPageSize 的约数,否则最后一个会造成浪费。
在看源码前,先看下内存分配的逻辑:
1. 如果不指定分配大小,则默认分配一个最小单元(最小单元由 bufferPoolChunkSize 决定);
2. 如果指定分配大小,则分配放得下分配大小的最小单元的整数倍(向上取整) ,举个例子,如果需要 10k 的大小,则在 bufferPoolChunkSize 参数默认为 4k 的情况下,会分配 3 个内存 chunk;
3. 分配逻辑为:
(1)遍历缓冲池从 N+1 页到 bufferPoolPageNumber -1 页(上次分配过的记为第 N 页),然后对单页加锁在每个页中从头寻找未被使用的连续 M 个最小单元 ;
(2)如果没找到,再从第 0 页找到第 N 页;
(3)成功分配内存后更新上次分配页,标记内存页中分配的单元;
(4)如果找不到可存放的单页(比如大于 bufferPoolPageSize ),直接分配 On-Heap 内存。
下面我们就开始看看对应的源码。
内存池初始化的代码调用在 DbleServer#startup *** 中:
//通过配置的相关参数初始化内存池bufferPool = new DirectByteBufferPool(bufferPoolPageSize, bufferPoolChunkSize, bufferPoolPageNumber);看下内存池初始化逻辑,代码在 DirectByteBufferPool 类的构造 *** 中:
public DirectByteBufferPool(int pageSize, short chunkSize, short pageCount) { //初始化内存总页数 allPages = new ByteBufferPage[pageCount]; //内存页中的内存块大小 this.chunkSize = chunkSize; //每个内存页的大小 this.pageSize = pageSize; //内存页总数 this.pageCount = pageCount; //记录上次分配过的页 prevAllocatedPage = new AtomicInteger(0); //循环初始化内存页 for (int i = 0; i < pageCount; i++) { allPages[i] = new ByteBufferPage(ByteBuffer.allocateDirect(pageSize), chunkSize); } //记录堆外内存的使用大小 memoryUsage = new ConcurrentHashMap<>(); }继续看下内存页的初始化逻辑,在 ByteBufferPage 类的构造 *** 中:
public ByteBufferPage(ByteBuffer buf, int chunkSize) { //内存块大小 this.chunkSize = chunkSize; //计算总内存块数 chunkCount = buf.capacity() / chunkSize; //非常巧妙的位图结构,用于标记内存块的使用情况 chunkAllocateTrack = new BitSet(chunkCount); //内存页的大小 this.buf = buf;}上述代码完成了堆外内存的初始化。
下面让我们看看内存的分配逻辑代码。
分配内存的操作主要在 DirectByteBufferPool#allocate *** 中:
public ByteBuffer allocate(int size) { //计算需要分配的chunk数 final int theChunkCount = size / chunkSize + (size % chunkSize == 0 ? 0 : 1); //本次分配的开始页数,为上次分配过的页数(N)+1 int selectedPage = prevAllocatedPage.incrementAndGet() % allPages.length; //从N+1页到bufferPoolPageNumber-1页遍历分配,allocateBuffer *** 下面会进一步分析 ByteBuffer byteBuf = allocateBuffer(theChunkCount, selectedPage, allPages.length); //没有分配成功,则从0页到N页继续遍历分配 if (byteBuf == null) { byteBuf = allocateBuffer(theChunkCount, 0, selectedPage); } final long threadId = Thread.currentThread().getId(); //分配成功的话,则记录分配到的内存大小 if (byteBuf != null) { if (memoryUsage.containsKey(threadId)) { memoryUsage.put(threadId, memoryUsage.get(threadId) + byteBuf.capacity()); } else { memoryUsage.put(threadId, (long) byteBuf.capacity()); } } //如果遍历完所有页还没有分配成功,则直接分配堆上内存 if (byteBuf == null) { //ByteBuffer.allocate *** 为分配JVM堆内存 return ByteBuffer.allocate(size); } return byteBuf; }继续看下 DirectByteBufferPool#allocateBuffer *** :
private ByteBuffer allocateBuffer(int theChunkCount, int startPage, int endPage) { //从指定页数开始遍历分配内存,分配成功则标记当前分配的页数,然后直接返回 for (int i = startPage; i < endPage; i++) { //调用了ByteBufferPage类的allocateChunk *** 进行内存块的分配,该 *** 下面也会进一步分析 ByteBuffer buffer = allPages[i].allocateChunk(theChunkCount); if (buffer != null) { prevAllocatedPage.getAndSet(i); return buffer; } } return null; }继续看一下 ByteBufferPage#allocateChunk *** ,该 *** 比较长,分配页中的连续内存块逻辑就在此 *** 中:
public ByteBuffer allocateChunk(int theChunkCount) { //对页加状态锁,防止并发操作异常 if (!allocLockStatus.compareAndSet(false, true)) { return null; } int startChunk = -1; int continueCount = 0; try { //下面的逻辑为在页中找到符合内存分配大小的连续内存块 for (int i = 0; i < chunkCount; i++) { if (!chunkAllocateTrack.get(i)) { if (startChunk == -1) { startChunk = i; continueCount = 1; if (theChunkCount == 1) { break; } } else { if (++continueCount == theChunkCount) { break; } } } else { startChunk = -1; continueCount = 0; } } //成功找到后则返回分配的内存块,并且标记相应的内存块位置 if (continueCount == theChunkCount) { int offStart = startChunk * chunkSize; int offEnd = offStart + theChunkCount * chunkSize; buf.limit(offEnd); buf.position(offStart); ByteBuffer newBuf = buf.slice(); markChunksUsed(startChunk, theChunkCount); return newBuf; } else { //分配失败返回null return null; } } finally { //解锁 allocLockStatus.set(false); }}到这里,内存分配的逻辑就大概讲完了。
有分配也得有回收,为了文章的完整性,内存回收也顺带讲一下。
内存回收逻辑比较简单,分两种情况,如果是分配的堆内存,则直接 clear 等待 GC 回收,如果是堆外内存,则遍历所有页,找到对应页然后加锁,找到对应块的位置,标记为未使用就可以了。
内存回收的主要代码在 DirectByteBufferPool#recycle *** 中:
public void recycle(ByteBuffer theBuf) { //堆内存的回收 if (!(theBuf instanceof DirectBuffer)) { theBuf.clear(); return; } final long size = theBuf.capacity(); //堆外内存的回收 boolean recycled = false; DirectBuffer thisNavBuf = (DirectBuffer)theBuf; int chunkCount = theBuf.capacity() / chunkSize; DirectBuffer parentBuf = (DirectBuffer) thisNavBuf.attachment(); int startChunk = (int) ((thisNavBuf.address() - parentBuf.address()) / this.chunkSize); //遍历页然后将对应块标记为未使用即可 for (ByteBufferPage allPage : allPages) { if ((recycled = allPage.recycleBuffer((ByteBuffer) parentBuf, startChunk, chunkCount))) { break; } } final long threadId = Thread.currentThread().getId(); if (memoryUsage.containsKey(threadId)) { memoryUsage.put(threadId, memoryUsage.get(threadId) - size); } if (!recycled) { LOGGER.info("warning ,not recycled buffer " + theBuf); } }内存的分配与回收到这里也讲完了,还记得开始时候说的内存的主要作用吗?一个是网络读写时使用,一个是结果集暂存时使用。
网络读写时使用的例子可以参考这里,NIOSocketWR#asyncRead *** :
public void asyncRead() throws IOException { ByteBuffer theBuffer = con.readBuffer; if (theBuffer == null) { //这里分配了从通道读取数据时候的内存 theBuffer = con.processor.getBufferPool().allocate(con.processor.getBufferPool().getChunkSize()); con.readBuffer = theBuffer; } int got = channel.read(theBuffer); con.onReadData(got);}暂存结果集使用的例子可以参考这里,SingleNodeHandler#rowResponse *** :
//省略无关内容......//这里分配内存,将结果集写入,然后待发送到客户端buffer = session.getSource().writeToBuffer(row, allocBuffer());......当然内存分配的代码调用远不止上面举例的两处,这里只是和内存管理的作用做个呼应。其他地方大家可以自己看。
总结DBLE 的内存管理模块源码阅读的内容如上所述,希望能够对大家更深入的理解 DBLE 有所帮助。
相关文章
本站已关闭游客评论,请登录或者注册后再评论吧~