学习Java的小伙伴在学习Java虚拟机运行时数据区中学习过堆和直接内存,其实这里的堆和直接内存分别就对应着堆内内存和堆外内存,这篇文章就重点介绍堆外内存,Java程序是如何使用堆外内存的等一系列问题。
一、堆内内存(on-heap memory)
堆内内存就是我们日常说的堆,堆内内存 = 新生代+老年代+持久代。堆内内存完全遵循JVM虚拟机的内存管理机制,采用垃圾收集器(GC)统一进行内存管理。
注意:JDK8中已经没有持久代了。
二、堆外内存(off-heap memory)
和堆内内存相对应,堆外内存就是把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。
1.堆外内存的特点
(1)对于大内存有良好的伸缩性
(2)对垃圾回收停顿的改善可以明显感觉到
(3)在进程间可以共享,减少虚拟机间的复制
2.什么情况下使用堆外内存?
(1)堆外内存适用于生命周期中等或较长的对象。( 如果是生命周期较短的对象,在YGC的时候就被回收了,就不存在大内存且生命周期较长的对象在FGC对应用造成的性能影响 )。
(2)直接的文件拷贝操作,或者I/O操作。直接使用堆外内存就能少去内存从用户内存拷贝到系统内存的操作,因为I/O操作是系统内核内存和设备间的通信,而不是通过程序直接和外设通信的。
(3)同时,还可以使用池+堆外内存的组合方式,来对生命周期较短,但涉及到I/O操作的对象进行堆外内存的再使用。(Netty中就使用了该方式)
3.使用堆外内存的优缺点:
(1)优点:
1)减少了垃圾回收
因为垃圾回收会暂停其他的工作,这就是STW(Stop-The-World)机制了。2)加快了复制的速度
堆内在flush到远程时,会先复制到直接内存(非堆内存),然后再发送;而堆外内存相当于省略掉了这个工作。
(2)缺点:
内存难以控制,使用了堆外内存就间接失去了JVM管理内存的可行性,改由自己来管理,当发生内存溢出时排查起来非常困难。
4.堆外内存与内存池的比较
(1)内存池
主要用于两类对象:
①生命周期较短,且结构简单的对象,在内存池中重复利用这些对象能增加CPU缓存的命中率,从而提高性能;
②加载含有大量重复对象的大片数据,此时使用内存池能减少垃圾回收的时间。
(2)堆外内存
它和内存池一样,也能缩短垃圾回收时间,但是它适用的对象和内存池完全相反。内存池往往适用于生命期较短的可变对象,而生命期中等或较长的对象,正是堆外内存要解决的。
5.DirectByteBuffer和ByteBuffer对比理解?
DirectByteBuffer是ByteBuffer的一个子类。为什么会说ByteBuffer的性能低下呢?因为在使用ByteBuffer进行I/O操作时会执行以下操作:
(1)将堆内存中缓冲区数据拷贝到临时缓冲区
(2)对临时缓冲区的数据执行低层次I/O操作
(3)临时缓冲区对象离开作用域,并最终被回收成为无用数据
与之相对,DirectByteBuffer由于将内存分配在了堆外内存因此可以直接执行较低层次的I/O操作数据,减少了拷贝次数因此也获得了较高的性能。
5.java.nio.DirectByteBuffer-直接缓冲
我们可以用DirectByteBuffer对象进行堆外内存的管理和使用,它会在对象创建的时候就分配堆外内存。DirectByteBuffer类是在Java Heap外分配内存,对堆外内存的申请主要是通过成员变量unsafe来操作。
(1)关系图
(2)DirectByteBuffer如何工作?
DirectByteBuffer对象在创建过程中会先通过Unsafe接口直接通过os::malloc来分配内存,然后将内存的起始地址和大小存到DirectByteBuffer对象里,这样就可以直接操作这些内存。这些内存只有在DirectByteBuffer回收掉之后才有机会被回收,因此如果这些对象大部分都移到了old,但是一直没有触发CMS GC或者Full GC,那么悲剧将会发生,因为你的物理内存被他们耗尽了,因此为了避免这种悲剧的发生,通过-XX:MaxDirectMemorySize来指定最大的堆外内存大小,当使用达到了阈值的时候将调用System.gc来做一次full gc,以此来回收掉没有被使用的堆外内存。
(3)DirectByteBuffer内存申请与回收
1)内存申请
在构造DirectByteBuffer时就已经执行了内存申请操作。
源码分析:
DirectByteBuffer.DirectByteBuffer
// Primary constructor
// 构造函数
DirectByteBuffer(int cap) { // package-private 包级私有
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
//内存分配预处理
Bits.reserveMemory(size, cap);
long base = 0;
try {
//申请堆外内存,返回缓冲区内存的首地址
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// 四舍五入到页面边界
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
//此行代码用于实现当DirectByteBuffer被回收时,堆外内存也会被释放
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
Bits.reserveMemory
//当分配或释放直接内存时,应该调用这些方法。
//它们允许用户控制一个进程可以访问的直接内存的数量。
//所有大小都是用字节指定的。
static void reserveMemory(long size, int cap) {
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
// 乐观地尝试预定直接内存(DirectMemory)的内存
// optimist!
if (tryReserveMemory(size, cap)) {
return;
}
// 如果预定内存失败,则对直接内存中无用的内存执行回收操作
final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
// retry while helping enqueue pending Reference objects
// which includes executing pending Cleaner(s) which includes
// Cleaner(s) that free direct buffer memory
///重试,同时帮助排队挂起的引用对象,包括执行挂起的Cleaner(s),其中包括释放直接缓冲区内存的Cleaner(s)
while (jlra.tryHandlePendingReference()) {
if (tryReserveMemory(size, cap)) {
return;
}
}
// 触发VM的Reference处理(Full GC)
// trigger VM's Reference processing
System.gc();
// 执行多次循环,尝试进行内存回收操作,如果多次尝试失败之后,则抛出OutOfMemory异常
// a retry loop with exponential back-off delays
// (this gives VM some time to do it's job)
boolean interrupted = false;
try {
long sleepTime = 1;
int sleeps = 0;
while (true) {
if (tryReserveMemory(size, cap)) {
return;
}
if (sleeps >= MAX_SLEEPS) {
break;
}
if (!jlra.tryHandlePendingReference()) {
try {
Thread.sleep(sleepTime);
sleepTime <<= 1;
sleeps++;
} catch (InterruptedException e) {
interrupted = true;
}
}
}
// no luck
throw new OutOfMemoryError("Direct buffer memory");
} finally {
if (interrupted) {
// 不要吞下中断
// don't swallow interrupts
Thread.currentThread().interrupt();
}
}
}
此方法的主要功能就是检查当前DirectMemory内存是否足够构建DirectByteBuffer的缓冲区,并通过CAS的方式设置当前已使用的内存。
Bits.tryReserveMemory
//尝试预定内存
private static boolean tryReserveMemory(long size, int cap) {
// -XX:MaxDirectMemorySize 限制总容量而不是实际的内存使用,当缓冲区页对齐时,实际内存使用会有所不同。
long totalCap;
//检查内存是否足够
while (cap <= maxMemory - (totalCap = totalCapacity.get())) {
//如果内存足够,则尝试CAS设置totalCapacity
if (totalCapacity.compareAndSet(totalCap, totalCap + cap)) {
reservedMemory.addAndGet(size);
count.incrementAndGet();
return true;
}
}
return false;
}
2)内存释放
DirectByteBuffer中的直接内存缓冲区释放的方式有两种:
1)ReferenceHandler线程会自动检查有无被回收的DirectByteBuffer,如果有则执行Cleaner.clean方法释放其对应的直接内存。
2)通过调用SharedSecrets.getJavaLangRefAccess()方法来释放内存,具体见Reference类代码分析。
内存释放的关键:
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
this指的当前堆外内存;
Deallocator指的内存释放器(Deallocator的底层是有个run方法,调用unsafe.freeMemory
去释放空间)。