# Netty 核心原理十七 内存池设计与JDK基础

本节将详细描述如下原理:

  1. 内存类型
  2. Buffer原理
  3. ByteBuffer原理
  4. ByteBuf原理

内存类型

在Java中,我们知道:

  1. JVM属于运行在OS上的一个进程
  2. 进程拥有自己的:代码段、数据段、堆、堆栈
  3. 这时我们称JVM的堆为C-Heap(C堆),注意:这是JVM进程的原生堆内存
  4. 为了满足JVM对自身对象的支持,这时我们从JVM原生的C-Heap(C堆)中创建一片内存Java-Heap(Java堆),用于存放Java的对象Oop(普通对象指针,在研究JVM时,我们看到Oop就是Java的对象,了解即可)
  5. 这时,我们就可以定义:堆外内存(C-Heap)、堆内内存(Java-Heap)

在Java中我们可以使用byte[]数组来表示堆内内存,使用DirectByteBuffer来表示堆外内存,为了满足Java NIO模型,那么这时,那么这时我们使用Buffer作为NIO 缓冲区的根类来包装byte[]数组与DirectByteBuffer,事实上读者可以在后面看到,实际对于堆外内存的控制,将通过Unsafe类来完成操作,因为对于堆外内存来说,不属于Java-Heap部分,与GC无关,将会存在内存泄漏的风险,所以将其定义到Unsafe类中。

Buffer原理

我们说在NIO模型中,使用Buffer抽象类来包装堆内内存byte[]数组和堆外内存Unsafe类分配的C-Heap内存。那么本节来看下Buffer类及其子类如何包装这两个不同的内存,同时又是如何调用Unsafe类来完成C-Heap的分配。

Buffer原理

通过源码我们得知如下信息:

  1. 使用capacity表示实际内存(读者注意:不管是堆外内存和堆内内存,他们有一个共同点,那就是均为数组,所以我们这里将其作为数组看待)的容量
  2. 使用position表示当前操作的实际内存的数组下标
  3. 使用limit表示当前操作的下标限制,比如:我们可以定义position为0,limit为5,限制当前读取或者写入操作最大下标
  4. 使用mark标记当前处理的position位置,便于在后面我们可以通过mark标记位将position的位置还原
  5. 使用address表示在使用C-Heap时保存数组的首地址
public abstract class Buffer {

 // 关系: mark <= position <= limit <= capacity

 private int mark = -1;

 private int position = 0;

 private int limit;

 private int capacity;

 long address; // 包装C-Heap时使用

 

 // 反转position与limit:将position置0,然后将当前position的位置设置为limit的值。如何用?初始时:limit等于capacity,position为0,那么我可以从0下标开始写,然后写入的最大长度为capacity,比如我们写了5,那么这时,我想要读取其中的数据,那么我就可以调用该方法,将position置0,然后limit置为5,这时我就可以读取写入的数据~

 public final Buffer flip() {

  limit = position;

  position = 0;

  mark = -1;

  return this;

 }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

ByteBuffer原理

通过源码我们得知如下信息:

