使用堆外内存

有时候对内存进行大对象的读写,会引起JVM长时间的停顿,有时候则是希望最大程度地提高JVM的效率,我们需要自己来管理内存(看起来很像是Java像C++祖宗的妥协吧)。据我所知,很多缓存框架都会使用它,比如我以前使用过的EhCache(给它包装了个酷一点的名字,叫BigMemory),以及现在项目中的Memcached。在nio以前,是没有光明正大的做法的,有一个work around的办法是直接访问Unsafe类。如果你使用Eclipse,默认是不允许访问sun.misc下面的类的,你需要稍微修改一下,给Type Access Rules里面添加一条所有类都可以访问的规则:

使用堆外内存

在使用Unsafe类的时候:

1
Unsafe f = Unsafe.getUnsafe();

发现还是被拒绝了,抛出异常:

1
java.lang.SecurityException: Unsafe

正如Unsafe的类注释中写道:

Although the class and all methods are public, use of this class is limited because only trusted code can obtain instances of it.

于是,只能无耻地使用反射来做这件事; 

1
2
3
4
Field f = Unsafe. class .getDeclaredField( "theUnsafe" );
f.setAccessible( true );
Unsafe us = (Unsafe) f.get( null );
long id = us.allocateMemory( 1000 );

其中,allocateMemory返回一个指针,并且其中的数据是未初始化的。如果要释放这部分内存的话,需要调用freeMemory或者reallocateMemory方法。Unsafe对象提供了一系列put/get方法,例如putByte,但是只能一个一个byte地put,我不知道这样会不会影响效率,为什么不提供一个putByteArray的方法呢?

从nio时代开始,可以使用ByteBuffer等类来操纵堆外内存了: 

1
ByteBuffer buffer = ByteBuffer.allocateDirect(numBytes);

像Memcached等等很多缓存框架都会使用堆外内存,以提高效率,反复读写,去除它的GC的影响。可以通过指定JVM参数来确定堆外内存大小限制(有的VM默认是无限的,比如JRocket,JVM默认是64M): 

1
- XX :MaxDirectMemorySize=512m

对于这种direct buffer内存不够的时候会抛出错误: 

1
java.lang.OutOfMemoryError: Direct buffer memory

对于heap的OOM我们可以通过执行jmap -heap来获取堆内内存情况,例如以下输出取自我上周定位的一个问题: 

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
using parallel threads in the new generation.
using thread-local object allocation.
Concurrent Mark-Sweep GC
 
Heap Configuration:
    MinHeapFreeRatio = 40
    MaxHeapFreeRatio = 70
    MaxHeapSize      = 2147483648 ( 2048 . 0MB )
    NewSize          = 16777216 ( 16 . 0MB )
    MaxNewSize       = 33554432 ( 32 . 0MB )
    OldSize          = 50331648 ( 48 . 0MB )
    NewRatio         = 7
    SurvivorRatio    = 8
    PermSize         = 16777216 ( 16 . 0MB )
    MaxPermSize      = 67108864 ( 64 . 0MB )
 
Heap Usage:
New Generation (Eden + 1 Survivor Space):
    capacity = 30212096 ( 28 . 8125MB )
    used     = 11911048 ( 11 . 359260559082031MB )
    free     = 18301048 ( 17 . 45323944091797MB )
    39 . 42476549789859 % used
Eden Space:
    capacity = 26869760 ( 25 . 625MB )
    used     = 11576296 ( 11 . 040016174316406MB )
    free     = 15293464 ( 14 . 584983825683594MB )
    43 . 08298994855183 % used
From Space:
    capacity = 3342336 ( 3 . 1875MB )
    used     = 334752 ( 0 . 319244384765625MB )
    free     = 3007584 ( 2 . 868255615234375MB )
    10 . 015510110294118 % used
To Space:
    capacity = 3342336 ( 3 . 1875MB )
    used     = 0 ( 0 . 0MB )
    free     = 3342336 ( 3 . 1875MB )
    0 . 0 % used
concurrent mark-sweep generation:
    capacity = 2113929216 ( 2016 . 0MB )
    used     = 546999648 ( 521 . 6595153808594MB )
    free     = 1566929568 ( 1494 . 3404846191406MB )
    25 . 875968024844216 % used
