JVM源码分析之堆外内存(直接内存)

一、堆外内存定义

  内存对象分配在JVM中堆之外的内存,也能够称为直接内存,这些内存直接受操做系统管理(而不是JVM),这样作的好处是可以在必定程度上减小垃圾回收对应用程序形成的影响。通常咱们使用Unsafe和NIO包下ByteBuffer来建立堆外内存。java

二、为何使用堆外内存

  一、减小了垃圾回收算法

  使用堆外内存的话,堆外内存是直接受操做系统管理( 而不是虚拟机 )。这样作的结果就是能保持一个较小的堆内内存,以减小垃圾收集对应用的影响。框架

  二、提高复制速度(io效率)源码分析

  堆内内存由JVM管理,属于“用户态”;而堆外内存由OS管理,属于“内核态”。若是从堆内向磁盘写数据时,数据会被先复制到堆外内存,即内核缓冲区,而后再由OS写入磁盘,使用堆外内存避免了这个操做。this

三、堆外内存申请

 JDK的ByteBuffer类提供了一个接口allocateDirect(int capacity)进行堆外内存的申请,底层经过unsafe.allocateMemory(size)实现。Netty、Mina等框架提供的接口也是基于ByteBuffer封装的。spa

import java.nio.ByteBuffer;
public class DirectOom {    
    public static void main(String[] args) {        
        //直接分配128M的直接内存(100M) 
        ByteBuffer bb = ByteBuffer.allocateDirect(128*1024*1204);    
    }
}
复制代码

 源码分析以下:操作系统

DirectByteBuffer(int cap) {                 

        super(-1, 0, cap, cap);
        //内存是否按页分配对齐
        boolean pa = VM.isDirectMemoryPageAligned();
        //获取每页内存大小
        int ps = Bits.pageSize();
        //分配内存的大小,若是是按页对齐方式,须要再加一页内存的容量
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        //用Bits类保存总分配内存(按页分配)的大小和实际内存的大小
        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)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
    	//设置一个清理对象
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }
复制代码

四、堆外内存释放

虽然堆外内存直接受操做系统管理,可是不表明JVM不进行内存回收,要了解堆外内存释放,必须了解如下内容:code

1)当初始化一块堆外内存时,对象的引用关系以下:cdn

其中firstCleaner类的静态变量,Cleaner对象在初始化时会被添加到Clener链表中,和first造成引用关系,ReferenceQueue是用来保存须要回收的Cleaner对象。对象

源码分析以下(和对象的引用关系对照):

cleaner对象在如下代码中建立

first对象和ReferenceQueue对象是在如下代码中,

2)若是该DirectByteBuffer对象在一次GC中被回收了,会发生什么?

此时,只有Cleaner对象惟一保存了堆外内存的数据(开始地址、大小和容量),在下一次FGC时,把该Cleaner对象放入到ReferenceQueue中,并触发clean方法。

Cleaner对象的clean方法主要有两个做用: 一、把自身从Clener链表删除,从而在下次GC时可以被回收 二、释放堆外内存

若是JVM一直没有执行FullGC的话,无效的Cleaner对象就没法放入到ReferenceQueue中,从而堆外内存也一直得不到释放,内存岂不是会爆?

其实在初始化DirectByteBuffer对象时,若是当前堆外内存的条件很苛刻时(不够时),会主动调用System.gc()强制执行FullGC,代码以下:

五、总结

若是咱们大面积使用堆外内存而且没有限制,那早晚会致使内存溢出,毕竟程序是跑在一台资源受限的机器上,由于这块内存的回收不是你直接能控制的,固然你能够经过别的一些途径,好比反射,直接使用Unsafe接口等,可是这些务必给你带来了一些烦恼,Java与生俱来的优点被你彻底抛弃了—开发不须要关注内存的回收,由gc算法自动去实现。另外上面的gc机制与堆外内存的关系也说了,若是一直触发不了cms gc或者full gc,那么后果可能很严重。

你的赞和关注是我继续创做的动力~

相关文章
相关标签/搜索