JVM中对象在内存中的分布如下:
- 新生代:一般来说新创建的对象都分配在这里;
- 年老代:经过几次垃圾回收,新生代的对象就会放在年老代里面。年老代中的对象保存的时间更久。
- 永久代:这里面存放的是class相关的信息,一般是不会进行垃圾回收的。
JVM会替我们执行垃圾回收,主要包括young gc和full gc。jvm内存溢出可以通过jmap -heap或者jstat -gcutil工具来诊断。
1、ByteBuffer堆外内存介绍
ByteBuffer堆外内存使用:
- 从nio时代开始,可以使用ByteBuffer等类来操纵堆外内存了,使用ByteBuffer分配本地内存则非常简单,直接:ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);
- 可以通过指定JVM参数来确定堆外内存大小限制:-XX:MaxDirectMemorySize=512m
- 对于这种direct buffer内存不够的时候会抛出错误: java.lang.OutOfMemoryError: Direct buffer memory
堆外内存泄露的问题定位通常比较麻烦,可以借助google-perftools这个工具,它可以输出不同方法申请堆外内存的数量。最后,JDK存在一些direct buffer的bug(比如这个和这个),可能引发OOM,所以也不妨升级JDK的版本看能否解决问题。
在C语言的内存分配和释放函数malloc/free,必须要一一对应,否则就会出现内存泄露或者是野指针的非法访问。java中ByteBuffer申请的堆外内存需要手动释放吗?ByteBuffer申请的堆外内存也是由GC负责回收的,Hotspot在GC时会扫描Direct ByteBuffer对象是否有引用,如没有,当堆内的引用被gc回收时通过虚拟引用回收其占用的堆外内存!(前提是没有关闭DisableExplicitGC)
我们先简单看一个例子:
/**
* @VM args:-XX:MaxDirectMemorySize=40m -verbose:gc -XX:+PrintGCDetails
* -XX:+DisableExplicitGC //增加此参数会内存溢出java.lang.OutOfMemoryError: Direct buffer memory
*/
public static void TestDirectByteBuffer() {
List<ByteBuffer> list = new ArrayList<ByteBuffer>();
while(true) {
ByteBuffer buffer = ByteBuffer.allocateDirect(1 * 1024 * 1024);
//list.add(buffer);
}
}
1)-XX:MaxDirectMemorySize=40m -verbose:gc -XX:+PrintGCDetails
代码会一直运行下午,同时会看到系统频繁的进行垃圾回收;
2)-XX:MaxDirectMemorySize=40m -verbose:gc -XX:+PrintGCDetails -XX:+DisableExplicitGC
增加了-XX:+DisableExplicitGC,这个参数作用是禁止显示调用GC。代码如何显示调用GC呢,通过System.gc()函数调用。如果加上了这个JVM启动参数,那么代码中调用System.gc()没有任何效果,相当于是没有这行代码一样。
显然堆内存(包括新生代和老年代)内存很充足,但是堆外内存溢出了。也就是说使用了java nio中的direct memory,那么-XX:+DisableExplicitGC一定要小心,存在潜在的内存泄露风险。(后面会讲到Direct Memory的申请时会使用System.gc())
3)-XX:MaxDirectMemorySize=40m -verbose:gc -XX:+PrintGCDetails
JVM参数把-XX:+DisableExplicitGC去掉,代码将list.add(buffer); 注释放开再次运行,仍然会报java.lang.OutOfMemoryError: Direct buffer memory
原因是堆内list把堆外内存对象的引用一直持有,导致堆内的引用无法被gc,从而堆外内存也无法回收。
2、ByteBuffer堆外内存回收的矛盾点:
我们知道java代码无法强制JVM何时进行垃圾回收,也就是说垃圾回收这个动作的触发,完全由JVM自己控制,它会挑选合适的时机回收堆内存中的无用java对象。代码中显示调用System.gc(),只是建议JVM进行垃圾回收,但是到底会不会执行垃圾回收是不确定的,一般来说是,系统比较空闲的时候(比如JVM中活动的线程很少的时候),或是内存不足,才进行垃圾回收。使用ByteBuffer堆外内存回收的矛盾在于:堆内存由JVM自己管理,堆外内存必须要由我们自己释放;堆内存的消耗速度远远小于堆外内存的消耗,但要命的是必须先释放堆内存中的对象(引用),才能释放堆外内存,但是我们又不能强制JVM释放堆内存。例如:ByteBuffer bb = ByteBuffer.allocateDirect(1024),这段代码的执行会在堆外占用1k的内存,Java堆内只会占用一个对象的指针引用的大小,堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光。
再者,假设堆内 ByteBuffer对象的引用升级到了老年代,导致这个引用会长期存在无法回收,这时堆外的内存将长期无法得到回收。
3、ByteBuffer源码分析,堆外内存申请:
ByteBuffer.allocateDirect(cap);
进行内存申请的时候,会调用:DirectByteBuffer(int cap)构造函数,如下:
DirectByteBuffer(int cap) { // package-private
// 初始化Buffer的四个核心属性
super(-1, 0, cap, cap);
// 判断是否需要页面对齐,通过参数-XX:+PageAlignDirectMemory控制,默认为false
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 {
// 调用unsafe方法分配内存
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
// 分配失败,释放内存
Bits.unreserveMemory(size, cap);
throw x;
}
// 初始化内存空间为0
unsafe.setMemory(base, size, (byte) 0);
// 设置内存起始地址
if (pa && (base % ps != 0)) {
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 使用Cleaner机制注册内存回收处理函数
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
首先,这个构造函数里的Bits.reserveMemory(size, cap)方法判断是否有足够的空间可供申请:
// 该方法主要用于判断申请的堆外内存是否超过了用例指定的最大值
// 如果还有足够空间可以申请,则更新对应的变量
// 如果已经没有空间可以申请,则抛出OOME
// 参数解释:
// size:根据是否按页对齐,得到的真实需要申请的内存大小
// cap:用户指定需要的内存大小(<=size)
static void reserveMemory(long size, int cap) {
// 因为涉及到更新多个静态统计变量,这里需要Bits类锁
synchronized (Bits.class) {
// 获取最大可以申请的对外内存大小,默认值是64MB
// 可以通过参数-XX:MaxDirectMemorySize=<size>设置这个大小
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
// -XX:MaxDirectMemorySize限制的是用户申请的大小,而不考虑对齐情况
// 所以使用两个变量来统计:
// reservedMemory:真实的目前保留的空间
// totalCapacity:目前用户申请的空间
if (cap <= maxMemory - totalCapacity) {
reservedMemory += size;
totalCapacity += cap;
count++;
return; // 如果空间足够,更新统计变量后直接返回
}
}
// 如果已经没有足够空间,则尝试GC
System.gc();
try {
Thread.sleep(100);
} catch (InterruptedException x) {
// Restore interrupt status
Thread.currentThread().interrupt();
}
synchronized (Bits.class) {
// GC后再次判断,如果还是没有足够空间,则抛出OOME
if (totalCapacity + cap > maxMemory)
throw new OutOfMemoryError("Direct buffer memory");
reservedMemory += size;
totalCapacity += cap;
count++;
}
}
可以看到:
- 如果空间不足,会调用System.gc()尝试释放内存,然后再进行判断,如果还是没有足够的空间,抛出OOME。
- 确定有足够的空间后,使用sun.misc.Unsafe#allocateMemory申请内存;
- 最后,DirectByteBuffer使用Cleaner机制进行空间回收
说明:
- sun.misc.Unsafe.allocateMemory这个函数是通过JNI调用C的malloc来申请内存;
- 申请内存时,可以通过-XX:+PageAlignDirectMemory:指定申请的内存是否需要按页对齐,默认不对其;
- 默认堆外内存大小为可用的最大Java堆大小(后面会讲到)
4、ByteBuffer源码分析,堆外内存回收:
上面我们可以看到,当使用ByteBuffer申请时,在DirectByteBuffer构造函数最后,会注册一个Cleaner的内存回收函数。
堆内的DirectByteBuffer对象本身会被垃圾回收正常的处理,但是堆外的内存就不会被GC回收了,所以需要一个机制,在DirectByteBuffer回收时,同时回收其堆外申请的内存。
Java中可选的特性有finalize函数(对象被gc回收前的准备工作),但是finalize机制是Java官方不推荐的,官方推荐的做法是使用虚引用来处理对象被回收时的后续处理工作。同时Java提供了Cleaner类来简化这个实现,Cleaner是PhantomReference的子类,可以在PhantomReference被加入ReferenceQueue时触发对应的Runnable回调。
DirectByteBuffer就是使用Cleaner机制来实现本身被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() {
if (address == 0) {
// Paranoia
return;
}
// 使用unsafe方法释放内存
unsafe.freeMemory(address);
address = 0;
// 更新统计变量
Bits.unreserveMemory(size, capacity);
}
}
说明:sun.misc.Unsafe.freeMemory方法使用C标准库的free函数释放内存空间。
5、DirectByteBuffer读写逻辑
public ByteBuffer put(int i, byte x) {
unsafe.putByte(ix(checkIndex(i)), ((x)));
return this;
}
public byte get(int i) {
return ((unsafe.getByte(ix(checkIndex(i)))));
}
private long ix(int i) {
return address + (i << 0);
}
DirectByteBuffer使用sun.misc.Unsafe.getByte(long)和sun.misc.Unsafe.putByte(long, byte)这两个方法来读写堆外内存空间的指定位置的字节数据。不过这两个方法本地实现比较复杂,这里就不分析了。
6、默认可以申请的堆外内存大小:
用户可以通过-XX:MaxDirectMemorySize=<size>这个参数来控制可以申请多大的DirectByteBuffer内存。但是默认情况下这个大小是多少呢?
// A user-settable upper limit on the maximum amount of allocatable direct
// buffer memory. This value may be changed during VM initialization if
// "java" is launched with "-XX:MaxDirectMemorySize=<size>".
//
// The initial value of this field is arbitrary; during JRE initialization
// it will be reset to the value specified on the command line, if any,
// otherwise to Runtime.getRuntime().maxMemory().
//
private static long directMemory = 64 * 1024 * 1024;
// Returns the maximum amount of allocatable direct buffer memory.
// The directMemory variable is initialized during system initialization
// in the saveAndRemoveProperties method.
//
public static long maxDirectMemory() {
return directMemory;
}
这里directMemory默认赋值为64MB,那对外内存的默认大小是64MB吗?不是,仔细看注释,注释中说,这个值会在JRE启动过程中被重新设置为用户指定的值,如果用户没有指定,则会设置为Runtime.getRuntime().maxMemory()。
这个过程发生在sun.misc.VM#saveAndRemoveProperties函数中,这个函数会被java.lang.System#initializeSystemClass调用:
public static void saveAndRemoveProperties(Properties props) {
if (booted)
throw new IllegalStateException("System initialization has completed");
savedProps.putAll(props);
// Set the maximum amount of direct memory. This value is controlled
// by the vm option -XX:MaxDirectMemorySize=<size>.
// The maximum amount of allocatable direct buffer memory (in bytes)
// from the system property sun.nio.MaxDirectMemorySize set by the VM.
// The system property will be removed.
String s = (String)props.remove("sun.nio.MaxDirectMemorySize");
if (s != null) {
if (s.equals("-1")) {
// -XX:MaxDirectMemorySize not given, take default
directMemory = Runtime.getRuntime().maxMemory();
} else {
long l = Long.parseLong(s);
if (l > -1)
directMemory = l;
}
}
//...
}
所以默认情况下,可以申请的DirectByteBuffer大小为Runtime.getRuntime().maxMemory(),而这个值等于可用的最大Java堆大小,也就是我们-Xmx参数指定的值。
参考:http://www.importnew.com/29817.html