Perm Generation:
    capacity = 45715456 ( 43 . 59765625MB )
    used     = 27495544 ( 26 . 22179412841797MB )
    free     = 18219912 ( 17 . 37586212158203MB )
    60 . 144962788952604 % used

可见堆内存都是正常的,重新回到业务日志里寻找异常,发现出现在堆外内存的分配上: 

1
2
3
4
5
java.lang.OutOfMemoryError
  at sun.misc.Unsafe.allocateMemory(Native Method )
  at java.nio.DirectByteBuffer.(DirectByteBuffer.java: 101 )
  at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java: 288 )
  at com.schooner.MemCached.SchoonerSockIOPool $TCPSockIO .(Unknown Source)

对于这个参数分配过小的情况下造成OOM,不妨执行jmap -histo:live看看(也可以用JConsole之类的外部触发GC),因为它会强制一次full GC,如果堆外内存明显下降,很有可能就是堆外内存过大引起的OOM。

BTW,如果在执行jmap命令时遇到:

1
Error attaching to process: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process

这个算是JDK的一个bug(链接),只要是依赖于SA(Serviceability Agent)的工具,比如jinfo/jstack/jmap都会存在这个问题,但是Oracle说了“won’t fix”……

Ubuntu 10.10 and newer has a new default security policy that affects Serviceability commands. This policy prevents a process from attaching to another process owned by the same UID if the target process is not a descendant of the attaching process.

不过它也是给了解决方案的,需要修改/etc/sysctl.d/10-ptrace.conf:

1
kernel.yama.ptrace_scope = 0

堆外内存泄露的问题定位通常比较麻烦,可以借助google-perftools这个工具,它可以输出不同方法申请堆外内存的数量。当然,如果你是64位系统,你需要先安装libunwind库

最后,JDK存在一些direct buffer的bug(比如这个这个),可能引发OOM,所以也不妨升级JDK的版本看能否解决问题。

文章系本人原创,转载请保持完整性并注明出自《四火的唠叨》

