JVM——堆外内存详解

2,169 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第22天,点击查看活动详情

内存是好东西,我们常听堆内存,很多人却不知道还有一个堆外内存。那什么是堆外内存呢???原文

一、堆内内存

“Java 虚拟机具有一个堆(Heap),堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。”

JVM启动时分配的,就叫作堆内存(即堆内内存)

对象的堆内存由称为垃圾回收器的自动内存管理系统回收。

此外,堆的内存不需要是连续空间,因此堆的大小没有具体要求,既可以固定,也可以扩大和缩小。

我们在jvm参数中只要使用-Xms,-Xmx等参数就可以设置堆的大小和最大值,理解jvm的堆还需要知道下面这个公式: 堆内内存 = 新生代+老年代+持久代

image.png

20181204225115944.png

在使用堆内内存(on-heap memory)的时候,完全遵守JVM虚拟机的内存管理机制,采用垃圾回收器(GC)统一进行内存管理,GC会在某些特定的时间点进行一次彻底回收,也就是Full GC,GC会对所有分配的堆内内存进行扫描。

注意:在这个过程中会对JAVA应用程序的性能造成一定影响,还可能会产生Stop The World。

二、堆外内存

显然,看名字就知道堆外内存与堆内内存是相对应的:Java 虚拟机管理堆之外的内存,称为非堆内存,即堆外内存

换句话说:堆外内存就是把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响

那堆外内存都有哪些东西呢?

Java 虚拟机具有一个由所有线程共享的方法区。方法区属于非堆内存。它存储每个类结构,如运行时常数池、字段和方法数据,以及方法和构造方法的代码。它是在 Java 虚拟机启动时创建的。

方法区在逻辑上属于堆,但 Java 虚拟机实现可以选择不对其进行回收或压缩。与堆类似,方法区的内存不需要是连续空间,因此方法区的大小可以固定,也可以扩大和缩小。。

除了方法区外,Java 虚拟机实现可能需要用于内部处理或优化的内存,这种内存也是非堆内存。例如,JIT 编译器需要内存来存储从 Java 虚拟机代码转换而来的本机代码,从而获得高性能。

三、堆外内存的申请和释放

JDK的ByteBuffer类提供了一个接口allocateDirect(int capacity)进行堆外内存的申请。

底层通过unsafe .allocateMemory(size) 实现,Netty、Mina等框架提供的接口也是基于ByteBuffer封装的。

四、堆外内存的回收机制

上文说到,“unsafe.allocateMemory(size)的最底层是通过malloc方法申请的,但是这块内存需要进行手动释放,JVM并不会进行回收,幸好Unsafe提供了另一个接口freeMemory可以对申请的堆外内存进行释放。”。那岂不是每一次申请堆外内存的时候,都需要在代码中显式释放吗?

很明显,并不是这样的,这种情况的出现对于Java这门语言来说显然不够合理。那既然JVM不会管理这些堆外内存,它们又是怎么回收的呢?

这里就要祭出大杀器了:DirectByteBuffer

JDK中使用DirectByteBuffer对象来表示堆外内存,每个DirectByteBuffer对象在初始化时,都会创建一个对应的Cleaner对象,这个Cleaner对象会在合适的时候执行unsafe.freeMemory(address),从而回收这块堆外内存。

20181204230058751.png

五、System.gc的作用有哪些

  • 做一次full gc
  • 执行后会暂停整个进程。
  • System.gc我们可以禁掉,使用-XX:+DisableExplicitGC,
  • 其实一般在cms gc下我们通过-XX:+ExplicitGCInvokesConcurrent也可以做稍微高效一点的gc,也就是并行gc。
  • 最常见的场景是RMI/NIO下的堆外内存分配等

注: 如果我们使用了堆外内存,并且用了DisableExplicitGC设置为true,那么就是禁止使用System.gc,这样堆外内存将无从触发极有可能造成内存溢出错误(这种情况在四中有提及),在这种情况下可以考虑使用ExplicitGCInvokesConcurrent参数。

说起Full gc我们最先想到的就是stop thd world,这里要先提到VMThread,在jvm里有这么一个线程不断轮询它的队列,这个队列里主要是存一些VM_operation的动作,比如最常见的就是内存分配失败要求做GC操作的请求等,在对gc这些操作执行的时候会先将其他业务线程都进入到安全点,也就是这些线程从此不再执行任何字节码指令,只有当出了安全点的时候才让他们继续执行原来的指令,因此这其实就是我们说的stop the world(STW),整个进程相当于静止了

六、使用堆外内存的优点

任何一个事物使用起来有优点就会有缺点,堆外内存的缺点就是内存难以控制,使用了堆外内存就间接失去了JVM管理内存的可行性,改由自己来管理,当发生内存溢出时排查起来非常困难。 所以,还是那句话,使用的时候要多留心呀~

  • 可以扩展至更大的内存空间。比如超过1TB甚至比主存还大的空间;
  • 减少了垃圾回收(因为垃圾回收会暂停其他的工作);
  • 可以在进程间共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现(堆内在flush到远程时,会先复制到直接内存(非堆内存),然后在发送;而堆外内存相当于省略掉了这个工作);
  • 它的持久化存储可以支持快速重启,同时还能够在测试环境中重现生产数据;
  • 站在系统设计的角度来看,使用堆外内存可以为你的设计提供更多可能。最重要的提升并不在于性能,而是决定性的。