  1. 定义byte[] hb 用于存储实际堆内内存的数组
  2. 封装了创建堆内内存 HeapByteBuffer 和 堆外内存 DirectByteBuffer的变量
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>{

 final byte[] hb; // 实际保存数据的堆内内存数组(读者考虑下:如果是CharBuffer呢?很明显,变味了char[]数组)

 

 public static ByteBuffer allocateDirect(int capacity) {

  return new DirectByteBuffer(capacity);

 }

 

 public static ByteBuffer allocate(int capacity) {

  if (capacity < 0)

   throw new IllegalArgumentException();

  return new HeapByteBuffer(capacity, capacity);

 }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

HeapByteBuffer

没什么特殊的,由于是堆内内存,所以直接创建数组对象即可。

class HeapByteBuffer extends ByteBuffer{

 HeapByteBuffer(int cap, int lim) {   // package-private

  super(-1, 0, lim, cap, new byte[cap], 0);

 }

}
1
2
3
4
5
6
7
8
9

DirectByteBuffer

该类继承自MappedByteBuffer,基础好的读者应该知道,该MappedByteBuffer用于支持文件与进程的映射,底层通过mmap函数来实现,这里由于我们只关注C-Heap和Java-Heap,所以以后再展开讲解。通过源码我们得知:

  1. DirectByteBuffer在创建时,将会调用Unsafe来开辟C-Heap内存
  2. 同时创建Cleaner对象,该对象为虚引用对象,将会包装DirectByteBuffer对象,当DirectByteBuffer对象被GC时,释放堆外内存,保证不会发生内存泄漏
  3. 同时通过源码我们得知:可以通过反射获取到内部的Cleaner对象来手动释放内存
class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer {

 protected static final Unsafe unsafe = Bits.unsafe(); // 获取Unsafe对象

 

 // 用于在DirectByteBuffer对象被GC后,在虚引用中Cleaner中调用,释放堆外内存,这样就避免了内存泄漏,读者注意:这里有一个前提条件:DirectByteBuffer对象被GC后,如果没有被GC,那么堆外内存将不会被释放。

 private static class Deallocator implements Runnable{

  private static Unsafe unsafe = Unsafe.getUnsafe();

  private long address; // 保存堆外内存的地址

  private long size;

  private int capacity;

  private Deallocator(long address, long size, int capacity) {

   assert (address != 0);

   this.address = address;

   this.size = size;

   this.capacity = capacity;

  }



  public void run() { // 由ReferenceHandler线程调用Cleaner,然后Cleaner调用该方法完成释放

   if (address == 0) {

    return;

   }

   unsafe.freeMemory(address);

   address = 0;

   Bits.unreserveMemory(size, capacity);

  }



 }

 private final Cleaner cleaner;

 public Cleaner cleaner() { return cleaner; }

 

 DirectByteBuffer(int cap) {     // package-private

  super(-1, 0, cap, cap); // 初始mark、position、limit、capacity

  ...

  long base = 0;

  try {

   base = unsafe.allocateMemory(size); // 分配C-Heap内存

  } catch (OutOfMemoryError x) {

   Bits.unreserveMemory(size, cap);

   throw x;

  }

  unsafe.setMemory(base, size, (byte) 0);

  ...

  cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); // 创建Cleaner对象,当该对象被移除时调用Deallocator的run方法完成堆外内存释放

  att = null;

 }

}

// 了解下即可,虚引用,当关联的对象 DirectByteBuffer 被释放时 由ReferenceHandler线程调用,后面笔者会专门写一篇强、软、弱、虚的文章来解释

public class Cleaner extends PhantomReference<Object> {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93

ByteBuf原理

通过前面对于JDK的Buffer及其子类的学习,不难看到,异常复杂,需要用户感知这四个标记位,学习成本较高,所以Netty对ByteBuffer进行了重新设计,提供了ByteBuf 类:

  1. 该类代表了一个随机和顺序的字节数组
  2. 封装了对于堆内内存byte[]数组和原生JDK ByteBuffer(堆外内存)的操作
  3. 提供了两个索引下标来支持顺序读和写操作:readerIndex(读操作下标)、writerIndex(写操作下标)
  4. 使用capacity变量和maxCapacity作为容量限制
  5. 这两个下标满足:0 <= readerIndex <= writerIndex <= capacity
  6. 当ByteBuf实际存放信息的对象为byte[]字节数组时,我们可以直接通过array()方法来获取该数组
  7. 当ByteBuf实际存放信息的对象为ByteBuffer对象时,我们可以直接通过nioBuffer()方法来获取该对象
  8. 实现ReferenceCounted 接口,定义了使用引用计数来对池化的ByteBuf对象执行回收操作
  9. Netty的ByteBuf分为池化与非池化,对于非池化而言,同Buffer一样,内部保存byte[]数组或者DirectByteBuffer。对于池化而言,PooledHeapByteBuf类和PooledDirectByteBuf类两者定义的泛型类型不一样,同时,在其中执行的操作也不一样,因为对于堆内内存而言,直接使用数组下标,而对于堆外内存而言,需要调用DirectByteBuffer或者Unsafe的方法来操作(如果我们使用Unsafe自己分配堆外内存时)

对于PoolArea、PoolChunk内存池的对象,将在下一篇进行讲解,读者这里了解整体框架即可。

public interface ReferenceCounted {

 /**

 * 返回当前引用计数

 */

 int refCnt();



 /**

 * 当前引用计数 + 1

 */

