堆外内存
2024-04-02 本文已影响0人
修行者12138
什么是堆外内存
堆外内存也叫直接内存(Direct Memory),并不是JVM内存区域的一部分,也不是《Java虚拟机规范》中定义的内存区域。
JDK1.4引入了NIO包(new input/output),引入了一种基于通道(Channel)和缓冲区(Buffer)的IO方式,可以使用native函数直接分配堆外内存,然后通过一个存储在Java堆里的DirectByteBuffer对象作为这块内存的引用进行操作。在一些场景中可以显著提高性能,因为避免了在Java对和Native堆中来回复制数据。
申请堆外内存时,如果空间不足,会抛出java.lang.OutOfMemoryError: Direct buffer memory
堆外内存的优势
- 堆外内存受操作系统管理,不受JVM管理,可以减少JVM GC压力,减轻JVM垃圾回收对程序性能的影响。
- 提高IO效率
- 堆内内存属于用户态的缓冲区,堆外内存属于内核态的缓冲区
- 如果从堆向磁盘/网卡写数据,需要CPU把数据复制从用户态复制到内核态,再由操作系统DMA从内核态复制到磁盘/网卡。
-
如果直接从堆外内存向磁盘/网卡写数据,可以省略掉用户态到内核态的复制
image.png
堆外内存的缺点
- 需要自行分配和回收内存,增加了复杂度
- 如果回收内存处理不当,容易引发内存泄漏,并且难以排查
堆外内存引发内存泄漏排查: https://mp.weixin.qq.com/s/55slokngVRgqEav6c3TxOA
堆外内存的使用
- 调用java.nio.ByteBuffer的allocateDirect方法,最终会调用sun.misc.Unsafe#allocateMemory,相当于C++的malloc函数
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
- 可以通过设置-XX:MaxDirectMemorySize=10M控制堆外内存的使用上限
使用场景
- 常驻在本地内存的本地缓存,比如国家、省份、城市信息等,长期驻扎在老年代。
堆外内存的回收
- 等到full gc的时候垃圾回收
堆外内存对应的Java对象引用在堆里,受JVM垃圾回收的管理,一旦该对象引用被回收,JVM会释放对应的堆外内存 - 程序手动回收
private void clean (ByteBuffer byteBuffer) {
if (byteBuffer.isDirect()) {
((DirectBuffer)byteBuffer).cleaner().clean();
}
}
堆外内存缓存框架
https://mp.weixin.qq.com/s/YYMnXRFyozHpsmRwN-vvGQ
- 引入依赖
<dependency>
<groupId>org.caffinitas.ohc</groupId>
<artifactId>ohc-core</artifactId>
<version>0.7.4</version>
</dependency>
- 自定义序列化、反序列器,调用put、get方法进行写入和读取,操作类似HashMap
import org.apache.commons.io.Charsets;
import org.caffinitas.ohc.CacheSerializer;
import org.caffinitas.ohc.OHCache;
import org.caffinitas.ohc.OHCacheBuilder;
import java.nio.ByteBuffer;
public class OhcDemo {
public static void main(String[] args) {
OHCache ohCache = OHCacheBuilder.<String, String>newBuilder()
.keySerializer(OhcDemo.stringSerializer)
.valueSerializer(OhcDemo.stringSerializer)
.build();
ohCache.put("hello","why");
System.out.println("ohCache.get(hello) = " + ohCache.get("hello"));
}
public static final CacheSerializer<String> stringSerializer = new CacheSerializer<String>() {
public void serialize(String s, ByteBuffer buf) {
// 得到字符串对象UTF-8编码的字节数组
byte[] bytes = s.getBytes(Charsets.UTF_8);
// 用前16位记录数组长度
buf.put((byte) ((bytes.length >>> 8) & 0xFF));
buf.put((byte) ((bytes.length) & 0xFF));
buf.put(bytes);
}
public String deserialize(ByteBuffer buf) {
// 获取字节数组的长度
int length = (((buf.get() & 0xff) << 8) + ((buf.get() & 0xff)));
byte[] bytes = new byte[length];
// 读取字节数组
buf.get(bytes);
// 返回字符串对象
return new String(bytes, Charsets.UTF_8);
}
public int serializedSize(String s) {
byte[] bytes = s.getBytes(Charsets.UTF_8);
// 设置字符串长度限制,2^16 = 65536
if (bytes.length > 65535)
throw new RuntimeException("encoded string too long: " + bytes.length + " bytes");
return bytes.length + 2;
}
};
}