JVM源码分析之堆外内存彻底解读

概述

广义的堆外内存

说到堆外内存,那你们确定想到堆内内存,这也是咱们你们接触最多的,咱们在jvm参数里一般设置-Xmx来指定咱们的堆的最大值,不过这还不是咱们理解的Java堆,-Xmx的值是新生代和老生代的和的最大值,咱们在jvm参数里一般还会加一个参数-XX:MaxPermSize来指定持久代的最大值,那么咱们认识的Java堆的最大值实际上是-Xmx和-XX:MaxPermSize的总和,在分代算法下,新生代,老生代和持久代是连续的虚拟地址,由于它们是一块儿分配的,那么剩下的均可以认为是堆外内存(广义的)了,这些包括了jvm自己在运行过程当中分配的内存,codecache,jni里分配的内存,DirectByteBuffer分配的内存等等java

狭义的堆外内存

而做为java开发者,咱们常说的堆外内存溢出了,实际上是狭义的堆外内存,这个主要是指java.nio.DirectByteBuffer在建立的时候分配内存,咱们这篇文章里也主要是讲狭义的堆外内存,由于它和咱们平时碰到的问题比较密切算法

JDK/JVM里DirectByteBuffer的实现

DirectByteBuffer一般用在通讯过程当中作缓冲池,在mina,netty等nio框架中家常便饭,先来看看JDK里的实现:框架

image.png

经过上面的构造函数咱们知道,真正的内存分配是使用的Bits.reserveMemory方法jvm

image.png

经过上面的代码咱们知道能够经过-XX:MaxDirectMemorySize来指定最大的堆外内存,那么咱们首先引入两个问题函数

  • 堆外内存默认是多大
  • 为何要主动调用System.gc()

堆外内存默认是多大

若是咱们没有经过-XX:MaxDirectMemorySize来指定最大的堆外内存,那么默认的最大堆外内存是多少呢,咱们仍是经过代码来分析 上面的代码里咱们看到调用了sun.misc.VM.maxDirectMemory()源码分析

image.png

看到上面的代码以后是否是误觉得默认的最大值是64M?其实不是的,说到这个值得从java.lang.System这个类的初始化提及线程

image.png

上面这个方法在jvm启动的时候对System这个类作初始化的时候执行的,所以执行时间很是早,咱们看到里面调用了sun.misc.VM.saveAndRemoveProperties(props):3d

image.png

若是咱们经过-Dsun.nio.MaxDirectMemorySize指定了这个属性,只要它不等于-1,那效果和加了-XX:MaxDirectMemorySize同样的,若是两个参数都没指定,那么最大堆外内存的值来自于directMemory = Runtime.getRuntime().maxMemory(),这是一个native方法代理

image.png

其中在咱们使用CMS GC的状况下的实现以下,实际上是新生代的最大值-一个survivor的大小+老生代的最大值,也就是咱们设置的-Xmx的值里除去一个survivor的大小就是默认的堆外内存的大小了netty

image.png

为何要主动调用System.gc

既然要调用System.gc,那确定是想经过触发一次gc操做来回收堆外内存,不过我想先说的是堆外内存不会对gc形成什么影响(这里的System.gc除外),可是堆外内存的回收其实依赖于咱们的gc机制,首先咱们要知道在java层面和咱们在堆外分配的这块内存关联的只有与之关联的DirectByteBuffer对象了,它记录了这块内存的基地址以及大小,那么既然和gc也有关,那就是gc能经过操做DirectByteBuffer对象来间接操做对应的堆外内存了。DirectByteBuffer对象在建立的时候关联了一个PhantomReference,说到PhantomReference它其实主要是用来跟踪对象什么时候被回收的,它不能影响gc决策,可是gc过程当中若是发现某个对象除了只有PhantomReference引用它以外,并无其余的地方引用它了,那将会把这个引用放到java.lang.ref.Reference.pending队列里,在gc完毕的时候通知ReferenceHandler这个守护线程去执行一些后置处理,而DirectByteBuffer关联的PhantomReference是PhantomReference的一个子类,在最终的处理里会经过Unsafe的free接口来释放DirectByteBuffer对应的堆外内存块 JDK里ReferenceHandler的实现:

image.png

可见若是pending为空的时候,会经过lock.wait()一直等在那里,其中唤醒的动做是在jvm里作的,当gc完成以后会调用以下的方法VM_GC_Operation::doit_epilogue(),在方法末尾会调用lock的notify操做,至于pending队列何时将引用放进去的,实际上是在gc的引用处理逻辑中放进去的,针对引用的处理后面能够专门写篇文章来介绍

image.png

对于System.gc的实现,以前写了一篇文章来重点介绍,JVM源码分析之SystemGC彻底解读,它会对新生代的老生代都会进行内存回收,这样会比较完全地回收DirectByteBuffer对象以及他们关联的堆外内存,咱们dump内存发现DirectByteBuffer对象自己实际上是很小的,可是它后面可能关联了一个很是大的堆外内存,所以咱们一般称之为『冰山对象』,咱们作ygc的时候会将新生代里的不可达的DirectByteBuffer对象及其堆外内存回收了,可是没法对old里的DirectByteBuffer对象及其堆外内存进行回收,这也是咱们一般碰到的最大的问题,若是有大量的DirectByteBuffer对象移到了old,可是又一直没有作cms gc或者full gc,而只进行ygc,那么咱们的物理内存可能被慢慢耗光,可是咱们还不知道发生了什么,由于heap明明剩余的内存还不少(前提是咱们禁用了System.gc)。

为何要使用堆外内存

DirectByteBuffer在建立的时候会经过Unsafe的native方法来直接使用malloc分配一块内存,这块内存是heap以外的,那么天然也不会对gc形成什么影响(System.gc除外),由于gc耗时的操做主要是操做heap以内的对象,对这块内存的操做也是直接经过Unsafe的native方法来操做的,至关于DirectByteBuffer仅仅是一个壳,还有咱们通讯过程当中若是数据是在Heap里的,最终也仍是会copy一份到堆外,而后再进行发送,因此为何不直接使用堆外内存呢。对于须要频繁操做的内存,而且仅仅是临时存在一会的,都建议使用堆外内存,而且作成缓冲池,不断循环利用这块内存。

为何不能大面积使用堆外内存

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

推荐阅读

JVM菜鸟进阶高手之路十一(eden survivor分配问题)

Javassist实现JDK动态代理

相关文章
相关标签/搜索