广义堆外内存概述一说到堆外内存,大家一定会想到堆上内存,这是我们接触最多的内存.我们通常会在jvm参数中设置-Xmx来指定我们堆的最大值,但这并不是我们理解的Java堆。-Xmx的值是新生代和老年代之和的最大值。我们通常会在jvm参数中加上一个参数-XX:MaxPermSize来指定持久代的最大值,那么我们所知道的Java堆的最大值其实就是-Xmx和-XX:MaxPermSize的总和,在分代算法下,新生代,老年代和永久代是一个连续的虚拟地址,因为他们是一起分配的,那么剩下的可以认为是堆外内存(广义上),其中包括jvm自己分配的内存运行过程中,codecache,jni中分配的内存,DirectByteBuffer分配的内存等狭义堆外内存作为java开发者,我们常说堆外内存溢出了,其实是狭义的堆外内存感觉。这里主要是指java.nio.DirectByteBuffer创建时的分配。内存,本文我们主要讲的是狭义的堆外内存,因为它与我们平时遇到的问题密切相关JDK/JVM中DirectByteBuffer的实现DirectByteBuffer在通信过程中通常用作缓冲池,在mina、netty、nio框架中并不少见,我们先来看看JDK中的实现:通过上面的构造函数,我们知道真正的内存分配是使用Bits.reserveMemory方法。从上面的代码我们知道可以通过-XX:MaxDirectMemorySize***堆外内存来指定,那么我们首先引入两个问题:堆外内存的默认大小是多少?为什么我们要主动调用System.gc()?内存,那么默认***堆外内存是多少呢?我们通过代码来分析一下上面的代码。看到上面的代码我们看到调用了sun.misc.VM.maxDirectMemory()。最大值是64M?事实上,它不是。说到这里,值得从java.lang.System类的初始化说起。上面的方法是在jvm启动的时候初始化System类的时候执行的,所以执行时间很早,我们看到里面调用了sun.misc.VM.saveAndRemoveProperties(props):如果我们通过-Dsun.nio.MaxDirectMemorySize指定这个属性,只要不等于-1,效果就和加上-XX:MaxDirectMemorySize一样。如果两个参数都不指定,那么***堆外内存的值来自directMemory=Runtime.getRuntime().maxMemory(),这是一个native方法。当我们使用CMSGC时,实现如下。其实就是新生代的最大值-asurvivorSize+oldgeneration的最大值,也就是我们设置的-Xmx的值除了asurvivor的大小就是off-heap的默认大小记忆。为什么要主动调用System.gc?既然要调用System.gc,那肯定是想通过触发gc操作来回收堆外内存,但是我首先要说的是,堆外内存不会对gc产生任何影响(System.gc除外)。这里是gc),但是堆外内存的回收其实还是要靠我们的gc机制,首先我们要知道在java层面,只有我们分配的堆外内存关联的DirectByteBuffer对象关联it,里面记录了这块内存的基址和大小,所以既然它也和gc有关,也就是说gc可以通过操作DirectByteBuffer对象来间接操作对应的堆外内存DirectByteBuffer对象在创建时与PhantomReference相关联。说到PhantomReference,主要是用来跟踪对象什么时候被回收的。它不能影响gc的决定。但是,如果在gc过程中发现一个对象只被PhantomReference引用,没有其他地方引用它,那么这个引用就会被放到java.lang.ref.Reference.pending队列中,并且ReferenceHandler守护线程在gc完成后会被通知进行一些后处理,DirectByteBuffer关联PhantomReference是PhantomReference的子类。在最后的处理中,DirectByteBuffer对应的堆外内存块会通过Unsafe的free接口释放。JDK中ReferenceHandler的实现:可以看出,如果pending为空,会通过lock.wait()一直在那里等待,唤醒动作是在jvm中完成的。当gc完成后,会调用下面的方法VM_GC_Operation::doit_epilogue(),在方法结束时会调用锁的notify操作。至于pendingqueue什么时候会放引用,其实是在gc的引用处理逻辑中放的。参考处理后,可以专门写一篇文章介绍System.gc的实现。之前写过一篇文章重点介绍,SystemGC之JVM源码分析全面解读,它会回收新生代和老年代的内存,这样DirectByteBuffer对象及其关联的堆外内存会被回收得更多彻底。我们转储了内存,发现DirectByteBuffer对象本身其实很小,但是后面可能会关联创建一个非常大的堆外内存,所以我们通常称它为“冰山对象”。我们在做ygc的时候,会在新生代回收不可达的DirectByteBuffer对象和它们的堆外内存,但是不能回收老年代不可达的DirectByteBuffer对象。回收DirectByteBuffer对象及其堆外内存也是我们平时遇到的最大问题。如果大量的DirectByteBuffer对象被移到了old,但是没有做cmsgc或者fullgc,只是ygc,那么我们的物理内存可能会慢慢耗尽,但是我们还不知道发生了什么,因为heap显然已经大量剩余内存(前提是我们禁用System.gc)为什么使用堆外内存DirectByteBuffer在创建的时候会直接使用malloc通过Unsafe的native方法分配一块内存。这块内存在堆外,自然不会对gc有任何影响(System.gc除外),因为gc的耗时操作主要是操作堆中的对象,对这块内存的操作也是直接通过Unsafe的native方法进行操作,相当于DirectByteBuffer只是一个壳子,我们通信过程中如果数据在Heap,最终会被复制到堆外,然后发送,所以为什么不直接使用堆外内存。对于需要频繁操作且只是暂时存在一段时间的内存,建议使用堆外内存,将其做成缓冲池不断回收这块内存。为什么不能大面积使用堆外内存呢?如果我们不加限制地大面积使用堆外内存,迟早会导致内存溢出。毕竟程序是在资源有限的机器上运行的,因为这块内存的回收不是你直接完成的。是可以控制的,当然你也可以用其他的方式,比如反射,直接使用Unsafe接口等等,但是这些肯定会给你带来一些麻烦,Java的先天优势被你彻底抛弃了——开发不需要要注意内存的回收是通过gc算法自动实现的。另外,上面的gc机制和堆外内存的关系也提到了。如果无法触发cmsgc或fullgc,后果可能很严重。【本文为专栏作家李家鹏原创文章,转载请微信公众号(你个假傻逼,id:lovestblog)联系作者获取授权转载】点此查看作者更多好文
