@@ -134,56 +134,91 @@ public native void freeMemory(long address);
134134``` java 
135135private  void  memoryTest() {
136136    int  size =  4 ;
137-     long  addr =  unsafe. allocateMemory(size);
138-     long  addr3 =  unsafe. reallocateMemory(addr, size *  2 );
139-     System . out. println(" addr: " + addr);
140-     System . out. println(" addr3: " + addr3);
137+     //  1. 分配初始内存
138+     long  oldAddr =  unsafe. allocateMemory(size);
139+     System . out. println(" Initial address: "   +  oldAddr);
140+ 
141+     //  2. 向初始内存写入数据
142+     unsafe. putInt(oldAddr, 16843009 ); //  写入 0x01010101
143+     System . out. println(" Value at oldAddr: "   +  unsafe. getInt(oldAddr));
144+ 
145+     //  3. 重新分配内存
146+     long  newAddr =  unsafe. reallocateMemory(oldAddr, size *  2 );
147+     System . out. println(" New address: "   +  newAddr);
148+ 
149+     //  4. reallocateMemory 已经将数据从 oldAddr 拷贝到 newAddr
150+     //  所以 newAddr 的前4个字节应该和 oldAddr 的内容一样
151+     System . out. println(" Value at newAddr (first 4 bytes): "   +  unsafe. getInt(newAddr));
152+ 
153+     //  关键:之后所有操作都应该基于 newAddr,oldAddr 已失效!
141154    try  {
142-         unsafe. setMemory(null ,addr ,size,(byte )1 );
143-         for  (int  i =  0 ; i <  2 ; i++ ) {
144-             unsafe. copyMemory(null ,addr,null ,addr3+ size* i,4 );
145-         }
146-         System . out. println(unsafe. getInt(addr));
147-         System . out. println(unsafe. getLong(addr3));
148-     }finally  {
149-         unsafe. freeMemory(addr);
150-         unsafe. freeMemory(addr3);
155+         //  5. 在新内存块的后半部分写入新数据
156+         unsafe. putInt(newAddr +  size, 33686018 ); //  写入 0x02020202
157+ 
158+         //  6. 读取整个8字节的long值
159+         System . out. println(" Value at newAddr (full 8 bytes): "   +  unsafe. getLong(newAddr));
160+ 
161+     } finally  {
162+         //  7. 只释放最后有效的内存地址
163+         unsafe. freeMemory(newAddr);
164+         //  如果尝试 freeMemory(oldAddr),将会导致 double free 错误!
151165    }
152166}
153167``` 
154168
155169先看结果输出:
156170
157171``` plain 
158- addr: 2433733895744 
159- addr3: 2433733894944 
160- 16843009 
161- 72340172838076673 
172+ Initial address: 140467048086752 
173+ Value at oldAddr: 16843009 
174+ New address: 140467048086752 
175+ Value at newAddr (first 4 bytes): 16843009 
176+ Value at newAddr (full 8 bytes): 144680345659310337 
162177``` 
163178
164- 分析一下运行结果,首先使用 ` allocateMemory ` 方法申请 4 字节长度的内存空间,调用 ` setMemory ` 方法向每个字节写入内容为 ` byte ` 类型的 1,当使用 Unsafe 调用 ` getInt ` 方法时,因为一个 ` int ` 型变量占 4 个字节,会一次性读取 4 个字节,组成一个 ` int ` 的值,对应的十进制结果为 16843009。 
179+ ` reallocateMemory `  的行为类似于 C 语言中的 realloc 函数,它会尝试在不移动数据的情况下扩展或收缩内存块。其行为主要有两种情况: 
165180
166- 你可以通过下图理解这个过程:
181+ 1 .  ** 原地扩容** :如果当前内存块后面有足够的连续空闲空间,` reallocateMemory `  会直接在原地址上扩展内存,并返回原始地址。
182+ 2 .  ** 异地扩容** :如果当前内存块后面空间不足,它会寻找一个新的、足够大的内存区域,将旧数据拷贝过去,然后释放旧的内存地址,并返回新地址。
167183
168- ![ ] ( https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717144344005.png ) 
184+ ** 结合本次的运行结果,我们可以进行如下分析: ** 
169185
170- 在代码中调用 ` reallocateMemory ` 方法重新分配了一块 8 字节长度的内存空间,通过比较 ` addr ` 和 ` addr3 ` 可以看到和之前申请的内存地址是不同的。在代码中的第二个 for 循环里,调用 ` copyMemory ` 方法进行了两次内存的拷贝,每次拷贝内存地址 ` addr ` 开始的 4 个字节,分别拷贝到以 ` addr3 ` 和 ` addr3+4 ` 开始的内存空间上: 
186+ ** 第一步:初始分配与写入 ** 
171187
172- ![ ] ( https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717144354582.png ) 
188+ -  ` unsafe.allocateMemory(size) `  分配了 4 字节的堆外内存,地址为 ` 140467048086752 ` 。
189+ -  ` unsafe.putInt(oldAddr, 16843009) `  向该地址写入了 int 值 ` 16843009 ` ,其十六进制表示为 ` 0x01010101 ` 。` getInt `  读取正确,证明写入成功。
173190
174- 拷贝完成后,使用 ` getLong ` 方法一次性读取 8 个字节,得到 ` long ` 类型的值为 72340172838076673。 
191+ ** 第二步:原地内存扩容 ** 
175192
176- 需要注意,通过这种方式分配的内存属于 堆外内存 ,是无法进行垃圾回收的,需要我们把这些内存当做一种资源去手动调用` freeMemory ` 方法进行释放,否则会产生内存泄漏。通用的操作内存方式是在` try ` 中执行对内存的操作,最终在` finally ` 块中进行内存的释放。
193+ -  ` long newAddr = unsafe.reallocateMemory(oldAddr, size * 2) `  尝试将内存块扩容至 8 字节。
194+ -  观察输出 New address: ` 140467048086752 ` ,我们发现 ` newAddr `  与 ` oldAddr `  的值** 完全相同** 。
195+ -  这表明本次操作触发了“原地扩容”。系统在原地址 ` 140467048086752 `  后面找到了足够的空间,直接将内存块扩展到了 8 字节。在这个过程中,旧的地址 ` oldAddr `  依然有效,并且就是 ` newAddr ` ,数据也并未发生移动。
177196
178- ** 为什么要使用堆外内存? ** 
197+ ** 第三步:验证数据与写入新数据 ** 
179198
180- -  对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响。
181- -  提升程序 I/O 操作的性能。通常在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
199+ -  ` unsafe.getInt(newAddr) `  再次读取前 4 个字节,结果仍是 ` 16843009 ` ,验证了原数据完好无损。
200+ -  ` unsafe.putInt(newAddr + size, 33686018) `  在扩容出的后 4 个字节(偏移量为 4)写入了新的 int 值 ` 33686018 ` (十六进制为 ` 0x02020202 ` )。
201+ 
202+ ** 第四步:读取完整数据** 
203+ 
204+ -  ` unsafe.getLong(newAddr) `  从起始地址读取一个 long 值(8 字节)。此时内存中的 8 字节内容为 ` 0x01010101 `  (低地址) 和 ` 0x02020202 `  (高地址) 的拼接。
205+ -  在小端字节序(Little-Endian)的机器上,这 8 字节在内存中会被解释为十六进制数 ` 0x0202020201010101 ` 。
206+ -  这个十六进制数转换为十进制,结果正是 ` 144680345659310337 ` 。这完美地解释了最终的输出结果。
207+ 
208+ ** 第五步:安全的内存释放** 
209+ 
210+ -  ` finally `  块中,` unsafe.freeMemory(newAddr) `  安全地释放了整个 8 字节的内存块。
211+ -  由于本次是原地扩容(` oldAddr == newAddr ` ),所以即使错误地多写一句 ` freeMemory(oldAddr) `  也会导致二次释放的严重错误。
182212
183213#### 典型应用  
184214
185215` DirectByteBuffer `  是 Java 用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在 Netty、MINA 等 NIO 框架中应用广泛。` DirectByteBuffer `  对于堆外内存的创建、使用、销毁等逻辑均由 Unsafe 提供的堆外内存 API 来实现。
186216
217+ ** 为什么要使用堆外内存?** 
218+ 
219+ -  对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响。
220+ -  提升程序 I/O 操作的性能。通常在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
221+ 
187222下图为 ` DirectByteBuffer `  构造函数,创建 ` DirectByteBuffer `  的时候,通过 ` Unsafe.allocateMemory `  分配内存、` Unsafe.setMemory `  进行内存初始化,而后构建 ` Cleaner `  对象用于跟踪 ` DirectByteBuffer `  对象的垃圾回收,以实现当 ` DirectByteBuffer `  被垃圾回收时,分配的堆外内存一起被释放。
188223
189224``` java 
0 commit comments