<think>好的,用户之前问过关于Caffeine缓存库的问题,现在又想知道Java怎么使用堆外内存。可能需要了解用户的学习阶段,是Java开发者吗?还是在做性能优化? 首先,用户可能在使用Java处理大数据或者需要高性能的场景,遇到了堆内存限制或GC问题,所以想用堆外内存。需要解释清楚堆外内存的概念、使用场景,以及具体的实现方法。 要确保覆盖基本方法,比如ByteBuffer,还有第三方库如Unsafe和Netty的ByteBuf。可能用户对JVM内存结构不太熟悉,需要简单说明堆内和堆外的区别,以及优缺点。 要注意用户可能的误区,比如堆外内存的管理问题,手动释放的重要性,避免内存泄漏。另外,可能需要提醒安全方面的考虑,比如Unsafe的使用风险。 用户可能想知道实际应用中的例子,比如什么时候应该用堆外内存,比如网络编程、缓存层或者避免GC停顿。需要给出代码示例,但也要注意不同方法的适用性。 另外,用户可能对性能优化感兴趣,所以需要比较堆外和堆内的性能差异,以及监控和管理堆外内存的工具,比如JMX或者第三方库。 还要考虑到Java版本的不同,比如新版Java是否有改进,或者推荐使用更安全的API,比如MemorySegment,尽管可能还在孵化阶段。 最后,总结最佳实践,帮助用户正确使用堆外内存,避免常见错误。</think>在 Java 中使用堆外内存(Off-Heap Memory)是突破 JVM内存限制的重要技术手段,特别适合处理大规模数据、网络编程和需要减少 GC 停顿的场景。以下是具体实现方法和关键技术要点: --- ### 一、堆外内存核心原理 $$ \text{内存布局} = \underbrace{\text{JVM Heap}}_{\text{GC 管理}} \quad + \quad \underbrace{\text{Direct Memory}}_{\text{手动管理}} $$ - **优势**:绕过 GC 管理,避免 Full GC 停顿 - **劣势**:需手动内存管理,存在泄漏风险 - **典型应用**:Netty 的零拷贝、缓存系统、科学计算 --- ### 二、标准 API 实现 #### 1. ByteBuffer(NIO) ```java // 分配 1MB 堆外内存(单位:字节) ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024); try { // 写入数据 buffer.putInt(2023); buffer.putDouble(3.1415926535); // 读取数据前必须 flip buffer.flip(); int year = buffer.getInt(); double pi = buffer.getDouble(); } finally { // 显式释放(实际由 Cleaner 管理) ((DirectBuffer) buffer).cleaner().clean(); } ``` **特点**: - 自动内存对齐(默认按平台对齐方式) - 最大容量受 `-XX:MaxDirectMemorySize` 限制(默认与堆内存一致) --- #### 2. Unsafe(底层操作) ```java Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); Unsafe unsafe = (Unsafe) theUnsafe.get(null); // 分配 100 字节堆外内存 long address = unsafe.allocateMemory(100); try { // 写入字符串(需处理字符编码) byte[] strBytes = "Hello Off-Heap".getBytes(StandardCharsets.UTF_8); unsafe.copyMemory(strBytes, 16, null, address, strBytes.length); // 读取操作 byte[] readBuffer = new byte[strBytes.length]; unsafe.copyMemory(null, address, readBuffer, 16, strBytes.length); System.out.println(new String(readBuffer, StandardCharsets.UTF_8)); } finally { unsafe.freeMemory(address); // 必须显式释放 } ``` **注意事项**: - JDK 9+ 需要添加 JVM 参数:`--add-opens java.base/jdk.internal.misc=ALL-UNNAMED` - 需自行处理内存对齐问题 --- ### 三、第三方库方案 #### 1. Netty 的 ByteBuf ```java // 创建非池化堆外内存 ByteBuf buffer = Unpooled.directBuffer(1024); try { buffer.writeInt(2023); buffer.writeDouble(3.1415926535); // 读取需要手动维护读写索引 int year = buffer.readInt(); double pi = buffer.readDouble(); } finally { buffer.release(); // 引用计数释放 } ``` **优点**:支持内存池、引用计数等高级特性 --- #### 2. Javolution 的 DirectMemory ```java DirectMemory mem = new DirectMemory(1024); mem.setByte(0, (byte) 0xAA); // 直接地址操作 mem.getByte(0); mem.deallocate(); // 释放内存 ``` --- ### 四、内存管理关键技术 #### 1. 内存跟踪工具 ```bash # 查看堆外内存使用 jcmd <pid> VM.native_memory summary # 开启详细追踪(需开启 NMT) -XX:NativeMemoryTracking=detail ``` #### 2. 性能优化技巧 - **内存池化**:使用 Netty 的 `PooledByteBufAllocator` - **缓存行对齐**:避免伪共享(Padding 到 64 字节) - **批量操作**:使用 `Unsafe.copyMemory` 代替单元素操作 --- ### 五、新型 API(JDK 14+) #### Foreign-Memory Access API(孵化阶段) ```java try (MemorySegment segment = MemorySegment.allocateNative(100)) { VarHandle intHandle = MemoryLayout.ofSequence(MemoryLayouts.JAVA_INT) .varHandle(int.class, MemoryLayout.PathElement.sequenceElement()); intHandle.set(segment.baseAddress(), 0, 123); // 类型安全访问 int value = (int) intHandle.get(segment.baseAddress(), 0); } ``` **优势**:类型安全的内存访问,避免直接操作指针 --- ### 六、典型应用场景对比 ```plaintext 场景 | 推荐方案 -------------------|------------------- 网络IO缓冲区 | Netty ByteBuf 大文件内存映射 | MappedByteBuffer 跨进程共享内存 | Unsafe + 共享内存 科学计算 | Javolution ``` --- ### 注意事项 1. **内存泄漏预防**:必须成对使用 allocate/free 2. **异常处理**:添加 `OutOfMemoryError` 处理逻辑 3. **性能监控**:使用 `BufferPoolMXBean` 监控 DirectBuffer 使用 4. **安全限制**:Unsafe 操作需开启模块访问权限 > **最佳实践**:建议通过 `Runtime.getRuntime().maxMemory()` 设置合理的 `-XX:MaxDirectMemorySize`(通常设置为堆内存的 50-80%)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值