 ReferenceCounted retain();



 /**

 * 当前引用计数 + increment

 */

 ReferenceCounted retain(int increment);



 /**

 * 记录该节点当前的访问位置,用于调试。如果确定该对象为内存泄露对象,则将该操作记录的信息提供给用户

 */

 ReferenceCounted touch();



 /**

 * 同touch(),只不过这里记录了hint对象,该对象同样用于记录信息,用于在发生内存泄漏时给用户提示

 */

 ReferenceCounted touch(Object hint);



 /**

 * 当前引用计数 - 1

 */

 boolean release();



 /**

 * 当前引用计数 - decrement

 */

 boolean release(int decrement);

}



// 同Buffer类一样,定义父类支持方法

public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> {

 public abstract byte[] array(); // 当包装对象为byte数组时获取该数组本身

 public abstract ByteBuffer nioBuffer(); // 当包装对象为ByteBuffer对象时获取该对象本身

 public abstract int writableBytes(); // 获取当前可写的字节数:this.capacity - this.writerIndex

 public abstract int readableBytes(); // 获取当前可读的字节数:this.writerIndex - this.readerIndex

 public abstract int writerIndex(); // 获取当前写下标

 public abstract ByteBuf writerIndex(int writerIndex); // 设置当前写下标

 public abstract int readerIndex(); // 获取当前读下标

 public abstract ByteBuf readerIndex(int readerIndex); // 获取当前写下标

 public abstract int capacity(); // 获取当前容量

 public abstract ByteBuf capacity(int newCapacity); // 设置当前容量

 public abstract int maxCapacity(); // 获取最大容量

 public abstract ByteBuf discardReadBytes(); // 丢弃已经读取的字节

}



// 抽象类,用于实现ByteBuf中定义的方法支撑变量

public abstract class AbstractByteBuf extends ByteBuf {

 static final ResourceLeakDetector<ByteBuf> leakDetector =

  ResourceLeakDetectorFactory.instance().newResourceLeakDetector(ByteBuf.class); // 全局静态常量对象,用于监测内存泄漏,我们稍后就会讲解

 int readerIndex; // 读下标

 int writerIndex; // 写下标

 private int markedReaderIndex; // 标记读下标,方便回退

 private int markedWriterIndex; // 标记写下标,方便回退

 private int maxCapacity; // 数组容量

}



// 非池化堆内内存

public class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf {

 private final ByteBufAllocator alloc; // 内存分配器。表明当前ByteBuf所属分配器

 byte[] array; // 保存数据的内存数组

}



// 非池化堆外内存

public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf {

 private final ByteBufAllocator alloc; // 所属内存分配器

 private ByteBuffer buffer; // 存放数据的DirectByteBuffer对象

}



// 池化内存的父类,这里读者只需要知道:不管是堆内内存byte[]和堆外内存DirectByteBuffer或者自己使用Unsafe分配的堆外内存,都是使用内存池来管理,这里只需要持有内存池的引用即可,也即 long handle 与 T memory

abstract class PooledByteBuf<T> extends AbstractReferenceCountedByteBuf {

 private final Recycler.Handle<PooledByteBuf<T>> recyclerHandle; // 用于回收PooledByteBuf的对象池

 protected PoolChunk<T> chunk; // 当前所属内存池,这里了解即可,我们在后面会详细讲解

 protected long handle; // 用于指向PoolChunk内存池中的分配空间数据,同样了解即可

 protected T memory; // 数据保存数据的内存

}



// 池化堆内内存

class PooledHeapByteBuf extends PooledByteBuf<byte[]> {}



// 池化堆外内存

final class PooledDirectByteBuf extends PooledByteBuf<ByteBuffer> {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177

总结

对于NIO来说,使用Buffer抽象类来定义四个变量来完成对Java-Heap和C-Heap的操作,对于Netty而言,使用ByteBuf定义readIndex和writeIndex来简化Buffer中四个变量的操作,同时实现了unpool非池化的两种类型ByteBuf和pooled池化的两种类型ByteBuf,而对于如何池化,我们在下一篇讲解,总之,读者只需要掌握到:ByteBuf及其子类只是封装CRUD内存的操作和变量,而具体的内存将会在PoolArea和PoolChunk中管理。