在我们日常的开发过程中,遇到问题除了普通的异常(空指针啊,数组越界啊 and so on),我们遇到的比较大的问题无非就是 OOM ,频繁 FullGC 或者是多线程方面的问题(这块我说不上话:new_moon_with_face:),我们大都数产生的问题也都是与 JVM 相关的,而今日则谈谈与它有关联的另外一个地方。
身为一个 java 开发者,我们首先熟悉的是 JVM (尽管对里面的各种各种回收算法还不算很清晰),它帮我们管理着各个对象(是的,我们都有对象 )的生命周期,助于程序能够正常的运行下去。但是还有一块区域与它隔岸相望->非堆内存(如下图)。
我们可以清晰的看出 NonHeap 在程序中的位置(以上画图并不代表他们在内存中所占的空间比例情况)。
我们能确定的是堆里面的东西是我们去自己操作的,而 NonHeap 就是 JVM 留给自己用的,所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中。
本地起来一个小的Demo,我们通过 Arthas 可以去查看堆空间与非堆空间的情况,以及划分的区域。
普通的开发者应该是用不到的(像我这样:new_moon_with_face::new_moon_with_face::new_moon_with_face:),高级以上的开发应该会使用到,因为他们知道如何使一个普通的程序变得不普通。
在 JAVA 中,可以通过 Unsafe 和 NIO 包下的 ByteBuffer 来操作非堆内存。
一看这名字就知道不安全:joy_cat:,不过也的确不怎么安全。它位于 sun.misc 包下的一个类,主要提供一些用于执行低级别、不安全操作的方法。内部 API 大多数是对系统内存直接操作的,这会提高我们程序的运行效率等等,但是也同样会很容易发生错误,他里面操作类似于C语言一样的指针操作,会增加了程序相关指针问题的风险。
我们可以稍微:ghost:康康:ghost:其中部分方法:
// 分配内存 , 相当于 C++ 的 malloc 函数 public native long allocateMemory(long bytes); // 扩充内存 public native long reallocateMemory(long address, long bytes); // 释放内存 public native void freeMemory(long address); // 在给定的内存块中设置值 public native void setMemory(Object o, long offset, long bytes, byte value); // 内存拷贝 public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes); // 获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有 : getInt,getDouble,getLong,getChar 等 public native Object getObject(Object o, long offset); // 为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有 :putInt,putDouble,putLong,putChar 等 public native void putObject(Object o, long offset, Object x); // 获取给定地址的 byte 类型的值(当且仅当该内存地址为 allocateMemory 分配时,此方法结果为确定的) public native byte getByte(long address); // 为给定地址设置 byte 类型的值(当且仅当该内存地址为 allocateMemory 分配时,此方法结果才是确定的) public native void putByte(long address, byte x); 复制代码
除了以上直接操作内存相关的方法,还有一些用于CAS的方法, j.u.c 底下的并发集合操作以及相关的锁操作其实大部分都是调用了 Unsafe 里面的方法来控制。
DirectByteBuffer 是 Java 用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在 Netty 等 NIO 框架中应用广泛。DirectByteBuffer 对于堆外内存的创建、使用、销毁等逻辑也是由 Unsafe 提供的堆外内存 API 来实现,其构造函数就可以直接分配内存。
相关 API 康康:
// 分配size大小内存 unsafe.allocateMemory(size); // 从base位置开始初始化size大小内存 unsafe.setMemory(base, size, (byte) 0); 复制代码
我们了解到 Heap 的回收都是依赖的是 jvm 各个区域的回收算法实现,那么非堆的回收是如何进行呢?以及什么情况下去进行呢?
目前了解到的两种方式:
第一种暂且不过多讨论了。 第二种这里提一提:
我们通过 DirectByteBuffer 源码查看下当前的类结构,主要注意的是当前对象里面包含了一个 Deallocator 私有静态内部类以及私有成员属性 Cleaner :
private final Cleaner cleaner;
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.freeMemory(address); //释放内存
address = 0;
Bits.unreserveMemory(size, capacity);
}
}
复制代码
从上面我们可以大概知道最后进行非堆内存的回收肯定是静态内部类进行操作的。同时也与成员变量有关系,那么他是怎么进行操作的呢?
这里我们要注意的就是 Cleaner 对象了。
Cleaner 继承自 Java 四大引用类型之一的虚引用 PhantomReference (我们了解到无法通过虚引用获取与之关联的对象实例,且当对象仅被虚引用引用时,在任何发生 GC 的时候,其均可被回收),通常 PhantomReference 与引用队列 ReferenceQueue 结合使用,可以实现虚引用关联对象被垃圾回收时能够进行系统通知、资源清理等功能。如下图所示,当某个被 Cleaner 引用的对象将被回收时, JVM 垃圾收集器会将此对象的引用放入到对象引用中的 pending 链表中,等待 Reference-Handler 进行相关处理。其中, Reference-Handler 为一个拥有最高优先级的守护线程,会循环不断的处理 pending 链表中的对象引用,执行 Cleaner 的 clean 方法进行相关清理工作。
所以当 DirectByteBuffer 仅被 Cleaner 引用(即为虚引用)时,其可以在任意 GC 时段被回收。当 DirectByteBuffer 实例对象被回收时,在 Reference-Handler 线程操作中,会调用 Cleaner 的 clean 方法根据创建 Cleaner 时传入的 Deallocator 来进行堆外内存的释放。
我们了解过堆的作用,那么我们就好奇下非堆在我们的程序中占着什么样子的作用?
总结下有两点:
第一点说明:
我们知道 jvm 中的所有 gc 是针对于当前容器内的对象进行回收处理的,在 Ygc 阶段,涉及到垃圾标记的过程,从 GCRoot 开始标记,一旦扫描到引用到了老年代的对象则中断本次扫描,加速 Ygc 的进度,但是 Ygc 阶段中的 old-gen sacnning 阶段则用于扫描被老年代引用的对象,那么一旦老年代过大,则 Ygc 所需要的时间就过长(时间与大小成正比),则不利于当前程序的垃圾回收。所以一旦引入非堆,我们就可以保持较小的堆内存规模,从而保证 gc 的正常进行。
第二点说明:
这里面涉及的主要关于服务器的用户态以及内核态,我们了解到在服务器上面操作的一个文件传输出去,会涉及到用户态转内核态,然后内核态转用户态等等步骤,其中有些操作是消耗 cpu 资源的(从内存地址缓存区读取以及写入),我们就会其中的操作是可以省略的,我们可以直接将文件从磁盘到内存地址缓存区,然后再到套接字缓冲区,这就是所谓的零拷贝技术。
以上部分就是简单的说下非堆在 java 中的作用。使用非堆我觉得大部分的程序员应该还使用不到(我是暂且摸不到的),不过大家可以了解下,增长知识准没错:see_no_evil::see_no_evil:。最后祝大家过个好年~